@kalada/provider-routing 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,124 @@
1
- # Temporary Holding Version
1
+ # @kalada/provider-routing (experimental)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Two separate opt-in, dependency-free experimental capabilities: whole-document diagnostic
4
+ routing and host-owned parser composition. Contract version
5
+ `DIAGNOSTIC_CONTRACT_VERSION === 1` identifies this provisional diagnostic shape;
6
+ the package is not a stable language kernel API. Changes to the experimental contract
7
+ may require consumer updates. There is no implicit Expressions provider: install
8
+ `@kalada/language-service` separately to opt into its Expressions adapter.
9
+
10
+ ```ts
11
+ import { createDiagnosticRouter } from "@kalada/provider-routing";
12
+
13
+ const router = createDiagnosticRouter([{
14
+ languageId: "my-language",
15
+ diagnose(document) {
16
+ return { status: "supported", diagnostics: [] };
17
+ },
18
+ }]);
19
+ const outcome = router.diagnose("my-language", {
20
+ uri: "file:///example.txt",
21
+ text: "hello",
22
+ version: 1,
23
+ environmentGeneration: "schema-1",
24
+ });
25
+ ```
26
+
27
+ The caller supplies an explicit language ID and whole-document snapshot for each request.
28
+ `version` is a nonnegative safe integer document revision; `environmentGeneration` is a
29
+ nonempty opaque identity for the caller's environment. The router copies and freezes
30
+ the snapshot before invoking a provider. Callers must compare returned document identity
31
+ with their *current* document and environment before displaying diagnostics; the router
32
+ cannot discover a changed external environment. Duplicate or empty registrations,
33
+ unknown language IDs, and malformed document identity throw. Registration reads the
34
+ language ID once; providers should be trusted code, not downloaded plugins.
35
+
36
+ `supported` carries an empty diagnostic list; `invalid` carries one or more diagnostics;
37
+ `unsupported` has no diagnostics. Each diagnostic has an uppercase code and a half-open
38
+ `[start, end)` UTF-16 offset range within the snapshot (including zero-width ranges).
39
+ Malformed provider results become `invalid` with `PROVIDER_INVALID_RESULT`; thrown
40
+ provider/accessor errors become `PROVIDER_FAILURE`. Input over 100,000 UTF-16 code units
41
+ becomes `PROVIDER_SOURCE_LIMIT` without invoking the provider. At most 100 diagnostics
42
+ are accepted; code length is at most 128, and URI/environment identity length at most
43
+ 2,048 code units. These limits are provisional, not calibrated platform budgets.
44
+ Returned diagnostics and snapshots are frozen copies, not live provider values.
45
+
46
+ `COMPOSITION_CONTRACT_VERSION === 1` identifies the separate provisional host-composition
47
+ contract. A trusted host parser supplies **declared** slot offsets (not discovered by the
48
+ router), a versioned host-language/grammar-position profile, allowlisted guest IDs and
49
+ host-owned opening/closing markers. An explicit guest choice overrides the static profile
50
+ default; neither may bypass the allowlist, and no selection means unsupported (no fallback).
51
+ For reverse embedding, register a *separate* reverse host profile. The host keeps delimiters;
52
+ the selected guest lexes its interior and returns its own opaque subtree, UTF-16 range,
53
+ consumed stop, reason and diagnostics. Success requires `status: "valid"`,
54
+ `reason: "host-close"`, a progressing claimed interior, an actual host close at stop, no diagnostics and a subtree;
55
+ `reason: "eof"` cannot authorize a tree. The router validates progress, local bounds, closing marker,
56
+ ownership (including diagnostic codes/ranges), selection, currentness and cancellation before returning a navigable host/guest
57
+ tree. It does not parse the host or know how to skip guest strings/comments: guests must
58
+ implement their language's supported lexical boundary themselves and report the **first**
59
+ applicable close in their grammar. A host may supply an independently known slot `maxStop`
60
+ to reject a later return; this optional safe UTF-16 offset may equal the interior start
61
+ for an early failure. The router does not scan guest text for earlier `}` (which
62
+ might be quoted), nor can it detect a dishonest trusted provider's earlier neutral close
63
+ without such a bound. Bounded `unsupported`, `partial`, and `invalid` returns may stop at
64
+ the opening offset or EOF, even without a closing marker. They retain the guest reason
65
+ and copied owner-tagged diagnostics within `[start, stop]` (including zero-width at stop),
66
+ but do not consume a host close or authorize a sibling. Invalid bounds, owner, status or
67
+ diagnostics fail closed without guest diagnostics. Unsupported, partial, invalid, stale,
68
+ cancelled and budget outcomes have no tree and confer no emission/edit authority.
69
+
70
+ ```ts
71
+ import { createCompositionRouter } from "@kalada/provider-routing";
72
+ const router = createCompositionRouter([{
73
+ version: 1, hostLanguageId: "host", position: "expression",
74
+ open: "{", close: "}", allowedGuests: ["tiny"], defaultGuest: "tiny",
75
+ }], [{ languageId: "tiny", parse({ start, meter }) {
76
+ meter.charge(3); // cooperative guest work, including nested calls
77
+ return { owner: "tiny", status: "valid", range: { start, end: start + 3 },
78
+ stop: start + 3, reason: "host-close", diagnostics: [], subtree: { kind: "tiny" } };
79
+ } }]);
80
+ const input = { uri: "file:///a", text: "{abc}", version: 1, environmentGeneration: "env" };
81
+ const result = router.compose({ snapshot: input, hostLanguageId: "host",
82
+ slots: [{ position: "expression", start: 0 }], isCurrent: () => true,
83
+ limits: { work: 100, depth: 4, diagnostics: 10 } });
84
+ ```
85
+
86
+ The router copies/freezes source identity and profile declarations; `isCurrent` must check
87
+ the caller's live document **and environment** both before and after callbacks, and consumers
88
+ must recheck before publishing. Optional `context` carries host-owned opaque expected-type
89
+ and location facts, not type checking or permission to write. Nested compositions must pass
90
+ the same meter. Host charges its own text and opening/closing markers once; guests charge
91
+ actual interior work (at least one unit for nonempty successful interior), including nested
92
+ calls, via that meter. Work/depth/diagnostic bounds are cooperative and qualitative, not a CPU or
93
+ memory sandbox: never register untrusted executable providers. Guest subtrees and opaque
94
+ context values are language/host-owned references, not deep-frozen or admitted artifacts.
95
+
96
+ This package does **not** implement a host parser, generic AST/CST, type composition,
97
+ expression evaluator, editor/LSP services, or CF01 completion (#108). The real Kalada
98
+ adapter and independent packed two-guest proof belong to #145.
99
+ Packed independent domain and Expressions opt-in cross-consumer proof is tracked by #137.
100
+
101
+ ### Experimental partial recovery (#151)
102
+
103
+ Optionally pass `hostContinuation: { validate(input) { ... } }` to `compose` for a trusted
104
+ host grammar to certify a connector to the **next predeclared slot**. The frozen input
105
+ contains the copied snapshot, the guest-reported host `close` range, a frozen copy of the
106
+ next slot and the same cooperative meter. After independently validating its own grammar
107
+ between `close.end` and `nextSlot.start`, the host charges at least the connector's UTF-16
108
+ length and returns `{ owner: hostLanguageId, range: { start: close.end,
109
+ end: nextSlot.start } }`. A Boolean, a guessed slot, or scanning a guest interior is not
110
+ proof. The router checks the original source's closing/opening markers, profile selection,
111
+ bounded offsets, ordering, meter and live snapshot identity before invoking the next guest.
112
+
113
+ Only a bounded, progressing `status: "partial", reason: "safe-host-close"` guest exit
114
+ whose lexed stop points at the actual close can initiate recovery. The guest must account
115
+ for its interior work on the shared meter; `invalid`, `unsupported`, EOF or ambiguous
116
+ exits cannot resume. On a recovered `partial` outcome, `attempts` contain only failed
117
+ guest exits as frozen owner-tagged bounded ranges/reasons/copied diagnostics;
118
+ `candidates` contain only opaque language-owned nodes from independently valid siblings.
119
+ A fully valid document has a `tree` instead of `attempts` or `candidates`. The failed
120
+ subtree is never read. Neither `attempts` nor `candidates` is an authenticated host CST
121
+ or the authoritative `tree`;
122
+ they grant no lowering, emission, editing or publication authority. Consumers must recheck
123
+ source/version/environment identity before displaying even diagnostic recovery data.
124
+ No Formbar runtime or CF01 pass is implied; this is trusted cooperative code, not a sandbox.
package/dist/index.cjs ADDED
@@ -0,0 +1,539 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ COMPOSITION_CONTRACT_VERSION: () => COMPOSITION_CONTRACT_VERSION,
24
+ DIAGNOSTIC_CONTRACT_VERSION: () => DIAGNOSTIC_CONTRACT_VERSION,
25
+ createCompositionRouter: () => createCompositionRouter,
26
+ createDiagnosticRouter: () => createDiagnosticRouter
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/composition-meter.ts
31
+ var Meter = class {
32
+ work = 0;
33
+ depth = 0;
34
+ diagnostics = 0;
35
+ exhausted = false;
36
+ active = 0;
37
+ limits;
38
+ constructor(limits) {
39
+ this.limits = limits;
40
+ }
41
+ debit(key, amount, limit) {
42
+ if (this.exhausted || !Number.isSafeInteger(amount) || amount < 0 || this[key] + amount > limit) {
43
+ this.exhausted = true;
44
+ return false;
45
+ }
46
+ this[key] += amount;
47
+ return true;
48
+ }
49
+ charge(units) {
50
+ return this.debit("work", units, this.limits.work);
51
+ }
52
+ report(count) {
53
+ return this.debit("diagnostics", count, this.limits.diagnostics);
54
+ }
55
+ enter() {
56
+ if (this.exhausted || this.active >= this.limits.depth) {
57
+ this.exhausted = true;
58
+ return false;
59
+ }
60
+ this.active++;
61
+ this.depth = Math.max(this.depth, this.active);
62
+ return true;
63
+ }
64
+ leave() {
65
+ this.active--;
66
+ }
67
+ };
68
+
69
+ // src/composition-validation.ts
70
+ var MAX_ID = 2048;
71
+ var MAX_DIAGNOSTICS = 100;
72
+ var id = (s) => typeof s === "string" && !!s.trim() && s.length <= MAX_ID;
73
+ var offset = (n, length) => Number.isSafeInteger(n) && n >= 0 && n <= length;
74
+ var range = (start, end) => Object.freeze({ start, end });
75
+ function outcome(status, reason, document, diagnostics = []) {
76
+ return Object.freeze({
77
+ status,
78
+ reason,
79
+ snapshot: document,
80
+ diagnostics: Object.freeze([...diagnostics])
81
+ });
82
+ }
83
+ function checkpoint(request, document) {
84
+ if (request.isCancelled?.()) return "cancelled";
85
+ const original = request.snapshot;
86
+ if (original.uri !== document.uri || original.text !== document.text || original.version !== document.version || original.environmentGeneration !== document.environmentGeneration)
87
+ return "stale";
88
+ if (!request.isCurrent(document)) return "stale";
89
+ }
90
+ function declaredSlotsClear(slots, index, boundary, length) {
91
+ let previous = slots[index]?.start;
92
+ if (previous === void 0 || !offset(previous, length)) return false;
93
+ for (let i = index + 1; i < slots.length; i++) {
94
+ const start = slots[i]?.start;
95
+ if (start === void 0 || !offset(start, length)) return false;
96
+ if (start <= previous || start < boundary) return false;
97
+ previous = start;
98
+ }
99
+ return true;
100
+ }
101
+ function boundedExit(result, owner, start, length, maxStop) {
102
+ const stop = result?.stop;
103
+ return result?.owner === owner && offset(stop, length) && stop >= start && (maxStop === void 0 || stop <= maxStop) && result.range?.start === start && result.range?.end === stop && typeof result.reason === "string" && !!result.reason && (result.status === "valid" || result.status === "invalid" || result.status === "partial" || result.status === "unsupported");
104
+ }
105
+ function snapshot(input) {
106
+ const copy = Object.freeze({
107
+ uri: input?.uri,
108
+ text: input?.text,
109
+ version: input?.version,
110
+ environmentGeneration: input?.environmentGeneration
111
+ });
112
+ if (!id(copy.uri) || typeof copy.text !== "string" || !Number.isSafeInteger(copy.version) || copy.version < 0 || !id(copy.environmentGeneration))
113
+ throw new Error("Invalid snapshot");
114
+ return copy;
115
+ }
116
+ function profileCopy(p) {
117
+ const allowedGuests = [...p.allowedGuests];
118
+ if (p.version !== 1 || !id(p.hostLanguageId) || !id(p.position) || !id(p.open) || !id(p.close) || !allowedGuests.length || allowedGuests.some((guest) => !id(guest)) || new Set(allowedGuests).size !== allowedGuests.length || p.defaultGuest !== void 0 && !allowedGuests.includes(p.defaultGuest))
119
+ throw new Error("Invalid or ambiguous composition profile");
120
+ return Object.freeze({
121
+ version: 1,
122
+ hostLanguageId: p.hostLanguageId,
123
+ position: p.position,
124
+ open: p.open,
125
+ close: p.close,
126
+ defaultGuest: p.defaultGuest,
127
+ allowedGuests: Object.freeze(allowedGuests)
128
+ });
129
+ }
130
+ function diagnosticCopy(items, regionStart, stop, owner) {
131
+ if (!Array.isArray(items) || items.length > MAX_DIAGNOSTICS) return;
132
+ const result = [];
133
+ for (const item of items) {
134
+ const code = item?.code, start = item?.range?.start, end = item?.range?.end;
135
+ if (typeof code !== "string" || code.length > 128 || !/^[A-Z][A-Z0-9_]*$/u.test(code) || item?.owner !== owner || !offset(start, stop) || !offset(end, stop) || start < regionStart || end < start)
136
+ return;
137
+ result.push(Object.freeze({ owner, code, range: range(start, end) }));
138
+ }
139
+ return Object.freeze(result);
140
+ }
141
+
142
+ // src/composition-recovery.ts
143
+ function clearBoundary(request, document, index, end) {
144
+ return declaredSlotsClear(request.slots, index, end, document.text.length);
145
+ }
146
+ function clearFailure(request, document, index, failed) {
147
+ if (!failed.attempt) return true;
148
+ const end = failed.safe ? failed.safe.stop + failed.safe.profile.close.length : failed.attempt.range.end;
149
+ return clearBoundary(request, document, index, end);
150
+ }
151
+ function recoveryFailure(request, document, index, failed, attempts, candidates, reason) {
152
+ if (["budget", "stale", "cancelled"].includes(failed.outcome.status))
153
+ return { outcome: failed.outcome };
154
+ if (!clearFailure(request, document, index, failed))
155
+ return { outcome: outcome("invalid", "OVERLAPPING_SLOT", document) };
156
+ if (failed.attempt) attempts.push(failed.attempt);
157
+ if (failed.safe && index + 1 < request.slots.length) return { safe: failed.safe };
158
+ return { outcome: partial(document, reason, attempts, candidates) };
159
+ }
160
+ function attempt(result, diagnostics) {
161
+ return Object.freeze({
162
+ owner: result.owner,
163
+ range: Object.freeze({ start: result.range.start, end: result.stop }),
164
+ status: result.status,
165
+ reason: result.reason,
166
+ diagnostics
167
+ });
168
+ }
169
+ function partial(document, reason, attempts, candidates) {
170
+ return Object.freeze({
171
+ status: "partial",
172
+ reason,
173
+ snapshot: document,
174
+ diagnostics: Object.freeze(attempts.flatMap((item) => item.diagnostics)),
175
+ attempts: Object.freeze([...attempts]),
176
+ candidates: Object.freeze([...candidates])
177
+ });
178
+ }
179
+ function connectorInput(document, meter, stop, start, next) {
180
+ return Object.freeze({
181
+ snapshot: document,
182
+ close: Object.freeze({ start: stop, end: start }),
183
+ nextSlot: Object.freeze({
184
+ position: next.position,
185
+ start: next.start,
186
+ maxStop: next.maxStop,
187
+ explicitGuest: next.explicitGuest
188
+ }),
189
+ meter
190
+ });
191
+ }
192
+ function connector(request, document, meter, stop, profile, next) {
193
+ const continuation = request.hostContinuation;
194
+ const start = stop + profile.close.length;
195
+ if (!continuation || typeof continuation.validate !== "function" || !Number.isSafeInteger(next?.start) || next.start < start || next.start > document.text.length)
196
+ return false;
197
+ if (!meter.enter()) return false;
198
+ let value;
199
+ const before = meter.work;
200
+ try {
201
+ value = continuation.validate(connectorInput(document, meter, stop, start, next));
202
+ } finally {
203
+ meter.leave();
204
+ }
205
+ const owner = value?.owner;
206
+ const range2 = value?.range;
207
+ const from = range2?.start;
208
+ const to = range2?.end;
209
+ return !meter.exhausted && owner === request.hostLanguageId && from === start && to === next.start && meter.work - before >= to - from;
210
+ }
211
+
212
+ // src/composition.ts
213
+ var MAX_TEXT = 1e5;
214
+ var MAX_WORK = 1e6;
215
+ var MAX_DEPTH = 32;
216
+ var MAX_DIAGNOSTICS2 = 100;
217
+ function select(slot, host, profiles, guests) {
218
+ const profile = profiles.find((p) => p.hostLanguageId === host && p.position === slot.position);
219
+ if (!profile) return;
220
+ const chosen = slot.explicitGuest ?? profile.defaultGuest;
221
+ if (!chosen || !profile.allowedGuests.includes(chosen)) return;
222
+ const guest = guests.get(chosen);
223
+ return guest ? { profile, guest } : void 0;
224
+ }
225
+ function node(owner, start, end, children = [], subtree) {
226
+ return Object.freeze({
227
+ owner,
228
+ range: range(start, end),
229
+ children: Object.freeze([...children]),
230
+ subtree
231
+ });
232
+ }
233
+ function createCompositionRouter(profiles, providers) {
234
+ const registered = profiles.map(profileCopy);
235
+ const keys = registered.map((p) => `${p.hostLanguageId}\0${p.position}`);
236
+ if (new Set(keys).size !== keys.length) throw new Error("Overlapping host positions");
237
+ const guests = /* @__PURE__ */ new Map();
238
+ for (const provider of providers) {
239
+ const languageId = provider?.languageId, parse = provider?.parse;
240
+ if (!id(languageId) || typeof parse !== "function" || guests.has(languageId))
241
+ throw new Error("Invalid or duplicate guest");
242
+ guests.set(languageId, Object.freeze({ languageId, parse: parse.bind(provider) }));
243
+ }
244
+ return Object.freeze({
245
+ compose(request) {
246
+ const document = snapshot(request.snapshot);
247
+ const limits = Object.freeze({ ...request.limits });
248
+ if (!Number.isSafeInteger(limits.work) || limits.work < 1 || limits.work > MAX_WORK || !Number.isSafeInteger(limits.depth) || limits.depth < 1 || limits.depth > MAX_DEPTH || !Number.isSafeInteger(limits.diagnostics) || limits.diagnostics < 0 || limits.diagnostics > MAX_DIAGNOSTICS2)
249
+ throw new Error("Invalid budget");
250
+ const meter = request.meter ?? new Meter(limits);
251
+ try {
252
+ return compose(request, document, meter, registered, guests);
253
+ } catch {
254
+ return outcome("invalid", "COMPOSITION_FAILURE", document);
255
+ }
256
+ }
257
+ });
258
+ }
259
+ function compose(request, document, meter, profiles, guests) {
260
+ const state = checkpoint(request, document);
261
+ if (state) return outcome(state, state, document);
262
+ if (document.text.length > MAX_TEXT) return outcome("budget", "SOURCE_LIMIT", document);
263
+ if (!id(request.hostLanguageId) || !Array.isArray(request.slots) || request.slots.length > MAX_DIAGNOSTICS2)
264
+ return outcome("invalid", "INVALID_HOST", document);
265
+ if (!meter.enter()) return outcome("budget", "DEPTH_LIMIT", document);
266
+ try {
267
+ return walk(request, document, meter, profiles, guests);
268
+ } finally {
269
+ meter.leave();
270
+ }
271
+ }
272
+ function walk(request, document, meter, profiles, guests) {
273
+ const children = [];
274
+ let cursor = 0;
275
+ for (const [index, slot] of request.slots.entries()) {
276
+ const state = checkpoint(request, document);
277
+ if (state) return outcome(state, state, document);
278
+ if (!offset(slot?.start, document.text.length) || slot.start < cursor)
279
+ return outcome("invalid", "OVERLAPPING_SLOT", document);
280
+ const processed = processSlot(request, document, meter, profiles, guests, slot, cursor);
281
+ if ("outcome" in processed)
282
+ return walkFailure(request, document, meter, profiles, guests, index, processed);
283
+ const { start, stop, close, guest, subtree } = processed;
284
+ if (!clearBoundary(request, document, index, stop + close.length))
285
+ return outcome("invalid", "OVERLAPPING_SLOT", document);
286
+ if (slot.start > cursor) children.push(node(request.hostLanguageId, cursor, slot.start));
287
+ children.push(node(request.hostLanguageId, slot.start, start));
288
+ children.push(node(guest, start, stop, [], subtree));
289
+ cursor = stop + close.length;
290
+ children.push(node(request.hostLanguageId, stop, cursor));
291
+ }
292
+ return finishWalk(request, document, meter, children, cursor);
293
+ }
294
+ function walkFailure(request, document, meter, profiles, guests, index, failed) {
295
+ if (!clearFailure(request, document, index, failed))
296
+ return outcome("invalid", "OVERLAPPING_SLOT", document);
297
+ return failed.safe && request.hostContinuation && index + 1 < request.slots.length ? recover(request, document, meter, profiles, guests, index, failed) : failed.outcome;
298
+ }
299
+ function finishWalk(request, document, meter, children, cursor) {
300
+ if (!meter.charge(document.text.length - cursor) || meter.exhausted)
301
+ return outcome("budget", "WORK_LIMIT", document);
302
+ const state = checkpoint(request, document);
303
+ if (state) return outcome(state, state, document);
304
+ if (cursor < document.text.length)
305
+ children.push(node(request.hostLanguageId, cursor, document.text.length));
306
+ return Object.freeze({
307
+ status: "valid",
308
+ reason: "complete",
309
+ snapshot: document,
310
+ diagnostics: Object.freeze([]),
311
+ tree: node(request.hostLanguageId, 0, document.text.length, children)
312
+ });
313
+ }
314
+ function resumeSlot(request, document, meter, profiles, guests, slot, previous) {
315
+ const state = checkpoint(request, document);
316
+ if (state) return { outcome: outcome(state, state, document) };
317
+ if (!slot) return { outcome: outcome("invalid", "MISSING_SLOT", document) };
318
+ const entry = select(slot, request.hostLanguageId, profiles, guests);
319
+ if (!entry || !document.text.startsWith(entry.profile.open, slot.start))
320
+ return { outcome: outcome("invalid", "MISSING_NEXT_OPEN", document) };
321
+ if (!previous.closeCharged && !meter.charge(previous.profile.close.length))
322
+ return { outcome: outcome("budget", "WORK_OR_DIAGNOSTIC_LIMIT", document) };
323
+ if (!connector(request, document, meter, previous.stop, previous.profile, slot))
324
+ return {
325
+ outcome: outcome(meter.exhausted ? "budget" : "invalid", "CONNECTOR_REJECTED", document)
326
+ };
327
+ const after = checkpoint(request, document);
328
+ if (after) return { outcome: outcome(after, after, document) };
329
+ return processSlot(request, document, meter, profiles, guests, slot, slot.start);
330
+ }
331
+ function recover(request, document, meter, profiles, guests, index, failed) {
332
+ const attempts = [failed.attempt];
333
+ const candidates = [];
334
+ let previous = {
335
+ ...failed.safe,
336
+ closeCharged: false
337
+ };
338
+ for (let i = index + 1; i < request.slots.length; i++) {
339
+ const slot = request.slots[i];
340
+ const processed = resumeSlot(request, document, meter, profiles, guests, slot, previous);
341
+ if ("outcome" in processed) {
342
+ const decision = recoveryFailure(
343
+ request,
344
+ document,
345
+ i,
346
+ processed,
347
+ attempts,
348
+ candidates,
349
+ failed.outcome.reason
350
+ );
351
+ if (decision.outcome) return decision.outcome;
352
+ previous = { ...decision.safe, closeCharged: false };
353
+ continue;
354
+ }
355
+ if (!clearBoundary(request, document, i, processed.stop + processed.close.length))
356
+ return outcome("invalid", "OVERLAPPING_SLOT", document);
357
+ candidates.push(node(processed.guest, processed.start, processed.stop, [], processed.subtree));
358
+ previous = { stop: processed.stop, profile: processed.profile, closeCharged: true };
359
+ }
360
+ if (meter.exhausted) return outcome("budget", "WORK_LIMIT", document);
361
+ const state = checkpoint(request, document);
362
+ return state ? outcome(state, state, document) : partial(document, failed.outcome.reason, attempts, candidates);
363
+ }
364
+ function admit(result, entry, start, document, meter, maxStop, guestWork) {
365
+ const guestRange = result.range;
366
+ const value = {
367
+ owner: result.owner,
368
+ stop: result.stop,
369
+ range: { start: guestRange.start, end: guestRange.end },
370
+ reason: result.reason,
371
+ status: result.status,
372
+ diagnostics: result.diagnostics
373
+ };
374
+ if (!boundedExit(value, entry.guest.languageId, start, document.text.length, maxStop))
375
+ return { outcome: outcome("invalid", "INVALID_GUEST_EXIT", document) };
376
+ const copied = diagnosticCopy(value.diagnostics, start, value.stop, entry.guest.languageId);
377
+ if (!copied) return { outcome: outcome("invalid", "INVALID_GUEST_DIAGNOSTIC", document) };
378
+ if (!meter.report(copied.length) || meter.exhausted)
379
+ return { outcome: outcome("budget", "WORK_OR_DIAGNOSTIC_LIMIT", document) };
380
+ if (value.status !== "valid")
381
+ return {
382
+ outcome: outcome(value.status, value.reason, document, copied),
383
+ attempt: attempt(value, copied),
384
+ safe: value.status === "partial" && value.reason === "safe-host-close" && value.stop > start && guestWork >= value.stop - start && document.text.startsWith(entry.profile.close, value.stop) ? { stop: value.stop, profile: entry.profile } : void 0
385
+ };
386
+ return admitValid(result, value, copied, entry, start, document, meter, guestWork);
387
+ }
388
+ function admitValid(result, value, copied, entry, start, document, meter, guestWork) {
389
+ if (value.stop <= start || !document.text.startsWith(entry.profile.close, value.stop) || copied.length || value.reason !== "host-close" || !Number.isSafeInteger(guestWork) || guestWork < 1)
390
+ return { outcome: outcome("invalid", "INVALID_GUEST_EXIT", document) };
391
+ const subtree = result.subtree;
392
+ if (subtree === void 0)
393
+ return { outcome: outcome("invalid", "MISSING_GUEST_SUBTREE", document) };
394
+ if (!meter.charge(entry.profile.close.length))
395
+ return { outcome: outcome("budget", "WORK_OR_DIAGNOSTIC_LIMIT", document) };
396
+ return {
397
+ start,
398
+ stop: value.stop,
399
+ close: entry.profile.close,
400
+ profile: entry.profile,
401
+ guest: entry.guest.languageId,
402
+ subtree
403
+ };
404
+ }
405
+ function processSlot(request, document, meter, profiles, guests, slot, cursor) {
406
+ const entry = select(slot, request.hostLanguageId, profiles, guests);
407
+ if (!entry) return { outcome: outcome("unsupported", "UNDECLARED_GUEST_OR_POSITION", document) };
408
+ if (!document.text.startsWith(entry.profile.open, slot.start))
409
+ return { outcome: outcome("invalid", "MISSING_OPEN", document) };
410
+ const start = slot.start + entry.profile.open.length;
411
+ if (slot.maxStop !== void 0 && (!offset(slot.maxStop, document.text.length) || slot.maxStop < start))
412
+ return { outcome: outcome("invalid", "INVALID_MAX_STOP", document) };
413
+ if (!meter.charge(start - cursor) || !meter.enter())
414
+ return { outcome: outcome("budget", "WORK_OR_DEPTH_LIMIT", document) };
415
+ const workBefore = meter.work;
416
+ let result;
417
+ try {
418
+ result = entry.guest.parse(
419
+ Object.freeze({
420
+ snapshot: document,
421
+ start,
422
+ close: entry.profile.close,
423
+ context: slot.context && Object.freeze({
424
+ expectedType: slot.context.expectedType,
425
+ location: slot.context.location
426
+ }),
427
+ meter
428
+ })
429
+ );
430
+ } finally {
431
+ meter.leave();
432
+ }
433
+ const state = checkpoint(request, document);
434
+ if (state) return { outcome: outcome(state, state, document) };
435
+ if (meter.exhausted) return { outcome: outcome("budget", "GUEST_BUDGET", document) };
436
+ const work = meter.work - workBefore;
437
+ const admitted = admit(result, entry, start, document, meter, slot.maxStop, work);
438
+ const afterAdmission = checkpoint(request, document);
439
+ return afterAdmission ? { outcome: outcome(afterAdmission, afterAdmission, document) } : admitted;
440
+ }
441
+
442
+ // src/contracts.ts
443
+ var COMPOSITION_CONTRACT_VERSION = 1;
444
+
445
+ // src/index.ts
446
+ var DIAGNOSTIC_CONTRACT_VERSION = 1;
447
+ var MAX_TEXT2 = 1e5;
448
+ var MAX_IDENTITY = 2048;
449
+ var MAX_DIAGNOSTICS3 = 100;
450
+ var MAX_CODE = 128;
451
+ function invalid(code) {
452
+ return Object.freeze({
453
+ status: "invalid",
454
+ diagnostics: Object.freeze([
455
+ Object.freeze({ code, range: Object.freeze({ start: 0, end: 0 }) })
456
+ ])
457
+ });
458
+ }
459
+ function validDocument(value) {
460
+ return typeof value?.uri === "string" && value.uri.length > 0 && value.uri.length <= MAX_IDENTITY && typeof value.text === "string" && Number.isSafeInteger(value.version) && value.version >= 0 && typeof value.environmentGeneration === "string" && value.environmentGeneration.length > 0 && value.environmentGeneration.length <= MAX_IDENTITY;
461
+ }
462
+ function validDiagnostic(code, start, end, length) {
463
+ return /^[A-Z][A-Z0-9_]*$/u.test(code) && code.length <= MAX_CODE && Number.isSafeInteger(start) && Number.isSafeInteger(end) && start >= 0 && end >= start && end <= length;
464
+ }
465
+ function copyDiagnostic(item, length) {
466
+ const code = item?.code;
467
+ const range2 = item?.range;
468
+ const start = range2?.start;
469
+ const end = range2?.end;
470
+ if (typeof code !== "string" || typeof start !== "number" || typeof end !== "number" || !validDiagnostic(code, start, end, length))
471
+ return void 0;
472
+ return Object.freeze({ code, range: Object.freeze({ start, end }) });
473
+ }
474
+ function safeOutcome(value, length) {
475
+ const status = value?.status;
476
+ if (status === "unsupported") {
477
+ if ("diagnostics" in value) return invalid("PROVIDER_INVALID_RESULT");
478
+ return Object.freeze({ status: "unsupported" });
479
+ }
480
+ if (status !== "supported" && status !== "invalid") return invalid("PROVIDER_INVALID_RESULT");
481
+ const source = value.diagnostics;
482
+ if (!Array.isArray(source)) return invalid("PROVIDER_INVALID_RESULT");
483
+ const count = source.length;
484
+ if (!Number.isSafeInteger(count) || count < 0 || count > MAX_DIAGNOSTICS3)
485
+ return invalid("PROVIDER_INVALID_RESULT");
486
+ const diagnostics = [];
487
+ for (let index = 0; index < count; index++) {
488
+ const item = source[index];
489
+ const copied = copyDiagnostic(item, length);
490
+ if (!copied) return invalid("PROVIDER_INVALID_RESULT");
491
+ diagnostics.push(copied);
492
+ }
493
+ if (status === "supported" !== (diagnostics.length === 0))
494
+ return invalid("PROVIDER_INVALID_RESULT");
495
+ if (status === "supported")
496
+ return Object.freeze({ status: "supported", diagnostics: Object.freeze(diagnostics) });
497
+ return Object.freeze({ status: "invalid", diagnostics: Object.freeze(diagnostics) });
498
+ }
499
+ function createDiagnosticRouter(providers) {
500
+ const registered = /* @__PURE__ */ new Map();
501
+ for (const provider of providers) {
502
+ const languageId = provider?.languageId;
503
+ if (typeof languageId !== "string" || !languageId.trim())
504
+ throw new Error("Invalid language ID");
505
+ if (registered.has(languageId)) throw new Error("Duplicate language ID");
506
+ if (typeof provider.diagnose !== "function") throw new Error("Invalid provider");
507
+ registered.set(languageId, provider);
508
+ }
509
+ return Object.freeze({
510
+ diagnose(languageId, input) {
511
+ const provider = registered.get(languageId);
512
+ if (!provider) throw new Error("Unknown language ID");
513
+ const document = Object.freeze({
514
+ uri: input?.uri,
515
+ text: input?.text,
516
+ version: input?.version,
517
+ environmentGeneration: input?.environmentGeneration
518
+ });
519
+ if (!validDocument(document)) throw new Error("Invalid document identity");
520
+ let outcome2;
521
+ if (document.text.length > MAX_TEXT2) outcome2 = invalid("PROVIDER_SOURCE_LIMIT");
522
+ else {
523
+ try {
524
+ outcome2 = safeOutcome(provider.diagnose(document), document.text.length);
525
+ } catch {
526
+ outcome2 = invalid("PROVIDER_FAILURE");
527
+ }
528
+ }
529
+ return Object.freeze({ ...outcome2, languageId, document });
530
+ }
531
+ });
532
+ }
533
+ // Annotate the CommonJS export names for ESM import in node:
534
+ 0 && (module.exports = {
535
+ COMPOSITION_CONTRACT_VERSION,
536
+ DIAGNOSTIC_CONTRACT_VERSION,
537
+ createCompositionRouter,
538
+ createDiagnosticRouter
539
+ });