@qitqode/qitqore 0.1.0 → 0.3.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/decision-policy.d.ts +132 -0
  4. package/dist/decision-policy.d.ts.map +1 -0
  5. package/dist/decision-policy.js +308 -0
  6. package/dist/decision-policy.js.map +1 -0
  7. package/dist/device-authorization.d.ts +67 -0
  8. package/dist/device-authorization.d.ts.map +1 -0
  9. package/dist/device-authorization.js +122 -0
  10. package/dist/device-authorization.js.map +1 -0
  11. package/dist/errors.d.ts +36 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +108 -17
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.d.ts +11 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/recovery-capabilities.d.ts +26 -0
  20. package/dist/recovery-capabilities.d.ts.map +1 -0
  21. package/dist/recovery-capabilities.js +113 -0
  22. package/dist/recovery-capabilities.js.map +1 -0
  23. package/dist/recovery-directive.d.ts +20 -0
  24. package/dist/recovery-directive.d.ts.map +1 -0
  25. package/dist/recovery-directive.js +116 -0
  26. package/dist/recovery-directive.js.map +1 -0
  27. package/dist/recovery-lease.d.ts +80 -0
  28. package/dist/recovery-lease.d.ts.map +1 -0
  29. package/dist/recovery-lease.js +257 -0
  30. package/dist/recovery-lease.js.map +1 -0
  31. package/dist/recovery-status.d.ts +22 -0
  32. package/dist/recovery-status.d.ts.map +1 -0
  33. package/dist/recovery-status.js +113 -0
  34. package/dist/recovery-status.js.map +1 -0
  35. package/dist/recovery.d.ts +13 -0
  36. package/dist/recovery.d.ts.map +1 -1
  37. package/dist/recovery.js +94 -55
  38. package/dist/recovery.js.map +1 -1
  39. package/dist/redaction.d.ts +64 -0
  40. package/dist/redaction.d.ts.map +1 -0
  41. package/dist/redaction.js +154 -0
  42. package/dist/redaction.js.map +1 -0
  43. package/dist/route-contract.d.ts +36 -0
  44. package/dist/route-contract.d.ts.map +1 -0
  45. package/dist/route-contract.js +89 -0
  46. package/dist/route-contract.js.map +1 -0
  47. package/dist/sse.d.ts +4 -0
  48. package/dist/sse.d.ts.map +1 -1
  49. package/dist/sse.js +16 -1
  50. package/dist/sse.js.map +1 -1
  51. package/dist/stream-payload.d.ts +40 -0
  52. package/dist/stream-payload.d.ts.map +1 -0
  53. package/dist/stream-payload.js +96 -0
  54. package/dist/stream-payload.js.map +1 -0
  55. package/dist/transport.d.ts +10 -0
  56. package/dist/transport.d.ts.map +1 -1
  57. package/dist/transport.js +28 -0
  58. package/dist/transport.js.map +1 -1
  59. package/package.json +12 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QitQode contributors, Spannings LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -8,6 +8,27 @@ It is not a CLI, agent runtime, authentication store, policy engine, scheduler,
8
8
  or billing/settlement system. Importing the package creates no directories,
9
9
  reads credentials, changes process settings, or makes network requests.
10
10
 
