@jarenjs/contract 0.75.0 → 0.83.2

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
@@ -1,5 +1,7 @@
1
1
  # @jarenjs/contract
2
2
 
3
+ Providers and permanent commands compose with injected transport, authority and receipt repositories. The [adoption evidence ledger](../../docs/ADOPTION-EVIDENCE.md) separates synthetic qualification from actual provider guarantees; unknown single-send outcomes require explicit reconciliation.
4
+
3
5
  Operation contracts for the Jaren suite. A **`$contract` document** — the
4
6
  sibling of `$model`, `$fsm` and `jaren-app` — declares the operations two
5
7
  Jaren ends may exchange: JSON in, JSON out, each with a *kind* (`read` or
@@ -734,6 +736,8 @@ Every subpath a consumer can import, derived from the manifest by
734
736
  | `@jarenjs/contract/schemas/jaren-contract.draft-07.schema.json` | schema | — |
735
737
  | `@jarenjs/contract/schemas/jaren-contract.schema.json` | schema | — |
736
738
  | `@jarenjs/contract/package.json` | metadata | — |
739
+ | `@jarenjs/contract/provider` | JavaScript | declared |
740
+ | `@jarenjs/contract/command` | JavaScript | declared |
737
741
  <!--/fact-->
738
742
 
739
743
  Author JSON template catalogs and MessageSpec references with the
@@ -771,3 +775,8 @@ and the producer's unwritten chunk separately from bytes already accepted
771
775
  by TCP. The in-process Fetch leg can measure produced-minus-consumed
772
776
  bytes directly. Complete-byte hashes, cursor pulls and cancellation
773
777
  finalizers remain part of the same end-to-end test.
778
+
779
+ `@jarenjs/contract/provider` supplies bounded provider execution, compiled JSON
780
+ REST/GraphQL dialects and private run authority. Partial observations retain
781
+ wire text and never become complete snapshots. See
782
+ [provider descriptors and ingestion](docs/PROVIDER-FORMAT.md).
@@ -10,7 +10,7 @@
10
10
  */
11
11
  export { contractAppBinding } from './binding.js';
12
12
  export { createContractEffect } from './effect.js';
13
- export { createContractSubscription } from './subscription.js';
13
+ export { createContractSubscription, createRunPageHandler } from './subscription.js';
14
14
  export type ContractAppBinding = import('./binding.js').ContractAppBinding;
15
15
  export type ContractAppBindingOptions = import('./binding.js').ContractAppBindingOptions;
16
16
  export type TaskSlot = import('./binding.js').TaskSlot;
@@ -83,3 +83,13 @@ export type StreamProps = {
83
83
  */
84
84
  export declare function createContractSubscription(client: StreamClientLike, options?: ContractSubscriptionOptions): (props: StreamProps, dispatch: (name: string, payload?: any) => void) => (() => void);
85
85
  export { CLIENT_ERRORS };
86
+ /**
87
+ * Authorize each bounded run-page read before touching durable history. Register
88
+ * as an ordinary read handler; callers may poll or resume from their last cursor.
89
+ * @param {{ page: (id: string, options: any) => any, authorize: (input: any, context: any) => any, maxPage?: number }} options
90
+ */
91
+ export declare function createRunPageHandler(options: {
92
+ page: (id: string, options: any) => any;
93
+ authorize: (input: any, context: any) => any;
94
+ maxPage?: number;
95
+ }): (input: any, context: any) => Promise<any>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Compile once around the existing neutral operation pipeline. Identity and
3
+ * current authorization are application policy, never inferred from a receipt.
4
+ * Handlers receive the repository's transaction as ctx.host; current-state reads
5
+ * belong to a separate read operation. Transport TTL policy remains independent.
6
+ * @param {import('./compile.js').CompiledOperation} operation
7
+ * @param {{ repository: { execute: Function }, identity: (input: any, context: any) => any,
8
+ * authorize: (input: any, context: any) => any, handler: (input: any, context: any) => any,
9
+ * commitFailures?: string[], references?: (result: any, context: any) => any[],
10
+ * refuse?: (reason: string, context: any) => any }} options
11
+ */
12
+ export declare function createCommand(operation: import('./compile.js').CompiledOperation, options: {
13
+ repository: {
14
+ execute: Function;
15
+ };
16
+ identity: (input: any, context: any) => any;
17
+ authorize: (input: any, context: any) => any;
18
+ handler: (input: any, context: any) => any;
19
+ commitFailures?: string[];
20
+ references?: (result: any, context: any) => any[];
21
+ refuse?: (reason: string, context: any) => any;
22
+ }): Readonly<{
23
+ execute: (input: any, context?: any) => Promise<any>;
24
+ /** A deliberate carrier wrapper around the one settlement implementation.
25
+ * @param {any} input @param {any} context */
26
+ handler(input: any, context: any): Promise<any>;
27
+ }>;
@@ -49,6 +49,7 @@ export declare const CONTRACT_CODES: Readonly<{
49
49
  JC0018: "a subscribe operation declares a policy.task other than switch — a subscription slot is replaced, never queued";
50
50
  JC0019: "a subscribe operation is bound to a method other than GET — a stream is fetched, not sent";
51
51
  JC0020: "a subscribe operation declares a policy.idempotency other than none — a subscription registers, it does not commit";
52
+ JC0021: "a provider protocol descriptor is malformed or names an undeclared transform capability";
52
53
  JC0060: "the OpenAPI projection met a schema keyword it cannot map honestly: a boolean required (draft-04 style) or a same-document $ref that lands outside $defs (both dropped and reported under lenient), or a components member inside a schema";
53
54
  JC0061: "the public projection is not canonicalizable, so no revision exists — a string with an unpaired surrogate, say; docPath points at the offending value inside the projection";
54
55
  JC1001: "serveHttp, serveLocal or servePort: handlers is not an object, a key names no operation of the contract, a value is not a function, or an option (a channel without postMessage, say) is malformed";
@@ -62,6 +63,8 @@ export declare const CONTRACT_CODES: Readonly<{
62
63
  JC1009: "encodeSseEvent (the stream wire): an event, id or data string the SSE frame cannot carry — a bare carriage return inside data, a line terminator inside event or id";
63
64
  JC1010: "client.subscribe was asked for an operation that is not a subscribe operation (invoke carries reads and commands; subscribe carries streams)";
64
65
  JC1011: "a ledger commit or fail named a ref that settles no started record: the key expired, was reclaimed under a newer generation, or was settled already — the settlement is refused; the binding reports it to onError and the response still goes out";
66
+ JC1012: "a provider executor, descriptor host or run capability is malformed";
67
+ JC1013: "a durable command settlement capability is malformed";
65
68
  JC2001: "no operation matches the request method and path (404)";
66
69
  JC2002: "the path shape is served under other methods (405, Allow lists them)";
67
70
  JC2003: "the request body exceeds policy.limits.maxBodyBytes, by content-length or by read length (413)";
@@ -99,6 +102,7 @@ export declare const CONTRACT_CODES: Readonly<{
99
102
  JC2095: "a requested resume was refused — informational, carried as resumed:false in the fresh snapshot's event data, never an outcome";
100
103
  JC2096: "the stream's bounded queue would overflow — the consumer reads slower than the source emits, or a replay page outran it (kind network, retryable; the stream ends with an error event carrying this code and the carrier tears the connection down)";
101
104
  JC2097: "the client's reconnect budget is exhausted: every attempt after a network loss failed the same way (kind network, not retryable; details carry the attempts made and the last network code, client-side)";
105
+ JC2110: "a durable command was refused or failed validation";
102
106
  }>;
103
107
  /**
104
108
  * A defect in the contract document itself, raised while
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Compile selectors/transforms once using the existing Query/JSLT engines.
3
+ * Named callbacks receive cloned protocol data only, never the run host.
4
+ * These are trusted host functions, not a sandbox for arbitrary JavaScript.
5
+ * @param {any} document
6
+ * @param {{ callbacks?: Record<string, (value: any) => any>, compileSchema?: (schema: any) => (value: any) => boolean }} [options]
7
+ */
8
+ export declare function compileProvider(document: any, options?: {
9
+ callbacks?: Record<string, (value: any) => any>;
10
+ compileSchema?: (schema: any) => (value: any) => boolean;
11
+ }): Readonly<{
12
+ document: any;
13
+ pages: (input: any, context: any) => AsyncGenerator<{
14
+ state: any;
15
+ reason: any;
16
+ pages: number;
17
+ rows: number;
18
+ bytes: number;
19
+ attempts: number;
20
+ cursor: any;
21
+ } | {
22
+ state: any;
23
+ reason: any;
24
+ pages: number;
25
+ rows: number;
26
+ bytes: number;
27
+ attempts: number;
28
+ cursor: any;
29
+ response: any;
30
+ raw?: undefined;
31
+ text?: undefined;
32
+ ids?: undefined;
33
+ continuation?: undefined;
34
+ complete?: undefined;
35
+ errors?: undefined;
36
+ cost?: undefined;
37
+ version?: undefined;
38
+ } | {
39
+ state: any;
40
+ reason: any;
41
+ pages: number;
42
+ rows: number;
43
+ bytes: number;
44
+ attempts: number;
45
+ cursor: any;
46
+ text: any;
47
+ raw?: undefined;
48
+ ids?: undefined;
49
+ continuation?: undefined;
50
+ complete?: undefined;
51
+ errors?: undefined;
52
+ cost?: undefined;
53
+ version?: undefined;
54
+ } | {
55
+ state: string;
56
+ raw: any;
57
+ text: any;
58
+ rows: any[];
59
+ ids: any[];
60
+ cursor: any;
61
+ continuation: any;
62
+ complete: boolean;
63
+ errors: any;
64
+ cost: any;
65
+ version: any;
66
+ reason: string | null;
67
+ bytes: any;
68
+ attempts: any;
69
+ }, void, unknown>;
70
+ /** Collect a bounded pull; streaming consumers should iterate pages instead.
71
+ * @param {any} input @param {any} context @returns {Promise<any>} */
72
+ pull(input: any, context: any): Promise<any>;
73
+ }>;
@@ -0,0 +1,104 @@
1
+ import { createAttemptBudget } from '@jarenjs/core/retry';
2
+ import { ContractHostError } from '../errors.js';
3
+ /** @param {string} reason @returns {ContractHostError} */
4
+ export declare const providerHostError: (reason: string) => ContractHostError;
5
+ export type ProviderRequest = {
6
+ url: string;
7
+ method?: string;
8
+ /**
9
+ * - public headers only; credentials belong to host transport
10
+ */
11
+ headers?: Record<string, string>;
12
+ body?: string;
13
+ safety: 'safe-read' | 'provider-idempotent' | 'single-send';
14
+ /**
15
+ * - required evidence for provider-idempotent
16
+ */
17
+ idempotencyKey?: string;
18
+ /**
19
+ * - opaque scheduler scope, never credentials
20
+ */
21
+ account?: string;
22
+ };
23
+ export type ProviderExecutorOptions = {
24
+ transport?: (request: ProviderRequest, context: {
25
+ signal: AbortSignal;
26
+ attempt: number;
27
+ maxAttempts: 1;
28
+ }) => Promise<Response>;
29
+ attempts?: number;
30
+ overallMs?: number;
31
+ attemptMs?: number;
32
+ /**
33
+ * - response bytes across all attempts
34
+ */
35
+ maxBytes?: number;
36
+ maxRequestBytes?: number;
37
+ baseMs?: number;
38
+ maxMs?: number;
39
+ retryAfter?: 'http' | 'milliseconds' | 'none';
40
+ retryAfterHeader?: string;
41
+ concurrency?: number;
42
+ maxQueue?: number;
43
+ maxScopes?: number;
44
+ spacingMs?: number;
45
+ now?: () => number;
46
+ random?: () => number;
47
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
48
+ };
49
+ /**
50
+ * @typedef {Object} ProviderRequest
51
+ * @property {string} url
52
+ * @property {string} [method]
53
+ * @property {Record<string, string>} [headers] - public headers only; credentials belong to host transport
54
+ * @property {string} [body]
55
+ * @property {'safe-read' | 'provider-idempotent' | 'single-send'} safety
56
+ * @property {string} [idempotencyKey] - required evidence for provider-idempotent
57
+ * @property {string} [account] - opaque scheduler scope, never credentials
58
+ */
59
+ /**
60
+ * @typedef {Object} ProviderExecutorOptions
61
+ * @property {(request: ProviderRequest, context: { signal: AbortSignal, attempt: number, maxAttempts: 1 }) => Promise<Response>} [transport]
62
+ * @property {number} [attempts]
63
+ * @property {number} [overallMs]
64
+ * @property {number} [attemptMs]
65
+ * @property {number} [maxBytes] - response bytes across all attempts
66
+ * @property {number} [maxRequestBytes]
67
+ * @property {number} [baseMs]
68
+ * @property {number} [maxMs]
69
+ * @property {'http' | 'milliseconds' | 'none'} [retryAfter]
70
+ * @property {string} [retryAfterHeader]
71
+ * @property {number} [concurrency]
72
+ * @property {number} [maxQueue]
73
+ * @property {number} [maxScopes]
74
+ * @property {number} [spacingMs]
75
+ * @property {() => number} [now]
76
+ * @property {() => number} [random]
77
+ * @property {(ms: number, signal?: AbortSignal) => Promise<void>} [sleep]
78
+ */
79
+ /**
80
+ * Injected transports must issue exactly one request and honor the supplied
81
+ * signal; SDK retry loops must be disabled. Shutdown awaits transport and body
82
+ * settlement even when a transport ignores its signal. JSON outcomes never
83
+ * contain exceptions, request headers, controllers or live response handles.
84
+ * @param {ProviderExecutorOptions} [options]
85
+ */
86
+ export declare function createProviderExecutor(options?: ProviderExecutorOptions): Readonly<{
87
+ /** @param {ProviderRequest} request
88
+ * @param {{ signal?: AbortSignal, deadline?: number, maxBytes?: number, budget?: ReturnType<typeof createAttemptBudget>, beforeDispatch?: (request: ProviderRequest) => boolean | Promise<boolean> }} [context]
89
+ * @returns {Promise<any>} */
90
+ execute(request: ProviderRequest, context?: {
91
+ signal?: AbortSignal;
92
+ deadline?: number;
93
+ maxBytes?: number;
94
+ budget?: ReturnType<typeof createAttemptBudget>;
95
+ beforeDispatch?: (request: ProviderRequest) => boolean | Promise<boolean>;
96
+ }): Promise<any>;
97
+ /** Stop new work and drain requests, body readers and retry waits. */
98
+ close(): Promise<void>;
99
+ stats: () => {
100
+ active: number;
101
+ queued: number;
102
+ closed: boolean;
103
+ };
104
+ }>;
@@ -0,0 +1,4 @@
1
+ /** Bounded provider protocols with host-injected transport and authority. */
2
+ export { createProviderExecutor } from './execute.js';
3
+ export { compileProvider } from './compile.js';
4
+ export { withProviderRun } from './run.js';
@@ -0,0 +1,33 @@
1
+ export type ProviderAuthority = {
2
+ runId: string;
3
+ actor: string;
4
+ environment: string;
5
+ destination: string;
6
+ revision: string;
7
+ lease: string;
8
+ };
9
+ /**
10
+ * @typedef {{ runId: string, actor: string, environment: string,
11
+ * destination: string, revision: string, lease: string }} ProviderAuthority
12
+ */
13
+ /**
14
+ * Run a callback with private, isolated transport resources. current() must
15
+ * re-read live authority and return the matching evidence, or null to revoke.
16
+ * The host owns membership, destination resolution, OAuth and account locks.
17
+ * Only opaque evidence and JSON callback results can leave this lifetime.
18
+ * @param {ProviderAuthority} authority
19
+ * @param {{ identify: Function, acquire: Function,
20
+ * current: (evidence: ProviderAuthority, host: any, operation: any) => ProviderAuthority | null | Promise<ProviderAuthority | null>,
21
+ * transport: (request: import('./execute.js').ProviderRequest, context: any) => Promise<Response> }} host
22
+ * @param {(run: any) => any} work
23
+ * @param {import('./execute.js').ProviderExecutorOptions & { signal?: AbortSignal }} [options]
24
+ * @returns {Promise<any>}
25
+ */
26
+ export declare function withProviderRun(authority: ProviderAuthority, host: {
27
+ identify: Function;
28
+ acquire: Function;
29
+ current: (evidence: ProviderAuthority, host: any, operation: any) => ProviderAuthority | null | Promise<ProviderAuthority | null>;
30
+ transport: (request: import('./execute.js').ProviderRequest, context: any) => Promise<Response>;
31
+ }, work: (run: any) => any, options?: import('./execute.js').ProviderExecutorOptions & {
32
+ signal?: AbortSignal;
33
+ }): Promise<any>;
@@ -432,6 +432,7 @@ carries the same codes and a test holds them equal.
432
432
  | JC0018 | a subscribe operation declares a `policy.task` other than `switch` — a subscription slot is replaced, never queued (§17) |
433
433
  | JC0019 | a subscribe operation is bound to a method other than `GET` — a stream is fetched, not sent (§17) |
434
434
  | JC0020 | a subscribe operation declares a `policy.idempotency` other than `none` — a subscription registers, it does not commit (§17) |
435
+ | JC0021 | a provider descriptor is malformed or names an undeclared transform capability |
435
436
 
436
437
  `JC0021–JC0049` are reserved for further document-level rules and are
437
438
  appended to this table when they land; `JC0050–JC0069` are the binding-
@@ -792,6 +793,9 @@ wire response:
792
793
  | `JC1009` | the stream wire's SSE encoder was handed text the frame cannot carry: a bare carriage return inside `data`, a line terminator inside `event` or `id` (§18) |
793
794
  | `JC1010` | `client.subscribe` was asked for an operation that is not a subscribe operation (§19) |
794
795
  | `JC1011` | a ledger `commit`/`fail` named a ref that settles no started record — expired, reclaimed under a newer generation, or settled already (§8); refused by the ledger, reported to `onError` by the binding |
796
+ | `JC1013` | a durable command settlement capability is malformed |
797
+ | `JC2110` | a durable command was refused or failed validation |
798
+ | `JC1012` | a provider executor, descriptor host or run capability is malformed (PROVIDER-FORMAT.md) |
795
799
 
796
800
  ### §7.4 Headers
797
801
 
@@ -2597,3 +2601,40 @@ a body that ends without an `end` event is then `onEnd({ reason:
2597
2601
  as the HTTP client does and then does nothing with it: a channel has no
2598
2602
  network loss to reconnect from (a closed channel is `JC2074`, final),
2599
2603
  and one options object serves both clients.
2604
+
2605
+ ## Durable business commands
2606
+
2607
+ `createCommand(operation, options)` from `@jarenjs/contract/command` composes
2608
+ business settlement with the existing neutral input/output/error pipeline.
2609
+ It accepts a JSON command with `policy.idempotency: "none"`, an injected
2610
+ `repository.execute(identity, work, context)`, `identity(input, context)`,
2611
+ `authorize(input, context)` and `handler(input, context)`. The identity declares
2612
+ tenant, environment, aggregate, operation, command key, hash version and payload
2613
+ hash. The host is responsible for a collision-resistant hash over every field
2614
+ that affects the command; canonical JSON itself is a lossless alternative.
2615
+
2616
+ The handler receives the repository transaction as `ctx.host`. Expected entity
2617
+ revisions, domain writes, receipt writes and `ctx.host.jobs.enqueue` belong to
2618
+ that transaction. The neutral pipeline validates the handler's result before
2619
+ settlement. Any invalid output/error, failed receipt write or failed commit
2620
+ rolls back the transaction. Only failure codes explicitly listed in
2621
+ `commitFailures` may commit observations; ordinary declared failures roll back.
2622
+ The optional `references(outcome, context)` preserves application audit IDs.
2623
+
2624
+ Use the same `command.handler` in HTTP/local/port handler tables and
2625
+ `command.execute(input, context)` in jobs. Direct execution reports `committed`,
2626
+ `replay`, `uncommitted` or `refused`; committed/replayed outcomes live in
2627
+ `receipt.outcome`. `historic: true` means the stored effect, never current entity
2628
+ state. A separate read operation observes current state. Every invocation checks
2629
+ current authorization before accessing a receipt. Refused execution exposes no
2630
+ historic result. Bindings render a refusal as a generic host fault unless the
2631
+ host supplies `refuse(reason, context)` returning a declared operation failure.
2632
+
2633
+ Transport response caching is deliberately disallowed on a durable command:
2634
+ an HTTP ledger replay could bypass the command's current authorization hook.
2635
+ Existing HTTP TTL policies and local `capabilities.idempotency: false` retain
2636
+ their meanings. Business replay is supplied by the mapped receipt repository,
2637
+ not by the transport capability. Cancellation cannot undo an already committed
2638
+ receipt or establish that a remote effect did not happen.
2639
+
2640
+ See [durable composition](DURABLE.md) for the crash matrix and public recipe.
@@ -0,0 +1,66 @@
1
+ # Durable business operations
2
+
3
+ Commands, mapped receipts, existing jobs and workflow checkpoints compose through
4
+ injected capabilities. Application models own business history, field mapping,
5
+ authorization, revision policy and arithmetic. No second authoritative ledger or
6
+ scheduler is created. These are local atomicity and recovery guarantees; external
7
+ exactly-once delivery is not claimed.
8
+
9
+ | Boundary | Durable observation | Automatic send/replay |
10
+ |---|---|---|
11
+ | Same command identity/hash on HTTP, local or job | Original validated outcome | Business effect replays without another mutation |
12
+ | Different payload/hash version | Identity mismatch | Refused before mutation |
13
+ | Current actor unauthorized | No historic disclosure | Refused before receipt read |
14
+ | Output/error/receipt/commit failure | Entire local transaction rolls back | No business receipt exists |
15
+ | Declared committed failure | Validated observation and stable references | Historic failure replays |
16
+ | Lease/HTTP TTL expires | Business receipt remains | Receipt wins before work admission |
17
+ | Before external sending intent | Prepared leg | A valid job fence may admit its first send |
18
+ | Sending intent, lost response or lost settlement | Unresolved leg | No automatic resend |
19
+ | One confirmed leg, one unresolved leg | Both per-leg observations retained | Confirmed leg skipped; unresolved leg awaits evidence |
20
+ | Provider idempotency guarantee and explicit retry decision | Original key and finite attempt budget | Only the approved next attempt is admitted |
21
+ | Probe absent without guarantee | Uncertainty retained | Refused as retry proof |
22
+ | Lease takeover | New job fence | Stale begin/settlement refused |
23
+ | Observer navigation | Run persists; observation detaches | Resume by durable cursor |
24
+ | Explicit cancellation | Intent, worker drain, final observation | Resources release after final persistence |
25
+ | Incompatible checkpoint | Stored history retained | Refused before reuse |
26
+ | Reset/compaction | Receipt identity and audit references retained | Cannot authorize unresolved replay |
27
+
28
+ Use installed public exports:
29
+
30
+ ```js
31
+ import { createCommand } from '@jarenjs/contract/command';
32
+ import { createDbReceipts, createDbEffectStore, createDbRunStore } from '@jarenjs/linq/db';
33
+ import { createExternalEffects, createDomainRun } from '@jarenjs/flow';
34
+ import { createProviderExecutor } from '@jarenjs/contract/provider';
35
+ import { createRunPageHandler } from '@jarenjs/contract/app';
36
+ import { createRunObservation } from '@jarenjs/app';
37
+
38
+ // Application declarations: client, compiledContract, identity, authorize, adjust.
39
+ const receipts = createDbReceipts(client, { receipts: 'history', leases: 'claims' });
40
+ const command = createCommand(compiledContract.operations['item.adjust'], {
41
+ repository: receipts, identity, authorize, handler: adjust,
42
+ });
43
+ // HTTP/local use { 'item.adjust': command.handler }; jobs call command.execute.
44
+ ```
45
+
46
+ `test/consumer/durable.js` exercises the installed composition with synthetic data.
47
+ The receipt, external-effect and run tests include cross-process races, interrupted
48
+ multi-leg execution, second-run no-op checks and current authorization. The measured
49
+ cost probe is `npm run benchmark:durable`; its results below report overhead as
50
+ well as resource bounds. Real provider guarantees, downstream cutover, native
51
+ executables, PostgreSQL behavior and operator reconciliation acceptance require
52
+ separate host/application qualification. Synthetic Node/Bun results cannot prove
53
+ those claims. The retained reference fixtures remain unchanged.
54
+
55
+ <!--fact:durable.measurements-->
56
+
57
+ Measured on v24.19.0, linux/x64, AMD Ryzen 9 5900HX with Radeon Graphics.
58
+
59
+ | Commands | Domain/outbox ms | Durable command ms | Receipt replay ms | Added cost ratio | Sends / unresolved resends | Second writes / revisions | Events | Heap / RSS MiB | Teardown ms / resources |
60
+ |---:|---:|---:|---:|---:|---|---|---:|---|---|
61
+ | 32 | 7.09 | 15.88 | 5.42 | 2.24x | 2 / 0 | 0 / 0 | 8 | 23.69 / 112.07 | 0.28 / 0 |
62
+ | 128 | 14.73 | 33.22 | 13.35 | 2.26x | 2 / 0 | 0 / 0 | 8 | 41.05 / 123.25 | 0.13 / 0 |
63
+
64
+ Synthetic local SQLite commands and interrupted multi-leg effects. Baseline executes the same domain/outbox work without a receipt; durable execution adds validation, authorization and immutable replay. Limits are fixed acceptance ceilings, not performance claims. Real providers, downstream acceptance, native executables, PostgreSQL and operator reconciliation are pending.
65
+
66
+ <!--/fact-->
@@ -0,0 +1,157 @@
1
+ # Provider descriptors, authority and complete ingestion
2
+
3
+ `@jarenjs/contract/provider` exports `createProviderExecutor`, `compileProvider`
4
+ and `withProviderRun`. The executor composes core scheduling with one injected
5
+ HTTP attempt. The compiler describes JSON REST and explicitly declared GraphQL
6
+ dialects. Credentials, destination rules and provider schemas remain host inputs.
7
+
8
+ ## Executor
9
+
10
+ ```js
11
+ import { createProviderExecutor, compileProvider } from '@jarenjs/contract/provider';
12
+
13
+ const executor = createProviderExecutor({ attempts: 3, overallMs: 30000,
14
+ attemptMs: 10000, maxBytes: 262144, concurrency: 4, maxQueue: 64 });
15
+ const provider = compileProvider({
16
+ $provider: '0.1', id: 'inventory', apiVersion: '2026-01',
17
+ endpoint: 'https://inventory.example/items', protocol: 'rest',
18
+ method: 'GET', safety: 'safe-read',
19
+ query: { environment: '$.input.environment' },
20
+ response: { rows: '$.items', id: '$.id', cursor: '$.next', version: '$.version' },
21
+ pagination: { cursorParam: 'cursor', empty: 'complete' },
22
+ limits: { pages: 3, rows: 256, bytes: 262144 },
23
+ });
24
+ try {
25
+ const result = await provider.pull({ environment: 'test' }, { executor });
26
+ // Inspect result.state and the preserved result.observations before using it.
27
+ } finally { await executor.close(); }
28
+ ```
29
+
30
+ `execute(request, context)` requires an absolute HTTP(S) URL without embedded
31
+ credentials and explicit safety: `safe-read`, `provider-idempotent`, or
32
+ `single-send`. Provider-idempotent also requires `idempotencyKey`; the injected
33
+ transport must bind that key using the provider's documented mechanism. The
34
+ default transport refuses provider-idempotent requests with
35
+ `idempotency-transport-required` because it has no dialect-specific key binding. An
36
+ idempotency key does not qualify business reconciliation or durable receipts.
37
+ Single-send transport/body uncertainty returns `unresolved` and never resends
38
+ inside the executor. Supply the same core attempt budget in `context.budget`
39
+ when an outer job or workflow can invoke the same request again.
40
+
41
+ The optional injected `transport(request, {signal,attempt,maxAttempts})` returns
42
+ a Fetch `Response`. It must make exactly one request (`maxAttempts:1`), disable
43
+ SDK retries, and honor abort. The default uses `fetch` with redirects disabled.
44
+ Custom redirect handling and idempotency binding remain transport obligations.
45
+ JSON results preserve status, exact response text, consumed byte count, attempts
46
+ and Retry-After, but never raw exceptions, credentials or response handles.
47
+ `state` is `ok`, `failed`, `refused`, `unresolved` or `cancelled`.
48
+
49
+ Defaults are three total attempts, an overall deadline of 30 seconds, ten seconds
50
+ per attempt, 262144 response bytes across attempts and 262144 request-body bytes.
51
+ `baseMs` defaults to 500 and `maxMs` to 8000. `now`, `random` and `sleep` are
52
+ injectable. Core concurrency, queue, scope and spacing options are forwarded.
53
+ The `account` request field combines with URL origin for fair rate scopes.
54
+ `context.deadline` and `context.maxBytes` can tighten the executor's limits.
55
+ Stream readers stop on byte exhaustion; late replies cannot become successes
56
+ after abort/deadline. Close drains transport, body readers and retry waits.
57
+
58
+ Retryable transport failures and HTTP 408/429/5xx consume the same total budget.
59
+ `retryAfter` selects `http` (seconds/date), `milliseconds`, or `none`;
60
+ `retryAfterHeader` names the header. A server delay which cannot fit the remaining
61
+ deadline refuses with `deadline`. It is never shortened to the backoff cap.
62
+ AI's declared clamp and the contract client's jitter remain their explicit
63
+ [compatibility policies](../../core/docs/SCHEDULING.md).
64
+
65
+ ## Versioned descriptor
66
+
67
+ `$provider:'0.1'`, `id`, `apiVersion`, `endpoint`, `protocol`, `method`, `safety`
68
+ and `response` are required. API version is public data available to request
69
+ selectors; the descriptor explicitly binds it in the endpoint, headers or query.
70
+ Unknown fields are refused at compile time (`JC0021`). Malformed executor/run
71
+ host options use `JC1012`.
72
+
73
+ | Declaration | Contract |
74
+ |---|---|
75
+ | `headers` | Static public string headers. Authentication/cookie/key headers are refused; host transport owns credentials. |
76
+ | `query`, `body` | Existing Query documents over `{input,cursor,apiVersion,partition,sourceVersion}`; query returns scalar parameters, body becomes canonical JSON. GET/HEAD bodies are refused. |
77
+ | `graphql` | POST with `{query,variables?}`; variables is a Query document. `pagination.cursorVariable` receives the continuation. |
78
+ | `response.rows`, `id` | Query selectors for an array and each source identity. String identities remain strings; numeric IDs must be safe integers. |
79
+ | `response.cursor`, `hasMore` | Scalar continuation and optional explicit boolean. GraphQL terminal cursors may remain present when hasMore is false. |
80
+ | `response.errors`, `cost`, `version` | Extracted protocol evidence. GraphQL errors default to `$.errors`; any partial errors make the pull incomplete. |
81
+ | `response.transform` | `{kind:'query',expression}`, `{kind:'jslt',expression}`, or `{callback:name}`. Source raw/text/IDs remain beside transformed rows. |
82
+ | `inputSchema`, `response.schema` | Optional schemas compiled by injected `compileSchema`; absence of that capability refuses the descriptor. |
83
+ | `pagination` | `cursorParam`, `cursorVariable`, and explicit `empty:'complete'` or `'incomplete'`. Empty pages with continuation are always incomplete. |
84
+ | `limits` | Positive finite `pages`, `rows`, `bytes`; defaults match the example. Budgets apply across one iterator, including failed request bytes. |
85
+ | `capability` | `json` is supported. `upload`, `media`, `bulk`, `binary` refuse before dispatch. |
86
+
87
+ Named selector callbacks are supplied in `compileProvider(doc,{callbacks})`.
88
+ They receive cloned protocol data only. They are trusted host functions, not a
89
+ JavaScript sandbox; the framework never supplies run resources to them.
90
+ Schema, Query and JSLT each retain their existing compiler and semantics.
91
+
92
+ `pages(input,context)` is an async iterator. Each `page` contains exact `text`,
93
+ parsed `raw`, transformed `rows`, source `ids`, input `cursor`, `continuation`,
94
+ `complete`, `version`, `errors`, `cost`, `reason`, `bytes` and `attempts`.
95
+ The consumer's next pull admits the next request. A terminal `complete`,
96
+ `incomplete` or `refused` record reports totals; `pull` collects that bounded
97
+ sequence into `observations`. Repeated cursors, duplicate IDs, empty intermediate
98
+ pages, partial errors, broken transforms and exhausted credits remain incomplete.
99
+ Accepted partial pages remain inspectable. Bytes refused at a stream limit are
100
+ discarded, and malformed response text is returned without claiming parsed rows.
101
+ Raw text retains wire distinctions that JSON numbers alone cannot represent.
102
+
103
+ ## Private run authority
104
+
105
+ `withProviderRun(evidence,host,work,options)` composes the existing
106
+ identify/acquire/release hooks. Evidence is a closed JSON record of nonempty
107
+ opaque strings: `runId`, `actor`, `environment`, `destination`, `revision`, `lease`.
108
+ The host implements `identify(meta)`, `acquire(input,identity,enter)`,
109
+ `current(evidence,privateHost,operation)` and `transport(request,context)`.
110
+ `current` re-reads live authority and returns matching evidence or null.
111
+ The framework compares every field before dispatch and publication, including
112
+ after asynchronous refresh. All allowlists and membership rules stay in current.
113
+
114
+ The callback receives a run with enumerable `evidence` only. Its private
115
+ `execute`, `check`, `publish` and `signal` members cannot enter ordinary JSON
116
+ state. Transport alone receives `context.host`; privileged clients are unique
117
+ to concurrent runs. Host acquisition must serialize account switching where
118
+ required. Shutdown stops admission and drains workers and callbacks before
119
+ acquired and identity resources release, in that order. Released run callbacks
120
+ cannot dispatch or publish again. The library suppresses raw host errors;
121
+ applications must also keep secrets out of explicitly returned business data.
122
+
123
+ Flow DAG/workflow `run(...,{resources})` passes resources as the task handler's
124
+ third argument, separate from JSON input, checkpoint identity and trace data.
125
+ Tasks with resources drain on abort before the workflow rejects. Compile the
126
+ workflow once, then provide the particular run on each invocation. Authority
127
+ is reacquired on resume; persisted references never restore live credentials.
128
+
129
+ ## Complete snapshots and qualification
130
+
131
+ [`createIngestion`](../../flow/docs/WORKFLOW-FORMAT.md#complete-provider-ingestion)
132
+ composes bounded pages with the existing workflow engine and an injected store.
133
+ [`createDbIngestionStore`](../../linq/docs/DB-CLIENT.md#complete-ingestion-store)
134
+ co-commits pages/checkpoints and publishes only complete requested partitions.
135
+ Source snapshot or monotonic revision evidence is mandatory, including a version
136
+ on every page. Identical published input is a no-op; application reconciliation
137
+ owns manual provenance. No network operation holds a page transaction.
138
+
139
+ Offline REST, partial GraphQL and archive-link transcripts and two independent
140
+ bounded consumers are executable through `npm run test:packed` on Node and Bun.
141
+ Node's abrupt-process recovery, rollback and zero-write tests run in the unit
142
+ suite. Real provider authority/read-back, native external SDK behavior and
143
+ downstream cutover remain separate qualifications. External writes await durable
144
+ receipt/reconciliation capability; streaming DAG inputs are not required here.
145
+
146
+ <!--fact:providers.measurements-->
147
+
148
+ Measured on v24.19.0, linux/x64, AMD Ryzen 9 5900HX with Radeon Graphics.
149
+
150
+ | Consumer | Rows | Requests / budget | Response bytes / budget | Retained reader ms | Native ingestion ms | Second writes / revisions | Teardown ms / remaining resources | Sampled heap / RSS MiB |
151
+ |---|---:|---|---|---:|---:|---|---|---|
152
+ | catalog | 256 | 3 / 3 | 66426 / 262144 | 0.96 | 64.45 | 0 / 0 | 0.27 / 0 | 35.69 / 116.45 |
153
+ | archive-stock | 512 | 6 / 6 | 135886 / 524288 | 1.31 | 79.46 | 0 / 0 | 0.11 / 0 | 42.55 / 173.56 |
154
+
155
+ Offline synthetic Node SQLite ingestion. The retained reader only extracts recorded transcripts; native timings include opening storage, descriptor execution, private authority checks, page/checkpoint commits, publication and zero-write replay. Their timings describe different work. No real provider latency, credentials, external write reconciliation or production cutover is qualified.
156
+
157
+ <!--/fact-->
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/contract",
3
3
  "private": false,
4
- "version": "0.75.0",
4
+ "version": "0.83.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -59,7 +59,15 @@
59
59
  "default": "./src/project/index.js"
60
60
  },
61
61
  "./schemas/*": "./schemas/*",
62
- "./package.json": "./package.json"
62
+ "./package.json": "./package.json",
63
+ "./provider": {
64
+ "types": "./dist/types/provider/index.d.ts",
65
+ "default": "./src/provider/index.js"
66
+ },
67
+ "./command": {
68
+ "types": "./dist/types/command.d.ts",
69
+ "default": "./src/command.js"
70
+ }
63
71
  },
64
72
  "files": [
65
73
  "dist/types/",
@@ -102,9 +110,9 @@
102
110
  "prepack": "npm run build:types"
103
111
  },
104
112
  "dependencies": {
105
- "@jarenjs/core": "^0.75.0",
106
- "@jarenjs/json": "^0.75.0",
107
- "@jarenjs/validate": "^0.75.0",
108
- "@jarenjs/emit": "^0.75.0"
113
+ "@jarenjs/core": "^0.83.2",
114
+ "@jarenjs/json": "^0.83.2",
115
+ "@jarenjs/validate": "^0.83.2",
116
+ "@jarenjs/emit": "^0.83.2"
109
117
  }
110
118
  }