@jarenjs/linq 0.75.0 → 0.83.3

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/src/db/runs.js ADDED
@@ -0,0 +1,164 @@
1
+ //@ts-check
2
+ /** Existing domain run identities and bounded event pages over mapped application tables. */
3
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
4
+ import { mappedRecords, recordTransaction, copyRecord, refuseRecord } from './records.js';
5
+
6
+ const zero = Object.freeze({ changes: 0, writes: 0, revisions: 0 });
7
+ /**
8
+ * Checkpoints remain the existing workflow's format. Status mapping and public
9
+ * summaries are application policy; events never contain the checkpoint input,
10
+ * provider credentials, live resources or raw exceptions.
11
+ * @param {any} client
12
+ * @param {{ runs: any, events: any, statuses?: Record<string, string>,
13
+ * summary?: (snapshot: any) => any, canReset?: (tx: any, record: any) => any,
14
+ * maxPage?: number, maxBytes?: number }} options
15
+ */
16
+ export function createDbRunStore(client, options) {
17
+ const runs = mappedRecords(client, options?.runs), events = mappedRecords(client, options?.events);
18
+ if (runs.name === events.name) throw new TypeError('runs and events must be distinct collections');
19
+ const { maxPage = 128, maxBytes = 262144 } = options;
20
+ for (const limit of [maxPage, maxBytes]) if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('run observation limits must be positive finite integers');
21
+ const statuses = { running: 'running', waiting: 'waiting', done: 'done', failed: 'failed', cancelled: 'cancelled', ...options.statuses };
22
+ if (Object.values(statuses).some((value) => typeof value !== 'string' || !value)) throw new TypeError('status mapping values must be nonempty strings');
23
+ const summary = options.summary ?? ((snapshot) => ({ status: snapshot.status, generation: snapshot.generation }));
24
+ const inside = (fn) => recordTransaction(client, fn);
25
+ const bytes = (value) => new TextEncoder().encode(canonicalizeJson(value)).byteLength;
26
+ const bounded = (page) => {
27
+ if (bytes(page) > maxBytes) throw refuseRecord('run page exceeds byte budget');
28
+ return page;
29
+ };
30
+ const eventId = (id, revision) => canonicalizeJson([id, revision]);
31
+ const requireRun = async (tx, id) => {
32
+ const record = await runs.get(tx, id);
33
+ if (!record || !Number.isSafeInteger(record.revision) || record.revision < 0) throw refuseRecord('unknown or malformed domain run');
34
+ return record;
35
+ };
36
+ const guard = async (tx, record, lease) => {
37
+ if (record.jobId !== lease?.jobId) throw refuseRecord('foreign run job');
38
+ await tx.jobs.assertLease(lease);
39
+ };
40
+ const same = (record, identity) => record.workflow === identity.workflow && record.schemaVersion === identity.schemaVersion && record.jobId === identity.jobId;
41
+ const write = async (tx, record) => {
42
+ if (!Number.isSafeInteger(record.revision + 1)) throw refuseRecord('run revision exhausted');
43
+ record.revision++;
44
+ const event = { id: eventId(record.id, record.revision), runId: record.id, revision: record.revision,
45
+ status: record.status, summary: record.summary, cancelRequested: record.cancelRequested };
46
+ if (bytes({ state: 'page', events: [event], cursor: record.revision, revision: record.revision, more: false, status: record.status, summary: record.summary }) > maxBytes) throw refuseRecord('public run summary exceeds byte budget');
47
+ await runs.put(tx, record);
48
+ await events.put(tx, event, true);
49
+ return { record, changes: 1, writes: 2, revisions: 1 };
50
+ };
51
+ return Object.freeze({
52
+ /** Attach without renaming or re-enqueueing an existing domain run.
53
+ * @param {{ id: string, jobId: string, workflow: string, schemaVersion: string }} identity @param {any} lease */
54
+ attach(identity, lease) {
55
+ const frozen = copyRecord(identity);
56
+ if (['id', 'jobId', 'workflow', 'schemaVersion'].some((name) => typeof frozen[name] !== 'string' || !frozen[name])) throw new TypeError('run identity needs id/jobId/workflow/schemaVersion');
57
+ return inside(async (tx) => {
58
+ if (frozen.jobId !== lease?.jobId) throw refuseRecord('foreign run job');
59
+ await tx.jobs.assertLease(lease);
60
+ const prior = await runs.get(tx, frozen.id);
61
+ if (prior) {
62
+ if (!same(prior, frozen)) throw refuseRecord('incompatible run workflow or schema identity');
63
+ return { record: prior, ...zero };
64
+ }
65
+ return write(tx, { ...frozen, revision: 0, status: statuses.running, summary: null, checkpoint: null, cancelRequested: false });
66
+ });
67
+ },
68
+ /** Verify provenance before exposing any old checkpoint to the engine.
69
+ * @param {string} id @param {any} identity @param {any} lease */
70
+ load(id, identity, lease) {
71
+ return inside(async (tx) => {
72
+ const record = await requireRun(tx, id);
73
+ await guard(tx, record, lease);
74
+ if (!same(record, identity)) throw refuseRecord('incompatible run workflow or schema identity');
75
+ if (record.cancelRequested) throw refuseRecord('run cancellation stops admission');
76
+ return record.checkpoint;
77
+ });
78
+ },
79
+ /** Existing workflow CAS, mapped status/event and checkpoint co-commit.
80
+ * @param {string} id @param {any} snapshot @param {number} expectedGeneration @param {any} lease */
81
+ save(id, snapshot, expectedGeneration, lease) {
82
+ const checkpoint = copyRecord(snapshot);
83
+ return inside(async (tx) => {
84
+ const record = await requireRun(tx, id);
85
+ await guard(tx, record, lease);
86
+ if (record.cancelRequested) throw refuseRecord('run cancellation stops admission');
87
+ if ((record.checkpoint?.generation ?? 0) !== expectedGeneration) return false;
88
+ if (checkpoint.runId !== id || checkpoint.generation !== expectedGeneration + 1 || !Object.hasOwn(statuses, checkpoint.status))
89
+ throw refuseRecord('checkpoint run, generation or status mismatch');
90
+ record.checkpoint = checkpoint;
91
+ record.status = statuses[checkpoint.status];
92
+ record.summary = copyRecord(summary(checkpoint));
93
+ await write(tx, record);
94
+ return true;
95
+ });
96
+ },
97
+ /** Trusted read: public authorization belongs to the contract handler.
98
+ * @param {string} id */
99
+ get: (id) => inside((tx) => requireRun(tx, id)),
100
+ /** Bounded revision cursor. Missing history explicitly requires a fresh summary.
101
+ * @param {string} id @param {{ after?: number, limit?: number }} [options] */
102
+ page(id, { after = 0, limit = maxPage } = {}) {
103
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > maxPage) throw new TypeError('run page needs a revision cursor and bounded limit');
104
+ return inside(async (tx) => {
105
+ const record = await requireRun(tx, id);
106
+ if (after > record.revision) throw refuseRecord('future run cursor');
107
+ const page = [], end = Math.min(record.revision, after + limit);
108
+ const envelope = { state: 'page', events: page, cursor: after, revision: record.revision, more: true, status: record.status, summary: record.summary };
109
+ for (let revision = after + 1; revision <= end; revision++) {
110
+ const event = await events.get(tx, eventId(id, revision));
111
+ if (!event) return bounded({ state: 'reset-required', cursor: record.revision, revision: record.revision, events: [], summary: record.summary, status: record.status });
112
+ if (event.runId !== id || event.revision !== revision) throw refuseRecord('run event identity mismatch');
113
+ if (bytes({ ...envelope, cursor: revision, events: [...page, event] }) > maxBytes) break;
114
+ page.push(event);
115
+ }
116
+ const cursor = page.at(-1)?.revision ?? after;
117
+ if (cursor === after && cursor < record.revision) throw refuseRecord('run page exceeds byte budget');
118
+ return bounded({ state: 'page', events: page, cursor, revision: record.revision, more: cursor < record.revision,
119
+ status: record.status, summary: record.summary });
120
+ });
121
+ },
122
+ /** Current authority is required in the caller's command; revision rejects stale intent.
123
+ * @param {string} id @param {number} expectedRevision @param {{ actor: string, reason: string }} evidence */
124
+ requestCancel(id, expectedRevision, evidence) {
125
+ if (!evidence?.actor || !evidence.reason) throw refuseRecord('cancellation needs actor and reason');
126
+ const proof = copyRecord(evidence);
127
+ return inside(async (tx) => {
128
+ const record = await requireRun(tx, id);
129
+ if (record.cancelRequested) return { record, ...zero };
130
+ if (record.revision !== expectedRevision) throw refuseRecord('stale cancellation revision');
131
+ if (record.status === statuses.done) throw refuseRecord('completed run cannot be cancelled');
132
+ record.cancelRequested = true; record.cancellation = proof;
133
+ return write(tx, record);
134
+ });
135
+ },
136
+ /** Called after workers drain and before resources close.
137
+ * @param {string} id @param {'cancelled' | 'failed'} state @param {any} lease */
138
+ finish(id, state, lease) {
139
+ if (!['cancelled', 'failed'].includes(state)) throw new TypeError('finish state must be cancelled or failed');
140
+ return inside(async (tx) => {
141
+ const record = await requireRun(tx, id);
142
+ await guard(tx, record, lease);
143
+ if (record.status === statuses[state]) return { record, ...zero };
144
+ record.status = statuses[state]; record.summary = { status: state };
145
+ return write(tx, record);
146
+ });
147
+ },
148
+ /** Reset touches only this run checkpoint, never receipts/effects. Host safety
149
+ * must consult authoritative history in this transaction; default is refusal.
150
+ * @param {string} id @param {number} expectedRevision @param {any} evidence @param {any} lease */
151
+ reset(id, expectedRevision, evidence, lease) {
152
+ if (!evidence?.actor || !evidence.reason || typeof options.canReset !== 'function') throw refuseRecord('reset needs explicit actor/reason and a transactional safety policy');
153
+ return inside(async (tx) => {
154
+ const record = await requireRun(tx, id);
155
+ await guard(tx, record, lease);
156
+ if (record.revision !== expectedRevision || ![statuses.cancelled, statuses.failed, statuses.waiting].includes(record.status)) throw refuseRecord('reset needs an inactive current revision');
157
+ if (await options.canReset(tx, copyRecord(record)) !== true) throw refuseRecord('receipt or unresolved effect prevents reset');
158
+ record.checkpoint = null; record.cancelRequested = false; record.status = statuses.running;
159
+ record.summary = { status: 'reset' }; record.reset = copyRecord(evidence);
160
+ return write(tx, record);
161
+ });
162
+ },
163
+ });
164
+ }
@@ -0,0 +1,86 @@
1
+ //@ts-check
2
+ /** Structural lexical ranges reuse the shared resident range implementation. */
3
+ import { createArrayRangeProvider, rangeBytes, rangeIdentity } from '@jarenjs/core/range';
4
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
5
+
6
+ /**
7
+ * Materialize only a credited, complete lexical membership. The source retains
8
+ * authoritative rows; this adapter owns one bounded range copy and its cursors.
9
+ * @param {any} source @param {string} text @param {any} [spec] @param {any} [options]
10
+ * @returns {Promise<any>}
11
+ */
12
+ export async function createLexicalRangeProvider(source, text, spec = {}, options = {}) {
13
+ const maxMatches = options.maxMatches ?? 1000, maxSourceBytes = options.maxSourceBytes ?? 1024 * 1024;
14
+ if (!Number.isSafeInteger(maxMatches) || maxMatches < 1 || !Number.isSafeInteger(maxSourceBytes) || maxSourceBytes < 2)
15
+ throw new TypeError('Invalid lexical range credits');
16
+ if (spec.after !== undefined || spec.limit !== undefined) throw new TypeError('Lexical ranges own continuation and result limits');
17
+ spec = structuredClone(spec);
18
+ const query = options.query ?? canonicalizeJson({ text, spec, source: options.source ?? null });
19
+ let disposed = false, dirty = false, epoch = 0, provider = null, refreshing = null;
20
+ const observers = new Set();
21
+ const unsubscribe = source.subscribe((event) => {
22
+ dirty = true; epoch++;
23
+ for (const observer of observers) { try { observer({ ...event, query, type: 'reset', revision: epoch, snapshot: source.sourceRevision }); }
24
+ catch { /* Subscribers cannot suppress a sibling's reset. */ } }
25
+ });
26
+ const refresh = async () => {
27
+ if (disposed) throw new Error('Lexical range disposed');
28
+ if (refreshing) return refreshing;
29
+ refreshing = (async () => {
30
+ const result = await source.search(text, { ...spec, limit: maxMatches });
31
+ if (disposed) throw new Error('Lexical range disposed');
32
+ if (result.state !== 'complete' || result.hasMore || result.hits.length !== result.total)
33
+ throw new RangeError(`Lexical range requires complete membership: ${result.reason ?? 'match-credits'}`);
34
+ const rows = [], snapshot = result.sourceRevision;
35
+ let bytes = 2;
36
+ for (const hit of result.hits) {
37
+ const row = source.row(hit.id, snapshot); bytes += rangeBytes(row) + (rows.length ? 1 : 0);
38
+ if (bytes > maxSourceBytes) throw new RangeError('Lexical range source byte credits');
39
+ rows.push(row);
40
+ }
41
+ if (provider === null) provider = createArrayRangeProvider(rows, { ...options, query, snapshot });
42
+ else if (provider.snapshot !== snapshot) provider.replace(rows, snapshot);
43
+ dirty = false; return { state: 'complete', sourceRevision: snapshot, rows: rows.length, bytes };
44
+ })().finally(() => { refreshing = null; });
45
+ return refreshing;
46
+ };
47
+ try { await refresh(); }
48
+ catch (error) { unsubscribe(); throw error; }
49
+ return {
50
+ query, get snapshot() { return provider.snapshot; }, capabilities: provider.capabilities, refresh,
51
+ indexOf: (key) => provider.indexOf(key),
52
+ subscribe(fn) {
53
+ if (disposed || typeof fn !== 'function') throw new TypeError('Invalid lexical range subscriber');
54
+ if (observers.size >= 8) throw new RangeError('Lexical range subscription credits');
55
+ observers.add(fn); return () => observers.delete(fn);
56
+ },
57
+ async request(request, signal) {
58
+ const refusal = (state, reason) => ({ ...rangeIdentity(request), state, reason, used: { pages: 0, rows: 0, bytes: 0, work: 0 } });
59
+ if (disposed) return refusal('error', 'disposed');
60
+ if (signal?.aborted) return refusal('error', 'cancelled');
61
+ const current = epoch;
62
+ const ready = await source.refresh();
63
+ if (disposed) return refusal('error', 'disposed');
64
+ if (signal?.aborted) return refusal('error', 'cancelled');
65
+ if (ready.state !== 'complete') return refusal(ready.state === 'budget-exhausted' ? ready.state : 'error', ready.reason);
66
+ if (dirty || current !== epoch || source.sourceRevision !== provider.snapshot) return refusal('invalidated', 'source-changed');
67
+ return provider.request(request, signal);
68
+ },
69
+ async *export(request, signal) {
70
+ const ready = await source.refresh();
71
+ if (ready.state !== 'complete' || dirty || disposed || source.sourceRevision !== provider.snapshot) throw new Error('Incomplete lexical snapshot export');
72
+ for await (const page of provider.export(request, signal)) {
73
+ const current = await source.refresh();
74
+ if (current.state !== 'complete' || dirty || disposed || source.sourceRevision !== provider.snapshot)
75
+ throw new Error('Incomplete lexical snapshot export');
76
+ yield page;
77
+ }
78
+ },
79
+ stats: () => ({ ...provider.stats(), refreshing: refreshing ? 1 : 0, subscriptions: observers.size, dirty }),
80
+ async dispose() {
81
+ disposed = true; unsubscribe(); observers.clear();
82
+ await Promise.allSettled([refreshing]); await provider.dispose();
83
+ if (options.disposeSource) await source.dispose();
84
+ },
85
+ };
86
+ }
package/src/errors.js CHANGED
@@ -15,7 +15,8 @@ import { CodedError } from '@jarenjs/core/errors';
15
15
  * this package can raise, proven in sync with QUERY-PEN.md §9's
