@openmixer/plugin-qualify 0.0.0-stage → 0.3.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.
Files changed (114) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +203 -2
  3. package/bin/plugin-qualify.mjs +77 -0
  4. package/dist/clap-loader.d.ts +147 -0
  5. package/dist/clap-loader.js +252 -0
  6. package/dist/clap-loader.js.map +1 -0
  7. package/dist/clap-qualify.d.ts +126 -0
  8. package/dist/clap-qualify.js +272 -0
  9. package/dist/clap-qualify.js.map +1 -0
  10. package/dist/host-profiles.d.ts +42 -0
  11. package/dist/host-profiles.js +106 -0
  12. package/dist/host-profiles.js.map +1 -0
  13. package/dist/hosting-codes.d.ts +62 -0
  14. package/dist/hosting-codes.js +72 -0
  15. package/dist/hosting-codes.js.map +1 -0
  16. package/dist/hosting-suitability.d.ts +354 -0
  17. package/dist/hosting-suitability.js +654 -0
  18. package/dist/hosting-suitability.js.map +1 -0
  19. package/dist/index.d.ts +17 -0
  20. package/dist/index.js +20 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/loader.d.ts +26 -0
  23. package/dist/loader.js +9 -0
  24. package/dist/loader.js.map +1 -0
  25. package/dist/lv2-loader.d.ts +7 -0
  26. package/dist/lv2-loader.js +50 -0
  27. package/dist/lv2-loader.js.map +1 -0
  28. package/dist/measurement/cli.d.ts +168 -0
  29. package/dist/measurement/cli.js +353 -0
  30. package/dist/measurement/cli.js.map +1 -0
  31. package/dist/measurement/fixtures.d.ts +23 -0
  32. package/dist/measurement/fixtures.js +129 -0
  33. package/dist/measurement/fixtures.js.map +1 -0
  34. package/dist/measurement/format.d.ts +483 -0
  35. package/dist/measurement/format.js +627 -0
  36. package/dist/measurement/format.js.map +1 -0
  37. package/dist/measurement/guards.d.ts +33 -0
  38. package/dist/measurement/guards.js +61 -0
  39. package/dist/measurement/guards.js.map +1 -0
  40. package/dist/measurement/index.d.ts +36 -0
  41. package/dist/measurement/index.js +39 -0
  42. package/dist/measurement/index.js.map +1 -0
  43. package/dist/measurement/merge.d.ts +143 -0
  44. package/dist/measurement/merge.js +315 -0
  45. package/dist/measurement/merge.js.map +1 -0
  46. package/dist/measurement/plan.d.ts +127 -0
  47. package/dist/measurement/plan.js +141 -0
  48. package/dist/measurement/plan.js.map +1 -0
  49. package/dist/measurement/probe-json.d.ts +61 -0
  50. package/dist/measurement/probe-json.js +253 -0
  51. package/dist/measurement/probe-json.js.map +1 -0
  52. package/dist/measurement/provenance.d.ts +91 -0
  53. package/dist/measurement/provenance.js +173 -0
  54. package/dist/measurement/provenance.js.map +1 -0
  55. package/dist/measurement/sweep-jsonl.d.ts +40 -0
  56. package/dist/measurement/sweep-jsonl.js +154 -0
  57. package/dist/measurement/sweep-jsonl.js.map +1 -0
  58. package/dist/measurement/vocabulary.d.ts +125 -0
  59. package/dist/measurement/vocabulary.js +217 -0
  60. package/dist/measurement/vocabulary.js.map +1 -0
  61. package/dist/qualify-cli.d.ts +83 -0
  62. package/dist/qualify-cli.js +224 -0
  63. package/dist/qualify-cli.js.map +1 -0
  64. package/dist/qualify-declarations.d.ts +98 -0
  65. package/dist/qualify-declarations.js +76 -0
  66. package/dist/qualify-declarations.js.map +1 -0
  67. package/dist/qualify-options.d.ts +22 -0
  68. package/dist/qualify-options.js +36 -0
  69. package/dist/qualify-options.js.map +1 -0
  70. package/dist/rating.d.ts +32 -0
  71. package/dist/rating.js +30 -0
  72. package/dist/rating.js.map +1 -0
  73. package/dist/tool-paths.d.ts +12 -0
  74. package/dist/tool-paths.js +29 -0
  75. package/dist/tool-paths.js.map +1 -0
  76. package/fixtures/clap-scan/controls.json +8 -0
  77. package/fixtures/clap-scan/fake.json +16 -0
  78. package/fixtures/clap-scan/mixed.json +18 -0
  79. package/fixtures/zero-path.lv2/manifest.ttl +3 -0
  80. package/fixtures/zero-path.lv2/zero-path.c +85 -0
  81. package/fixtures/zero-path.lv2/zero-path.ttl +21 -0
  82. package/package.json +91 -3
  83. package/src/clap-loader.ts +393 -0
  84. package/src/clap-qualify.ts +387 -0
  85. package/src/host-profiles.ts +122 -0
  86. package/src/hosting-codes.ts +71 -0
  87. package/src/hosting-suitability.ts +982 -0
  88. package/src/index.ts +19 -0
  89. package/src/loader.ts +30 -0
  90. package/src/lv2-loader.ts +50 -0
  91. package/src/measurement/cli.ts +458 -0
  92. package/src/measurement/fixtures.ts +141 -0
  93. package/src/measurement/format.ts +1064 -0
  94. package/src/measurement/guards.ts +64 -0
  95. package/src/measurement/index.ts +39 -0
  96. package/src/measurement/merge.ts +434 -0
  97. package/src/measurement/plan.ts +219 -0
  98. package/src/measurement/probe-json.ts +271 -0
  99. package/src/measurement/provenance.ts +220 -0
  100. package/src/measurement/sweep-jsonl.ts +173 -0
  101. package/src/measurement/vocabulary.ts +298 -0
  102. package/src/qualify-cli.ts +277 -0
  103. package/src/qualify-declarations.ts +117 -0
  104. package/src/qualify-options.ts +49 -0
  105. package/src/rating.ts +53 -0
  106. package/src/tool-paths.ts +32 -0
  107. package/tools/_declarations.py +21 -0
  108. package/tools/benchmark.py +2462 -0
  109. package/tools/gen-python-declarations.d.mts +4 -0
  110. package/tools/gen-python-declarations.mjs +92 -0
  111. package/tools/hosting-sweep.mjs +51 -0
  112. package/tools/lv2-measure.mjs +204 -0
  113. package/tools/lv2_presets.py +132 -0
  114. package/tools/scan.py +432 -0
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * EVERY NUMBER plugin-qualify uses, declared once. The CLI's defaults and `--help`, the rating, and the Python tools (through the generated
5
+ * `tools/_declarations.py`) all READ these; a consumer holding its own copy is what the package's
6
+ * perturbation test catches (R-094).
7
+ */
8
+ /**
9
+ * The reference rate cost is quoted at: the rig's rate, 96 kHz.
10
+ */
11
+ export const COST_REFERENCE_RATE = 96000;
12
+ /**
13
+ * The reference quantum in frames: the rig's buffer, and the block `benchmark.py` runs.
14
+ */
15
+ export const QUANTUM_FRAMES = 1024;
16
+ /** The rate the offline capture window is sized at; other rates scale from it. */
17
+ export const MEASURE_BASE_RATE = 48000;
18
+ /**
19
+ * The lowest rate the console runs a show at. A reading below it is informational for the scaling
20
+ * class: kept in `perRate` and served at its own rate, but never alone enough to flag a plugin
21
+ * `nonconforming` (`lv2-measurement-interchange.md` §1a).
22
+ */
23
+ export const OPERATIONAL_RATE_FLOOR = 48000;
24
+ /** The rates a run measures when `--rates` is not given — recorded in `run.rates`. */
25
+ export const QUALIFY_RATES = [44100, MEASURE_BASE_RATE, COST_REFERENCE_RATE, 192000];
26
+ /** The quanta (frames) a run rates cost at when `--quanta` is not given. */
27
+ export const QUALIFY_QUANTA = [128, 256, 512, QUANTUM_FRAMES];
28
+ /** Seconds one plugin's offline pass may take before its worker is killed and it reads as a timeout. */
29
+ export const PLUGIN_TIMEOUT_S = 15;
30
+ /** Seconds the offline worker may take to start (lilv world load on a large LV2_PATH). */
31
+ export const BOOT_TIMEOUT_S = 120;
32
+ /** Seconds one plugin's cost pass may take (a sustained tone runs longer than an impulse). */
33
+ export const COST_TIMEOUT_S = 60;
34
+ /**
35
+ * The ONE setting that locates the CLAP scanner (spec §7): the environment variable that names
36
+ * the `omx-clap-scan` binary, and what is run when it is unset (looked up on `PATH`).
37
+ */
38
+ export const CLAP_SCAN_TOOL_ENV = "OMX_CLAP_SCAN";
39
+ export const CLAP_SCAN_TOOL_DEFAULT = "omx-clap-scan";
40
+ /** Seconds one run of the CLAP scanner may take over everything it was pointed at. */
41
+ export const CLAP_SCAN_TIMEOUT_S = 300;
42
+ /**
43
+ * The CLAP prober (`native/clap-qualify.c`) and the interposer it runs under (`native/rt-interpose.c`):
44
+ * each located by one environment variable, else the package's own build in `native/build/`.
45
+ */
46
+ export const CLAP_QUALIFY_TOOL_ENV = "OMX_CLAP_QUALIFY";
47
+ export const RT_INTERPOSE_ENV = "OMX_RT_INTERPOSE";
48
+ /** The CLAP sweep: blocks of `CLAP_SWEEP_FRAMES` frames per rate, every parameter moved through events. */
49
+ export const CLAP_SWEEP_BLOCKS = 512;
50
+ export const CLAP_SWEEP_FRAMES = 512;
51
+ /** Seconds one `.clap` may take to qualify at every rate before the prober is killed. */
52
+ export const CLAP_QUALIFY_TIMEOUT_S = 120;
53
+ /**
54
+ * Defaults. The cycle floor is the operator's ruling of 2026-09-05 (§14.4, option c) and the
55
+ * soak is §3's default show length. The per-instance cost ceiling is the HOST's budget and
56
+ * lives in {@link HOST_BUDGETS} (plugin-qualify §3a), not here.
57
+ */
58
+ export const DEFAULT_HOSTING_POLICY = {
59
+ cycleFloor: 1000,
60
+ soakSeconds: 3 * 60 * 60,
61
+ costTailRatioCeiling: 4,
62
+ soakDeadDropDb: 40,
63
+ };
64
+ /**
65
+ * Each shipped host profile's budget, by profile id. `openmixer-console` is §14.2's 5 % per
66
+ * instance at the rig's rate and buffer. `mod-host` runs a whole pedalboard on one JACK thread,
67
+ * so it gets the same per-instance share, at 48 kHz / 128 frames. `jalv` gives each plugin its own
68
+ * process and client, so a quarter core per instance, at 48 kHz / 256 frames. The last two are
69
+ * declared defaults, not a measurement of anyone's board — a project brings its own profile file.
70
+ */
71
+ export const HOST_BUDGETS = {
72
+ "openmixer-console": { coreFractionCeiling: 0.05, rate: COST_REFERENCE_RATE, quantum: QUANTUM_FRAMES },
73
+ jalv: { coreFractionCeiling: 0.25, rate: MEASURE_BASE_RATE, quantum: 256 },
74
+ "mod-host": { coreFractionCeiling: 0.05, rate: MEASURE_BASE_RATE, quantum: 128 },
75
+ };
76
+ //# sourceMappingURL=qualify-declarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualify-declarations.js","sourceRoot":"","sources":["../src/qualify-declarations.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,sDAAsD;AACtD;;;;GAIG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAEzC;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAE5C,sFAAsF;AACtF,MAAM,CAAC,MAAM,aAAa,GAAsB,CAAC,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAExG,4EAA4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC;AAEjF,wGAAwG;AACxG,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC,0FAA0F;AAC1F,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC,8FAA8F;AAC9F,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAClD,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAC;AAEtD,sFAAsF;AACtF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AACxD,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD,2GAA2G;AAC3G,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AACrC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAsB1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,UAAU,EAAE,IAAI;IAChB,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE;IACxB,oBAAoB,EAAE,CAAC;IACvB,cAAc,EAAE,EAAE;CACnB,CAAC;AAOF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,mBAAmB,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,cAAc,EAAE;IACtG,IAAI,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,GAAG,EAAE;IAC1E,UAAU,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,GAAG,EAAE;CACzB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /** How the result is printed. `text` = plugin, verdict, the reason that decided it. */
2
+ export declare const OUTPUT_FORMATS: readonly ["json", "text"];
3
+ export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
4
+ /** The exit codes, as the spec states them. */
5
+ export declare const EXIT_CODE: {
6
+ /** Every named plugin rated `suitable`. */
7
+ readonly allQualified: 0;
8
+ /** At least one plugin measured and rated below `suitable`. */
9
+ readonly someNotQualified: 1;
10
+ /** A plugin could not be measured, or the arguments/targets could not be resolved. */
11
+ readonly couldNotMeasure: 2;
12
+ };
13
+ export interface QualifyOption {
14
+ /** `--rates`; a positional is not an option. */
15
+ readonly flag: string;
16
+ /** The value's name when the option takes one. */
17
+ readonly value?: string;
18
+ readonly describe: string;
19
+ /** The declared default, printed by `--help` — absent for a switch. */
20
+ readonly default?: string;
21
+ }
22
+ export declare const QUALIFY_OPTIONS: readonly QualifyOption[];
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The `plugin-qualify` CLI's OPTIONS, declared once. The parser, `--help` and the tests all read this table; every default is a declaration
5
+ * from `declarations.ts`, never a literal here.
6
+ */
7
+ import { QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
8
+ import { HOST_PROFILES, OPENMIXER_CONSOLE_PROFILE } from "./host-profiles.js";
9
+ /** How the result is printed. `text` = plugin, verdict, the reason that decided it. */
10
+ export const OUTPUT_FORMATS = ["json", "text"];
11
+ /** The exit codes, as the spec states them. */
12
+ export const EXIT_CODE = {
13
+ /** Every named plugin rated `suitable`. */
14
+ allQualified: 0,
15
+ /** At least one plugin measured and rated below `suitable`. */
16
+ someNotQualified: 1,
17
+ /** A plugin could not be measured, or the arguments/targets could not be resolved. */
18
+ couldNotMeasure: 2,
19
+ };
20
+ export const QUALIFY_OPTIONS = [
21
+ { flag: "--all", describe: "every plugin lilv finds (LV2_PATH honoured)" },
22
+ { flag: "--rates", value: "list", describe: "sample rates to measure at, comma-separated", default: QUALIFY_RATES.join(",") },
23
+ { flag: "--quanta", value: "list", describe: "quanta (frames) to rate cost at, comma-separated", default: QUALIFY_QUANTA.join(",") },
24
+ { flag: "--out", value: "dir", describe: "write one interchange JSON document per run into this directory" },
25
+ { flag: "--format", value: OUTPUT_FORMATS.join("|"), describe: "json = the verdicts as JSON; text = plugin, verdict, deciding reason", default: "text" },
26
+ { flag: "--isolate", describe: "a fresh worker per plugin, so a crash or hang takes only that plugin" },
27
+ { flag: "--no-host-sweep", describe: "offline lilv measurement only; no mod-host hosting sweep" },
28
+ {
29
+ flag: "--host-profile",
30
+ value: "file|id",
31
+ describe: `judge against this host (${HOST_PROFILES.map((p) => p.id).join(", ")}, or a profile JSON file); repeat or comma-separate for one verdict per profile`,
32
+ default: OPENMIXER_CONSOLE_PROFILE.id,
33
+ },
34
+ { flag: "--help", describe: "this text" },
35
+ ];
36
+ //# sourceMappingURL=qualify-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualify-options.js","sourceRoot":"","sources":["../src/qualify-options.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,sDAAsD;AACtD;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE9E,uFAAuF;AACvF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAU,CAAC;AAGxD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,2CAA2C;IAC3C,YAAY,EAAE,CAAC;IACf,+DAA+D;IAC/D,gBAAgB,EAAE,CAAC;IACnB,sFAAsF;IACtF,eAAe,EAAE,CAAC;CACV,CAAC;AAYX,MAAM,CAAC,MAAM,eAAe,GAA6B;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,6CAA6C,EAAE;IAC1E,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,6CAA6C,EAAE,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;IAC7H,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,kDAAkD,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;IACpI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iEAAiE,EAAE;IAC5G,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,sEAAsE,EAAE,OAAO,EAAE,MAAM,EAAE;IACxJ,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,sEAAsE,EAAE;IACvG,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,0DAA0D,EAAE;IACjG;QACE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,SAAS;QAChB,QAAQ,EAAE,4BAA4B,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,iFAAiF;QAChK,OAAO,EAAE,yBAAyB,CAAC,EAAE;KACtC;IACD,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE;CAC1C,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { VerdictRating } from "./measurement/vocabulary.js";
2
+ /**
3
+ * How well a plugin fits a destination role. The four words are the interchange's
4
+ * (`VERDICT_RATINGS`, `measurement/vocabulary.ts`), so a written verdict speaks this vocabulary.
5
+ *
6
+ * - `suitable` — every dimension resolved and inside its budget.
7
+ * - `conditional` — nothing objectively disqualifies it, but something needs the operator's
8
+ * attention: a taste warning, a topology difference, a look-ahead control that can be
9
+ * turned up past the budget, a cost that eats most of the allowance.
10
+ * - `unknown` — an OBJECTIVE dimension could not be decided because the measurement is
11
+ * missing or does not resolve at this rate. Not a pass and not a failure: an admission.
12
+ * - `unsuitable` — an objective dimension failed on measured evidence.
13
+ */
14
+ export type SuitabilityRating = VerdictRating;
15
+ /**
16
+ * Severity order for folding several reasons into one verdict:
17
+ * `unsuitable` > `unknown` > `conditional` > `suitable`.
18
+ *
19
+ * `unknown` outranks `conditional` on purpose. A conditional plugin is one the console has
20
+ * fully measured and has an opinion about; an unknown one is a plugin the console cannot
21
+ * vouch for at all. Reporting "there is a caution" while an objective dimension is
22
+ * un-measured would present the smaller finding as the whole story.
23
+ */
24
+ export declare const RATING_SEVERITY: Readonly<Record<SuitabilityRating, number>>;
25
+ /** The worst (most severe) of some ratings; `suitable` for an empty list. */
26
+ export declare function worstRating(ratings: readonly SuitabilityRating[]): SuitabilityRating;
27
+ /**
28
+ * Physics or taste. The distinction drives what a reason is ALLOWED to do
29
+ * (`contributedRating` in the catalog, `contributedHostingRating` here) and, downstream, whether an operator override is a formality
30
+ * or a decision — conflating them is how a tool becomes either nagging or dangerous.
31
+ */
32
+ export type SuitabilityKind = "objective" | "advisory";
package/dist/rating.js ADDED
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ //
4
+ // The rating vocabulary every verdict speaks: four ratings, their severity order, the
5
+ // worst-wins fold, and whether a reason is physics or taste.
6
+ /**
7
+ * Severity order for folding several reasons into one verdict:
8
+ * `unsuitable` > `unknown` > `conditional` > `suitable`.
9
+ *
10
+ * `unknown` outranks `conditional` on purpose. A conditional plugin is one the console has
11
+ * fully measured and has an opinion about; an unknown one is a plugin the console cannot
12
+ * vouch for at all. Reporting "there is a caution" while an objective dimension is
13
+ * un-measured would present the smaller finding as the whole story.
14
+ */
15
+ export const RATING_SEVERITY = {
16
+ suitable: 0,
17
+ conditional: 1,
18
+ unknown: 2,
19
+ unsuitable: 3,
20
+ };
21
+ /** The worst (most severe) of some ratings; `suitable` for an empty list. */
22
+ export function worstRating(ratings) {
23
+ let worst = "suitable";
24
+ for (const rating of ratings) {
25
+ if (RATING_SEVERITY[rating] > RATING_SEVERITY[worst])
26
+ worst = rating;
27
+ }
28
+ return worst;
29
+ }
30
+ //# sourceMappingURL=rating.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rating.js","sourceRoot":"","sources":["../src/rating.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,sDAAsD;AACtD,EAAE;AACF,sFAAsF;AACtF,6DAA6D;AAkB7D;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAgD;IAC1E,QAAQ,EAAE,CAAC;IACX,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;CACd,CAAC;AAEF,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,OAAqC;IAC/D,IAAI,KAAK,GAAsB,UAAU,CAAC;IAC1C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,eAAe,CAAC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC;YAAE,KAAK,GAAG,MAAM,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `tools/` — `scan.py`, `benchmark.py`, `lv2-measure.mjs`, `hosting-sweep.mjs`. Resolved when a
3
+ * console asks, never at import: a consumer that only reads this package's declarations from a
4
+ * browser-like module graph (web-ui's conformance suites, `http:` module URLs) must load it.
5
+ */
6
+ export declare function toolsDir(): string;
7
+ /** The measurement runner a console spawns for Setup > Plugin analysis. */
8
+ export declare function lv2MeasureToolPath(): string;
9
+ /** The package's own build of the CLAP prober (`make -C native`). */
10
+ export declare function clapQualifyToolPath(): string;
11
+ /** The package's own build of the interposer the CLAP prober runs under. */
12
+ export declare function rtInterposePath(): string;
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * Where the package's own tools live, resolved from this module so it follows the package
5
+ * wherever it is installed (a checkout, an RPM's bundled node_modules, a pnpm store). The
6
+ * tools are shipped RUNTIME artefacts, not dev scripts.
7
+ */
8
+ import { fileURLToPath } from "node:url";
9
+ /**
10
+ * `tools/` — `scan.py`, `benchmark.py`, `lv2-measure.mjs`, `hosting-sweep.mjs`. Resolved when a
11
+ * console asks, never at import: a consumer that only reads this package's declarations from a
12
+ * browser-like module graph (web-ui's conformance suites, `http:` module URLs) must load it.
13
+ */
14
+ export function toolsDir() {
15
+ return fileURLToPath(new URL("../tools/", import.meta.url));
16
+ }
17
+ /** The measurement runner a console spawns for Setup > Plugin analysis. */
18
+ export function lv2MeasureToolPath() {
19
+ return fileURLToPath(new URL("../tools/lv2-measure.mjs", import.meta.url));
20
+ }
21
+ /** The package's own build of the CLAP prober (`make -C native`). */
22
+ export function clapQualifyToolPath() {
23
+ return fileURLToPath(new URL("../native/build/clap-qualify", import.meta.url));
24
+ }
25
+ /** The package's own build of the interposer the CLAP prober runs under. */
26
+ export function rtInterposePath() {
27
+ return fileURLToPath(new URL("../native/build/rt_interpose.so", import.meta.url));
28
+ }
29
+ //# sourceMappingURL=tool-paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-paths.js","sourceRoot":"","sources":["../src/tool-paths.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,sDAAsD;AACtD;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,QAAQ;IACtB,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB;IAChC,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,mBAAmB;IACjC,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,8BAA8B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe;IAC7B,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,iCAAiC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpF,CAAC"}
@@ -0,0 +1,8 @@
1
+ {"scanner":"clap-qualify","version":"1","files":[
2
+ {"path":"controls.clap","plugins":[
3
+ {"id":"org.freemixer.plugin-qualify.fixture.controls","name":"controls","vendor":"plugin-qualify","version":"1","description":"plugin-qualify fixture","url":"","features":["audio-effect"],"params":[
4
+ {"id":3,"name":"shape","module":"Oscillator","min":0,"max":2,"default":0,"flags":["stepped","automatable","enum"],"labels":[{"value":0,"label":"Sine"},{"value":1,"label":"Square"},{"value":2,"label":"Saw"}]},
5
+ {"id":5,"name":"cutoff","module":"Filter/Envelope","min":20,"max":20000,"default":1000,"flags":["automatable"]}
6
+ ],"remote_controls":[{"id":7,"section":"Main","name":"Performance","params":[5,3,null,null,null,null,null,null],"forPreset":false}],"audio_ports":{"inputs":[{"id":0,"name":"in","role":"main","channels":2}],"outputs":[{"id":0,"name":"out","role":"main","channels":2}]},"note_ports":{"inputs":0,"outputs":0}}
7
+ ]}
8
+ ]}
@@ -0,0 +1,16 @@
1
+ {"scanner":"omx-clap-scan","version":"0.1.0","files":[
2
+ {"path":"fake.clap","plugins":[
3
+ {"id":"org.omx-clap-host.test.wide","name":"wide","vendor":"omx-clap-host","version":"0","description":"wide","url":"","features":["audio-effect"],"params":[
4
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
5
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":4}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":4}]},"note_ports":{"inputs":0,"outputs":0},"latency":64},
6
+ {"id":"org.omx-clap-host.test.sidechain","name":"sidechain","vendor":"omx-clap-host","version":"0","description":"sidechain","url":"","features":["audio-effect"],"params":[
7
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
8
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2},{"id":1,"name":"in 1","role":"aux","channels":1}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":0,"outputs":0},"latency":64},
9
+ {"id":"org.omx-clap-host.test.notes","name":"notes","vendor":"omx-clap-host","version":"0","description":"notes","url":"","features":["audio-effect"],"params":[
10
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
11
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":1,"outputs":0},"latency":64},
12
+ {"id":"org.omx-clap-host.test.passthrough","name":"passthrough","vendor":"omx-clap-host","version":"0","description":"passthrough","url":"","features":["audio-effect"],"params":[
13
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
14
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":0,"outputs":0},"latency":64}
15
+ ]}
16
+ ]}
@@ -0,0 +1,18 @@
1
+ {"scanner":"omx-clap-scan","version":"0.1.0","files":[
2
+ {"path":"./a_crash.clap","error":"crashed with signal 6 (Aborted)"},
3
+ {"path":"./broken.clap","error":"can't open ./broken.clap: ./broken.clap: file too short"},
4
+ {"path":"./fake.clap","plugins":[
5
+ {"id":"org.omx-clap-host.test.wide","name":"wide","vendor":"omx-clap-host","version":"0","description":"wide","url":"","features":["audio-effect"],"params":[
6
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
7
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":4}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":4}]},"note_ports":{"inputs":0,"outputs":0},"latency":64},
8
+ {"id":"org.omx-clap-host.test.sidechain","name":"sidechain","vendor":"omx-clap-host","version":"0","description":"sidechain","url":"","features":["audio-effect"],"params":[
9
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
10
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2},{"id":1,"name":"in 1","role":"aux","channels":1}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":0,"outputs":0},"latency":64},
11
+ {"id":"org.omx-clap-host.test.notes","name":"notes","vendor":"omx-clap-host","version":"0","description":"notes","url":"","features":["audio-effect"],"params":[
12
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
13
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":1,"outputs":0},"latency":64},
14
+ {"id":"org.omx-clap-host.test.passthrough","name":"passthrough","vendor":"omx-clap-host","version":"0","description":"passthrough","url":"","features":["audio-effect"],"params":[
15
+ {"id":0,"name":"latency","module":"","min":0,"max":4096,"default":64,"flags":["stepped"]}
16
+ ],"audio_ports":{"inputs":[{"id":0,"name":"in 0","role":"main","channels":2}],"outputs":[{"id":0,"name":"out 0","role":"main","channels":2}]},"note_ports":{"inputs":0,"outputs":0},"latency":64}
17
+ ]}
18
+ ]}
@@ -0,0 +1,3 @@
1
+ @prefix lv2: <http://lv2plug.in/ns/lv2core#> .
2
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
3
+ <urn:openmixer:eval:zero-path> a lv2:Plugin ; lv2:binary <zero-path.so> ; rdfs:seeAlso <zero-path.ttl> .
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /* Evaluation fixture: an audio
4
+ * pass-through that emits, on its notify port, a patch:Set whose value is an atom:Path of body
5
+ * size ZERO — what NAM does with no model loaded (mod-host PR #98, 7e1f143). Emitted on the first
6
+ * cycle after activate and then every 32 cycles, so a racked instance keeps feeding mod-host's
7
+ * feedback relay (RunPostPonedEvents, POSTPONED_PROCESS_OUTPUT_BUFFER). */
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <lv2/core/lv2.h>
11
+ #include <lv2/atom/atom.h>
12
+ #include <lv2/atom/forge.h>
13
+ #include <lv2/atom/util.h>
14
+ #include <lv2/patch/patch.h>
15
+ #include <lv2/urid/urid.h>
16
+
17
+ #define ZP_URI "urn:openmixer:eval:zero-path"
18
+ enum { P_CONTROL = 0, P_NOTIFY = 1, P_IN = 2, P_OUT = 3 };
19
+
20
+ typedef struct {
21
+ const LV2_Atom_Sequence *control;
22
+ LV2_Atom_Sequence *notify;
23
+ const float *in;
24
+ float *out;
25
+ LV2_Atom_Forge forge;
26
+ LV2_URID patch_Set, patch_property, patch_value, atom_Path, model;
27
+ unsigned cycle;
28
+ } Z;
29
+
30
+ static LV2_Handle instantiate(const LV2_Descriptor *d, double rate, const char *path,
31
+ const LV2_Feature *const *features)
32
+ {
33
+ LV2_URID_Map *map = NULL;
34
+ for (int i = 0; features[i]; i++)
35
+ if (!strcmp(features[i]->URI, LV2_URID__map)) map = features[i]->data;
36
+ if (!map) return NULL;
37
+ Z *z = calloc(1, sizeof(Z));
38
+ lv2_atom_forge_init(&z->forge, map);
39
+ z->patch_Set = map->map(map->handle, LV2_PATCH__Set);
40
+ z->patch_property = map->map(map->handle, LV2_PATCH__property);
41
+ z->patch_value = map->map(map->handle, LV2_PATCH__value);
42
+ z->atom_Path = map->map(map->handle, LV2_ATOM__Path);
43
+ z->model = map->map(map->handle, ZP_URI "#model");
44
+ (void)d; (void)rate; (void)path;
45
+ return z;
46
+ }
47
+
48
+ static void connect_port(LV2_Handle h, uint32_t port, void *data)
49
+ {
50
+ Z *z = h;
51
+ switch (port) {
52
+ case P_CONTROL: z->control = data; break;
53
+ case P_NOTIFY: z->notify = data; break;
54
+ case P_IN: z->in = data; break;
55
+ case P_OUT: z->out = data; break;
56
+ }
57
+ }
58
+
59
+ static void activate(LV2_Handle h) { ((Z *)h)->cycle = 0; }
60
+
61
+ static void run(LV2_Handle h, uint32_t n)
62
+ {
63
+ Z *z = h;
64
+ if (z->in && z->out) memmove(z->out, z->in, n * sizeof(float));
65
+ if (!z->notify) return;
66
+ const uint32_t cap = z->notify->atom.size;
67
+ lv2_atom_forge_set_buffer(&z->forge, (uint8_t *)z->notify, cap);
68
+ LV2_Atom_Forge_Frame seq, obj;
69
+ lv2_atom_forge_sequence_head(&z->forge, &seq, 0);
70
+ if (z->cycle++ % 32 == 0) {
71
+ lv2_atom_forge_frame_time(&z->forge, 0);
72
+ lv2_atom_forge_object(&z->forge, &obj, 0, z->patch_Set);
73
+ lv2_atom_forge_key(&z->forge, z->patch_property);
74
+ lv2_atom_forge_urid(&z->forge, z->model);
75
+ lv2_atom_forge_key(&z->forge, z->patch_value);
76
+ lv2_atom_forge_atom(&z->forge, 0, z->atom_Path); /* the zero-size body */
77
+ lv2_atom_forge_pop(&z->forge, &obj);
78
+ }
79
+ lv2_atom_forge_pop(&z->forge, &seq);
80
+ }
81
+
82
+ static void cleanup(LV2_Handle h) { free(h); }
83
+
84
+ static const LV2_Descriptor desc = { ZP_URI, instantiate, connect_port, activate, run, NULL, cleanup, NULL };
85
+ LV2_SYMBOL_EXPORT const LV2_Descriptor *lv2_descriptor(uint32_t i) { return i == 0 ? &desc : NULL; }
@@ -0,0 +1,21 @@
1
+ @prefix atom: <http://lv2plug.in/ns/ext/atom#> .
2
+ @prefix doap: <http://usefulinc.com/ns/doap#> .
3
+ @prefix lv2: <http://lv2plug.in/ns/lv2core#> .
4
+ @prefix patch: <http://lv2plug.in/ns/ext/patch#> .
5
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
6
+ @prefix urid: <http://lv2plug.in/ns/ext/urid#> .
7
+
8
+ <urn:openmixer:eval:zero-path#model> a lv2:Parameter ; rdfs:label "model" ; rdfs:range atom:Path .
9
+
10
+ <urn:openmixer:eval:zero-path> a lv2:Plugin , lv2:UtilityPlugin ;
11
+ doap:name "omx eval zero-size Path" ; lv2:requiredFeature urid:map ;
12
+ patch:writable <urn:openmixer:eval:zero-path#model> ;
13
+ patch:readable <urn:openmixer:eval:zero-path#model> ;
14
+ lv2:port [ a lv2:InputPort , atom:AtomPort ; atom:bufferType atom:Sequence ;
15
+ atom:supports patch:Message ; lv2:designation lv2:control ;
16
+ lv2:index 0 ; lv2:symbol "control" ; lv2:name "Control" ] ,
17
+ [ a lv2:OutputPort , atom:AtomPort ; atom:bufferType atom:Sequence ;
18
+ atom:supports patch:Message ; lv2:designation lv2:control ;
19
+ lv2:index 1 ; lv2:symbol "notify" ; lv2:name "Notify" ] ,
20
+ [ a lv2:AudioPort , lv2:InputPort ; lv2:index 2 ; lv2:symbol "in" ; lv2:name "In" ] ,
21
+ [ a lv2:AudioPort , lv2:OutputPort ; lv2:index 3 ; lv2:symbol "out" ; lv2:name "Out" ] .
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@openmixer/plugin-qualify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.0",
4
+ "description": "Qualify audio plugins for real-time hosting: scan, offline latency and cost measurement, a mod-host hosting sweep, and a fail-closed rating.",
5
+ "type": "module",
6
+ "license": "GPL-3.0-or-later",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/FreeMixer/plugin-qualify.git"
10
+ },
11
+ "author": "Pau Aliagas <linuxnow@gmail.com>",
12
+ "homepage": "https://github.com/FreeMixer/plugin-qualify#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/FreeMixer/plugin-qualify/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "bin": {
20
+ "plugin-qualify": "./bin/plugin-qualify.mjs"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./src/index.ts"
29
+ },
30
+ "./declarations": {
31
+ "types": "./dist/qualify-declarations.d.ts",
32
+ "import": "./dist/qualify-declarations.js",
33
+ "default": "./src/qualify-declarations.ts"
34
+ },
35
+ "./measurement": {
36
+ "types": "./dist/measurement/index.d.ts",
37
+ "import": "./dist/measurement/index.js",
38
+ "default": "./src/measurement/index.ts"
39
+ },
40
+ "./measurement/fixtures": {
41
+ "types": "./dist/measurement/fixtures.d.ts",
42
+ "import": "./dist/measurement/fixtures.js",
43
+ "default": "./src/measurement/fixtures.ts"
44
+ },
45
+ "./tools/hosting-sweep.mjs": "./tools/hosting-sweep.mjs",
46
+ "./tools/lv2-measure.mjs": "./tools/lv2-measure.mjs",
47
+ "./rating": {
48
+ "types": "./dist/rating.d.ts",
49
+ "import": "./dist/rating.js",
50
+ "default": "./src/rating.ts"
51
+ },
52
+ "./host-profiles": {
53
+ "types": "./dist/host-profiles.d.ts",
54
+ "import": "./dist/host-profiles.js",
55
+ "default": "./src/host-profiles.ts"
56
+ },
57
+ "./hosting-suitability": {
58
+ "types": "./dist/hosting-suitability.d.ts",
59
+ "import": "./dist/hosting-suitability.js",
60
+ "default": "./src/hosting-suitability.ts"
61
+ },
62
+ "./package.json": "./package.json",
63
+ "./fixtures/*": "./fixtures/*"
64
+ },
65
+ "files": [
66
+ "bin",
67
+ "dist",
68
+ "src",
69
+ "!src/**/*.test.ts",
70
+ "tools",
71
+ "!tools/test_*.py",
72
+ "!**/__pycache__",
73
+ "!**/*.pyc",
74
+ "fixtures",
75
+ "README.md",
76
+ "LICENSE"
77
+ ],
78
+ "devDependencies": {
79
+ "@types/node": "^24.9.0",
80
+ "typescript": "^6.0.3",
81
+ "vitest": "^4.1.10"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public",
85
+ "provenance": true
86
+ },
87
+ "scripts": {
88
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
89
+ "test": "vitest run src",
90
+ "lint": "tsc -p tsconfig.lint.json",
91
+ "test:py": "python3 -m pytest tools -q",
92
+ "check:generated": "node tools/gen-python-declarations.mjs --check"
93
+ }
6
94
  }