@vestra-player/sdk 1.5.25 → 1.5.27

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.27
4
+
5
+ **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Plugins pick the fix up with the player's next SDK upgrade (the player builds the `plugin` object with its own copy); nothing in a plugin changes.
6
+
7
+ - **Generated streams reach their full frame rate on Windows.** A stream from `openStream` or a file resolver (the NES emulator: one 1/60 s picture per `frame()`) played at 23–26 frames a second on Windows instead of 60. The SDK asked the player how much audio the sound card had played, rendered at most one frame, and waited 5 ms before asking again; Windows rounds that wait up to its 15.6 ms timer tick, and with the round trip one pass took about 40 ms, so one frame per pass could not keep up. macOS timers are about 1 ms, which hid it. Each pass now calls `frame()` as many times in a row as it takes to refill the whole lead (about 50 ms of audio, unchanged, so latency is unchanged), at most 16 times, and stops early when a frame carries no audio; and a pass that rendered frames and already took longer than 5 ms is followed at once instead of after another timer tick. A pass that rendered nothing (paused, lead full) still waits 5 ms after it ends, so an idle or paused stream asks the player no more often than before. Measured against a real-time audio clock with the NES core and its homebrew ROM: with a 15–30 ms round trip 1.5.26 rendered 22–32 frames a second and let the device run dry, 1.5.27 renders 60 and keeps it fed; only a round trip near 50 ms (the whole lead) still lets it run dry now and then. Controls (start, pause, seek) still apply between frames and never while one renders, and `stop()` still ends after the frame being rendered.
8
+ - Docs: *Example: signal generator* says `frame()` may be called several times back to back. `llms.txt` and `llms-full.txt` regenerated.
9
+
10
+ ## 1.5.26
11
+
12
+ **Who must change: authors whose plugin uses a member without one of the capabilities it needs, and only to `check`, `build` or `pack` again.** No API changes, `apiVersion` stays `vestra/1`, no player version needed, and packages already built keep running as before: the player decides at run time exactly as it did. What changes is that `vestra-plugin check` (and so `build` and `pack`) now fails on such a use, including uses that never failed at run time: a listener (`onChange`) without `events` never fires instead of failing; a subtitle change (`select`, `load`, `remove`, `setStyle`) without `subtitles.read` fails only when it reads the current selection; a capability's **Also needs** (`processing.video` without `playback.read`) was documented but not enforced. Such a plugin packed with 1.5.25 and stops packing with 1.5.26 until the missing capability is added to `manifest.json` (or the use removed). The player's built-in plugins and the plugin repository's plugins were checked with this release: no findings.
13
+
14
+ - **Undeclared capabilities are caught while writing and packing** (user ruling of 2026-10-27, doc/process/rounds/2026-10-capability-check.md in the player repository): every use of an API member whose capability `manifest.json` does not declare, with `file:line:column`, the member, the capability and the request. Type-aware: found through aliases, destructuring, parameters typed `Player` and so on; not through computed names, `any` or plain JavaScript. A member also needs its capabilities' **Also needs** (`requires`). Tests are not checked.
15
+ - **`vestra-plugin check`** runs it after the type check and fails on any finding; `build` and `pack` run `check` first. **`check --json`** prints the findings as JSON.
16
+ - **`@vestra-player/sdk/ts-plugin`**, a TypeScript language-service plugin: the same findings as errors in the editor, with the quick fix "Add '<capability>' to manifest.json". List it in `tsconfig.json` (`"plugins": [{ "name": "@vestra-player/sdk/ts-plugin" }]`) and use the workspace's TypeScript (VS Code: **Use Workspace Version**).
17
+ - **One rule with the player.** The SDK carries a copy of the player's generated capability table (`src/host/capabilities.json`: which request needs which capability, by event name or processing stage where it depends on one) and the requests each member sends. `CAPABILITY_INFO` is now built from them; its contents are unchanged. `HOST_CAPABILITIES` (`@vestra-player/sdk/runtime`, for the player's agreement test) is the copy.
18
+ - Docs: *Capabilities* gains *Catching a missing capability*; *Getting started* adds the `tsconfig.json` with the plugin and the VS Code step; *Command line* (the rule and `--json`); *Subtitles* and *Capabilities*: changes also need `subtitles.read` (they read the current selection first; the table now says so). `examples/tsconfig.json` lists the plugin. `llms.txt` and `llms-full.txt` regenerated.
19
+
3
20
  ## 1.5.25
