@siftline/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,4 +42,4 @@ ESM only. Node 22.14 or newer.
42
42
 
43
43
  ## Licence
44
44
 
45
- MIT see [LICENSE](./LICENSE).
45
+ MIT. See [LICENSE](./LICENSE).
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { a as entryType, c as parseRecipe, d as recipeSchema, f as score, i as defineRecipe, l as questionName, n as isObjectLike, p as serializeRecipe, r as choice, s as noul, t as parseJsonLines, u as questionSchema } from "./jsonl-Cyqrt9-a.mjs";
1
+ import { a as entryType, c as parseRecipe, d as recipeSchema, f as score, i as defineRecipe, l as questionName, n as decodeThrown, p as serializeRecipe, r as choice, s as noul, t as parseJsonLines, u as questionSchema } from "./jsonl-CzTtnvyn.mjs";
2
2
  import { z } from "zod";
3
3
  //#region package.json
4
- var version = "0.1.0";
4
+ var version = "0.1.1";
5
5
  //#endregion
6
6
  //#region src/decision.ts
7
7
  /** Strict: an unknown key is a bad Record, not a field to ignore. */
@@ -15,6 +15,8 @@ const answerValue = z.union([
15
15
  z.boolean(),
16
16
  z.number()
17
17
  ]);
18
+ const label = z.string();
19
+ const level = z.number().int().min(0);
18
20
  /**
19
21
  * Why `value` is not an answer to `asked`, or `null` when it is. Rules and Fixtures both
20
22
  * validate against the Recipe with it, so the two report the same words.
@@ -22,13 +24,13 @@ const answerValue = z.union([
22
24
  function answerValueProblem(asked, value) {
23
25
  const shown = JSON.stringify(value);
24
26
  if (asked.type === "choice") {
25
- if (typeof value !== "string") return `value ${shown} is not a label`;
26
- return Object.hasOwn(asked.criteria, value) ? null : `unknown label ${shown}`;
27
+ const parsed = label.safeParse(value);
28
+ if (!parsed.success) return `value ${shown} is not a label`;
29
+ return Object.hasOwn(asked.criteria, parsed.data) ? null : `unknown label ${shown}`;
27
30
  }
28
- if (asked.type === "noul") return typeof value === "boolean" ? null : `value ${shown} is not a boolean`;
31
+ if (asked.type === "noul") return value === true || value === false ? null : `value ${shown} is not a boolean`;
29
32
  const last = asked.criteria.length - 1;
30
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > last) return `level index ${shown} is out of range (0-${last})`;
31
- return null;
33
+ return level.max(last).safeParse(value).success ? null : `level index ${shown} is out of range (0-${last})`;
32
34
  }
33
35
  const unit = z.number().min(0).max(1);
34
36
  const levelIndex = z.string().regex(/^\d+$/);
@@ -163,13 +165,15 @@ const ruleSchema = z.object({
163
165
  condition: ruleConditionSchema,
164
166
  action: z.string().min(1).nullable()
165
167
  }).strict();
168
+ const levelAnswer = z.number();
166
169
  function matches(answers, condition) {
167
170
  const actual = answers[condition.question];
168
171
  if (actual === void 0) return false;
169
172
  if (condition.comparator === "is") return actual === condition.value;
170
- if (condition.comparator === "isOneOf") return typeof actual === "string" && condition.value.includes(actual);
171
- if (typeof actual !== "number") return false;
172
- return condition.comparator === "atLeast" ? actual >= condition.value : actual <= condition.value;
173
+ if (condition.comparator === "isOneOf") return condition.value.some((label) => label === actual);
174
+ const level = levelAnswer.safeParse(actual);
175
+ if (!level.success) return false;
176
+ return condition.comparator === "atLeast" ? level.data >= condition.value : level.data <= condition.value;
173
177
  }
174
178
  /** Pure, first match wins, no review gate. Cloud runs it again on corrected answers. */
