@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 +123 -2
- package/dist/index.cjs +539 -0
- package/dist/index.d.cts +171 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +509 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kalada/provider-routing (experimental)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
});
|