4
21
 
5
22
  **Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: `plugin.storage.json` and every other API do what they did. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.5.
@@ -11,6 +11,9 @@
11
11
  * directory. Works in any project that depends on @vestra-player/sdk.
12
12
  *
13
13
  * check strict type check of the plugin's .ts files and tests/*.ts
14
+ * (then every use of an API member whose capability manifest.json
15
+ * does not declare: file:line, member, capability; fails on any;
16
+ * --json prints the findings as JSON)
14
17
  * build check, then write dist/: .ts → .js with types stripped, other
15
18
  * package files copied, dist/manifest.json with `version`
16
19
  * test node --test on tests/*.test.{ts,mjs}
@@ -88,6 +91,9 @@ const i18n = await import(pathToFileURL(join(sdkRoot, "dist/i18n.js")).href);
88
91
  const i18nTools = await import(
89
92
  pathToFileURL(join(sdkRoot, "dist/i18n-tools.js")).href
90
93
  );
94
+ const capabilityCheck = await import(
95
+ pathToFileURL(join(sdkRoot, "dist/capability-check.js")).href
96
+ );
91
97
  const SDK = "@vestra-player/sdk";
92
98
 
93
99
  /** Never part of a production package. */
@@ -234,7 +240,7 @@ function run(command, args, options = {}) {
234
240
  }
235
241
 
236
242
  /** npm dependencies are installed from the lock file when missing. */