16
16
  * normative table by a test.
17
17
  */
18
- export const LINQ_CODES = Object.freeze({
18
+ // Freezing an unexported literal has no observable effect when the table is unused.
19
+ export const LINQ_CODES = /* @__PURE__ */ Object.freeze({
19
20
  JL0001: 'from() received neither an iterable nor a provider',
20
21
  JL0002: 'an expression proxy escaped its capture callback',
21
22
  JL0003: 'ofType/cast need an injected compileTypeTest',
@@ -38,6 +39,7 @@ export const LINQ_CODES = Object.freeze({
38
39
  JL2006: 'a provider answered an element terminal with something other than one array',
39
40
  JL2007: 'a ledger settlement named a ref that settles no started record',
40
41
  JL2008: 'a federated fetch reached its row or byte budget',
42
+ JL2009: 'a durable mapped record violated identity, retention or fencing',
41
43
  });
42
44
 
43
45
  /**
package/src/expression.js CHANGED
@@ -307,8 +307,7 @@ function shiftArgs(record, amount, unit) {
307
307
  function literalSpec(spec, method) {
308
308
  if (!isPlainJson(spec) || spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
309
309
  throw new LinqBuildError('JL0005',
310
- `${method}() takes a plain literal spec object; it is read once when the query`
311
- + ' compiles, so it cannot be an expression or carry a captured value');
310
+ `${method}() takes a plain literal spec object, not an expression or captured value`);
312
311
  }
313
312
  return spec;
314
313
  }
@@ -336,6 +335,10 @@ const METHODS = {
336
335
  // §8.7 strings
337
336
  startsWith: binary('$starts-with'), endsWith: binary('$ends-with'),
338
337
  contains: binary('$contains'), matches: binary('$match'),
338
+ lexical(record, provider, spec = {}) {
339
+ if (typeof provider !== 'string' || !provider) throw new LinqBuildError('JL0005', 'Lexical name');
340
+ return makeExpr({ $lexical: [provider, record.doc, literalSpec(spec, 'lexical')] }, record.epoch, false);
341
+ },
339
342
  upper: unary('$upper'), lower: unary('$lower'),
340
343
  length: unary('$string-length'),
341
344
  concat: binary('$concat'),
@@ -0,0 +1,15 @@
1
+ //@ts-check
2
+ /** Formula authoring emits the same JSON document consumed by json/formula. */
3
+ import { formulaDocument } from '@jarenjs/json/formula';
4
+ import { requireJson } from '../json-boundary.js';
5
+
6
+ /**
7
+ * Author a saved Query profile; expression is an existing Query document.
8
+ * @param {string} id
9
+ * @param {any} expression
10
+ * @param {object} [options] - Revision, bindings, schema/helper references and result mode.
11
+ * @returns {any} Frozen JSON, with no executable closures in the document.
12
+ */
13
+ export function defineFormula(id, expression, options = {}) {
14
+ return formulaDocument(requireJson({ $formula: '1', revision: '1', ...options, id, expression }, 'defineFormula'));
15
+ }
@@ -116,6 +116,8 @@ export function defineModel(spec) {
116
116
  }
117
117
  }
118
118
  const entity = { schema: builder.schema };
119
+ if (builder.state.invariants !== undefined) entity.invariants = builder.state.invariants;
120
+ if (builder.state.physical !== undefined) entity.physical = builder.state.physical;
119
121
  if (builder.state.renamedFrom !== undefined) entity['x-rename'] = builder.state.renamedFrom;
120
122
  setObjectMember(emitted, name, entity);
121
123
  }
@@ -218,6 +218,21 @@ export function withEntity(Base) {
218
218
  */
219
219
  renamedFrom(name) { return this.with({ renamedFrom: requireName(name, 'renamedFrom()') }); }
220
220
 
221
+ /** Explicit existing columns, lifted onto the entity declaration.
222
+ * @param {any} layout */
223
+ physical(layout) {
224
+ if (this.state.kind !== 'object') throw new LinqBuildError('JL0102', 'physical() belongs on an entity object');
225
+ return this.with({ physical: requireJson(layout, 'physical()') });
226
+ }
227
+
228
+
229
+ /** Declare persistence predicates, with explicit writer enforcement.
230
+ * @param {any[]} rules */
231
+ invariants(rules) {
232
+ if (this.state.kind !== 'object') throw new LinqBuildError('JL0102', 'invariants() belongs on an entity object');
233
+ return this.with({ invariants: requireJson(rules, 'invariants()') });
234
+ }
235
+
221
236
  /** As in the schema pen, but `x-entity` is owned here. @param {Record<string, any>} annotations */
222
237
  meta(annotations) {
223
238
  if (annotations !== null && typeof annotations === 'object' && KEYWORD in annotations) {
@@ -5,24 +5,25 @@ import { LinqBuildError } from '../errors.js';
5
5
 
6
6
  /** The public project file vocabulary, held equal to Studio's schema by tests. */
7
7
  export const FILE_KINDS = Object.freeze(['app', 'jslt', 'query', 'state', 'data', 'schema', 'fsm', 'dag', 'model', 'contract']);
8
+ const FILE_OPTIONS = ['imports', 'input', 'model', 'collection'];
8
9
  const LAYOUT_KEYS = ['mode', 'ratio', 'autorun'];
9
10
 
10
11
  /** One file, preserving the caller's text byte-for-byte. */
11
- export function file(name, kind, text) {
12
+ export function file(name, kind, text, options = {}) {
12
13
  if (typeof name !== 'string' || name.length === 0 || !FILE_KINDS.includes(kind) || typeof text !== 'string')
13
14
  throw new LinqBuildError('JL0101', 'file() requires a nonempty name, a declared kind and text');
14
- return snapshot({ name, kind, text });
15
+ return snapshot({ name, kind, text, ...optionsOf(options, FILE_OPTIONS, 'file()') });
15
16
  }
16
17
 
17
18
  /** One file holding a public JSON value; pass another pen's `.schema` explicitly. */
18
- export function jsonFile(name, kind, document) { return file(name, kind, JSON.stringify(snapshot(document))); }
19
+ export function jsonFile(name, kind, document, options = {}) { return file(name, kind, JSON.stringify(snapshot(document)), options); }
19
20
 
20
21
  /** Validate the authoring shape, preserving duplicate names for Studio to judge. */
21
22
  function filesOf(files) {
22
23
  if (!Array.isArray(files)) throw new LinqBuildError('JL0101', 'files() takes an array of project files');
23
24
  return files.map((value) => {
24
- const f = optionsOf(value, ['name', 'kind', 'text'], 'file');
25
- return file(f.name, f.kind, f.text);
25
+ const { name, kind, text, ...options } = optionsOf(value, ['name', 'kind', 'text', ...FILE_OPTIONS], 'file');
26
+ return file(name, kind, text, options);
26
27
  });
27
28
  }
28
29
 
package/types/db.d.ts CHANGED
@@ -145,6 +145,7 @@ export type Continuation<S, M extends EntityMeta> = LoadContinuation & {
145
145
  /** A page's options over this graph (the store's `PageOptions`, the
146
146
  * continuation typed by the declared ordering). */
147
147
  export interface GraphPageOptions<S, M extends EntityMeta> extends EntityCursorOptions {
148
+ lookahead?: boolean;
148
149
  limit?: number;
149
150
  after?: Continuation<S, M>;
150
151
  maxBytes?: number;
@@ -205,6 +206,7 @@ export interface Graph<E extends MetaMap<E>, M extends EntityMeta, S> {
205
206
  * exactly the many-to-many members. */
206
207
  export type EntityHandle<E extends MetaMap<E>, M extends EntityMeta> =
207
208
  TypedEntitySet<E, M> & ChainStart<M['doc']> & {
209
+ range(spec: import('@jarenjs/db').LoadSpec, options: DbRangeOptions): Promise<DbRangeProvider<M['doc']>>;
208
210
  /** Open a graph: include one relation member with an optional spec. */
209
211
  include<K extends keyof M['relations'] & string, const I extends IncludeSpec<E, TargetMeta<E, M, K>> = true>(
210
212
  pick: (u: RelationPicker<M>) => Picked<K>, spec?: I | NoInfer<IncludeSpec<E, TargetMeta<E, M, K>>>,
@@ -217,6 +219,40 @@ export type EntityHandle<E extends MetaMap<E>, M extends EntityMeta> =
217
219
  live<T = M['doc']>(source?: AsyncSequence<T, any> | object, options?: LiveOptions): Promise<TypedLiveQuery<T>>;
218
220
  };
219
221
 
222
+ export interface DbRangeOptions {
223
+ keys: readonly string[];
224
+ resident?: boolean; seekIndex?: boolean; exactTotal?: boolean;
225
+ source?: string; query?: string; schemaVersion?: string;
226
+ profile?: 'safe' | import('@jarenjs/db').ProfileSpec;
227
+ maxRows?: number; maxBytes?: number; maxPages?: number; maxInFlight?: number; maxSubscriptions?: number;
228
+ runtime?: Partial<Runtime>;
229
+ }
230
+ export interface DbRangeRequest {
231
+ generation: number; requestId: string; query: string; snapshot: string;
232
+ range?: { start: number; end: number }; continuation?: string;
233
+ credits: { pages: number; rows: number; bytes: number; work: number };
234
+ }
235
+ export interface DbRangeResponse<T = unknown> {
236
+ generation: number; requestId: string; query: string; snapshot: string;
237
+ state: 'ready' | 'loading' | 'error' | 'invalidated' | 'budget-exhausted'; reason?: string;
238
+ rows?: readonly T[]; keys?: readonly string[]; continuation?: string | null;
239
+ total?: { kind: 'known'; value: number } | { kind: 'unknown' };
240
+ used: { pages: number; rows: number; bytes: number; work: number };
241
+ }
242
+ export interface DbRangeProvider<T = unknown> {
243
+ readonly query: string; readonly snapshot: string;
244
+ readonly capabilities: Readonly<{ seekIndex: boolean; seekKey: false; continuation: true;
245
+ live: true; exactTotal: boolean; completeExport: false }>;
246
+ request(request: DbRangeRequest, signal?: AbortSignal): Promise<DbRangeResponse<T>>;
247
+ subscribe(observer: (event: { type: 'reset'; reason: string; query: string; snapshot: string; revision: number; capture: string }) => void): () => boolean;
248
+ stats(): { sourceReads: number; sourceRows: number; sourceBytes: number; pending: number;
249
+ pages: number; rows: number; bytes: number; subscriptions: number; disposed: boolean };
250
+ dispose(): Promise<void>;
251
+ }
252
+ /** Source capture is required; keyset mode is sequential unless resident is explicit. */
253
+ export declare function createDbRangeProvider<T = unknown>(store: import('@jarenjs/db').Store | TypedStore<any>,
254
+ entity: string, spec: import('@jarenjs/db').LoadSpec, options: DbRangeOptions): Promise<DbRangeProvider<T>>;
255
+
220
256
  /** Present exactly when the model declares entities, as on the store. */
221
257
  export interface EntityClientMembers {
222
258
  /** The store's unit of work, flushed. */
@@ -331,6 +367,9 @@ export type TransactionClientOf<E extends MetaMap<E>, C = Record<string, unknown
331
367
  /** The scope-bound store — the escape hatch, still inside. It carries
332
368
  * no `close`: a transaction never owns the connection's lifetime. */
333
369
  readonly store: TransactionStore;
370
+ readonly sql: TransactionStore['sql'];
371
+ readonly jobs: TransactionStore['jobs'];
372
+ readonly sync: TransactionStore['sync'];
334
373
  readonly capabilities: StoreCapabilities;
335
374
  readonly entities: { readonly [K in keyof E & string]: EntityHandle<E, E[K]> };
336
375
  readonly collections: { readonly [K in keyof C & string]: CollectionHandle<C[K]> };
@@ -364,3 +403,65 @@ export function defineReplication(header: Omit<import('@jarenjs/db').Replication
364
403
  toDocument(): import('@jarenjs/db').ReplicationEnvelope;
365
404
  toJSON(): import('@jarenjs/db').ReplicationEnvelope;
366
405
  };
406
+
407
+ /** A complete lexical membership composed with the common structural range provider. */
408
+ export declare function createLexicalRangeProvider(source: import('@jarenjs/db/search').DbSearch,
409
+ text: string, request?: Record<string, unknown>, options?: {maxMatches?:number; maxSourceBytes?:number;
410
+ maxRows?:number; maxBytes?:number; maxPages?:number; maxInFlight?:number; query?:string;
411
+ source?:string; exactTotal?:boolean; seekIndex?:boolean; disposeSource?:boolean}): Promise<any>;
412
+
413
+ /** Atomic staging and publication over application-declared collections. */
414
+ export function createDbIngestionStore(client: Client<any> | TransactionClientOf<any>, options: {
415
+ staging: string; checkpoints: string; publications: string; facts?: string;
416
+ reconcile?: (existing: any, incoming: any, evidence: any) => any;
417
+ }): {
418
+ begin(plan: any): Promise<any>;
419
+ stage(plan: any, partition: string, page: any): Promise<any>;
420
+ invalidate(plan: any, reason: string): Promise<void>;
421
+ publish(plan: any, evidence: any, context?: { signal?: AbortSignal }): Promise<any>;
422
+ current(source: string): Promise<any>;
423
+ inspect(plan: any): Promise<any>;
424
+ };
425
+
426
+ /** Lossless canonical-record mapping onto an application-declared collection. */
427
+ export type DurableRecordMapping = string | (({ collection: string; entity?: never } | { entity: string; collection?: never }) & { key?: (id: string) => any; read: (stored: any) => any; write: (record: any) => any });
428
+ export interface CommandIdentity {
429
+ tenant: string; environment: string; aggregate: string; op: string; key: string; hashVersion: string; hash: string;
430
+ }
431
+ /** Permanent business outcomes and independently expiring execution authority. */
432
+ export function createDbReceipts(client: Client<any> | TransactionClientOf<any>, options: {
433
+ receipts: DurableRecordMapping; leases?: DurableRecordMapping; runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
434
+ }): {
435
+ lookup(identity: CommandIdentity): Promise<any>;
436
+ execute(identity: CommandIdentity, work: (tx: TransactionClientOf<any>) => Promise<{ outcome: any; references?: any[] }>, context?: { lease?: any }): Promise<any>;
437
+ claim(identity: CommandIdentity, options: { leaseMs: number }): Promise<any>;
438
+ release(identity: CommandIdentity, lease: any): Promise<any>;
439
+ sweep(identities: CommandIdentity[]): Promise<{ changes: number; writes: number; revisions: number }>;
440
+ migrate(records: any[]): Promise<{ changes: number; writes: number; revisions: number }>;
441
+ compact(identity: CommandIdentity, policy: { retainReplay: true; retainReferences: true; actor: string; reason: string }): Promise<any>;
442
+ };
443
+ /** Durable preparation, job-fenced sending and explicit reconciliation. */
444
+ export function createDbEffectStore(client: Client<any> | TransactionClientOf<any>, options: {
445
+ operations: DurableRecordMapping; maxLegs?: number; maxBytes?: number;
446
+ }): {
447
+ prepare(plan: any, prepare?: (tx: TransactionClientOf<any>) => any): Promise<any>;
448
+ get(id: string): Promise<any>;
449
+ begin(id: string, legId: string, revision: number, lease: import('@jarenjs/db').JobLease): Promise<any>;
450
+ settle(id: string, legId: string, revision: number, lease: import('@jarenjs/db').JobLease, outcome: any): Promise<any>;
451
+ recover(id: string, revision: number, lease: import('@jarenjs/db').JobLease): Promise<any>;
452
+ reconcile(id: string, legId: string, revision: number, lease: import('@jarenjs/db').JobLease, decision: any): Promise<any>;
453
+ };
454
+ /** Application run/checkpoint records and bounded public revision pages. */
455
+ export function createDbRunStore(client: Client<any> | TransactionClientOf<any>, options: {
456
+ runs: DurableRecordMapping; events: DurableRecordMapping; statuses?: Record<string, string>;
457
+ summary?: (snapshot: any) => any; canReset?: (tx: TransactionClientOf<any>, record: any) => any; maxPage?: number; maxBytes?: number;
458
+ }): {
459
+ attach(identity: { id: string; jobId: string; workflow: string; schemaVersion: string }, lease: import('@jarenjs/db').JobLease): Promise<any>;
460
+ load(id: string, identity: any, lease: import('@jarenjs/db').JobLease): Promise<any>;
461
+ save(id: string, snapshot: any, expectedGeneration: number, lease: import('@jarenjs/db').JobLease): Promise<boolean>;
462
+ get(id: string): Promise<any>;
463
+ page(id: string, options?: { after?: number; limit?: number }): Promise<any>;
464
+ requestCancel(id: string, revision: number, evidence: { actor: string; reason: string }): Promise<any>;
465
+ finish(id: string, state: 'cancelled' | 'failed', lease: import('@jarenjs/db').JobLease): Promise<any>;
466
+ reset(id: string, revision: number, evidence: any, lease: import('@jarenjs/db').JobLease): Promise<any>;
467
+ };
@@ -0,0 +1,9 @@
1
+ /** Author a saved JSON Query formula without executing it. */
2
+ export declare function defineFormula(id: string, expression: unknown, options?: {
3
+ revision?: string;
4
+ bindings?: Record<string, unknown>;
5
+ inputSchema?: { id: string; version: string };
6
+ resultSchema?: { id: string; version: string };
7
+ helpers?: { name: string; version: string }[];
8
+ resultMode?: 'value' | 'outcome';
9
+ }): any;
package/types/index.d.ts CHANGED
@@ -77,6 +77,8 @@ export interface StringExpr extends ExprBase<string>, EqExpr<string>, SpatialMet
77
77
  contains(value: string | StringExpr): BoolExpr;
78
78
  /** I-Regexp (RFC 9485) full match. */
79
79
  matches(pattern: string): BoolExpr;
80
+ /** Explicit lexical provider request; preserves ranked-result completeness. */
81
+ lexical(provider: string, request?: Record<string, unknown>): UnknownExpr;
80
82
  upper(): StringExpr;
81
83
  lower(): StringExpr;
82
84
  length(): NumberExpr;
@@ -363,6 +365,8 @@ export interface UnknownExpr
363
365
  endsWith(value: unknown): BoolExpr;
364
366
  contains(value: unknown): BoolExpr;
365
367
  matches(pattern: string): BoolExpr;
368
+ /** Explicit lexical provider request; preserves ranked-result completeness. */
369
+ lexical(provider: string, request?: Record<string, unknown>): UnknownExpr;
366
370
  upper(): UnknownExpr;
367
371
  lower(): UnknownExpr;
368
372
  length(): NumberExpr;
package/types/model.d.ts CHANGED
@@ -61,6 +61,20 @@ export interface EntityBlock {
61
61
  };
62
62
  }
63
63
 
64
+ /** Explicit column-only layout for an existing SQLite object. */
65
+ export interface PhysicalLayout {
66
+ readonly table: string;
67
+ readonly kind?: 'table' | 'view';
68
+ readonly keys?: readonly string[];
69
+ readonly columns: Readonly<Record<string, {
70
+ readonly name: string;
71
+ readonly codec: 'text' | 'integer' | 'number' | 'boolean' | 'json' | 'date' | 'datetime' | 'epoch-ms' | 'bigint' | 'decimal' | 'blob-hex';
72
+ readonly null: 'null' | 'absent' | 'reject';
73
+ readonly default?: 'database';
74
+ readonly generated?: boolean;
75
+ }>>;
76
+ }
77
+
64
78
  // ————— the entity-aware builders —————
65
79
 
66
80
  /** The base every untyped kind is built from, plus the vocabulary. */
@@ -203,6 +217,10 @@ export class EntityObjectBuilder<
203
217
  P extends Props, Open extends boolean = false, PV = never, PVIn = PV,
204
218
  N extends boolean = false, F extends Flag = never,
205
219
  > extends ObjectBuilder<P, Open, PV, PVIn, N, F> {
220
+ physical(layout: PhysicalLayout): this;
221
+ invariants(rules: readonly { name: string; on: readonly ('insert' | 'update' | 'delete')[];
222
+ enforcement: 'database' | 'store'; assert: unknown;
223
+ audit?: { entity: string; values: Readonly<Record<string, unknown>> } }[]): this;
206
224
  optional(): EntityObjectBuilder<P, Open, PV, PVIn, N, F | 'optional'>;
207
225
  nullable(): EntityObjectBuilder<P, Open, PV, PVIn, true, F>;
208
226
  open(): EntityObjectBuilder<P, true, PV, PVIn, N, F>;
@@ -7,7 +7,13 @@ export type JsonInput<T> = unknown extends T ? unknown
7
7
  : T extends object ? { readonly [K in keyof T]: JsonInput<T[K]> } : never;
8
8
 
9
9
  export type FileKind = 'app' | 'jslt' | 'query' | 'state' | 'data' | 'schema' | 'fsm' | 'dag' | 'model' | 'contract';
10
- export interface ProjectFile<Name extends string = string, Kind extends FileKind = FileKind> {
10
+ export interface ProjectFileOptions {
11
+ readonly imports?: Readonly<Record<string, string>>;
12
+ readonly input?: string;
13
+ readonly model?: string;
14
+ readonly collection?: string;
15
+ }
16
+ export interface ProjectFile<Name extends string = string, Kind extends FileKind = FileKind> extends ProjectFileOptions {
11
17
  readonly name: Name;
12
18
  readonly kind: Kind;
13
19
  readonly text: string;
@@ -24,8 +30,8 @@ export interface ProjectDocument {
24
30
  readonly layout?: ProjectLayout;
25
31
  }
26
32
  export const FILE_KINDS: readonly FileKind[];
27
- export function file<const N extends string, K extends FileKind>(name: N, kind: K, text: string): ProjectFile<N, K>;
28
- export function jsonFile<const N extends string, K extends FileKind, const D>(name: N, kind: K, document: D & JsonInput<D>): ProjectFile<N, K>;
33
+ export function file<const N extends string, K extends FileKind>(name: N, kind: K, text: string, options?: ProjectFileOptions): ProjectFile<N, K>;
34
+ export function jsonFile<const N extends string, K extends FileKind, const D>(name: N, kind: K, document: D & JsonInput<D>, options?: ProjectFileOptions): ProjectFile<N, K>;
29
35
  /** Names are a phantom; the public document has no extra registry. */
30
36
  export class ProjectBuilder<Names extends string = never> {
31
37
  protected constructor();