@openmixer/omx-contract 0.0.0 → 1.0.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.
@@ -0,0 +1,201 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ #ifndef OMX_CONTRACT_LIMITS_H
4
+ #define OMX_CONTRACT_LIMITS_H
5
+ /*
6
+ * GENERATED — DO NOT EDIT BY HAND.
7
+ * Produced by `omx-contract render --target c` from FreeMixer/omx-contract's data/, the declaration
8
+ * as data (each item's reason is its `doc`). Change the data, never this file: a consumer builds
9
+ * against the render of the omx-contract version it pins and commits none.
10
+ *
11
+ * A C contract (OMX_PRE/OMX_POST/OMX_INVARIANT) reads a limit from HERE, never a literal.
12
+ */
13
+
14
+ /* Per-parameter numeric travel, one block per `travels` table named <PREFIX>_LIMITS:
15
+ * OMX_<PREFIX>_<FIELD>_MIN/_MAX/_DEFAULT, float literals. */
16
+ #define OMX_COMP_THRESHOLD_DB_MIN -60.0f
17
+ #define OMX_COMP_THRESHOLD_DB_MAX 0.0f
18
+ #define OMX_COMP_THRESHOLD_DB_DEFAULT -18.0f
19
+ #define OMX_COMP_RATIO_MIN 1.0f
20
+ #define OMX_COMP_RATIO_MAX 20.0f
21
+ #define OMX_COMP_RATIO_DEFAULT 4.0f
22
+ #define OMX_COMP_KNEE_DB_MIN 0.0f
23
+ #define OMX_COMP_KNEE_DB_MAX 24.0f
24
+ #define OMX_COMP_KNEE_DB_DEFAULT 6.0f
25
+ #define OMX_COMP_ATTACK_MS_MIN 0.1f
26
+ #define OMX_COMP_ATTACK_MS_MAX 100.0f
27
+ #define OMX_COMP_ATTACK_MS_DEFAULT 5.0f
28
+ #define OMX_COMP_RELEASE_MS_MIN 5.0f
29
+ #define OMX_COMP_RELEASE_MS_MAX 3000.0f
30
+ #define OMX_COMP_RELEASE_MS_DEFAULT 200.0f
31
+ #define OMX_COMP_MAKEUP_DB_MIN 0.0f
32
+ #define OMX_COMP_MAKEUP_DB_MAX 24.0f
33
+ #define OMX_COMP_MAKEUP_DB_DEFAULT 0.0f
34
+ #define OMX_COMP_MIX_PCT_MIN 0.0f
35
+ #define OMX_COMP_MIX_PCT_MAX 100.0f
36
+ #define OMX_COMP_MIX_PCT_DEFAULT 100.0f
37
+
38
+ #define OMX_GATE_THRESHOLD_DB_MIN -80.0f
39
+ #define OMX_GATE_THRESHOLD_DB_MAX 0.0f
40
+ #define OMX_GATE_THRESHOLD_DB_DEFAULT -40.0f
41
+ #define OMX_GATE_RANGE_DB_MIN -90.0f
42
+ #define OMX_GATE_RANGE_DB_MAX 0.0f
43
+ #define OMX_GATE_RANGE_DB_DEFAULT -90.0f
44
+ #define OMX_GATE_KNEE_START_DB_MIN -80.0f
45
+ #define OMX_GATE_KNEE_START_DB_MAX 0.0f
46
+ #define OMX_GATE_KNEE_START_DB_DEFAULT -43.0f
47
+ #define OMX_GATE_KNEE_END_DB_MIN -80.0f
48
+ #define OMX_GATE_KNEE_END_DB_MAX 0.0f
49
+ #define OMX_GATE_KNEE_END_DB_DEFAULT -37.0f
50
+ #define OMX_GATE_ATTACK_MS_MIN 0.0f
51
+ #define OMX_GATE_ATTACK_MS_MAX 500.0f
52
+ #define OMX_GATE_ATTACK_MS_DEFAULT 1.0f
53
+ #define OMX_GATE_HOLD_MS_MIN 0.0f
54
+ #define OMX_GATE_HOLD_MS_MAX 2000.0f
55
+ #define OMX_GATE_HOLD_MS_DEFAULT 10.0f
56
+ #define OMX_GATE_RELEASE_MS_MIN 0.0f
57
+ #define OMX_GATE_RELEASE_MS_MAX 5000.0f
58
+ #define OMX_GATE_RELEASE_MS_DEFAULT 100.0f
59
+ #define OMX_GATE_HYSTERESIS_DB_MIN 0.0f
60
+ #define OMX_GATE_HYSTERESIS_DB_MAX 24.0f
61
+ #define OMX_GATE_HYSTERESIS_DB_DEFAULT 3.0f
62
+ #define OMX_GATE_RATIO_MIN 1.0f
63
+ #define OMX_GATE_RATIO_MAX 100.0f
64
+ #define OMX_GATE_RATIO_DEFAULT 16.0f
65
+
66
+ #define OMX_LIMITER_CEILING_DB_MIN -12.0f
67
+ #define OMX_LIMITER_CEILING_DB_MAX 0.0f
68
+ #define OMX_LIMITER_CEILING_DB_DEFAULT -1.0f
69
+ #define OMX_LIMITER_LOOKAHEAD_MS_MIN 0.5f
70
+ #define OMX_LIMITER_LOOKAHEAD_MS_MAX 5.0f
71
+ #define OMX_LIMITER_LOOKAHEAD_MS_DEFAULT 1.5f
72
+ #define OMX_LIMITER_RELEASE_MS_MIN 1.0f
73
+ #define OMX_LIMITER_RELEASE_MS_MAX 1000.0f
74
+ #define OMX_LIMITER_RELEASE_MS_DEFAULT 50.0f
75
+
76
+ #define OMX_PITCH_SEMITONES_MIN -12.0f
77
+ #define OMX_PITCH_SEMITONES_MAX 12.0f
78
+ #define OMX_PITCH_SEMITONES_DEFAULT 0.0f
79
+ #define OMX_PITCH_CENTS_MIN -50.0f
80
+ #define OMX_PITCH_CENTS_MAX 50.0f
81
+ #define OMX_PITCH_CENTS_DEFAULT 0.0f
82
+ #define OMX_PITCH_MIX_MIN 0.0f
83
+ #define OMX_PITCH_MIX_MAX 100.0f
84
+ #define OMX_PITCH_MIX_DEFAULT 100.0f
85
+
86
+ #define OMX_TRANSIENT_ATTACK_DB_MIN -24.0f
87
+ #define OMX_TRANSIENT_ATTACK_DB_MAX 24.0f
88
+ #define OMX_TRANSIENT_ATTACK_DB_DEFAULT 0.0f
89
+ #define OMX_TRANSIENT_SUSTAIN_DB_MIN -24.0f
90
+ #define OMX_TRANSIENT_SUSTAIN_DB_MAX 24.0f
91
+ #define OMX_TRANSIENT_SUSTAIN_DB_DEFAULT 0.0f
92
+ #define OMX_TRANSIENT_ATTACK_TIME_MS_MIN 2.0f
93
+ #define OMX_TRANSIENT_ATTACK_TIME_MS_MAX 50.0f
94
+ #define OMX_TRANSIENT_ATTACK_TIME_MS_DEFAULT 10.0f
95
+ #define OMX_TRANSIENT_SUSTAIN_TIME_MS_MIN 50.0f
96
+ #define OMX_TRANSIENT_SUSTAIN_TIME_MS_MAX 2000.0f
97
+ #define OMX_TRANSIENT_SUSTAIN_TIME_MS_DEFAULT 250.0f
98
+ #define OMX_TRANSIENT_OUTPUT_DB_MIN -24.0f
99
+ #define OMX_TRANSIENT_OUTPUT_DB_MAX 12.0f
100
+ #define OMX_TRANSIENT_OUTPUT_DB_DEFAULT 0.0f
101
+
102
+ /* The second C spelling of a travel, its item's `c.alias` { fact, field }:
103
+ * OMX_<FACT>_<FIELD>_MIN/_MAX/_DEFAULT. Removed by a MAJOR release once no consumer reads it. */
104
+ #define OMX_DELAY_TIME_MS_MIN 0.0f
105
+ #define OMX_DELAY_TIME_MS_MAX 2000.0f
106
+ #define OMX_DELAY_TIME_MS_DEFAULT 300.0f
107
+
108
+ /* Every scalar, sheet leaf and single travel's numbers, one block sorted by macro. An integer value
109
+ * is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */
110
+ #define OMX_ALLPASS_UNITY_TOLERANCE 0.0001f
111
+ #define OMX_ALLPASS_UNITY_TOLERANCE_DOUBLE 0.0001
112
+ #define OMX_BUTTERWORTH_Q 0.7071067811865476f
113
+ #define OMX_BUTTERWORTH_Q_DOUBLE 0.7071067811865476
114
+ #define OMX_CHORUS_SPREAD_RANGE_DEFAULT 0
115
+ #define OMX_CHORUS_SPREAD_RANGE_MAX 0.5f
116
+ #define OMX_CHORUS_SPREAD_RANGE_MAX_DOUBLE 0.5
117
+ #define OMX_CHORUS_SPREAD_RANGE_MIN 0
118
+ #define OMX_CHORUS_SPREAD_RANGE_STEP 0.01f
119
+ #define OMX_CHORUS_SPREAD_RANGE_STEP_DOUBLE 0.01
120
+ #define OMX_DELAY_MIX_RANGE_BY_KIND_FX_RETURN 1
121
+ #define OMX_DELAY_MIX_RANGE_DEFAULT 0.3f
122
+ #define OMX_DELAY_MIX_RANGE_DEFAULT_DOUBLE 0.3
123
+ #define OMX_DELAY_MIX_RANGE_MAX 1
124
+ #define OMX_DELAY_MIX_RANGE_MIN 0
125
+ #define OMX_DELAY_MIX_RANGE_STEP 0
126
+ #define OMX_DELAY_TONE_RANGE_DEFAULT 0.3f
127
+ #define OMX_DELAY_TONE_RANGE_DEFAULT_DOUBLE 0.3
128
+ #define OMX_DELAY_TONE_RANGE_MAX 1
129
+ #define OMX_DELAY_TONE_RANGE_MIN 0
130
+ #define OMX_DELAY_TONE_RANGE_STEP 0
131
+ #define OMX_DRIVE_BIAS_MAX 0.7071067811865476f
132
+ #define OMX_DRIVE_BIAS_MAX_DOUBLE 0.7071067811865476
133
+ #define OMX_DSP_KNOB_REFERENCE_RATE 96000
134
+ #define OMX_EQ_MAX_BANDS 24
135
+ #define OMX_FDELAY_MOD_READ_ORDER 3
136
+ #define OMX_FDELAY_READ_L1_NORM 1.25f
137
+ #define OMX_FDELAY_READ_L1_NORM_DOUBLE 1.25
138
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_DEFAULT 0.3f
139
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_DEFAULT_DOUBLE 0.3
140
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_MAX 0.99f
141
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_MAX_DOUBLE 0.99
142
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_MIN 0
143
+ #define OMX_FX_DELAY_FEEDBACK_RANGE_STEP 0
144
+ #define OMX_FX_DELAY_TIME_RANGE_DEFAULT 300
145
+ #define OMX_FX_DELAY_TIME_RANGE_MAX 2000
146
+ #define OMX_FX_DELAY_TIME_RANGE_MIN 0
147
+ #define OMX_FX_DELAY_TIME_RANGE_STEP 1
148
+ #define OMX_GEQ_BANDS 31
149
+ #define OMX_PITCH_PREFILTER_CEILING_HZ 20000
150
+ #define OMX_PITCH_PREFILTER_Q 0.7071067811865476f
151
+ #define OMX_PITCH_PREFILTER_Q_DOUBLE 0.7071067811865476
152
+ #define OMX_PITCH_WINDOW_MS 40
153
+ #define OMX_PROGRAM_RELEASE_CHARGE_MS 1000
154
+ #define OMX_PROGRAM_RELEASE_DISCHARGE_MS 5000
155
+ #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_FAST_MS 70
156
+ #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SHARE 0.5f
157
+ #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SHARE_DOUBLE 0.5
158
+ #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MAX_MS 1100
159
+ #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MIN_MS 110
160
+ #define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_DEFAULT 100
161
+ #define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_MAX 400
162
+ #define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_MIN 0
163
+ #define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_STEP 1
164
+ #define OMX_RT_HARD_TARGET_QUANTUM 128
165
+ #define OMX_RT_HARD_TARGET_RATE 192000
166
+ #define OMX_TRANSIENT_FAST_ATTACK_MS 0.5f
167
+ #define OMX_TRANSIENT_FAST_ATTACK_MS_DOUBLE 0.5
168
+ #define OMX_TRANSIENT_FAST_RELEASE_MS 20
169
+ #define OMX_TRANSIENT_FLOOR_LIN 0.00001f
170
+ #define OMX_TRANSIENT_FLOOR_LIN_DOUBLE 0.00001
171
+ #define OMX_TRANSIENT_REF_DB 6
172
+ #define OMX_XOVER_LR2_SECTION_Q 0.5f
173
+ #define OMX_XOVER_LR2_SECTION_Q_DOUBLE 0.5
174
+ #define OMX_XOVER_LR4_SECTION_Q 0.7071067811865476f
175
+ #define OMX_XOVER_LR4_SECTION_Q_DOUBLE 0.7071067811865476
176
+ #define OMX_XOVER_PARTITION_TOLERANCE 0.00001f
177
+ #define OMX_XOVER_PARTITION_TOLERANCE_DOUBLE 0.00001
178
+
179
+ /* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */
180
+ #define OMX_STANDARD_SAMPLE_RATES_COUNT 6u
181
+ #define OMX_STANDARD_SAMPLE_RATES_INIT { 44100, 48000, 88200, 96000, 176400, 192000 }
182
+
183
+ #endif /* OMX_CONTRACT_LIMITS_H */
184
+
185
+ /* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
186
+ * contract's rate-is-declared precondition accepts, through omx_rate_is_declared(), and the grid a
187
+ * native tool iterates. Guarded on its own so a tool built against a pinned copy of this header
188
+ * (lv2-inprocess-pin.sh) can include this tree's copy after it and gain the rates without mixing
189
+ * the pin's limits. */
190
+ #ifndef OMX_DECLARED_RATE_COUNT
191
+ #define OMX_DECLARED_RATE_COUNT 6u
192
+ static const float OMX_DECLARED_RATES[OMX_DECLARED_RATE_COUNT] = { 44100.0f, 48000.0f, 88200.0f, 96000.0f, 176400.0f, 192000.0f };
193
+ /* The declared rates that are whole multiples of `base` (a tool's default grid: the 48 kHz family),
194
+ * in declaration order, at most `cap` of them; returns how many were written to `out`. */
195
+ static inline int omx_declared_rates_multiple_of(unsigned base, int *out, int cap) {
196
+ int n = 0;
197
+ for (unsigned k = 0; k < OMX_DECLARED_RATE_COUNT && n < cap; k++)
198
+ if ((unsigned)OMX_DECLARED_RATES[k] % base == 0) out[n++] = (int)OMX_DECLARED_RATES[k];
199
+ return n;
200
+ }
201
+ #endif
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ #ifndef OMX_DELAY_PARAMS_H
4
+ #define OMX_DELAY_PARAMS_H
5
+ /*
6
+ * GENERATED — DO NOT EDIT BY HAND.
7
+ * Produced by `omx-contract render --target c` from FreeMixer/omx-contract's data/plugins/delay.json
8
+ * (DELAY_PLUGIN): FX_DELAY_TIME_RANGE, FX_DELAY_FEEDBACK_RANGE, DELAY_MIX_RANGE for kind input, DELAY_TONE_RANGE, FX_DELAY_PINGPONG_DEFAULT.
9
+ * Change the data, never this file. Order is append-only (omx-contract spec §3.3).
10
+ */
11
+ #include "omx_plugin_param.h"
12
+
13
+ enum {
14
+ OMX_DELAY_PARAM_TIME_MS = 0,
15
+ OMX_DELAY_PARAM_FEEDBACK = 1,
16
+ OMX_DELAY_PARAM_MIX = 2,
17
+ OMX_DELAY_PARAM_TONE = 3,
18
+ OMX_DELAY_PARAM_PINGPONG = 4,
19
+ OMX_DELAY_PARAM_COUNT = 5
20
+ };
21
+
22
+ static const omx_plugin_param OMX_DELAY_PARAMS[OMX_DELAY_PARAM_COUNT] = {
23
+ { "timeMs", "Time", "ms", 0.0f, 2000.0f, 300.0f, OMX_PLUGIN_PARAM_INTEGER },
24
+ { "feedback", "Feedback", "", 0.0f, 0.99f, 0.3f, 0u },
25
+ { "mix", "Mix", "", 0.0f, 1.0f, 0.3f, 0u },
26
+ { "tone", "Tone", "", 0.0f, 1.0f, 0.3f, 0u },
27
+ { "pingpong", "Pingpong", "", 0.0f, 1.0f, 0.0f, OMX_PLUGIN_PARAM_TOGGLE },
28
+ };
29
+
30
+ /* One macro per declared bound: what a C face reads where a constant is needed. */
31
+ #define OMX_DELAY_PARAM_TIME_MS_MIN 0.0f
32
+ #define OMX_DELAY_PARAM_TIME_MS_MAX 2000.0f
33
+ #define OMX_DELAY_PARAM_TIME_MS_DEFAULT 300.0f
34
+ #define OMX_DELAY_PARAM_FEEDBACK_MIN 0.0f
35
+ #define OMX_DELAY_PARAM_FEEDBACK_MAX 0.99f
36
+ #define OMX_DELAY_PARAM_FEEDBACK_DEFAULT 0.3f
37
+ #define OMX_DELAY_PARAM_MIX_MIN 0.0f
38
+ #define OMX_DELAY_PARAM_MIX_MAX 1.0f
39
+ #define OMX_DELAY_PARAM_MIX_DEFAULT 0.3f
40
+ #define OMX_DELAY_PARAM_TONE_MIN 0.0f
41
+ #define OMX_DELAY_PARAM_TONE_MAX 1.0f
42
+ #define OMX_DELAY_PARAM_TONE_DEFAULT 0.3f
43
+ #define OMX_DELAY_PARAM_PINGPONG_MIN 0.0f
44
+ #define OMX_DELAY_PARAM_PINGPONG_MAX 1.0f
45
+ #define OMX_DELAY_PARAM_PINGPONG_DEFAULT 0.0f
46
+
47
+ #endif /* OMX_DELAY_PARAMS_H */
@@ -0,0 +1,25 @@
1
+ /* SPDX-License-Identifier: GPL-3.0-or-later
2
+ * Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ *
4
+ * omx_plugin_param.h — the SHAPE of one generated plugin parameter, and nothing else: no number
5
+ * lives here. The rows are rendered by `omx-contract render --target c` into omx_<plugin>_params.h
6
+ * beside this file, from FreeMixer/omx-contract's data/plugins/ (omx-contract spec §4.2); a DPF
7
+ * plugin's initParameter reads them and types no range of its own.
8
+ */
9
+ #ifndef OMX_PLUGIN_PARAM_H
10
+ #define OMX_PLUGIN_PARAM_H
11
+
12
+ /** A whole-number travel (declared `step` >= 1 over integer bounds). */
13
+ #define OMX_PLUGIN_PARAM_INTEGER 1u
14
+ /** A declared boolean: domain {0, 1}, the come-up value declared. */
15
+ #define OMX_PLUGIN_PARAM_TOGGLE 2u
16
+
17
+ typedef struct {
18
+ const char *symbol; /* the declared field name — the LV2 symbol, the CLAP param's identity */
19
+ const char *name; /* derived from the symbol, never a label table */
20
+ const char *unit;
21
+ float min, max, def;
22
+ unsigned flags;
23
+ } omx_plugin_param;
24
+
25
+ #endif /* OMX_PLUGIN_PARAM_H */
package/lib/data.mjs ADDED
@@ -0,0 +1,182 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The declaration as data: load every `data/**.json` file, then resolve each item's references
5
+ * and derivations into plain values. The renderers are pure functions of the resolved items.
6
+ */
7
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
8
+ import { join, relative } from 'node:path';
9
+
10
+ /** Every `.json` file under `dir`, sorted by its path relative to `dir`. */
11
+ export function dataFiles(dir) {
12
+ const out = [];
13
+ const walk = (d) => {
14
+ for (const f of readdirSync(d).sort()) {
15
+ const p = join(d, f);
16
+ if (statSync(p).isDirectory()) walk(p);
17
+ else if (f.endsWith('.json')) out.push(p);
18
+ }
19
+ };
20
+ walk(dir);
21
+ return out.sort((a, b) => (relative(dir, a) < relative(dir, b) ? -1 : 1));
22
+ }
23
+
24
+ /**
25
+ * The raw data: `{ files: [{ path, rel, doc }], items: Map<name, { name, rel, item }> }`, items in
26
+ * file order then key order. `files` may be handed in as `{ rel: text }` (a tag's data, a test).
27
+ * @throws naming the file when one is not JSON, and an item declared by two files.
28
+ */
29
+ export function loadData(dir, texts) {
30
+ const files = [];
31
+ const entries = texts ? Object.entries(texts).sort(([a], [b]) => (a < b ? -1 : 1)) : dataFiles(dir).map((p) => [relative(join(dir, '..'), p), readFileSync(p, 'utf8')]);
32
+ const items = new Map();
33
+ for (const [rel, text] of entries) {
34
+ let doc;
35
+ try { doc = JSON.parse(text); } catch (e) { throw new Error(`${rel}: not JSON: ${e.message}`); }
36
+ files.push({ rel, text, doc });
37
+ for (const [name, item] of Object.entries(doc)) {
38
+ if (name === '$schema') continue;
39
+ if (items.has(name)) throw new Error(`${rel}: ${name}: already declared by ${items.get(name).rel}`);
40
+ items.set(name, { name, rel, item });
41
+ }
42
+ }
43
+ return { files, items };
44
+ }
45
+
46
+ /** An item's own value before resolution, by kind. */
47
+ export function rawValue(item) {
48
+ switch (item.kind) {
49
+ case 'scalar': return item.value;
50
+ case 'travels': return item.fields ?? item.travel;
51
+ case 'sheet': return item.value;
52
+ case 'list': return item.values;
53
+ case 'plugin': return item.params;
54
+ default: return undefined;
55
+ }
56
+ }
57
+
58
+ /** The closed list of derivations (spec §3.3). A new one is a change here with its own test. */
59
+ export const DERIVATIONS = {
60
+ count: (d, val) => {
61
+ const v = val(typeof d.of === 'string' ? { ref: d.of } : d.of);
62
+ if (!Array.isArray(v)) throw new Error(`count: ${JSON.stringify(d.of)} is not a list`);
63
+ return v.length;
64
+ },
65
+ sqrt: (d, val) => {
66
+ const v = val(d.of);
67
+ if (typeof v !== 'number' || v < 0) throw new Error(`sqrt: ${JSON.stringify(d.of)} is not a non-negative number`);
68
+ return Math.sqrt(v);
69
+ },
70
+ lagrangeReadL1: (d, val) => {
71
+ const order = val(d.order);
72
+ if (!Number.isInteger(order) || order < 1) throw new Error(`lagrangeReadL1: order ${order} is not a positive integer`);
73
+ return lagrangeReadL1(order);
74
+ },
75
+ };
76
+
77
+ /** The taps of the order-`order` Lagrange fractional read at fraction `f` (the fdelay kernel's read). */
78
+ export function lagrangeReadTaps(order, f) {
79
+ const off = Math.floor((order - 1) / 2);
80
+ const c = [];
81
+ for (let k = 0; k <= order; k++) {
82
+ let num = 1;
83
+ let den = 1;
84
+ for (let j = 0; j <= order; j++) {
85
+ if (j === k) continue;
86
+ num *= f - (j - off);
87
+ den *= k - j;
88
+ }
89
+ c.push(num / den);
90
+ }
91
+ return c;
92
+ }
93
+
94
+ /** The worst L1 norm of the read over 1024 fractions, rounded to 1e-9 (core's readKernelL1Norm). */
95
+ export function lagrangeReadL1(order) {
96
+ const FRACTION_STEPS = 1024;
97
+ let worst = 0;
98
+ for (let i = 0; i < FRACTION_STEPS; i++) {
99
+ const l1 = lagrangeReadTaps(order, i / FRACTION_STEPS).reduce((a, c) => a + Math.abs(c), 0);
100
+ if (l1 > worst) worst = l1;
101
+ }
102
+ return Math.round(worst * 1e9) / 1e9;
103
+ }
104
+
105
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
106
+
107
+ /**
108
+ * Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, value }`, where `value` has
109
+ * every `{ref}` and `{derive}` evaluated. A plugin's value is its parameters resolved against the
110
+ * items they read.
111
+ * @throws naming the item: a reference that does not resolve, a cycle, an unknown derivation.
112
+ */
113
+ export function resolveData(data) {
114
+ const done = new Map();
115
+ const busy = new Set();
116
+ const lookup = (path, from) => {
117
+ const [head, ...rest] = path.split('.');
118
+ const entry = data.items.get(head);
119
+ if (!entry) throw new Error(`${from}: reference ${path}: no item ${head}`);
120
+ let v = resolveItem(head).value;
121
+ for (const k of rest) {
122
+ if (!isObj(v) || !(k in v)) throw new Error(`${from}: reference ${path}: ${head} has no ${k}`);
123
+ v = v[k];
124
+ }
125
+ return v;
126
+ };
127
+ const val = (v, from) => {
128
+ if (Array.isArray(v)) return v.map((x) => val(x, from));
129
+ if (!isObj(v)) return v;
130
+ if ('ref' in v) return lookup(v.ref, from);
131
+ if ('derive' in v) {
132
+ const fn = DERIVATIONS[v.derive];
133
+ if (!fn) throw new Error(`${from}: unknown derivation ${v.derive}`);
134
+ try { return fn(v, (x) => val(x, from)); } catch (e) { throw new Error(`${from}: ${e.message}`); }
135
+ }
136
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, val(x, from)]));
137
+ };
138
+ function resolveItem(name) {
139
+ if (done.has(name)) return done.get(name);
140
+ if (busy.has(name)) throw new Error(`${name}: reference cycle through ${[...busy].join(' -> ')} -> ${name}`);
141
+ busy.add(name);
142
+ const { rel, item } = data.items.get(name);
143
+ const r = { name, rel, kind: item.kind, doc: item.doc };
144
+ if (item.why !== undefined) r.why = item.why;
145
+ if (item.unit !== undefined) r.unit = item.unit;
146
+ if (item.c !== undefined) r.c = item.c;
147
+ if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
148
+ if (item.kind === 'plugin') {
149
+ for (const k of ['plugin', 'header', 'prefix', 'source']) r[k] = item[k];
150
+ r.value = item.params.map((p) => resolveParam(name, p));
151
+ } else {
152
+ r.value = val(rawValue(item), name);
153
+ }
154
+ busy.delete(name);
155
+ done.set(name, r);
156
+ return r;
157
+ }
158
+ function resolveParam(plugin, p) {
159
+ const from = `${plugin}.${p.symbol}`;
160
+ const read = lookup(p.from, from);
161
+ const toggle = (p.flags ?? []).includes('toggle');
162
+ if (toggle) {
163
+ if (typeof read !== 'boolean') throw new Error(`${from}: a toggle reads a boolean, ${p.from} is ${JSON.stringify(read)}`);
164
+ return { symbol: p.symbol, min: 0, max: 1, def: read ? 1 : 0, step: 1, unit: '', toggle: true };
165
+ }
166
+ if (!isObj(read)) throw new Error(`${from}: ${p.from} is not a travel`);
167
+ let def = read.default;
168
+ if (p.forKind !== undefined) def = read.byKind?.[p.forKind] ?? read.default;
169
+ for (const [k, v] of [['min', read.min], ['max', read.max], ['default', def], ['step', read.step], ['unit', read.unit]]) {
170
+ if (v === undefined) throw new Error(`${from}: ${p.from} declares no ${k}`);
171
+ }
172
+ return { symbol: p.symbol, min: read.min, max: read.max, def, step: read.step, unit: read.unit, toggle: false };
173
+ }
174
+ const out = new Map();
175
+ for (const name of data.items.keys()) out.set(name, resolveItem(name));
176
+ return out;
177
+ }
178
+
179
+ /** The package version (package.json), which names the tag, the release and the npm version. */
180
+ export function packageVersion(root) {
181
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
182
+ }
package/lib/fmt.mjs ADDED
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The one formatting of a data file (spec §3.2): two-space indent, one key per line, keys in schema
5
+ * order, a trailing newline. Names (items, fields, sheet keys) keep the order the author gave them:
6
+ * a travels table's field order is its C block order.
7
+ */
8
+
9
+ const ORDER = {
10
+ item: ['kind', 'doc', 'why', 'unit', 'plugin', 'header', 'prefix', 'source', 'value', 'values', 'fields', 'travel', 'params', 'c'],
11
+ travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'byKind', 'doc', 'why'],
12
+ param: ['symbol', 'from', 'forKind', 'flags', 'scale', 'since', 'doc'],
13
+ c: ['alias', 'render'],
14
+ alias: ['fact', 'field'],
15
+ number: ['ref', 'derive', 'of', 'order'],
16
+ };
17
+
18
+ function ordered(obj, order) {
19
+ const known = order.filter((k) => k in obj);
20
+ const rest = Object.keys(obj).filter((k) => !order.includes(k));
21
+ return Object.fromEntries([...known, ...rest].map((k) => [k, obj[k]]));
22
+ }
23
+
24
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
25
+
26
+ function number(v) {
27
+ if (!isObj(v)) return v;
28
+ return Object.fromEntries(Object.entries(ordered(v, ORDER.number)).map(([k, x]) => [k, number(x)]));
29
+ }
30
+
31
+ function sheet(v) {
32
+ if (!isObj(v) || 'ref' in v || 'derive' in v) return number(v);
33
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, sheet(x)]));
34
+ }
35
+
36
+ function travel(t) {
37
+ const o = ordered(t, ORDER.travel);
38
+ for (const k of ['min', 'max', 'step', 'default']) if (k in o) o[k] = number(o[k]);
39
+ if (isObj(o.byKind)) o.byKind = Object.fromEntries(Object.entries(o.byKind).map(([k, x]) => [k, number(x)]));
40
+ return o;
41
+ }
42
+
43
+ function item(it) {
44
+ if (!isObj(it)) return it;
45
+ const o = ordered(it, ORDER.item);
46
+ if ('value' in o) o.value = o.kind === 'sheet' ? sheet(o.value) : number(o.value);
47
+ if (isObj(o.fields)) o.fields = Object.fromEntries(Object.entries(o.fields).map(([k, t]) => [k, isObj(t) ? travel(t) : t]));
48
+ if (isObj(o.travel)) o.travel = travel(o.travel);
49
+ if (Array.isArray(o.params)) o.params = o.params.map((p) => (isObj(p) ? ordered(p, ORDER.param) : p));
50
+ if (isObj(o.c)) {
51
+ o.c = ordered(o.c, ORDER.c);
52
+ if (isObj(o.c.alias)) o.c.alias = ordered(o.c.alias, ORDER.alias);
53
+ }
54
+ return o;
55
+ }
56
+
57
+ /** The canonical text of a parsed data file. */
58
+ export function formatDoc(doc) {
59
+ const out = {};
60
+ if ('$schema' in doc) out.$schema = doc.$schema;
61
+ for (const [k, v] of Object.entries(doc)) if (k !== '$schema') out[k] = item(v);
62
+ return `${JSON.stringify(out, null, 2)}\n`;
63
+ }
package/lib/semver.mjs ADDED
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * `omx-contract semver`: classify every change between two resolved declarations (spec §3.6) and
5
+ * refuse a version whose bump is shorter than the largest class found.
6
+ *
7
+ * major an item, field, parameter or alias removed; a unit or kind changed; a travel narrowed; a
8
+ * plugin's parameters reordered or changed; a constant a kernel computes with changed (a
9
+ * scalar, sheet leaf or derived value that is not a travel bound or default; a list member
10
+ * changed or removed)
11
+ * minor an item, field, sheet leaf, parameter or list member added (appended); a travel widened;
12
+ * a default moved inside its travel; a step changed
13
+ * patch doc and why text
14
+ */
15
+
16
+ const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
17
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
18
+
19
+ function travelChanges(at, a, b, say) {
20
+ if (b.min > a.min) say('major', `${at}: travel narrowed (min ${a.min} -> ${b.min})`);
21
+ if (b.max < a.max) say('major', `${at}: travel narrowed (max ${a.max} -> ${b.max})`);
22
+ if (b.min < a.min || b.max > a.max) say('minor', `${at}: travel widened ([${a.min}, ${a.max}] -> [${b.min}, ${b.max}])`);
23
+ if (a.unit !== b.unit) say('major', `${at}: unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
24
+ if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${at}: default ${a.default} -> ${b.default}`);
25
+ if (a.step !== b.step) say('minor', `${at}: step ${a.step} -> ${b.step}`);
26
+ if (a.defaultFrom !== b.defaultFrom) say('minor', `${at}: defaultFrom ${a.defaultFrom} -> ${b.defaultFrom}`);
27
+ const ka = a.byKind ?? {};
28
+ const kb = b.byKind ?? {};
29
+ for (const k of Object.keys(ka)) if (!(k in kb)) say('major', `${at}.byKind.${k} removed`);
30
+ for (const k of Object.keys(kb)) {
31
+ if (!(k in ka)) say('minor', `${at}.byKind.${k} added`);
32
+ else if (ka[k] !== kb[k]) say('minor', `${at}.byKind.${k}: ${ka[k]} -> ${kb[k]}`);
33
+ }
34
+ if ((a.doc ?? '') !== (b.doc ?? '') || (a.why ?? '') !== (b.why ?? '')) say('patch', `${at}: doc`);
35
+ }
36
+
37
+ function leafChanges(at, a, b, say) {
38
+ if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
39
+ for (const k of Object.keys(a)) {
40
+ if (!(k in b)) say('major', `${at}.${k} removed`);
41
+ else leafChanges(`${at}.${k}`, a[k], b[k], say);
42
+ }
43
+ for (const k of Object.keys(b)) if (!(k in a)) say('minor', `${at}.${k} added`);
44
+ } else if (a !== b) {
45
+ say('major', `${at}: ${JSON.stringify(a)} -> ${JSON.stringify(b)} (a constant a kernel computes with)`);
46
+ }
47
+ }
48
+
49
+ /** `{ level, changes: [{ level, item, what }] }` from `prev` to `next` (Maps name -> resolved item). */
50
+ export function classify(prev, next) {
51
+ const changes = [];
52
+ for (const [name, a] of prev) {
53
+ const say = (level, what) => changes.push({ level, item: name, what });
54
+ const b = next.get(name);
55
+ if (!b) { say('major', 'item removed or renamed'); continue; }
56
+ if (a.kind !== b.kind || (a.shape ?? '') !== (b.shape ?? '')) { say('major', `kind ${a.kind} -> ${b.kind}`); continue; }
57
+ if ((a.unit ?? '') !== (b.unit ?? '')) say('major', `unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
58
+ if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '')) say('patch', 'doc');
59
+ if (!same(a.c?.alias, b.c?.alias)) say(a.c?.alias ? 'major' : 'minor', a.c?.alias ? 'c.alias removed or renamed' : 'c.alias added');
60
+ if (!same(a.c?.render, b.c?.render)) say(b.c?.render === false ? 'major' : 'minor', 'C render of the list changed');
61
+ switch (a.kind) {
62
+ case 'travels':
63
+ if (a.shape === 'travel') travelChanges(name, a.value, b.value, say);
64
+ else {
65
+ for (const f of Object.keys(a.value)) {
66
+ if (!(f in b.value)) say('major', `${name}.${f} removed`);
67
+ else travelChanges(`${name}.${f}`, a.value[f], b.value[f], say);
68
+ }
69
+ for (const f of Object.keys(b.value)) if (!(f in a.value)) say('minor', `${name}.${f} added`);
70
+ }
71
+ break;
72
+ case 'scalar':
73
+ case 'sheet':
74
+ leafChanges(name, a.value, b.value, say);
75
+ break;
76
+ case 'list':
77
+ a.value.forEach((v, i) => {
78
+ if (i >= b.value.length) say('major', `${name}[${i}] removed`);
79
+ else if (b.value[i] !== v) say('major', `${name}[${i}]: ${v} -> ${b.value[i]}`);
80
+ });
81
+ if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} member(s) appended`);
82
+ break;
83
+ case 'plugin':
84
+ for (const k of ['plugin', 'header', 'prefix']) if (a[k] !== b[k]) say('major', `${k} ${a[k]} -> ${b[k]}`);
85
+ if (a.source !== b.source) say('patch', 'source text');
86
+ a.value.forEach((p, i) => {
87
+ const q = b.value[i];
88
+ if (!q) say('major', `parameter ${p.symbol} removed`);
89
+ else if (q.symbol !== p.symbol) say('major', `parameter ${i} ${p.symbol} -> ${q.symbol} (reordered or renamed)`);
90
+ else if (!same(p, q)) say(p.min < q.min || p.max > q.max || p.unit !== q.unit || p.toggle !== q.toggle ? 'major' : 'minor', `parameter ${p.symbol} changed`);
91
+ });
92
+ if (b.value.length > a.value.length) say('minor', `${b.value.length - a.value.length} parameter(s) appended`);
93
+ break;
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ for (const name of next.keys()) if (!prev.has(name)) changes.push({ level: 'minor', item: name, what: 'item added' });
99
+ const level = changes.reduce((m, c) => (RANK[c.level] > RANK[m] ? c.level : m), 'none');
100
+ return { level, changes };
101
+ }
102
+
103
+ const parse = (v) => {
104
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v);
105
+ if (!m) throw new Error(`not a version: ${v}`);
106
+ return m.slice(1).map(Number);
107
+ };
108
+
109
+ /** The class of the bump from version `from` to `to`: 'major' | 'minor' | 'patch' | 'none'; throws if `to` is lower. */
110
+ export function bumpOf(from, to) {
111
+ const [a, b] = [parse(from), parse(to)];
112
+ for (let i = 0; i < 3; i++) {
113
+ if (b[i] > a[i]) return ['major', 'minor', 'patch'][i];
114
+ if (b[i] < a[i]) throw new Error(`version ${to} is lower than ${from}`);
115
+ }
116
+ return 'none';
117
+ }
118
+
119
+ /** null when bumping `from` -> `to` covers `level`, else the refusal. */
120
+ export function refusal(level, from, to) {
121
+ let bump;
122
+ try { bump = bumpOf(from, to); } catch (e) { return e.message; }
123
+ return RANK[bump] >= RANK[level] ? null : `the changes since ${from} are ${level.toUpperCase()}, and ${from} -> ${to} is a ${bump} bump`;
124
+ }