237
- function dependencies(target) {
243
+ function dependencies(target, quiet = false) {
238
244
  const deps = {
239
245
  ...target.packageJson.dependencies,
240
246
  ...target.packageJson.devDependencies,
@@ -244,12 +250,15 @@ function dependencies(target) {
244
250
  present(join(target.directory, "node_modules"))
245
251
  )
246
252
  return;
247
- console.log(`${target.name}: installing dependencies (npm ci)`);
253
+ (quiet ? console.error : console.log)(
254
+ `${target.name}: installing dependencies (npm ci)`,
255
+ );
248
256
  // npm is npm.cmd on Windows, which spawn only runs through a shell.
249
257
  if (
250
258
  run("npm", ["ci", "--no-audit", "--no-fund"], {
251
259
  cwd: target.directory,
252
260
  shell: process.platform === "win32",
261
+ ...(quiet ? { stdio: ["ignore", process.stderr, "inherit"] } : {}),
253
262
  }) !== 0
254
263
  )
255
264
  fail(
@@ -296,10 +305,10 @@ function checkConfig(target, sources) {
296
305
  };
297
306
  }
298
307
 
299
- async function check(target) {
300
- for (const hint of target.hints ?? [])
301
- console.log(`${target.name}: hint: ${hint}`);
302
- dependencies(target);
308
+ async function check(target, report = null) {
309
+ const say = report ? () => {} : (line) => console.log(line);
310
+ for (const hint of target.hints ?? []) say(`${target.name}: hint: ${hint}`);
311
+ dependencies(target, !!report);
303
312
  const own = (await files(target.directory)).filter(
304
313
  (file) => isTypeScript(file) || file.endsWith(".mts"),
305
314
  );
@@ -312,20 +321,41 @@ async function check(target) {
312
321
  join(target.directory, file),
313
322
  );
314
323
  if (!sources.length) {
315
- console.log(`${target.name}: no TypeScript to check`);
324
+ say(`${target.name}: no TypeScript to check`);
325
+ report?.push({ plugin: target.name, findings: [] });
316
326
  return;
317
327
  }
318
328
  const tsc = compiler("typescript/lib/tsc.js");
319
329
  const scratch = await mkdtemp(join(tmpdir(), "vestra-plugin-check-"));
330
+ let findings;
320
331
  try {
321
332
  const config = join(scratch, "tsconfig.json");
322
333
  await writeFile(config, JSON.stringify(checkConfig(target, sources)));
323
- if (run(process.execPath, [tsc, "-p", config]) !== 0)
334
+ if (
335
+ run(process.execPath, [tsc, "-p", config], {
336
+ stdio: report ? ["ignore", process.stderr, "inherit"] : "inherit",
337
+ }) !== 0
338
+ )
324
339
  fail(`${target.name}: type check failed`);
340
+ say(`${target.name}: type check passed (${sources.length} files)`);
341
+ findings = await capabilityFindings(target, config);
325
342
  } finally {
326
343
  await rm(scratch, { recursive: true, force: true });
327
344
  }
328
- console.log(`${target.name}: type check passed (${sources.length} files)`);
345
+ if (report) report.push({ plugin: target.name, findings });
346
+ else
347
+ for (const finding of findings)
348
+ console.error(
349
+ `${relative(process.cwd(), finding.file) || finding.file}:${finding.line}:${finding.column}: ${finding.message} (request ${finding.method})`,
350
+ );
351
+ if (findings.length) {
352
+ const missing = [...new Set(findings.map((each) => each.capability))];
353
+ fail(
354
+ `${target.name}: ${findings.length} use${findings.length === 1 ? "" : "s"} of undeclared capabilities; add ${missing.map((each) => `"${each}"`).join(", ")} to manifest.json "capabilities" (or stop using those members)`,
355
+ );
356
+ }
357
+ say(`${target.name}: capabilities declared for every API use`);
358
+ if (report) return;
329
359
  // Translation warnings never fail the check.
330
360
  if (present(join(target.directory, "locales"))) {
331
361
  try {
@@ -338,6 +368,37 @@ async function check(target) {
338
368
  }
339
369
  }
340
370
 
371
+ /**
372
+ * Uses of API members whose capability the manifest does not declare, in
373
+ * the plugin's production files (not tests/), with the type check's
374
+ * compiler options (src/capability-check.ts).
375
+ */
376
+ async function capabilityFindings(target, config) {
377
+ const ts = await typescript();
378
+ const parsed = ts.getParsedCommandLineOfConfigFile(
379
+ config,
380
+ {},
381
+ {
382
+ ...ts.sys,
383
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) =>
384
+ fail(
385
+ `${target.name}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
386
+ ),
387
+ },
388
+ );
389
+ const files = (await sourceFiles(target)).typescript.map((file) =>
390
+ join(target.directory, file),
391
+ );
392
+ if (!parsed || !files.length) return [];
393
+ const program = ts.createProgram(parsed.fileNames, parsed.options);
394
+ const capabilities = Array.isArray(target.manifest.capabilities)
395
+ ? target.manifest.capabilities
396
+ : [];
397
+ return capabilityCheck
398
+ .findUndeclaredCapabilities(ts, program, { files, capabilities })
399
+ .map((finding) => ({ ...finding, file: resolve(finding.file) }));
400
+ }
401
+
341
402
  /** The plugin's own production .ts / .mts and .slint files. */
342
403
  async function sourceFiles(target) {
343
404
  const all = await files(target.directory);
@@ -703,6 +764,7 @@ async function pack(target, flag, outDirectory) {
703
764
 
704
765
  const USAGE = [
705
766
  "usage: vestra-plugin <check|build|test|pack> [plugin-dir...] [--version x.y.z] [--out dir]",
767
+ " vestra-plugin check [plugin-dir...] --json",
706
768
  " vestra-plugin i18n extract [plugin-dir...] [--locale <lang>]...",
707
769
  " vestra-plugin i18n check [plugin-dir...]",
708
770
  ].join("\n");
@@ -740,6 +802,12 @@ async function main() {
740
802
  }
741
803
  if (locales.length && subcommand !== "extract")
742
804
  fail("--locale applies to i18n extract");
805
+ const jsonIndex = args.indexOf("--json");
806
+ const report = jsonIndex >= 0 ? [] : null;
807
+ if (report) {
808
+ args.splice(jsonIndex, 1);
809
+ if (command !== "check") fail("--json applies to check");
810
+ }
743
811
  const flag = option("--version");
744
812
  const out = option("--out");
745
813
  const directories = args.length ? args : ["."];
@@ -747,11 +815,12 @@ async function main() {
747
815
  fail("--version applies to a single plugin");
748
816
  const failed = [];
749
817
  for (const directory of directories) {
818
+ const reported = report?.length ?? 0;
750
819
  try {
751
820
  const target = await plugin(directory);
752
821
  if (subcommand === "extract") await i18nExtract(target, locales);
753
822
  else if (subcommand === "check") await i18nCheck(target);
754
- else if (command === "check") await check(target);
823
+ else if (command === "check") await check(target, report);
755
824
  else if (command === "build") await build(target, flag);
756
825
  else if (command === "pack") await pack(target, flag, out);
757
826
  else await test(target);
@@ -759,8 +828,14 @@ async function main() {
759
828
  if (!error.expected) throw error;
760
829
  console.error(`✖ ${error.message}`);
761
830
  failed.push(directory);
831
+ if (report) {
832
+ if (report.length === reported) report.push({ findings: [] });
833
+ report[reported].error = error.message;
834
+ }
762
835
  }
836
+ if (report) report[reported] = { directory, ...report[reported] };
763
837
  }
838
+ if (report) console.log(JSON.stringify(report, null, 2));
764
839
  if (failed.length) {
765
840
  console.error(
766
841
  `${subcommand ? `i18n ${subcommand}` : command} failed for: ${failed.join(", ")}`,
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Which player requests each member of the plugin API sends: the SDK's half
3
+ * of the capability rule (the player's half, what each request needs, is
4
+ * `HOST_CAPABILITIES`). A member is written `Interface.member` as in
5
+ * `CAPABILITY_INFO`; members declared on `Scope` are written `Plugin.…`.
6
+ *
7
+ * List the requests that decide the member's capabilities: the one it
8
+ * sends to do its work, and for a listener the `events.subscribe` with its
9
+ * event name. A string parameter that the player's rule reads (`event`,
10
+ * `stage`) is given in `params`. `also: true` marks a request whose
11
+ * capability the member needs on top of its own (documented at the member,
12
+ * not listed in its capability's `CAPABILITY_INFO.apis`).
13
+ *
14
+ * Members that need no capability are left out, apart from a few that show
15
+ * a rule's "none" case. Members of objects that another listed member
16
+ * returns (a `Window` from `Windows.create`, a `Panel` from `Panels.mount`)
17
+ * are left out too: the call that created the object already needs the
18
+ * capability.
19
+ *
20
+ * Order matters for `CAPABILITY_INFO`: each capability lists its members in
21
+ * this table's order. test/capability-rules.test.ts checks every method and
22
+ * parameter against the player's table.
23
+ *
24
+ * @internal
25
+ */
26
+ /** @internal One request a member sends. */
27
+ export interface ApiRequest {
28
+ /** The wire method (`source.submit`). */
29
+ readonly method: string;
30
+ /** String parameters the player's rule for the method reads. */
31
+ readonly params?: Readonly<Record<string, string>>;
32
+ /** A further capability, documented at the member. */
33
+ readonly also?: true;
34
+ }
35
+ /** @internal */
36
+ export declare const API_REQUESTS: Readonly<Record<string, readonly ApiRequest[]>>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Which player requests each member of the plugin API sends: the SDK's half
3
+ * of the capability rule (the player's half, what each request needs, is
4
+ * `HOST_CAPABILITIES`). A member is written `Interface.member` as in
5
+ * `CAPABILITY_INFO`; members declared on `Scope` are written `Plugin.…`.
6
+ *
7
+ * List the requests that decide the member's capabilities: the one it
8
+ * sends to do its work, and for a listener the `events.subscribe` with its
9
+ * event name. A string parameter that the player's rule reads (`event`,
10
+ * `stage`) is given in `params`. `also: true` marks a request whose
11
+ * capability the member needs on top of its own (documented at the member,
12
+ * not listed in its capability's `CAPABILITY_INFO.apis`).
13
+ *
14
+ * Members that need no capability are left out, apart from a few that show
15
+ * a rule's "none" case. Members of objects that another listed member
16
+ * returns (a `Window` from `Windows.create`, a `Panel` from `Panels.mount`)
17
+ * are left out too: the call that created the object already needs the
18
+ * capability.
19
+ *
20
+ * Order matters for `CAPABILITY_INFO`: each capability lists its members in
21
+ * this table's order. test/capability-rules.test.ts checks every method and
22
+ * parameter against the player's table.
23
+ *
24
+ * @internal
25
+ */
26
+ const submit = [{ method: "source.submit" }, { method: "source.operation" }];
27
+ const readsSubtitles = { method: "subtitles.state", also: true };
28
+ const listen = (event, also) => also
29
+ ? { method: "events.subscribe", params: { event }, also }
30
+ : { method: "events.subscribe", params: { event } };
31
+ /** @internal */
32
+ export const API_REQUESTS = Object.freeze({
33
+ "Player.state": [{ method: "source.state" }],
34
+ "Player.refresh": [{ method: "source.state" }],
35
+ "Player.stats": [{ method: "playback.stats" }],
36
+ "Player.chapters": [{ method: "player.chapters" }],
37
+ "Player.audioDevices": [{ method: "player.audioDevices" }],
38
+ "Player.onChange": [listen("source.state")],
39
+ "Player.open": submit,
40
+ "Player.openStream": [
41
+ { method: "producer.prepare" },
42
+ { method: "source.submit", also: true },
43
+ ],
44
+ "Player.setPaused": submit,
45
+ "Player.seek": submit,
46
+ "Player.setRate": submit,
47
+ "Player.restart": submit,
48
+ "Player.close": submit,
49
+ "Player.scrub": submit,
50
+ "Player.setOutput": [{ method: "playback.set" }],
51
+ "Player.provideSequence": [{ method: "sequence.set" }],
52
+ "Player.next": [{ method: "sequence.step" }],
53
+ "Player.previous": [{ method: "sequence.step" }],
54
+ "Player.addAudioTrack": [{ method: "audio.addTrack" }],
55
+ "Player.selectAudioTrack": [{ method: "audio.selectTrack" }],
56
+ "Player.step": [{ method: "player.step" }],
57
+ "Player.setStepSeek": [{ method: "player.setStepSeek" }],
58
+ "Player.setLoop": [{ method: "player.setLoop" }],
59
+ "Player.setAudioDevice": [{ method: "player.setAudioDevice" }],
60
+ "Power.preventSleep": [{ method: "power.preventSleep" }],
61
+ "Subtitles.state": [{ method: "subtitles.state" }],
62
+ "Subtitles.onChange": [
63
+ listen("source.state"),
64
+ { method: "subtitles.state" },
65
+ ],
66
+ "Subtitles.refresh": [{ method: "subtitles.state" }],
67
+ // Changes read the current selection first (`refresh`), unless
68
+ // `ifCurrent` pins one; `load` always does.
69
+ "Subtitles.select": [{ method: "subtitles.select" }, readsSubtitles],
70
+ "Subtitles.load": [{ method: "subtitles.upload.begin" }, readsSubtitles],
71
+ "Subtitles.remove": [{ method: "subtitles.remove" }, readsSubtitles],
72
+ "Subtitles.setStyle": [{ method: "subtitles.style" }, readsSubtitles],
73
+ "Timeline.marks": [{ method: "timeline.marks" }],
74
+ "Timeline.onChange": [listen("timeline.changed")],
75
+ "Timeline.layer": [{ method: "timeline.set" }],
76
+ "MainWindow.onChange": [listen("window.state")],
77
+ "MainWindow.setVideoArea": [{ method: "window.setVideoArea" }],
78
+ "MainWindow.setAlwaysOnTop": [{ method: "window.setAlwaysOnTop" }],
79
+ "MainWindow.setCursorAutoHide": [{ method: "window.setCursorAutoHide" }],
80
+ "MainWindow.setPictureInPicture": [
81
+ { method: "window.setPictureInPicture" },
82
+ ],
83
+ "Appearance.onChange": [listen("appearance.changed")],
84
+ "Appearance.set": [{ method: "appearance.set" }],
85
+ "Appearance.setLanguage": [{ method: "appearance.setLanguage" }],
86
+ "Drops.provide": [{ method: "drops.provide" }],
87
+ "Panels.mount": [{ method: "ui.mount" }],
88
+ "Panels.bottom": [{ method: "layout.state" }],
89
+ "Panels.sidebar": [{ method: "layout.state" }],
90
+ "Panels.backdrop": [{ method: "layout.state" }],
91
+ "PanelSlot.onChange": [{ method: "layout.state" }, listen("window.state")],
92
+ "Panel.onChange": [{ method: "layout.state" }, listen("window.state")],
93
+ "Panel.onDrop": [{ method: "drops.listen" }],
94
+ "BackdropSlot.onChange": [
95
+ { method: "layout.state" },
96
+ listen("window.state"),
97
+ ],
98
+ "Resolvers.add": [{ method: "sourceResolvers.register" }],
99
+ "Resolvers.onFileTypesChange": [listen("resolvers.fileTypes")],
100
+ "Commands.add": [{ method: "commands.register" }],
101
+ "Host.run": [{ method: "host.run" }],
102
+ "Windows.create": [{ method: "ui.open" }],
103
+ "Dialogs.confirm": [{ method: "dialogs.confirm" }],
104
+ "Dialogs.openFile": [{ method: "dialogs.open" }],
105
+ "Dialogs.openFiles": [{ method: "dialogs.open" }],
106
+ "Dialogs.openDirectory": [{ method: "dialogs.open" }],
107
+ "Dialogs.saveFile": [{ method: "dialogs.open" }],
108
+ "Packages.list": [{ method: "packages.list" }],
109
+ "Packages.onChange": [listen("packages.changed")],
110
+ "Packages.validate": [{ method: "packages.validate" }],
111
+ "Packages.validateArchive": [{ method: "packages.validateArchive" }],
112
+ "Packages.setEnabled": [{ method: "packages.setEnabled" }],
113
+ "Packages.reload": [{ method: "packages.reload" }],
114
+ "Packages.uninstall": [{ method: "packages.uninstall" }],
115
+ "Plugin.video.filter": [
116
+ { method: "processing.replace", params: { stage: "video" } },
117
+ listen("source.state", true),
118
+ ],
119
+ "Plugin.video.view": [{ method: "video.view.set" }],
120
+ "Plugin.video.render": [{ method: "video.render.set" }],
121
+ "Plugin.audio.filter": [
122
+ { method: "processing.replace", params: { stage: "audio" } },
123
+ ],
124
+ "MediaCapture.capture": [
125
+ { method: "snapshot.state" },
126
+ { method: "snapshot.capture" },
127
+ ],
128
+ "Snapshot.export": [{ method: "jobs.state" }, { method: "jobs.start" }],
129
+ "Osd.show": [{ method: "osd.show" }],
130
+ "Osd.panel": [{ method: "osd.panel" }],
131
+ "Clipboard.readText": [{ method: "clipboard.readText" }],
132
+ "Clipboard.writeText": [{ method: "clipboard.writeText" }],
133
+ "Clipboard.writeImage": [{ method: "clipboard.writeImage" }],
134
+ "Bindings.list": [{ method: "bindings.list" }],
135
+ "Bindings.set": [{ method: "bindings.set" }],
136
+ "Bindings.reset": [{ method: "bindings.reset" }],
137
+ "Bindings.onChange": [listen("bindings.changed")],
138
+ "Settings.page": [{ method: "settings.register" }],
139
+ "Settings.show": [{ method: "settings.show" }],
140
+ "Secrets.get": [{ method: "secrets.get" }],
141
+ "Secrets.set": [{ method: "secrets.set" }],
142
+ "Secrets.delete": [{ method: "secrets.delete" }],
143
+ });
@@ -38,8 +38,11 @@ export interface CapabilityInfo {
38
38
  /**
39
39
  * Every manifest capability the player knows (`vestra/1`), in the player's
40
40
  * order, with the members it opens, the capabilities they also need and the
41
- * store's wording. The player's tests check that the names agree with the
42
- * player's `host/src/plugins.rs` and the labels with its plugin store.
41
+ * store's wording. Built from the player's own capability table (its
42
+ * `src/host/capabilities.json`, copied into the SDK) and the requests each
43
+ * member sends, the same rule `vestra-plugin check` and the SDK's
44
+ * TypeScript plugin apply; the player's tests check that the copy is
45
+ * current.
43
46
  *
44
47
  * A member not listed under any capability needs none (`plugin.tools`,
45
48
  * `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui`,
@@ -62,7 +65,7 @@ export declare const CAPABILITY_INFO: Readonly<Record<string, CapabilityInfo>>;
62
65
  /**
63
66
  * Manifest capabilities the player knows (`vestra/1`): the keys of
64
67
  * {@link CAPABILITY_INFO}, in the player's order. The same list as the
65
- * player's `host/src/plugins.rs`; the player's tests check that they agree.
68
+ * player's `host/capabilities.json`; the player's tests check that they agree.
66
69
  */
67
70
  export declare const CAPABILITIES: readonly string[];
68
71
  /** The plugin API version this SDK implements (`manifest.apiVersion`). */