11
+ ## Repository and workspace
12
+
13
+ The dedicated repository is
14
+ [`forgewisetech/qitqore`](https://github.com/forgewisetech/qitqore).
15
+ It is a private, independently buildable mirror of the canonical
16
+ `products/qitqore` directory in the Forgewise monorepo. Make source changes in
17
+ that canonical directory, then use the workspace's per-product export to update
18
+ the repository; do not maintain a second implementation in either consumer.
19
+ The repository does not contain QitQode, Wud, their credentials, or their state.
20
+
21
+ The library is tracked as its own product in Umbrella. Its own `products/qitqore`
22
+ folder, dedicated repository, and Umbrella tracking are the intended setup;
23
+ a separate Replit preview artifact is not required.
24
+
25
+ The published `@qitqode/qitqore@0.3.0` release is immutable and licensed under
26
+ the [MIT License](./LICENSE), the same license as QitQode. Its package includes
27
+ the `LICENSE` file. The copyright holder is QitQode contributors, Spannings LLC.
28
+
29
+ The `0.1.0` and `0.2.0` artifacts remain immutable; current consumer pins target
30
+ `0.2.0` until each consumer adopts `0.3.0`.
31
+
11
32
  ## Development
12
33
 
13
34
  - Requires Node.js 24 or newer.
@@ -21,6 +42,60 @@ reads credentials, changes process settings, or makes network requests.
21
42
  The package has no runtime dependencies. Its sole public entry point is the
22
43
  compiled ESM export at `.`.
23
44
 
45
+ ## Released consumer adoption
46
+
47
+ The latest public package is **`@qitqode/qitqore@0.3.0`**. Install it with
48
+ `npm install --save-exact @qitqode/qitqore@0.3.0`. The independently managed
49
+ QitQode CLI, its backend SDK, and Wud currently pin the exact `0.2.0` release in
50
+ their own manifests and native Bun/npm lockfiles; they do not import sibling
51
+ source. Adopting `0.3.0` in a consumer is a separate change to its exact pin and
52
+ regenerated lockfile.
53
+
54
+ QitQode retains AI SDK/native-tool adaptation, credential recovery, advisory
55
+ budgets and session policy. Session retries carry an explicit operation ID;
56
+ prompt equality does not identify an operation. Qortex session retries require
57
+ an explicit non-acceptance verdict and no observed output or started tool.
58
+ Unknown or interrupted execution stops without replay, preserving partial text,
59
+ reasoning and interrupted tool records. Wud retains credentials,
60
+ durable operations, authorization, reservations, leases and settlement.
61
+ Both use the shared request lifecycle, SSE decoder and bounded recovery.
62
+ Compatibility fallback requires explicit pre-execution validation evidence.
63
+ An unterminated stream preserves available text/accounting evidence but is not
64
+ reported as successful completion.
65
+
66
+ Hosts may pass `deadlineAt` to `runWithRecovery` to apply one absolute deadline
67
+ to every attempt, retry delay and backoff; a request timeout alone is not an
68
+ operation-wide limit. `getRecoveryStatus` and `getRecoveryCapabilities`
69
+ validate separate Qortex execution and route-capability contracts; neither is
70
+ billing evidence. `parseRecoveryDirective` accepts only bounded decision fields.
71
+ QitQore does not provide exactly-once effects: callers still need stable
72
+ operation IDs, receipts and reconciliation for ambiguous external operations.
73
+ Recovery status reports only a retained Qortex route result. It never proves that
74
+ a caller applied a local change, an external effect happened exactly once, or
75
+ acceptance verification succeeded.
76
+
77
+ From the monorepo, run
78
+ `node products/qitqore/scripts/verify-consumers.mjs --production-install --bun /path/to/bun`
79
+ under Node 24, with Bun 1.3.11. This checks exact manifest/lock integrity,
80
+ compiled public imports in all consumers, and a clean Wud production dependency
81
+ installation without either application's source. Temporary fixtures are removed.
82
+ Package-local tests remain deterministic and independent of this network install.
83
+ Wud's injected-transport and accounting regressions run without Mongo initialization:
84
+ `cd products/wud && npx vitest run --config vitest.qitqore.config.ts`.
85
+
86
+ Publication, packed-candidate verification, released-consumer adoption and
87
+ deployment are separate claims. The package was published and its public tarball
88
+ verified; adopting it locally does **not** push either consumer or deploy it.
89
+ Future core changes require a new immutable package version, package tests and
90
+ packing checks, separate publication approval, then exact consumer pins and
91
+ regenerated native lockfiles.
92
+
93
+ Release 0.1.0's registry manifest does not declare a license. QitQode's generated
94
+ notice inventory identifies that exact release as first-party code without
95
+ inventing a third-party license grant. That release is not rewritten. The
96
+ published `0.2.0` release declares `"license": "MIT"` and ships its `LICENSE`
97
+ file in the package.
98
+
24
99
  ## Host responsibilities
25
100
 
26
101
  Create a client with an explicit base URL and optional `fetch` implementation.
@@ -40,6 +115,96 @@ produced partial output, or started a side effect. Acceptance, partial output,
40
115
  and side effects block replay. Unknown outcomes require affirmative host
41
116
  evidence and authorization; an idempotency header alone is not evidence.
42
117
 
118
+ Hosts that maintain a durable attempt ledger can use `onAttemptStart` and
119
+ `onAttemptOutcome` to record dispatch and settlement evidence. The outcome hook
120
+ is informational and does not authorize replay; hosts must still use
121
+ `authorizeReplay` for every additional attempt. Hook payloads contain operation
122
+ and attempt identities plus bounded execution facts, never request bodies,
123
+ credentials, or response content.
124
+
43
125
  Errors expose bounded, redacted diagnostic fields and request IDs, not raw
44
126
  response bodies or request credentials. Hosts may retain their existing
45
127
  product-specific error mappings around these shared primitives.
128
+
129
+ ## Release 0.3.0
130
+
131
+ Version 0.3.0 adds features without removing or renaming any 0.2.0 export:
132
+
133
+ - **Decision-policy client.** New `createDecisionPolicyClient`,
134
+ `assertDecisionPolicyRequest`, `parseDecisionPolicyResult` and
135
+ `DECISION_POLICY_CONTRACT_VERSION`, described below.
136
+ - **Free-tier fallback stage.** `SseDecoder` events may carry
137
+ `routingStatus: "free-fallback"` and `routingStage: "auto-free" | "free-pool"`
138
+ from Qortex's routing headers. `classifyQortexStreamPayload` reports the same
139
+ stage as an optional `fallbackStage` on a `routing-status` control frame.
140
+ - **Untrusted stage values.** A routing-status frame whose `fallbackStage` is
141
+ present but is neither `auto-free` nor `free-pool` is now classified as plain
142
+ `json` instead of a routing-status control. A frame without `fallbackStage`
143
+ is classified exactly as in 0.2.0.
144
+
145
+ ## Decision-policy client
146
+
147
+ `createDecisionPolicyClient` is a typed, zero-dependency client for OmniQuery's
148
+ named decision policies, reached through a Qortex module route. It validates the
149
+ request and response contract, and it makes exactly one POST per call with no
150
+ retries. The host supplies the base URL, the module path, the function name and
151
+ the caller's credential headers. QitQore never chooses a policy, tier, model or
152
+ rubric, never loads or stores credentials, and never invents request or
153
+ idempotency identities. A result is returned as data for the host to decide on.
154
+
155
+ The response parser cross-checks the status against the findings and the
156
+ aggregate usage against the per-invocation segments. A tampered or inconsistent
157
+ result is a `QortexProtocolError`, not a decision. The client was first
158
+ published in `0.3.0`.
159
+
160
+ ## Shared wire contracts (0.2.0)
161
+
162
+ The 0.2.0 release adds one implementation of the public Qortex contracts that
163
+ QitQode and Wud previously duplicated. Each area states what QitQore does and
164
+ what stays with the host.
165
+
166
+ - **Recovery lease and control.** `parseRecoveryStatus` accepts Qortex's
167
+ optional `recoveryLeaseUsage` only when it names the status's request, and the
168
+ status's route whenever the status includes one.
169
+ `parseRecoveryControlEnvelope`, `encodeRecoveryControlEnvelope`, and
170
+ `decodeRecoveryControlHeader` validate the header value Qortex reads. The
171
+ decoder accepts only canonical base64url. The host allocates token budgets,
172
+ persists leases, decides which work may run, and settles outcomes.
173
+ - **Routes and headers.** `recoveryRouteID`, `qortexModuleEndpoint`,
174
+ `qortexRequestHeaders`, and `withRecoveryControlHeader` take every module name,
175
+ identity, and key from the caller. Scoped historical paths such as
176
+ `@bijikyu/omniquery` are preserved exactly. QitQore never chooses between the
177
+ canonical and historical module, never generates a request or idempotency
178
+ identity, and never adds credentials.
179
+ - **SSE payloads.** `SseDecoder` and `iterateSse` frame the stream.
180
+ `classifyQortexStreamPayload` then returns a tagged result: `text`, `json`,
181
+ `usage`, `cost`, `control` (the documented free-fallback routing signal),
182
+ `done`, `error`, or `empty`. The routing control keeps its generic status and
183
+ accepts only an optional `fallbackStage` of `auto-free` or `free-pool`; legacy
184
+ stage-less frames remain valid, while unknown stage values stay ordinary JSON.
185
+ Plain tokens stay `text`, malformed or non-object
186
+ JSON stays `text`, and unknown objects stay `json`. The host converts OpenAI
187
+ chunks, tool calls, finish reasons, accounting, and terminal success. A stream
188
+ that ends without a terminator remains incomplete.
189
+ - **Error evidence.** `qortexErrorEvidence` reads the top-level, `error`, and
190
+ `details` envelopes once and keeps only allowlisted fields: acceptance,
191
+ partial output, side-effect markers, a validated recovery directive, and
192
+ whether replay evidence was available. Any non-`false` acceptance signal
193
+ blocks replay, including values QitQore does not recognize. A terminal
194
+ `retryable: false` wins over `true`. Use `hasReplayBlockingEvidence` before
195
+ replaying; absence of evidence is never permission to replay.
196
+ - **Secret redaction.** `createSecretRedactor` is pure and opt-in. The host
197
+ passes exact values and key names. It returns redaction, chunk-safe stream
198
+ redaction, and bounded structural redaction. It does not read the environment,
199
+ keep a global registry, or discover credentials. Pass `patterns: false` for
200
+ model output, where credential-shape heuristics could alter ordinary prose.
201
+ - **Forgewise device authorization.** `parseDeviceAuthorizationStart` validates
202
+ the start response and resolves expiry from an explicit `now`.
203
+ `classifyDeviceTokenPoll` classifies one poll as success, pending, slow-down
204
+ (through `pending`), denied, expired, failed, or invalid. It does not poll,
205
+ sleep, open a browser, persist a code, install a credential, refresh a session,
206
+ or read a clock. The host owns all of those.
207
+
208
+ Acceptance, partial-output, side-effect, and directive fields are evidence for
209
+ the host's replay decision. They are not authorization, billing settlement, or
210
+ proof of an exactly-once effect.
@@ -0,0 +1,132 @@
1
+ import { QortexClient } from "./transport.js";
2
+ export declare const DECISION_POLICY_CONTRACT_VERSION = 1;
3
+ export type DecisionPolicyStatus = "decided" | "partial" | "abstained";
4
+ export type DecisionNativeTier = "decisions" | "quick-decisions";
5
+ export interface DecisionPolicySubject {
6
+ kind: "request" | "candidate" | "requirement_check";
7
+ id: string;
8
+ requirementID?: string;
9
+ checkID?: string;
10
+ evidenceIDs?: string[];
11
+ }
12
+ export interface DecisionPolicyFinding {
13
+ findingID: string;
14
+ subject: DecisionPolicySubject;
15
+ status: "decided" | "abstained";
16
+ source: "model" | "deterministic";
17
+ nativeType: "noul" | "choice" | "score" | null;
18
+ verdict: string | null;
19
+ selectedCandidateID?: string;
20
+ noul?: number;
21
+ probabilities?: Record<string, number>;
22
+ confidence?: number;
23
+ ordinalScore?: number;
24
+ legend?: Record<string, unknown>;
25
+ nativeTier?: DecisionNativeTier;
26
+ reasonCodes: string[];
27
+ }
28
+ export interface DecisionPolicyInvocation {
29
+ invocationID: string;
30
+ tier: DecisionNativeTier;
31
+ nativeUsage: {
32
+ input_tokens: number;
33
+ output_tokens: number;
34
+ };
35
+ usage: {
36
+ prompt_tokens: number;
37
+ completion_tokens: number;
38
+ total_tokens: number;
39
+ context_window: number;
40
+ };
41
+ }
42
+ export interface DecisionPolicyResult {
43
+ contractVersion: 1;
44
+ policy: string;
45
+ policyVersion: string;
46
+ status: DecisionPolicyStatus;
47
+ verdict: string | null;
48
+ findings: DecisionPolicyFinding[];
49
+ observationOnly: boolean;
50
+ reasonCodes: string[];
51
+ durationMs: number;
52
+ requestID?: string;
53
+ usage: {
54
+ invocations: DecisionPolicyInvocation[];
55
+ aggregate: {
56
+ promptTokens: number;
57
+ completionTokens: number;
58
+ totalTokens: number;
59
+ };
60
+ };
61
+ }
62
+ export interface DecisionPolicyRequest {
63
+ contractVersion: 1;
64
+ policy: string;
65
+ expectedPolicyVersion?: string;
66
+ state: Record<string, unknown>;
67
+ evidence?: Record<string, unknown>;
68
+ images?: Array<string | {
69
+ content_type: string;
70
+ base64: string;
71
+ }>;
72
+ timeoutMs?: number;
73
+ requestID?: string;
74
+ escalation?: "none" | "full-on-abstain";
75
+ }
76
+ export interface DecisionPolicyCallOptions {
77
+ /** Caller-supplied credential headers, for example the Forgewise or Qortex key. QitQode never creates them. */
78
+ headers: HeadersInit;
79
+ /** Host-supplied request identity. Never invented here. */
80
+ requestId?: string;
81
+ /** Host-supplied stable idempotency key. Never invented here. */
82
+ idempotencyKey?: string;
83
+ signal?: AbortSignal;
84
+ timeoutMs?: number;
85
+ }
86
+ export interface DecisionPolicyResponse {
87
+ data: DecisionPolicyResult;
88
+ status: number;
89
+ requestId?: string;
90
+ }
91
+ export interface DecisionPolicyClientOptions {
92
+ /** Explicit Qortex base URL. Required so the route origin is always an explicit host decision. */
93
+ baseUrl: string | URL;
94
+ /** Optional preconstructed transport. Its base URL must match `baseUrl`. */
95
+ client?: QortexClient;
96
+ fetch?: typeof globalThis.fetch;
97
+ timeoutMs?: number;
98
+ /** Host-selected approved module path, for example the one the host has tested against. No default. */
99
+ module: string;
100
+ /** Host-selected public function name, for example `evaluateDecisionPolicy`. No default. */
101
+ functionName: string;
102
+ }
103
+ export interface DecisionPolicyClient {
104
+ evaluate(request: DecisionPolicyRequest, options: DecisionPolicyCallOptions): Promise<DecisionPolicyResponse>;
105
+ }
106
+ /**
107
+ * Validate a caller's request locally before any network call.
108
+ * @param value - Untrusted request body.
109
+ * @returns The same request, typed.
110
+ * @throws TypeError naming the first violated rule. Values are never echoed.
111
+ */
112
+ export declare function assertDecisionPolicyRequest(value: unknown): DecisionPolicyRequest;
113
+ /**
114
+ * Parse and cross-check a policy result. The status must agree with the findings, and the aggregate must equal
115
+ * the sum of the per-invocation segments, so a malformed or tampered response cannot pass as a decided result.
116
+ * @param value - Unwrapped policy result from the Qortex envelope.
117
+ * @param context - Status and request ID for sanitized error evidence.
118
+ * @returns A typed result.
119
+ * @throws QortexProtocolError for any contract violation.
120
+ */
121
+ export declare function parseDecisionPolicyResult(value: unknown, context?: {
122
+ status?: number;
123
+ requestId?: string;
124
+ }): DecisionPolicyResult;
125
+ /**
126
+ * Create a typed decision-policy client for one explicitly selected Qortex module function.
127
+ * @param options - Base URL, optional transport, and the host-chosen module and function names.
128
+ * @returns A client whose `evaluate` sends one POST per call and never retries.
129
+ * @throws TypeError for an invalid module, function name or base URL.
130
+ */
131
+ export declare function createDecisionPolicyClient(options: DecisionPolicyClientOptions): DecisionPolicyClient;
132
+ //# sourceMappingURL=decision-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision-policy.d.ts","sourceRoot":"","sources":["../src/decision-policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Typed client for OmniQuery's named decision policies, reached through a Qortex module route.
3
+ *
4
+ * QitQore validates the contract and carries the call. It never chooses a policy, tier, model, rubric or
5
+ * temperature; never loads or stores credentials; never invents operation or idempotency IDs; and never retries.
6
+ * The host supplies the module path, function name, base URL and caller credential headers explicitly, so no
7
+ * historical alias is picked silently. Semantic outcomes are returned as data; this client decides nothing with them.
8
+ */
9
+ import { QortexProtocolError } from "./errors.js";
10
+ import { qortexModuleEndpoint, qortexRequestHeaders } from "./route-contract.js";
11
+ import { createQortexClient, unwrapSuccessEnvelope } from "./transport.js";
12
+ export const DECISION_POLICY_CONTRACT_VERSION = 1;
13
+ /** Largest request body the native decision layer accepts (13 MiB serialized). */
14
+ const MAX_REQUEST_BYTES = 13 * 1024 * 1024;
15
+ const MAX_TIMEOUT_MS = 60_000;
16
+ const MAX_IMAGES = 4;
17
+ const POLICY_NAME = /^[a-z][a-z0-9]*(?:\.[a-z0-9]+)*\.v[0-9]+$/;
18
+ const POLICY_VERSION = /^[a-z][a-z0-9]*(?:\.[a-z0-9]+)*\.v[0-9]+@[0-9]+\.[0-9]+\.[0-9]+$/;
19
+ const TIERS = new Set(["decisions", "quick-decisions"]);
20
+ const REQUEST_KEYS = new Set([
21
+ "contractVersion", "policy", "expectedPolicyVersion", "state", "evidence",
22
+ "images", "timeoutMs", "requestID", "escalation",
23
+ ]);
24
+ /** Options a caller may never send: tier, model and sampling are chosen server-side by the registry. */
25
+ const FORBIDDEN_REQUEST_KEYS = ["tier", "model", "temperature"];
26
+ function contractError(message) {
27
+ return new TypeError(message);
28
+ }
29
+ function isPlainObject(value) {
30
+ if (value === null || typeof value !== "object" || Array.isArray(value))
31
+ return false;
32
+ const prototype = Object.getPrototypeOf(value);
33
+ return prototype === Object.prototype || prototype === null;
34
+ }
35
+ /**
36
+ * Validate a caller's request locally before any network call.
37
+ * @param value - Untrusted request body.
38
+ * @returns The same request, typed.
39
+ * @throws TypeError naming the first violated rule. Values are never echoed.
40
+ */
41
+ export function assertDecisionPolicyRequest(value) {
42
+ if (!isPlainObject(value))
43
+ throw contractError("Decision policy request must be a plain object");
44
+ for (const key of FORBIDDEN_REQUEST_KEYS) {
45
+ if (Object.hasOwn(value, key))
46
+ throw contractError("Decision policy requests cannot choose a tier, model or sampling option");
47
+ }
48
+ for (const key of Object.keys(value)) {
49
+ if (!REQUEST_KEYS.has(key))
50
+ throw contractError("Decision policy request contains an undocumented field");
51
+ }
52
+ if (value.contractVersion !== DECISION_POLICY_CONTRACT_VERSION)
53
+ throw contractError("Decision policy contractVersion must be 1");
54
+ if (typeof value.policy !== "string" || value.policy.length > 80 || !POLICY_NAME.test(value.policy)) {
55
+ throw contractError("Decision policy name is invalid");
56
+ }
57
+ if (value.expectedPolicyVersion !== undefined && (typeof value.expectedPolicyVersion !== "string"
58
+ || value.expectedPolicyVersion.length > 120 || !POLICY_VERSION.test(value.expectedPolicyVersion))) {
59
+ throw contractError("expectedPolicyVersion must name an immutable policy version");
60
+ }
61
+ if (!isPlainObject(value.state))
62
+ throw contractError("Decision policy state must be a plain object");
63
+ if (value.evidence !== undefined && !isPlainObject(value.evidence))
64
+ throw contractError("Decision policy evidence must be a plain object");
65
+ if (value.images !== undefined) {
66
+ if (!Array.isArray(value.images) || value.images.length > MAX_IMAGES)
67
+ throw contractError("Decision policy images must be an array of at most four items");
68
+ for (const image of value.images) {
69
+ const valid = typeof image === "string"
70
+ ? /^data:/i.test(image)
71
+ : isPlainObject(image) && typeof image.content_type === "string" && typeof image.base64 === "string" && image.base64.length > 0;
72
+ if (!valid)
73
+ throw contractError("Decision policy image must be a data URL or a content_type and base64 pair");
74
+ }
75
+ }
76
+ if (value.timeoutMs !== undefined && (typeof value.timeoutMs !== "number" || !Number.isFinite(value.timeoutMs)
77
+ || value.timeoutMs <= 0 || value.timeoutMs > MAX_TIMEOUT_MS)) {
78
+ throw contractError("Decision policy timeoutMs must be between 1 and 60000");
79
+ }
80
+ if (value.requestID !== undefined && (typeof value.requestID !== "string" || value.requestID.length === 0 || value.requestID.length > 128)) {
81
+ throw contractError("Decision policy requestID must contain between 1 and 128 characters");
82
+ }
83
+ if (value.escalation !== undefined && value.escalation !== "none" && value.escalation !== "full-on-abstain") {
84
+ throw contractError("Decision policy escalation must be none or full-on-abstain");
85
+ }
86
+ return value;
87
+ }
88
+ function protocolError(message, status, requestId) {
89
+ return new QortexProtocolError(message, status, requestId);
90
+ }
91
+ function isFiniteNumber(value, min = -Infinity, max = Infinity) {
92
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
93
+ }
94
+ function isCount(value) {
95
+ return Number.isInteger(value) && value >= 0;
96
+ }
97
+ function isStringArray(value, max) {
98
+ return Array.isArray(value) && value.length <= max && value.every(item => typeof item === "string");
99
+ }
100
+ function isProbabilityMap(value) {
101
+ return isPlainObject(value) && Object.values(value).every(probability => isFiniteNumber(probability, 0, 1));
102
+ }
103
+ function parseSubject(value) {
104
+ if (!isPlainObject(value) || typeof value.id !== "string")
105
+ throw protocolError("Decision policy subject is malformed");
106
+ if (value.kind === "request")
107
+ return { kind: "request", id: value.id };
108
+ if (value.kind === "candidate")
109
+ return { kind: "candidate", id: value.id };
110
+ if (value.kind === "requirement_check" && typeof value.requirementID === "string"
111
+ && typeof value.checkID === "string" && isStringArray(value.evidenceIDs, 5)) {
112
+ return { kind: "requirement_check", id: value.id, requirementID: value.requirementID, checkID: value.checkID, evidenceIDs: [...value.evidenceIDs] };
113
+ }
114
+ throw protocolError("Decision policy subject kind is not recognized");
115
+ }
116
+ function parseFinding(value) {
117
+ if (!isPlainObject(value) || typeof value.findingID !== "string")
118
+ throw protocolError("Decision policy finding is malformed");
119
+ const status = value.status;
120
+ const source = value.source;
121
+ const nativeType = value.nativeType;
122
+ if ((status !== "decided" && status !== "abstained") || (source !== "model" && source !== "deterministic")) {
123
+ throw protocolError("Decision policy finding status is not recognized");
124
+ }
125
+ if (nativeType !== null && nativeType !== "noul" && nativeType !== "choice" && nativeType !== "score") {
126
+ throw protocolError("Decision policy native type is not recognized");
127
+ }
128
+ if (value.verdict !== null && typeof value.verdict !== "string")
129
+ throw protocolError("Decision policy verdict must be a string or null");
130
+ if (!isStringArray(value.reasonCodes, 16))
131
+ throw protocolError("Decision policy reason codes are malformed");
132
+ if (value.noul !== undefined && !isFiniteNumber(value.noul, 0, 1))
133
+ throw protocolError("Decision policy noul must be a probability");
134
+ if (value.probabilities !== undefined && !isProbabilityMap(value.probabilities))
135
+ throw protocolError("Decision policy probabilities must be probabilities");
136
+ if (value.confidence !== undefined && !isFiniteNumber(value.confidence, 0, 1))
137
+ throw protocolError("Decision policy confidence must be a probability");
138
+ if (value.ordinalScore !== undefined && !isFiniteNumber(value.ordinalScore))
139
+ throw protocolError("Decision policy ordinal score must be finite");
140
+ if (value.nativeTier !== undefined && !TIERS.has(value.nativeTier))
141
+ throw protocolError("Decision policy native tier is not recognized");
142
+ if (value.selectedCandidateID !== undefined && typeof value.selectedCandidateID !== "string")
143
+ throw protocolError("Decision policy selected candidate is malformed");
144
+ return {
145
+ findingID: value.findingID,
146
+ subject: parseSubject(value.subject),
147
+ status,
148
+ source,
149
+ nativeType,
150
+ verdict: value.verdict,
151
+ reasonCodes: [...value.reasonCodes],
152
+ ...(value.noul !== undefined && { noul: value.noul }),
153
+ ...(value.probabilities !== undefined && { probabilities: { ...value.probabilities } }),
154
+ ...(value.confidence !== undefined && { confidence: value.confidence }),
155
+ ...(value.ordinalScore !== undefined && { ordinalScore: value.ordinalScore }),
156
+ ...(value.legend !== undefined && isPlainObject(value.legend) && { legend: { ...value.legend } }),
157
+ ...(value.nativeTier !== undefined && { nativeTier: value.nativeTier }),
158
+ ...(value.selectedCandidateID !== undefined && { selectedCandidateID: value.selectedCandidateID }),
159
+ };
160
+ }
161
+ function parseInvocation(value) {
162
+ if (!isPlainObject(value) || typeof value.invocationID !== "string" || !TIERS.has(value.tier)) {
163
+ throw protocolError("Decision policy usage segment is malformed");
164
+ }
165
+ const native = value.nativeUsage;
166
+ const usage = value.usage;
167
+ if (!isPlainObject(native) || !isCount(native.input_tokens) || !isCount(native.output_tokens)) {
168
+ throw protocolError("Decision policy native usage is malformed");
169
+ }
170
+ if (!isPlainObject(usage) || !isCount(usage.prompt_tokens) || !isCount(usage.completion_tokens)
171
+ || !isCount(usage.total_tokens) || !isCount(usage.context_window)) {
172
+ throw protocolError("Decision policy usage is malformed");
173
+ }
174
+ return {
175
+ invocationID: value.invocationID,
176
+ tier: value.tier,
177
+ nativeUsage: { input_tokens: native.input_tokens, output_tokens: native.output_tokens },
178
+ usage: {
179
+ prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens,
180
+ total_tokens: usage.total_tokens, context_window: usage.context_window,
181
+ },
182
+ };
183
+ }
184
+ /**
185
+ * Parse and cross-check a policy result. The status must agree with the findings, and the aggregate must equal
186
+ * the sum of the per-invocation segments, so a malformed or tampered response cannot pass as a decided result.
187
+ * @param value - Unwrapped policy result from the Qortex envelope.
188
+ * @param context - Status and request ID for sanitized error evidence.
189
+ * @returns A typed result.
190
+ * @throws QortexProtocolError for any contract violation.
191
+ */
192
+ export function parseDecisionPolicyResult(value, context = {}) {
193
+ const fail = (message) => protocolError(message, context.status, context.requestId);
194
+ if (!isPlainObject(value))
195
+ throw fail("Decision policy result is malformed");
196
+ if (value.contractVersion !== DECISION_POLICY_CONTRACT_VERSION)
197
+ throw fail("Decision policy result uses an unsupported contract version");
198
+ if (typeof value.policy !== "string" || !POLICY_NAME.test(value.policy))
199
+ throw fail("Decision policy result names an invalid policy");
200
+ if (typeof value.policyVersion !== "string" || !POLICY_VERSION.test(value.policyVersion))
201
+ throw fail("Decision policy result has an invalid policy version");
202
+ if (value.status !== "decided" && value.status !== "partial" && value.status !== "abstained")
203
+ throw fail("Decision policy status is not recognized");
204
+ if (value.verdict !== null && typeof value.verdict !== "string")
205
+ throw fail("Decision policy verdict must be a string or null");
206
+ if (!Array.isArray(value.findings) || value.findings.length === 0 || value.findings.length > 128)
207
+ throw fail("Decision policy findings are malformed");
208
+ if (typeof value.observationOnly !== "boolean")
209
+ throw fail("Decision policy observationOnly must be a boolean");
210
+ if (!isStringArray(value.reasonCodes, 16) || !isFiniteNumber(value.durationMs, 0))
211
+ throw fail("Decision policy result metadata is malformed");
212
+ if (value.requestID !== undefined && typeof value.requestID !== "string")
213
+ throw fail("Decision policy requestID must be a string");
214
+ if (!isPlainObject(value.usage) || !Array.isArray(value.usage.invocations) || value.usage.invocations.length > 128)
215
+ throw fail("Decision policy usage is malformed");
216
+ const findings = value.findings.map(parseFinding);
217
+ const invocations = value.usage.invocations.map(parseInvocation);
218
+ const decidedCount = findings.filter(finding => finding.status === "decided").length;
219
+ const expectedStatus = decidedCount === findings.length ? "decided" : decidedCount === 0 ? "abstained" : "partial";
220
+ if (value.status !== expectedStatus)
221
+ throw fail("Decision policy status does not match its findings");
222
+ const aggregate = value.usage.aggregate;
223
+ const sum = (pick) => invocations.reduce((total, segment) => total + pick(segment), 0);
224
+ const expectedAggregate = {
225
+ promptTokens: sum(segment => segment.usage.prompt_tokens),
226
+ completionTokens: sum(segment => segment.usage.completion_tokens),
227
+ totalTokens: sum(segment => segment.usage.total_tokens),
228
+ };
229
+ if (!isPlainObject(aggregate) || aggregate.promptTokens !== expectedAggregate.promptTokens
230
+ || aggregate.completionTokens !== expectedAggregate.completionTokens || aggregate.totalTokens !== expectedAggregate.totalTokens) {
231
+ throw fail("Decision policy aggregate usage does not equal its invocations");
232
+ }
233
+ return {
234
+ contractVersion: 1,
235
+ policy: value.policy,
236
+ policyVersion: value.policyVersion,
237
+ status: value.status,
238
+ verdict: value.verdict,
239
+ findings,
240
+ observationOnly: value.observationOnly,
241
+ reasonCodes: [...value.reasonCodes],
242
+ durationMs: value.durationMs,
243
+ ...(value.requestID !== undefined && { requestID: value.requestID }),
244
+ usage: { invocations, aggregate: expectedAggregate },
245
+ };
246
+ }
247
+ function resolveTransport(options) {
248
+ if (options.client)
249
+ return options.client;
250
+ return createQortexClient({
251
+ baseUrl: options.baseUrl,
252
+ ...(options.fetch && { fetch: options.fetch }),
253
+ ...(options.timeoutMs !== undefined && { timeoutMs: options.timeoutMs }),
254
+ });
255
+ }
256
+ /**
257
+ * Create a typed decision-policy client for one explicitly selected Qortex module function.
258
+ * @param options - Base URL, optional transport, and the host-chosen module and function names.
259
+ * @returns A client whose `evaluate` sends one POST per call and never retries.
260
+ * @throws TypeError for an invalid module, function name or base URL.
261
+ */
262
+ export function createDecisionPolicyClient(options) {
263
+ // Resolving the endpoint once validates the base URL and the host-chosen module and function names up front.
264
+ const endpoint = qortexModuleEndpoint({ baseUrl: options.baseUrl, module: options.module, functionName: options.functionName });
265
+ const transport = resolveTransport(options);
266
+ return {
267
+ async evaluate(request, callOptions) {
268
+ const body = assertDecisionPolicyRequest(request);
269
+ if (callOptions === null || typeof callOptions !== "object" || callOptions.headers === undefined) {
270
+ throw contractError("Decision policy calls require caller-supplied headers");
271
+ }
272
+ const serialized = JSON.stringify(body);
273
+ if (new TextEncoder().encode(serialized).byteLength > MAX_REQUEST_BYTES) {
274
+ throw contractError("Decision policy request exceeds the 13 MiB limit");
275
+ }
276
+ const headers = new Headers(callOptions.headers);
277
+ headers.set("content-type", "application/json");
278
+ for (const [name, value] of Object.entries(qortexRequestHeaders({
279
+ ...(callOptions.requestId !== undefined && { requestId: callOptions.requestId }),
280
+ ...(callOptions.idempotencyKey !== undefined && { idempotencyKey: callOptions.idempotencyKey }),
281
+ }))) {
282
+ headers.set(name, value);
283
+ }
284
+ const policy = {
285
+ ...(callOptions.signal && { signal: callOptions.signal }),
286
+ ...(callOptions.timeoutMs !== undefined && { timeoutMs: callOptions.timeoutMs }),
287
+ };
288
+ // Exactly one attempt. Decision inference may already be metered or accepted, so a retry is a host decision.
289
+ const response = await transport.requestJson(endpoint, { method: "POST", headers, body: serialized }, policy);
290
+ const data = parseDecisionPolicyResult(unwrapSuccessEnvelope(response.data), {
291
+ status: response.status,
292
+ ...(response.requestId !== undefined && { requestId: response.requestId }),
293
+ });
294
+ if (data.policy !== body.policy) {
295
+ throw protocolError("Decision policy result names a different policy than requested", response.status, response.requestId);
296
+ }
297
+ if (body.expectedPolicyVersion !== undefined && data.policyVersion !== body.expectedPolicyVersion) {
298
+ throw protocolError("Decision policy result does not match the expected policy version", response.status, response.requestId);
299
+ }
300
+ return {
301
+ data,
302
+ status: response.status,
303
+ ...(response.requestId !== undefined && { requestId: response.requestId }),
304
+ };
305
+ },
306
+ };
307
+ }
308
+ //# sourceMappingURL=decision-policy.js.map