175
179
  function evaluateRules(answers, rules) {
@@ -221,7 +225,7 @@ function validateRules(rules, recipe) {
221
225
  report(`unknown question "${question}"`);
222
226
  continue;
223
227
  }
224
- if (!comparatorsFor[asked.type].includes(comparator)) {
228
+ if (!comparatorsFor[asked.type].some((allowed) => allowed === comparator)) {
225
229
  report(`comparator "${comparator}" is not valid for a ${asked.type} question`);
226
230
  continue;
227
231
  }
@@ -312,40 +316,25 @@ function createGate(limit) {
312
316
  return release;
313
317
  };
314
318
  }
315
- function errorType(thrown) {
316
- const body = thrown["body"];
317
- if (!isObjectLike(body)) return void 0;
318
- const detail = body["detail"];
319
- if (!isObjectLike(detail)) return void 0;
320
- const type = detail["error_type"];
321
- return typeof type === "string" ? type : void 0;
322
- }
319
+ const errorBodySchema = z.object({ detail: z.object({ error_type: z.string() }) });
323
320
  function reasonOf(thrown, status) {
324
- if (status === null) {
325
- const name = thrown["name"];
326
- return typeof name === "string" && /timeout/i.test(name) ? "timeout" : "network";
327
- }
328
- const type = errorType(thrown);
321
+ if (status === null) return thrown.name !== void 0 && /timeout/i.test(thrown.name) ? "timeout" : "network";
322
+ const body = errorBodySchema.safeParse(thrown.body);
323
+ const type = body.success ? body.data.detail.error_type : void 0;
329
324
  if (type === "api_usage_error" || type === "max_tokens_exceeded") return type;
330
325
  return "unknown";
331
326
  }
332
327
  function isAbort(cause, thrown, signal) {
333
328
  if (signal?.aborted === true && cause === signal.reason) return true;
334
- const name = thrown["name"];
335
- return name === "AbortError" || name === "APIUserAbortError";
329
+ return thrown.name === "AbortError" || thrown.name === "APIUserAbortError";
336
330
  }
337
- /** Duck typing, because core cannot import the SDK's error classes. Never returns. */
331
+ /** Core cannot import the SDK's error classes, so it decodes their fields. Never returns. */
338
332
  function mapClientError(cause, signal) {
339
- const thrown = isObjectLike(cause) ? cause : {};
333
+ const thrown = decodeThrown(cause);
340
334
  if (isAbort(cause, thrown, signal)) throw cause;
341
- const rawStatus = thrown["status"];
342
- const status = typeof rawStatus === "number" ? rawStatus : null;
343
- const rawMessage = thrown["message"];
344
- const message = typeof rawMessage === "string" && rawMessage !== "" ? rawMessage : String(cause);
345
- if (status === 429 || status === 529) {
346
- const retryAfterMs = thrown["retryAfterMs"];
347
- throw new JudgeExhaustedError(message, typeof retryAfterMs === "number" ? retryAfterMs : null, { cause });
348
- }
335
+ const status = thrown.status ?? null;
336
+ const message = thrown.message !== void 0 && thrown.message !== "" ? thrown.message : String(cause);
337
+ if (status === 429 || status === 529) throw new JudgeExhaustedError(message, thrown.retryAfterMs ?? null, { cause });
349
338
  throw new JudgeError(message, reasonOf(thrown, status), status, { cause });
350
339
  }
351
340
  function invalidAnswers(message) {
@@ -365,7 +354,7 @@ function rebuild(name, keys, source) {
365
354
  const probabilities = {};
366
355
  for (const key of keys) {
367
356
  const probability = source[key];
368
- if (typeof probability !== "number") throw invalidAnswers(`question ${name} has no probability for ${key}`);
357
+ if (probability === void 0) throw invalidAnswers(`question ${name} has no probability for ${key}`);
369
358
  probabilities[key] = probability;
370
359
  }
371
360
  return probabilities;
@@ -505,13 +494,14 @@ function parseFixture(line) {
505
494
  }
506
495
  /** The only writer. One compact line, no trailing newline, absent optionals omitted. */
507
496
  function serializeFixture(fixture) {
508
- const line = {};
509
- if (fixture.id !== void 0) line["id"] = fixture.id;
510
- if (fixture.origin !== void 0) line["origin"] = fixture.origin;
511
- if (fixture.by !== void 0) line["by"] = fixture.by;
512
- line["state"] = fixture.state;
513
- line["expect"] = fixture.expect;
514
- return JSON.stringify(line);
497
+ const { id, origin, by, state, expect } = fixture;
498
+ return JSON.stringify({
499
+ id,
500
+ origin,
501
+ by,
502
+ state,
503
+ expect
504
+ });
515
505
  }
516
506
  function parseFixtures(text) {
517
507
  return parseJsonLines(text, parseFixture, (line, cause) => new FixtureParseError(`fixture line ${line} is not a valid Fixture`, line, { cause }));
@@ -103,10 +103,19 @@ function serializeRecipe(recipe) {
103
103
  }, null, 2)}\n`;
104
104
  }
105
105
  //#endregion
106
- //#region src/object.ts
107
- /** Anything a property can be read off. Arrays count; the callers read named keys only. */
108
- function isObjectLike(value) {
109
- return typeof value === "object" && value !== null;
106
+ //#region src/thrown.ts
107
+ const thrownSchema = z.object({
108
+ name: z.string().optional().catch(void 0),
109
+ message: z.string().optional().catch(void 0),
110
+ status: z.number().optional().catch(void 0),
111
+ retryAfterMs: z.number().optional().catch(void 0),
112
+ requestId: z.string().optional().catch(void 0),
113
+ body: z.unknown().optional()
114
+ });
115
+ /** Anything that is not an object carries no fields. */
116
+ function decodeThrown(cause) {
117
+ const decoded = thrownSchema.safeParse(cause);
118
+ return decoded.success ? decoded.data : {};
110
119
  }
111
120
  //#endregion
112
121
  //#region src/jsonl.ts
@@ -127,4 +136,4 @@ function parseJsonLines(text, parseLine, fail) {
127
136
  return values;
128
137
  }
129
138
  //#endregion
130
- export { entryType as a, parseRecipe as c, recipeSchema as d, score as f, defineRecipe as i, questionName as l, isObjectLike as n, jsonValue as o, serializeRecipe as p, choice as r, noul as s, parseJsonLines as t, questionSchema as u };
139
+ export { entryType as a, parseRecipe as c, recipeSchema as d, score as f, defineRecipe as i, questionName as l, decodeThrown as n, jsonValue as o, serializeRecipe as p, choice as r, noul as s, parseJsonLines as t, questionSchema as u };
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as entryType, n as isObjectLike, o as jsonValue, t as parseJsonLines, u as questionSchema } from "./jsonl-Cyqrt9-a.mjs";
1
+ import { a as entryType, n as decodeThrown, o as jsonValue, t as parseJsonLines, u as questionSchema } from "./jsonl-CzTtnvyn.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/testing.ts
4
4
  const probability = z.number().min(0).max(1);
@@ -82,13 +82,14 @@ function createScriptedClient(script) {
82
82
  }
83
83
  };
84
84
  }
85
- function deepEqual(a, b) {
86
- if (a === b) return true;
87
- if (!isObjectLike(a) || !isObjectLike(b)) return false;
88
- if (Array.isArray(a) !== Array.isArray(b)) return false;
89
- const keys = Object.keys(a);
90
- if (keys.length !== Object.keys(b).length) return false;
91
- return keys.every((key) => key in b && deepEqual(a[key], b[key]));
85
+ /** Objects write their keys sorted, so two values that differ only in key order match. */
86
+ function canonicalJson(value) {
87
+ if (value === null || !(value instanceof Object)) return JSON.stringify(value);
88
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
89
+ return `{${Object.entries(value).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, member]) => `${JSON.stringify(key)}:${canonicalJson(member)}`).join(",")}}`;
90
+ }
91
+ function requestKey(request) {
92
+ return canonicalJson(jsonValue.parse(JSON.parse(JSON.stringify(request))));
92
93
  }
93
94
  /** Rebuilds what the SDK threw closely enough for the Judge's duck typing to see it. */
94
95
  function replayError(recorded) {
@@ -103,29 +104,28 @@ function replayError(recorded) {
103
104
  }
104
105
  /** Answers the line whose `request` is deep-equal to the incoming one. Key order is free. */
105
106
  function createReplayClient(lines) {
107
+ const keyed = lines.map((line) => ({
108
+ key: requestKey(line.request),
109
+ line
110
+ }));
106
111
  return { systemOne: async (request) => {
107
- const line = lines.find((candidate) => deepEqual(candidate.request, request));
112
+ const key = requestKey(request);
113
+ const line = keyed.find((candidate) => candidate.key === key)?.line;
108
114
  if (!line) throw new Error(`no replay line matches the request for model ${request.model} and questions ${Object.keys(request.questions).join(", ")}`);
109
115
  if ("error" in line) throw replayError(line.error);
110
116
  return line.response;
111
117
  } };
112
118
  }
113
- function asString(value) {
114
- return typeof value === "string" ? value : void 0;
115
- }
116
- function asNumber(value) {
117
- return typeof value === "number" ? value : null;
118
- }
119
- /** Duck typing, because core cannot name the SDK's error classes. */
119
+ /** Core cannot name the SDK's error classes, so it decodes their fields. */
120
120
  function recordError(cause) {
121
- const thrown = isObjectLike(cause) ? cause : {};
121
+ const thrown = decodeThrown(cause);
122
122
  return {
123
- name: asString(thrown["name"]) ?? "Error",
124
- message: asString(thrown["message"]) ?? String(cause),
125
- status: asNumber(thrown["status"]),
126
- retryAfterMs: asNumber(thrown["retryAfterMs"]),
127
- requestId: asString(thrown["requestId"]),
128
- body: thrown["body"]
123
+ name: thrown.name ?? "Error",
124
+ message: thrown.message ?? String(cause),
125
+ status: thrown.status ?? null,
126
+ retryAfterMs: thrown.retryAfterMs ?? null,
127
+ requestId: thrown.requestId,
128
+ body: thrown.body
129
129
  };
130
130
  }
131
131
  /** Wraps a live client and hands one replay line per call to `sink`. The live test's half. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftline/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The Siftline engine: recipes, rules, fixtures and the judge wrapper.",
5
5
  "keywords": [
6
6
  "classification",