@corbits/system-one 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ import { type } from "arktype";
2
+ import { SystemOneError } from "./errors.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Shared JSON shapes
5
+ // ---------------------------------------------------------------------------
6
+ // A plain JSON object: string keys, unknown values. The narrow rejects class
7
+ // instances, Maps, and arrays — all of which match an index signature but
8
+ // serialize wrong (`new Map()` stringifies to `{}`).
9
+ const isPlainObject = (value) => {
10
+ if (typeof value !== "object" || value === null)
11
+ return false;
12
+ const proto = Object.getPrototypeOf(value);
13
+ return proto === Object.prototype || proto === null;
14
+ };
15
+ export const JsonRecord = type({ "[string]": "unknown" }).narrow(isPlainObject);
16
+ // Anywhere the contract allows "string | object | array": question
17
+ // instructions, option/level descriptions, and noul criteria all accept a
18
+ // structured form (the model reads named fields via backtick references).
19
+ export const Description = type("string | unknown[]").or(JsonRecord);
20
+ // Evaluation state: a string for text, or an object/array for structured
21
+ // records — never a class instance or function, it must survive a JSON round
22
+ // trip to the backend.
23
+ export const State = Description;
24
+ const nonEmpty = (d) => typeof d === "string"
25
+ ? d.length >= 1
26
+ : Array.isArray(d)
27
+ ? d.length >= 1
28
+ : Object.keys(d).length >= 1;
29
+ // ---------------------------------------------------------------------------
30
+ // Questions (public edge)
31
+ // ---------------------------------------------------------------------------
32
+ //
33
+ // Caller-facing questions: an array with caller-chosen ids. The wire sends
34
+ // them as a map keyed by id (see `toWireQuestions`) — ids must be unique so
35
+ // the backend's per-id echo stays unambiguous. Every schema here parses a
36
+ // trust boundary, so `as T` casts never appear on these shapes.
37
+ //
38
+ // The shapes follow the live Jev contract: discriminator `type`
39
+ // (`'choice' | 'score' | 'noul'`), a required `instructions` (string or
40
+ // structured), and per-kind `criteria`. `boolean` is a caller-side alias for
41
+ // `noul` (the REST contract has no boolean kind) and is mapped at build time
42
+ // — see `toWireQuestions`.
43
+ // A multiple-choice question. `criteria` maps each option to its
44
+ // description (or null when an option needs no detail); at least one option
45
+ // and at most 255 per the vendor contract.
46
+ export const ChoiceQuestion = type({
47
+ id: "string",
48
+ type: "'choice'",
49
+ instructions: Description,
50
+ criteria: { "[string]": Description.or("null") },
51
+ "+": "reject",
52
+ }).narrow((q) => nonEmpty(q.instructions) &&
53
+ Object.keys(q.criteria).length >= 1 &&
54
+ Object.keys(q.criteria).length <= 255);
55
+ // A scored question. `criteria` is the ordered level descriptions (2–10);
56
+ // the backend answers with a fractional position across those indexes.
57
+ export const ScoreQuestion = type({
58
+ id: "string",
59
+ type: "'score'",
60
+ instructions: Description,
61
+ criteria: Description.array(),
62
+ "+": "reject",
63
+ }).narrow((q) => nonEmpty(q.instructions) &&
64
+ q.criteria.length >= 2 &&
65
+ q.criteria.length <= 10);
66
+ // A yes/no gate. `criteria` optionally describes what true/false mean.
67
+ export const NoulQuestion = type({
68
+ id: "string",
69
+ type: "'noul'",
70
+ instructions: Description,
71
+ "criteria?": {
72
+ "true?": Description,
73
+ "false?": Description,
74
+ "+": "reject",
75
+ },
76
+ "+": "reject",
77
+ }).narrow((q) => nonEmpty(q.instructions));
78
+ // Caller-side alias for `noul`: same shape, mapped to native noul on the
79
+ // wire. Exists so callers never have to spell the vendor's double-negative.
80
+ export const BooleanQuestion = type({
81
+ id: "string",
82
+ type: "'boolean'",
83
+ instructions: Description,
84
+ "criteria?": {
85
+ "true?": Description,
86
+ "false?": Description,
87
+ "+": "reject",
88
+ },
89
+ "+": "reject",
90
+ }).narrow((q) => nonEmpty(q.instructions));
91
+ // The question union a caller may ask in one evaluation. `.or` chains keep
92
+ // each branch's literal `type`, so the union still discriminates on it.
93
+ export const Question = ChoiceQuestion.or(ScoreQuestion)
94
+ .or(NoulQuestion)
95
+ .or(BooleanQuestion);
96
+ // A non-empty question list with unique ids: a duplicate would make the
97
+ // backend's per-id echo permanently ambiguous, so it is rejected at the
98
+ // input edge (typed `SystemOneError` / `ProtocolMismatchError`) rather than
99
+ // surfacing later as a silently collapsed wire map.
100
+ export const QuestionList = Question.array()
101
+ .atLeastLength(1)
102
+ .narrow((qs) => new Set(qs.map((q) => q.id)).size === qs.length);
103
+ // ---------------------------------------------------------------------------
104
+ // Input
105
+ // ---------------------------------------------------------------------------
106
+ // Which backend an evaluation targets. `official` is the System One endpoint,
107
+ // `gateway` the proxy endpoint, `custom` a caller-supplied URL. `url` only
108
+ // exists on `custom` — a `url` on another kind would be silently ignored,
109
+ // so the schema rejects it instead of accepting misleading config.
110
+ export const EndpointConfig = type({
111
+ kind: "'custom'",
112
+ url: "string",
113
+ "model?": "string",
114
+ "+": "reject",
115
+ }).or(type({
116
+ kind: "'official' | 'gateway'",
117
+ "model?": "string",
118
+ "+": "reject",
119
+ }));
120
+ // Per-evaluation knobs. An absent `endpoint` resolves to `official` at the
121
+ // `evaluate` edge, not here — schemas parse, they never fill in defaults.
122
+ // `timeoutMs` is deliberately `unknown`: any non-finite or negative value
123
+ // falls back to `DEFAULT_TIMEOUT_MS` at the edge (documented normalization,
124
+ // not a validation error), so the schema must not reject it first.
125
+ export const EvaluateConfig = type({
126
+ "endpoint?": EndpointConfig,
127
+ "timeoutMs?": "unknown",
128
+ "apiKey?": "string",
129
+ "+": "reject",
130
+ });
131
+ // One evaluation request: state plus at least one uniquely-id'd question,
132
+ // so an evaluation always asks something.
133
+ export const EvaluateInput = type({
134
+ state: State,
135
+ questions: QuestionList,
136
+ "config?": EvaluateConfig,
137
+ "+": "reject",
138
+ });
139
+ // ---------------------------------------------------------------------------
140
+ // Wire shapes (backend contract)
141
+ // ---------------------------------------------------------------------------
142
+ //
143
+ // The serialized request the backend actually receives: questions as a map
144
+ // keyed by caller id (ids echo back as answer keys and are never sent to the
145
+ // model), plus the required top-level `model`. Built by `toWireQuestions` +
146
+ // `toWireRequest`, never hand-assembled by callers.
147
+ /** One question in wire form: no id (the map key carries it). */
148
+ export const WireQuestion = type({
149
+ type: "'noul' | 'choice' | 'score'",
150
+ instructions: Description,
151
+ "criteria?": "unknown",
152
+ "+": "reject",
153
+ });
154
+ /** The exact POST body: model + state + id-keyed questions. */
155
+ export const WireRequest = type({
156
+ model: "string",
157
+ state: State,
158
+ questions: { "[string]": WireQuestion },
159
+ "+": "reject",
160
+ }).narrow((body) => Object.keys(body.questions).length >= 1);
161
+ /**
162
+ * Maps caller-facing questions to the wire map. The `boolean` alias becomes
163
+ * native `noul`; everything else passes through with its id as the key.
164
+ */
165
+ export function toWireQuestions(questions) {
166
+ const wire = {};
167
+ for (const question of questions) {
168
+ const mapped = {
169
+ type: question.type === "boolean" ? "noul" : question.type,
170
+ instructions: question.instructions,
171
+ };
172
+ if (question.criteria !== undefined)
173
+ mapped.criteria = question.criteria;
174
+ wire[question.id] = mapped;
175
+ }
176
+ return wire;
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // Results
180
+ // ---------------------------------------------------------------------------
181
+ // A calibrated 0..1 confidence. Narrowed, not clamped: an out-of-range
182
+ // backend value is a shape violation, never silently squeezed into range.
183
+ export const Confidence = type("number").narrow((n) => n >= 0 && n <= 1);
184
+ // Per-outcome probabilities as a record of finite 0..1 numbers keyed by
185
+ // outcome label (option ids for choice, level indexes as strings for score).
186
+ // The backend rounds to two decimals, so sums are approximate —
187
+ // `APPROXIMATE_SUM_TOLERANCE` below, not exact equality, is the check.
188
+ export const ProbabilityMap = type({ "[string]": "number" }).narrow((m) => Object.values(m).every((n) => Number.isFinite(n) && n >= 0 && n <= 1));
189
+ /** Tolerance for probability-sum checks (backend rounds to 2 decimals). */
190
+ export const APPROXIMATE_SUM_TOLERANCE = 0.02;
191
+ // One backend answer in wire form. All kind-specific fields are optional
192
+ // here; `checkAnswer` (evaluate) and `toDecision` enforce which ones a given
193
+ // `type` must carry. No `"+": "reject"`: the backend may attach per-answer
194
+ // metadata that `toDecision` strips.
195
+ export const WireAnswer = type({
196
+ type: "'noul' | 'choice' | 'score'",
197
+ "choice?": "string",
198
+ "score?": "number",
199
+ "noul?": Confidence,
200
+ "probabilities?": ProbabilityMap,
201
+ "confidence?": Confidence,
202
+ "legend?": { "[string]": "string" },
203
+ });
204
+ // Token usage reported on the response envelope.
205
+ export const WireUsage = type({
206
+ input_tokens: "number",
207
+ output_tokens: "number",
208
+ });
209
+ // The backend response envelope: answers keyed by question id, the
210
+ // responding (possibly versioned) model id, and token usage. `model` and
211
+ // `usage` are optional — a gateway or custom backend may omit them, and
212
+ // neither is worth failing an otherwise-valid evaluation over. No
213
+ // `"+": "reject"`: top-level extras validate but are never returned.
214
+ export const WireResponseBody = type({
215
+ "model?": "string",
216
+ "usage?": WireUsage,
217
+ answers: { "[string]": WireAnswer },
218
+ });
219
+ // A caller-facing decision: one per kind, discriminated by `type`, with each
220
+ // kind's contract-required fields required here. `toDecision` builds these
221
+ // from validated wire answers; backend extras are never echoed.
222
+ export const Decision = type({
223
+ id: "string",
224
+ type: "'noul'",
225
+ noul: Confidence,
226
+ "+": "reject",
227
+ })
228
+ .or(type({
229
+ id: "string",
230
+ type: "'choice'",
231
+ choice: "string",
232
+ probabilities: ProbabilityMap,
233
+ confidence: Confidence,
234
+ "+": "reject",
235
+ }))
236
+ .or(type({
237
+ id: "string",
238
+ type: "'score'",
239
+ score: "number",
240
+ legend: { "[string]": "string" },
241
+ probabilities: ProbabilityMap,
242
+ confidence: Confidence,
243
+ "+": "reject",
244
+ }));
245
+ /**
246
+ * Builds a caller-facing decision from a wire answer: the kind's contract
247
+ * fields only, so cross-kind extras are dropped and a malformed answer
248
+ * (e.g. a choice answer missing `choice`) throws a typed `SystemOneError`
249
+ * instead of producing a half-empty decision.
250
+ */
251
+ export function toDecision(id, answer) {
252
+ let candidate;
253
+ if (answer.type === "noul") {
254
+ candidate = { id, type: "noul", noul: answer.noul };
255
+ }
256
+ else if (answer.type === "choice") {
257
+ candidate = {
258
+ id,
259
+ type: "choice",
260
+ choice: answer.choice,
261
+ probabilities: answer.probabilities,
262
+ confidence: answer.confidence,
263
+ };
264
+ }
265
+ else {
266
+ candidate = {
267
+ id,
268
+ type: "score",
269
+ score: answer.score,
270
+ legend: answer.legend,
271
+ probabilities: answer.probabilities,
272
+ confidence: answer.confidence,
273
+ };
274
+ }
275
+ const parsed = Decision(candidate);
276
+ if (parsed instanceof type.errors) {
277
+ throw new SystemOneError("parse-error", `toDecision: answer "${id}" failed its kind's contract: ${parsed.summary}`);
278
+ }
279
+ return parsed;
280
+ }
281
+ // Token usage on a caller-facing result (camelCase like the rest of the
282
+ // public surface).
283
+ export const Usage = type({
284
+ inputTokens: "number",
285
+ outputTokens: "number",
286
+ "+": "reject",
287
+ });
288
+ // Backwards-compatible alias: the envelope shared by `evaluate()` and the
289
+ // provider adapter.
290
+ export const EvaluateResponseBody = WireResponseBody;
291
+ export const EvaluateResult = type({
292
+ decisions: Decision.array(),
293
+ modelId: "string",
294
+ backend: "'corbits-system-one' | 'gateway' | 'custom'",
295
+ latencyMs: "number",
296
+ "usage?": Usage,
297
+ fallback: "false",
298
+ "+": "reject",
299
+ });
300
+ // Every reason an evaluation can come back as data instead of decisions.
301
+ // Shared with the thrown taxonomy (see `SystemOneErrorCode` in errors.ts).
302
+ export const FallbackReason = type("'no-key' | 'timeout' | 'network' | 'http-error' | 'parse-error' | 'backend-unreachable'");
303
+ export const FallbackResult = type({
304
+ fallback: "true",
305
+ reason: FallbackReason,
306
+ latencyMs: "number",
307
+ "httpStatus?": "number",
308
+ "detail?": "string",
309
+ backendAttempted: "string",
310
+ "+": "reject",
311
+ });
@@ -0,0 +1,24 @@
1
+ /** One evaluation lifecycle event. */
2
+ export declare const SystemOneTelemetryEvent: import("arktype/internal/variants/object.ts").ObjectType<{
3
+ event: "evaluate.start" | "evaluate.success" | "evaluate.fallback";
4
+ backend: "custom" | "gateway" | "corbits-system-one";
5
+ latencyMs: number;
6
+ modelId?: string;
7
+ reason?: "no-key" | "timeout" | "network" | "http-error" | "parse-error" | "backend-unreachable";
8
+ detail?: string;
9
+ }, {}>;
10
+ export type SystemOneTelemetryEvent = typeof SystemOneTelemetryEvent.infer;
11
+ /** Maximum buffered events; beyond this the oldest drop first. */
12
+ export declare const MAX_BUFFERED_TELEMETRY_EVENTS = 256;
13
+ /**
14
+ * Records one telemetry event to the in-memory ring. Validates the shape
15
+ * (a malformed event is caller misuse and throws a typed `SystemOneError`)
16
+ * and never touches the network, disk, or console.
17
+ */
18
+ export declare function recordTelemetryEvent(event: SystemOneTelemetryEvent): void;
19
+ /**
20
+ * Drains the buffered telemetry events in arrival order and clears the
21
+ * buffer. The host calls this after each evaluation (or on its own cadence)
22
+ * to forward events to its own sink.
23
+ */
24
+ export declare function drainTelemetryEvents(): SystemOneTelemetryEvent[];
@@ -0,0 +1,51 @@
1
+ import { type } from "arktype";
2
+ import { SystemOneError } from "./errors.js";
3
+ import { FallbackReason } from "./schemas.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Telemetry
6
+ // ---------------------------------------------------------------------------
7
+ //
8
+ // Event shapes for evaluation observability. Data only, no sink: events are
9
+ // validated and buffered in a bounded in-memory ring the host drains with
10
+ // `drainTelemetryEvents` and forwards wherever it wants (stdout, OTLP, a
11
+ // host hook). A ring — not a pluggable sink — because a sink interface is a
12
+ // second public surface to version for no load-bearing reason: the host
13
+ // already owns every destination, it only needs lossless-per-call access to
14
+ // the events. The cap keeps a chatty loop from growing memory without bound
15
+ // (oldest events drop first). No event field ever carries key material.
16
+ /** One evaluation lifecycle event. */
17
+ export const SystemOneTelemetryEvent = type({
18
+ event: "'evaluate.start' | 'evaluate.success' | 'evaluate.fallback'",
19
+ backend: "'corbits-system-one' | 'gateway' | 'custom'",
20
+ "modelId?": "string",
21
+ latencyMs: "number",
22
+ "reason?": FallbackReason,
23
+ "detail?": "string",
24
+ "+": "reject",
25
+ });
26
+ /** Maximum buffered events; beyond this the oldest drop first. */
27
+ export const MAX_BUFFERED_TELEMETRY_EVENTS = 256;
28
+ const bufferedEvents = [];
29
+ /**
30
+ * Records one telemetry event to the in-memory ring. Validates the shape
31
+ * (a malformed event is caller misuse and throws a typed `SystemOneError`)
32
+ * and never touches the network, disk, or console.
33
+ */
34
+ export function recordTelemetryEvent(event) {
35
+ const parsed = SystemOneTelemetryEvent(event);
36
+ if (parsed instanceof type.errors) {
37
+ throw new SystemOneError("parse-error", `recordTelemetryEvent: invalid event: ${parsed.summary}`);
38
+ }
39
+ bufferedEvents.push(parsed);
40
+ if (bufferedEvents.length > MAX_BUFFERED_TELEMETRY_EVENTS) {
41
+ bufferedEvents.splice(0, bufferedEvents.length - MAX_BUFFERED_TELEMETRY_EVENTS);
42
+ }
43
+ }
44
+ /**
45
+ * Drains the buffered telemetry events in arrival order and clears the
46
+ * buffer. The host calls this after each evaluation (or on its own cadence)
47
+ * to forward events to its own sink.
48
+ */
49
+ export function drainTelemetryEvents() {
50
+ return bufferedEvents.splice(0, bufferedEvents.length);
51
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@corbits/system-one",
3
+ "version": "0.1.0",
4
+ "description": "Typed-decision evaluation client for System One (Jev-class) models: arktype question/decision schemas, endpoint configuration with gateway override, and timeout/fallback semantics.",
5
+ "license": "LGPL-2.1-only",
6
+ "author": "Sawyer Cutler <sawyer@dirtroad.dev>",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint .",
29
+ "format": "prettier --write .",
30
+ "format:check": "prettier --check .",
31
+ "test": "bun test",
32
+ "prepack": "bun run build",
33
+ "check": "bun run typecheck && bun run lint && bun run format:check && bun run test"
34
+ },
35
+ "dependencies": {
36
+ "arktype": "2.2.3"
37
+ },
38
+ "peerDependencies": {
39
+ "@intx/inference": ">=0.4.0",
40
+ "@intx/types": ">=0.4.0"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "9.34.0",
44
+ "@intx/inference": "0.4.0",
45
+ "@intx/types": "0.4.0",
46
+ "@types/bun": "1.3.9",
47
+ "eslint": "9.34.0",
48
+ "jiti": "2.5.1",
49
+ "prettier": "3.6.2",
50
+ "typescript": "5.9.3",
51
+ "typescript-eslint": "8.42.0"
52
+ },
53
+ "engines": {
54
+ "bun": ">=1.2.0"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/corbitsdev/corbits-system-one.git"
59
+ },
60
+ "homepage": "https://github.com/corbitsdev/corbits-system-one",
61
+ "bugs": {
62
+ "url": "https://github.com/corbitsdev/corbits-system-one/issues"
63
+ }
64
+ }