@vizejs/plugin-sdk 0.429.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 ADDED
@@ -0,0 +1,224 @@
1
+ # @vizejs/plugin-sdk
2
+
3
+ Author synchronous rules, fact providers, native template transforms and compiled
4
+ output hooks. Install `@vizejs/native` **0.429.0 or newer** to execute plugins.
5
+ The optional native peer lets this package install independently for authoring;
6
+ its helpers alone do not compile or lint Vue files. Node 22 or newer is required.
7
+
8
+ ## Lint rules and autofixes
9
+
10
+ ```js
11
+ import * as native from "@vizejs/native";
12
+ import { definePlugin, applyFixes } from "@vizejs/plugin-sdk";
13
+
14
+ const rule = definePlugin({
15
+ name: "team-rule",
16
+ version: "1.0.0",
17
+ visit: ["ui.bind"],
18
+ demands: [],
19
+ cacheInputs: [],
20
+ rules: {
21
+ check(ctx) {
22
+ for (const node of ctx.nodes) {
23
+ if (node.name === "disabled" && node.value === "true") {
24
+ ctx.report(node, "Use a literal boolean attribute.", "disabled");
25
+ }
26
+ }
27
+ },
28
+ },
29
+ });
30
+ const output = native.lintWithPlugins(source, [rule], {
31
+ filename: "Button.vue",
32
+ cache: true,
33
+ validateDeterminism: true,
34
+ });
35
+ const fixed = applyFixes(source, output.fixes);
36
+ ```
37
+
38
+ A report names a visited L2 node. Rust assigns its authored UTF-8 byte range;
39
+ a fix replaces that whole range. `applyFixes` preserves other source,
40
+ deduplicates identical suggestions and refuses conflicting edits atomically.
41
+ Apply suggestions deliberately, then rerun the compiler and linter.
42
+
43
+ Rules receive immutable batches. `ctx.facts(name)` requires the group in the
44
+ static `demands` list; no undeclared table crosses the boundary. Native groups
45
+ include `templateScopes` and the registered Croquis producers `bindings`,
46
+ `undefined-refs`, `component-usages`, `reactivity`, `provide-inject` and
47
+ `race-conditions` and `unused-bindings`. The latter computes its registered
48
+ `bindings` dependency. The `bindings` and `unused-bindings` spans are authored
49
+ SFC UTF-8 byte ranges, mapped by the shared producer even for reordered split
50
+ scripts. Other primary position fields retain their Croquis analysis frame:
51
+ undefined references/component usage are template-relative; reactivity,
52
+ provide/inject and race positions are script-relative (the merged normal/setup
53
+ script when both exist). Reports and fixes always use the host's SFC ranges. Public component interface pages are `component-signature`,
54
+ `prop-types`, `emit-types`, `slot-types`, `reactivity-classes` and
55
+ `component-references`. Each interface value preserves the producer's schema,
56
+ nullable unknowns and type dependency completeness. The signature's
57
+ `props_complete` separately records whether all property names were enumerated.
58
+ The source-only SDK route has no resolved catalog proof, so it reports false
59
+ without reading imported files from disk. Imported types which this
60
+ analysis cannot resolve remain explicit unknowns. Primary Croquis facts require
61
+ a JavaScript or TypeScript script; other script dialects are refused explicitly.
62
+
63
+ Plugin visits require inline HTML templates. Authored foreign template dialects
64
+ and external template blocks are refused before callbacks; the SDK does not
65
+ pretend their source is HTML or invent source maps for a preprocessing pass.
66
+
67
+ TypeScript infers each native group's exact map keys and deeply readonly values
68
+ from `ctx.facts("prop-types")`, including authored interface order and all emit
69
+ overloads. The package exports `FactGroups`, `FactKey`, `FactValue`, `FactEntry`
70
+ and the individual native contracts. Custom provider groups can use
71
+ `ctx.facts<MyFact>("design/labels")`; unknown groups default to `unknown`.
72
+
73
+ ## Fact providers
74
+
75
+ ```js
76
+ import { defineFactProvider, definePlugin } from "@vizejs/plugin-sdk";
77
+ const labels = defineFactProvider({
78
+ name: "design",
79
+ version: "1",
80
+ provides: ["design/labels"],
81
+ visit: ["ui.element"],
82
+ demands: ["bindings"],
83
+ cacheInputs: [],
84
+ provide(batch) {
85
+ return { "design/labels": batch.nodes.map((node) => [node.id, node.name]) };
86
+ },
87
+ });
88
+ const consumer = definePlugin({
89
+ name: "labels",
90
+ version: "1",
91
+ demands: ["design/labels"],
92
+ cacheInputs: [],
93
+ rules: {
94
+ check(ctx) {
95
+ const labels = ctx.facts("design/labels");
96
+ },
97
+ },
98
+ });
99
+ native.lintWithPlugins(source, [consumer], { cache: true, factProviders: [labels] });
100
+ ```
101
+
102
+ A provider declares every output group as `provider-name/group`, its required
103
+ facts and node visits. The host computes the dependency closure and refuses
104
+ cycles, collisions, undeclared output and malformed tables before using facts.
105
+ Each table contains `[stringOrNumericKey, jsonValue]` rows with unique keys.
106
+ Provider dependency stamps enter the consumer cache key automatically.
107
+
108
+ ## Native template transforms
109
+
110
+ ```js
111
+ import { defineTransformPlugin } from "@vizejs/plugin-sdk";
112
+ const design = defineTransformPlugin({
113
+ name: "design-system",
114
+ version: "1",
115
+ cacheInputs: [],
116
+ transform(batch) {
117
+ return batch.nodes
118
+ .filter((node) =>
119
+ node.attrs.some((attr) => attr.name === "class" && attr.value === "legacy-btn"),
120
+ )
121
+ .map((node) => ({
122
+ kind: "replace-static-attribute",
123
+ node: node.id,
124
+ name: "class",
125
+ value: "ds-button",
126
+ }));
127
+ },
128
+ });
129
+ const transformed = native.compileWithTransformPlugins(template, [design], {
130
+ filename: "Button.vue",
131
+ sourceMap: true,
132
+ cache: true,
133
+ cacheDir: ".vize-cache",
134
+ });
135
+ ```
136
+
137
+ The hook runs at the native pre-canonical L2 point. It can replace existing
138
+ static HTML attributes from the host's allowed set; dynamic attributes,
139
+ structural edits, template carriers and foreign namespaces refuse. Values are
140
+ semantic text, not entity spelling. Unknown targets or duplicate edits refuse
141
+ the entire response. The compiler applies typed edits to its real arena and
142
+ emits code and authored maps; no guest AST JSON is accepted. The result is a
143
+ module with prefixed identifiers. `plugins` contains per-plugin costs.
144
+
145
+ ## Formatter and output hooks
146
+
147
+ ```js
148
+ import { defineOutputPlugin } from "@vizejs/plugin-sdk";
149
+ const license = defineOutputPlugin({
150
+ name: "license",
151
+ version: "1",
152
+ family: "output",
153
+ cacheInputs: [],
154
+ output() {
155
+ return [{ placement: "prepend", comment: "Copyright Example" }];
156
+ },
157
+ });
158
+ const compiled = native.compileWithOutputPlugins(template, [license], options);
159
+ const decorated = native.applyOutputPlugins(transformed.result, [license], { cache: true });
160
+ ```
161
+
162
+ `family: "formatter"` returns `{start,end,text}` edits using UTF-8 byte offsets.
163
+ Only whitespace formatting which preserves the parsed program and comments is
164
+ accepted. `family: "output"` appends or prepends ordinary block comments;
165
+ source-map directives and executable additions refuse. The host rebases existing
166
+ map anchors and preserves source content. Each pipeline key includes preceding
167
+ output. Function-mode results carry `preamble` separately from `code`.
168
+
169
+ ## Determinism, caches and costs
170
+
171
+ Declare every captured configuration or ambient input in `cacheInputs`, even
172
+ when the list is empty. Names beginning `@vize/` are host-reserved. Source,
173
+ filename, native build, SDK runtime bytes, plugin version/code, static demands,
174
+ visits and inputs participate in keys. A cached lint miss always audits two
175
+ identical batches. Providers and transforms audit misses. Cached output hooks also always audit;
176
+ uncached output hooks audit by default. Different validated outputs refuse before caching. All hook families
177
+ have bounded in-process caches and opt-in `cacheDir` persistence. Disk hits
178
+ validate their schema, keys and host-confined outputs before reuse.
179
+
180
+ Costs include elapsed host work and JavaScript call time, including audit calls.
181
+ A cache hit executes no callback; its `jsNs` and batch crossing bytes are zero.
182
+ A determinism audit observes repeated output. Trusted callbacks can conceal
183
+ ambient reads; their declarations remain the author's responsibility.
184
+
185
+ ## Isolated callback sources
186
+
187
+ ```js
188
+ import {
189
+ createSandboxRunner,
190
+ SANDBOX_IMAGE,
191
+ SandboxRuntimeError,
192
+ } from "@vizejs/plugin-sdk/sandbox";
193
+ const isolated = createSandboxRunner(
194
+ {
195
+ name: "isolated-rule",
196
+ version: "1",
197
+ cacheInputs: [],
198
+ configuration: { message: "Use our design system" },
199
+ rules: { check: "(ctx, config) => { ctx.report(ctx.nodes[0], config.message); }" },
200
+ },
201
+ { timeoutMs: 5000, maxBytes: 1048576 },
202
+ );
203
+ native.lintWithPlugins(source, [isolated], { cache: true });
204
+ ```
205
+
206
+ Install Docker, start its daemon and pull the exported pinned `SANDBOX_IMAGE`
207
+ before use. Callback source and explicit JSON configuration cross stdin; host
208
+ closures and imports do not. Each invocation uses a nonroot, read-only container
209
+ without host mounts, host environment, external network or capabilities, with
210
+ memory/CPU/PID/file-descriptor limits. The host bounds request/output bytes and
211
+ execution time, then confirms container removal. Runtime checking and cleanup
212
+ each add at most five seconds to `timeoutMs`.
213
+
214
+ `SandboxRuntimeError.code` distinguishes `runtime_unavailable`,
215
+ `execution_stopped`, `execution_failed` and `cleanup_failed`. Unavailable Docker
216
+ or a missing image refuses execution; it never runs the guest as trusted code.
217
+ The image's own filesystem remains readable. Docker supplies the isolation;
218
+ this API does not claim virtual-machine isolation.
219
+
220
+ `definePlugin` and the other ordinary helpers execute trusted application code
221
+ synchronously in the calling Node process and have no execution deadline.
222
+ Freezing batches and auditing output do not isolate those callbacks. The measured
223
+ proxy handle remains for spike compatibility. `@vizejs/extension-sdk` is the
224
+ separate versioned WIT guest interface.
package/facts.d.ts ADDED
@@ -0,0 +1,348 @@
1
+ export interface TemplateScopeEntry {
2
+ readonly name: string;
3
+ readonly position: "value" | "key" | "index" | "slot";
4
+ }
5
+ export type BindingKind =
6
+ | "setup-let"
7
+ | "setup-maybe-ref"
8
+ | "setup-ref"
9
+ | "setup-reactive-const"
10
+ | "setup-const"
11
+ | "props"
12
+ | "props-aliased"
13
+ | "data"
14
+ | "options"
15
+ | "literal-const"
16
+ | "js-global-universal"
17
+ | "js-global-browser"
18
+ | "js-global-node"
19
+ | "js-global-deno"
20
+ | "js-global-bun"
21
+ | "vue-global"
22
+ | "external-module";
23
+ export type BindingFact =
24
+ | Readonly<{ scriptSetup: true }>
25
+ | Readonly<{
26
+ kind: BindingKind | null;
27
+ span: readonly [number, number] | null;
28
+ propKey: string | null;
29
+ }>;
30
+ export interface UndefinedRefFact {
31
+ readonly name: string;
32
+ readonly offset: number;
33
+ readonly context: string;
34
+ }
35
+ export interface UnusedBindingFact {
36
+ readonly span: readonly [number, number];
37
+ }
38
+ export interface FactRange {
39
+ readonly start: number;
40
+ readonly end: number;
41
+ }
42
+ export interface ComponentPropUsage extends FactRange {
43
+ readonly name: string;
44
+ readonly nameIsDynamic: boolean;
45
+ readonly value: string | null;
46
+ readonly isDynamic: boolean;
47
+ }
48
+ export interface ComponentEventUsage extends FactRange {
49
+ readonly name: string;
50
+ readonly nameIsDynamic: boolean;
51
+ readonly handler: string | null;
52
+ readonly modifiers: readonly string[];
53
+ }
54
+ export interface ComponentSlotUsage extends FactRange {
55
+ readonly name: string;
56
+ readonly nameIsDynamic: boolean;
57
+ readonly scopeVars: readonly string[];
58
+ readonly hasScope: boolean;
59
+ }
60
+ export interface ComponentSpreadUsage extends FactRange {
61
+ readonly expression: string;
62
+ }
63
+ export interface ComponentUsage extends FactRange {
64
+ readonly name: string;
65
+ readonly props: readonly ComponentPropUsage[];
66
+ readonly events: readonly ComponentEventUsage[];
67
+ readonly slots: readonly ComponentSlotUsage[];
68
+ readonly hasSpreadAttrs: boolean;
69
+ readonly spreadProps: readonly ComponentSpreadUsage[];
70
+ readonly scopeId: number;
71
+ readonly vifGuard: string | null;
72
+ }
73
+ export interface ComponentUsageSite {
74
+ readonly tag: string;
75
+ readonly usage: ComponentUsage | null;
76
+ readonly usageIndex: number | null;
77
+ readonly nameOrdinal: number | null;
78
+ }
79
+ export interface ComponentUsagesFact {
80
+ readonly module: string | null;
81
+ readonly export: string;
82
+ readonly sites: readonly ComponentUsageSite[];
83
+ }
84
+ export type ReactivityClass = "static" | "props-stable" | "reactive" | "unstable";
85
+ export type Verdict = "proven" | "refuted" | "unknown";
86
+ export type ReactivityEffect =
87
+ | "freeze"
88
+ | "capture"
89
+ | "read-prop"
90
+ | "read-reactive"
91
+ | "mutate-local"
92
+ | "mutate-global"
93
+ | "call-unknown"
94
+ | "allocate";
95
+ export type ReactiveSourceKind =
96
+ | "ref"
97
+ | "shallow-ref"
98
+ | "reactive"
99
+ | "shallow-reactive"
100
+ | "computed"
101
+ | "readonly"
102
+ | "shallow-readonly"
103
+ | "to-ref"
104
+ | "to-refs";
105
+ export type ReactivityLoss =
106
+ | Readonly<{
107
+ kind: "reactive-destructure" | "ref-value-destructure";
108
+ sourceName: string;
109
+ destructuredProps: readonly string[];
110
+ }>
111
+ | Readonly<{ kind: "ref-value-extract"; sourceName: string; targetName: string }>
112
+ | Readonly<{
113
+ kind: "reactive-property-extract";
114
+ sourceName: string;
115
+ propName: string;
116
+ targetName: string;
117
+ }>
118
+ | Readonly<{ kind: "props-destructure"; destructuredProps: readonly string[] }>
119
+ | Readonly<{
120
+ kind: "function-argument-extract";
121
+ sourceName: string;
122
+ argumentName: string;
123
+ calleeName: string;
124
+ }>
125
+ | Readonly<{
126
+ kind: "getter-call-extract";
127
+ contextName: string;
128
+ getterName: string;
129
+ targetName: string;
130
+ calleeName: string;
131
+ sourceName: string;
132
+ }>
133
+ | Readonly<{
134
+ kind: "plain-value-alias";
135
+ sourceName: string;
136
+ aliasName: string;
137
+ targetName: string;
138
+ }>
139
+ | Readonly<{ kind: "reactive-spread" | "reactive-reassign"; sourceName: string }>;
140
+ export type ReactivityFact =
141
+ | Readonly<{
142
+ kind: "source";
143
+ name: string;
144
+ sourceKind: ReactiveSourceKind;
145
+ declarationOffset: number;
146
+ class: ReactivityClass;
147
+ verdict: Verdict;
148
+ effects: readonly ReactivityEffect[];
149
+ }>
150
+ | Readonly<{
151
+ kind: "loss";
152
+ loss: ReactivityLoss;
153
+ start: number;
154
+ end: number;
155
+ }>;
156
+
157
+ export interface ProvideKey {
158
+ readonly kind: "string" | "symbol";
159
+ readonly name: string;
160
+ }
161
+ export type InjectPattern =
162
+ | Readonly<{ kind: "simple" }>
163
+ | Readonly<{ kind: "object" | "array"; props: readonly string[] }>
164
+ | Readonly<{ kind: "indirect"; injectVar: string; props: readonly string[]; offset: number }>;
165
+ export type ProvideInjectFact =
166
+ | Readonly<{
167
+ kind: "provide";
168
+ id: number;
169
+ key: ProvideKey;
170
+ value: string;
171
+ valueType: string | null;
172
+ fromComposable: string | null;
173
+ start: number;
174
+ end: number;
175
+ }>
176
+ | Readonly<{
177
+ kind: "inject";
178
+ key: ProvideKey;
179
+ localName: string;
180
+ defaultValue: string | null;
181
+ expectedType: string | null;
182
+ pattern: InjectPattern;
183
+ fromComposable: string | null;
184
+ start: number;
185
+ end: number;
186
+ }>
187
+ | Readonly<{
188
+ kind: "composable";
189
+ name: string;
190
+ source: string;
191
+ localName: string | null;
192
+ usesProvide: boolean;
193
+ usesInject: boolean;
194
+ usesReactivity: boolean;
195
+ start: number;
196
+ end: number;
197
+ }>;
198
+
199
+ export type RaceRisk =
200
+ | Readonly<{
201
+ kind: "async-watcher";
202
+ watcherName: string;
203
+ asyncOperation: string;
204
+ mutatedTargets: readonly string[];
205
+ }>
206
+ | Readonly<{
207
+ kind: "async-watch-effect" | "promise-continuation";
208
+ asyncOperation: string;
209
+ mutatedTargets: readonly string[];
210
+ }>
211
+ | Readonly<{
212
+ kind: "async-lifecycle";
213
+ hookName: string;
214
+ asyncOperation: string;
215
+ mutatedTargets: readonly string[];
216
+ }>
217
+ | Readonly<{ kind: "scheduled"; schedulerName: string; mutatedTargets: readonly string[] }>;
218
+ export interface RaceConditionFact extends FactRange {
219
+ readonly kind: RaceRisk;
220
+ }
221
+
222
+ export type UnresolvedTypeReason =
223
+ | "external-world-unavailable"
224
+ | "unbound"
225
+ | "missing-module"
226
+ | "incomplete-module"
227
+ | "unsupported-declaration"
228
+ | "unsupported-qualification"
229
+ | "ambiguous-export"
230
+ | "resolution-cycle"
231
+ | "resolution-limit";
232
+ export interface TypeDependency {
233
+ readonly name: string;
234
+ readonly kind: "alias" | "interface" | "imported" | "unresolved";
235
+ readonly module: string | null;
236
+ readonly export: string | null;
237
+ /** Authored type text, including literal whitespace. */
238
+ readonly body: string | null;
239
+ readonly parameters: string | null;
240
+ readonly extends: readonly string[];
241
+ readonly unresolved_reason: UnresolvedTypeReason | null;
242
+ }
243
+ export interface TypeEnvironment {
244
+ /** False means some reachable type information could not be resolved. */
245
+ readonly complete: boolean;
246
+ readonly declarations: readonly TypeDependency[];
247
+ }
248
+ export interface ExposeContract {
249
+ readonly name: string;
250
+ readonly type: string | null;
251
+ }
252
+ export interface SignatureContract {
253
+ readonly schema: 1;
254
+ readonly name: string;
255
+ readonly declared_name: string | null;
256
+ readonly generic: string | null;
257
+ readonly component_shape: "unspecified" | "class-api";
258
+ readonly script_setup: boolean;
259
+ readonly prop_type_arguments: string | null;
260
+ readonly emit_type_arguments: string | null;
261
+ readonly slot_type_arguments: string | null;
262
+ /** Public names in authored completion order, without folio key encoding. */
263
+ readonly props_complete: boolean;
264
+ readonly with_defaults: string | null;
265
+ readonly prop_order: readonly string[];
266
+ readonly slot_order: readonly string[];
267
+ readonly exposes_complete: boolean;
268
+ readonly exposes: readonly ExposeContract[];
269
+ readonly type_dependencies: TypeEnvironment;
270
+ }
271
+ export interface PropContract {
272
+ readonly schema: 1;
273
+ readonly name: string;
274
+ readonly type: string | null;
275
+ readonly required: boolean | null;
276
+ readonly default: string | null;
277
+ readonly model_modifiers: string | null;
278
+ readonly type_dependencies: TypeEnvironment;
279
+ }
280
+ export interface EmitContract {
281
+ readonly schema: 1;
282
+ readonly name: string;
283
+ readonly payload: string | null;
284
+ readonly overload_payloads: readonly (string | null)[];
285
+ readonly unresolved_type_arguments: string | null;
286
+ readonly validator_signatures: readonly string[];
287
+ readonly validator_type_annotations: readonly string[];
288
+ readonly type_dependencies: TypeEnvironment;
289
+ }
290
+ export interface SlotContract {
291
+ readonly schema: 1;
292
+ readonly name: string;
293
+ readonly props: string | null;
294
+ readonly type_dependencies: TypeEnvironment;
295
+ }
296
+ export type AlphaReactiveKind =
297
+ | "ref"
298
+ | "shallowRef"
299
+ | "reactive"
300
+ | "shallowReactive"
301
+ | "computed"
302
+ | "readonly"
303
+ | "shallowReadonly"
304
+ | "toRef"
305
+ | "toRefs";
306
+ export interface ReactivityContract {
307
+ readonly schema: 1;
308
+ readonly name: string;
309
+ readonly type: string | null;
310
+ readonly kind: AlphaReactiveKind | null;
311
+ readonly class: ReactivityClass | null;
312
+ readonly verdict: Verdict;
313
+ readonly effects: readonly ReactivityEffect[];
314
+ readonly type_dependencies: TypeEnvironment;
315
+ }
316
+ export interface ComponentContract {
317
+ readonly schema: 1;
318
+ readonly module: string | null;
319
+ readonly export: string;
320
+ }
321
+
322
+ /** Exact native group names and map key/value contracts. */
323
+ export interface FactGroups {
324
+ readonly templateScopes: ReadonlyMap<number, readonly TemplateScopeEntry[]>;
325
+ readonly bindings: ReadonlyMap<string, BindingFact>;
326
+ readonly "undefined-refs": ReadonlyMap<number, UndefinedRefFact>;
327
+ readonly "unused-bindings": ReadonlyMap<string, UnusedBindingFact>;
328
+ /** Keys are serialized JSON tuples [module | null, export]. */
329
+ readonly "component-usages": ReadonlyMap<string, ComponentUsagesFact>;
330
+ readonly reactivity: ReadonlyMap<`source:${number}` | `loss:${number}`, ReactivityFact>;
331
+ readonly "provide-inject": ReadonlyMap<
332
+ `provide:${number}` | `inject:${number}` | `composable:${number}`,
333
+ ProvideInjectFact
334
+ >;
335
+ readonly "race-conditions": ReadonlyMap<number, RaceConditionFact>;
336
+ /** Alpha keys are canonical folio declaration keys. */
337
+ readonly "component-signature": ReadonlyMap<string, SignatureContract>;
338
+ readonly "prop-types": ReadonlyMap<string, PropContract>;
339
+ readonly "emit-types": ReadonlyMap<string, EmitContract>;
340
+ readonly "slot-types": ReadonlyMap<string, SlotContract>;
341
+ readonly "reactivity-classes": ReadonlyMap<string, ReactivityContract>;
342
+ readonly "component-references": ReadonlyMap<string, ComponentContract>;
343
+ }
344
+ export type FactKey<K extends keyof FactGroups> =
345
+ FactGroups[K] extends ReadonlyMap<infer Key, unknown> ? Key : never;
346
+ export type FactValue<K extends keyof FactGroups> =
347
+ FactGroups[K] extends ReadonlyMap<unknown, infer Value> ? Value : never;
348
+ export type FactEntry<K extends keyof FactGroups> = readonly [FactKey<K>, FactValue<K>];
package/fixes.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface PluginFix {
2
+ ruleId?: string;
3
+ plugin?: string;
4
+ start: number;
5
+ end: number;
6
+ text: string;
7
+ }
8
+ /** Apply all nonoverlapping host-owned UTF-8 ranges, or throw without editing. */
9
+ export declare function applyFixes(source: string, fixes: readonly PluginFix[]): string;
package/fixes.js ADDED
@@ -0,0 +1,42 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ /** Apply host-owned UTF-8 ranges atomically; overlapping suggestions refuse. */
4
+ export function applyFixes(source, fixes) {
5
+ const bytes = Buffer.from(source);
6
+ const sorted = [...fixes].sort((left, right) => left.start - right.start || left.end - right.end);
7
+ const parts = [];
8
+ let cursor = 0;
9
+ let previous;
10
+ for (const fix of sorted) {
11
+ if (
12
+ !Number.isInteger(fix.start) ||
13
+ !Number.isInteger(fix.end) ||
14
+ fix.start < 0 ||
15
+ fix.end < fix.start ||
16
+ fix.end > bytes.length ||
17
+ typeof fix.text !== "string" ||
18
+ !boundary(bytes, fix.start) ||
19
+ !boundary(bytes, fix.end)
20
+ )
21
+ throw new RangeError("plugin autofix must use valid UTF-8 source boundaries");
22
+ if (
23
+ previous &&
24
+ fix.start === previous.start &&
25
+ fix.end === previous.end &&
26
+ fix.text === previous.text
27
+ )
28
+ continue;
29
+ if (fix.start < cursor || (previous && fix.start === previous.start)) {
30
+ throw new Error("overlapping plugin autofixes cannot be applied together");
31
+ }
32
+ parts.push(bytes.subarray(cursor, fix.start), Buffer.from(fix.text));
33
+ cursor = fix.end;
34
+ previous = fix;
35
+ }
36
+ parts.push(bytes.subarray(cursor));
37
+ return Buffer.concat(parts).toString();
38
+ }
39
+
40
+ function boundary(bytes, offset) {
41
+ return offset === bytes.length || (bytes[offset] & 0xc0) !== 0x80;
42
+ }
package/identity.js ADDED
@@ -0,0 +1,10 @@
1
+ // Cache identity includes the authoring runtime itself, including dirty edits.
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ const hash = createHash("sha256");
6
+ for (const file of ["index.js", "stages.js", "identity.js", "package.json"]) {
7
+ const bytes = readFileSync(new URL(file, import.meta.url));
8
+ hash.update(file).update(String(bytes.length)).update(bytes);
9
+ }
10
+ export const SDK_FINGERPRINT = hash.digest("hex");