@openmixer/omx-contract 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The parameter-table half of the C renderer: `generate` and its helpers, moved from openmixer's
5
+ * packages/omx-plugins/tools/params-gen.mjs (omx-contract spec §3.4). Two changes only: the plugin
6
+ * (header, prefix, source) is handed in instead of looked up in params-gen's PLUGINS, which is data
7
+ * now (data/plugins/), and the banner's "Produced by" lines are a parameter whose default is
8
+ * params-gen's own text, so the frozen openmixer sheet still renders byte for byte.
9
+ */
10
+
11
+ /** params-gen.mjs's banner lines, for a render of openmixer's own selection (test/fixtures/). */
12
+ export const PARAMS_GEN_PRODUCED_BY = (source) => ` * Produced by packages/omx-plugins/tools/params-gen.mjs from @freemixer/core:
13
+ * ${source}.
14
+ * Regenerate: \`node packages/omx-plugins/tools/params-gen.mjs\`, then commit the result
15
+ * (docs/design/specs/2026-09-25-omx-plugins-dpf.md §3c). Order is append-only (§3b).`;
16
+
17
+ /** The flag bits `include/omx_plugin_param.h` (PLUGIN_INCLUDE_DIR) declares. */
18
+ const FLAG_INTEGER = 'OMX_PLUGIN_PARAM_INTEGER';
19
+ const FLAG_TOGGLE = 'OMX_PLUGIN_PARAM_TOGGLE';
20
+
21
+ /** `timeMs` (unit ms) -> "Time"; `feedback` -> "Feedback". Derived, never a label table. */
22
+ export function labelOf(symbol, unit) {
23
+ const words = symbol.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(' ');
24
+ if (words.length > 1 && unit && words[words.length - 1].toLowerCase() === unit.toLowerCase()) words.pop();
25
+ const s = words.join(' ');
26
+ return s.charAt(0).toUpperCase() + s.slice(1);
27
+ }
28
+
29
+ /** `timeMs` -> `TIME_MS`. */
30
+ export function constName(symbol) {
31
+ return symbol.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
32
+ }
33
+
34
+ function floatLiteral(n) {
35
+ if (!Number.isFinite(n)) throw new Error(`params-gen: non-finite bound ${n}`);
36
+ return `${Number.isInteger(n) ? `${n}.0` : `${n}`}f`;
37
+ }
38
+
39
+ function cString(s) {
40
+ return JSON.stringify(s);
41
+ }
42
+
43
+ /** The header text for one kernel's resolved parameter list. Pure: no clock, no path, no host. */
44
+ export function generate(plugin, params) {
45
+ const { header, prefix, source } = plugin;
46
+ const producedBy = plugin.producedBy ?? PARAMS_GEN_PRODUCED_BY(source);
47
+ const guard = header.replace(/\W/g, '_').toUpperCase();
48
+ const enumLines = params.map((p, i) => ` ${prefix}_PARAM_${constName(p.symbol)} = ${i},`);
49
+ const rows = params.map((p) => {
50
+ const flags = [];
51
+ if (p.toggle) flags.push(FLAG_TOGGLE);
52
+ else if (p.step >= 1 && Number.isInteger(p.step) && Number.isInteger(p.min) && Number.isInteger(p.max)) flags.push(FLAG_INTEGER);
53
+ return ` { ${cString(p.symbol)}, ${cString(labelOf(p.symbol, p.unit))}, ${cString(p.unit)}, ${floatLiteral(p.min)}, ${floatLiteral(p.max)}, ${floatLiteral(p.def)}, ${flags.length ? flags.join(' | ') : '0u'} },`;
54
+ });
55
+ const macros = params.flatMap((p) => {
56
+ const n = `${prefix}_PARAM_${constName(p.symbol)}`;
57
+ return [`#define ${n}_MIN ${floatLiteral(p.min)}`, `#define ${n}_MAX ${floatLiteral(p.max)}`, `#define ${n}_DEFAULT ${floatLiteral(p.def)}`];
58
+ });
59
+ return `// SPDX-License-Identifier: GPL-3.0-or-later
60
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
61
+ #ifndef ${guard}
62
+ #define ${guard}
63
+ /*
64
+ * GENERATED — DO NOT EDIT BY HAND.
65
+ ${producedBy}
66
+ */
67
+ #include "omx_plugin_param.h"
68
+
69
+ enum {
70
+ ${enumLines.join('\n')}
71
+ ${prefix}_PARAM_COUNT = ${params.length}
72
+ };
73
+
74
+ static const omx_plugin_param ${prefix}_PARAMS[${prefix}_PARAM_COUNT] = {
75
+ ${rows.join('\n')}
76
+ };
77
+
78
+ /* One macro per declared bound: what a C face reads where a constant is needed. */
79
+ ${macros.join('\n')}
80
+
81
+ #endif /* ${guard} */
82
+ `;
83
+ }
package/render/c.mjs ADDED
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The `c` target: `omxcontract/omx_contract_limits.h`. Every define is spelled by openmixer's
5
+ * renderer, moved unchanged (c/contract-limits-render.mjs); only the frame (banner, section
6
+ * comments) is this repository's. A plugin's parameter header is rendered by omx-plugins from its
7
+ * declaration; c/params-generate.mjs stays here only for renderSheetC, the frozen fixture of the
8
+ * round-trip proof.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import {
12
+ genCoreLimitBlock, genDeclaredListsBlock, genDeclaredRates, genDeclaredScalarsBlock, genLimitBlock, renderContractLimits,
13
+ } from './c/contract-limits-render.mjs';
14
+ import { generate } from './c/params-generate.mjs';
15
+ import { doors } from './doors.mjs';
16
+
17
+ const PLUGIN_PARAM_H = readFileSync(new URL('./c/omx_plugin_param.h', import.meta.url), 'utf8');
18
+ const defined = (text) => new Set([...text.matchAll(/^#define (OMX_\w+)/gm)].map((m) => m[1]));
19
+
20
+ /** The rate tail of openmixer's header, outside the include guard, byte for byte. */
21
+ function ratesTail(rates) {
22
+ return `
23
+ /* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
24
+ * contract's rate-is-declared precondition accepts, through omx_rate_is_declared(), and the grid a
25
+ * native tool iterates. Guarded on its own so a tool built against a pinned copy of this header
26
+ * (lv2-inprocess-pin.sh) can include this tree's copy after it and gain the rates without mixing
27
+ * the pin's limits. */
28
+ #ifndef OMX_DECLARED_RATE_COUNT
29
+ ${genDeclaredRates(rates)}
30
+ /* The declared rates that are whole multiples of \`base\` (a tool's default grid: the 48 kHz family),
31
+ * in declaration order, at most \`cap\` of them; returns how many were written to \`out\`. */
32
+ static inline int omx_declared_rates_multiple_of(unsigned base, int *out, int cap) {
33
+ int n = 0;
34
+ for (unsigned k = 0; k < OMX_DECLARED_RATE_COUNT && n < cap; k++)
35
+ if ((unsigned)OMX_DECLARED_RATES[k] % base == 0) out[n++] = (int)OMX_DECLARED_RATES[k];
36
+ return n;
37
+ }
38
+ #endif
39
+ `;
40
+ }
41
+
42
+ /** The limits header of the resolved data. */
43
+ export function renderLimitsHeader(resolved) {
44
+ const d = doors(resolved);
45
+ const tables = d.NATIVE_LIMIT_TABLES.map(([prefix, table]) => genLimitBlock(prefix, table)).join('\n\n');
46
+ const coreLimits = new Map(d.ALIASES);
47
+ const aliases = [...coreLimits.keys()].map((fact) => genCoreLimitBlock(coreLimits, fact)).join('\n\n');
48
+ const above = defined([tables, aliases].join('\n'));
49
+ const scalars = d.DECLARED_SCALARS.length ? genDeclaredScalarsBlock(d.DECLARED_SCALARS, above) : '';
50
+ const lists = d.DECLARED_LISTS.length ? genDeclaredListsBlock(d.DECLARED_LISTS, defined([tables, aliases, scalars].join('\n'))) : '';
51
+ const sections = [
52
+ [tables, `/* Per-parameter numeric travel, one block per \`travels\` table named <PREFIX>_LIMITS:
53
+ * OMX_<PREFIX>_<FIELD>_MIN/_MAX/_DEFAULT, float literals. */`],
54
+ [aliases, `/* The second C spelling of a travel, its item's \`c.alias\` { fact, field }:
55
+ * OMX_<FACT>_<FIELD>_MIN/_MAX/_DEFAULT. Removed by a MAJOR release once no consumer reads it. */`],
56
+ [scalars, `/* Every scalar, sheet leaf and single travel's numbers, one block sorted by macro. An integer value
57
+ * is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */`],
58
+ [lists, `/* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */`],
59
+ ].filter(([body]) => body);
60
+ return `// SPDX-License-Identifier: GPL-3.0-or-later
61
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
62
+ #ifndef OMX_CONTRACT_LIMITS_H
63
+ #define OMX_CONTRACT_LIMITS_H
64
+ /*
65
+ * GENERATED — DO NOT EDIT BY HAND.
66
+ * Produced by \`omx-contract render --target c\` from FreeMixer/omx-contract's data/, the declaration
67
+ * as data (each item's reason is its \`doc\`). Change the data, never this file: a consumer builds
68
+ * against the render of the omx-contract version it pins and commits none.
69
+ *
70
+ * A C contract (OMX_PRE/OMX_POST/OMX_INVARIANT) reads a limit from HERE, never a literal.
71
+ */
72
+
73
+ ${sections.map(([body, comment]) => `${comment}\n${body}`).join('\n\n')}
74
+
75
+ #endif /* OMX_CONTRACT_LIMITS_H */
76
+ ${d.STANDARD_SAMPLE_RATES ? ratesTail(d.STANDARD_SAMPLE_RATES) : ''}`;
77
+ }
78
+
79
+ /** Every file of the `c` target: `[{ path, text }]`, paths relative to the output directory. */
80
+ export function renderC(resolved) {
81
+ return [{ path: 'omxcontract/omx_contract_limits.h', text: renderLimitsHeader(resolved) }];
82
+ }
83
+
84
+ /**
85
+ * The `c` target of a frozen openmixer sheet (test/fixtures/): `{ sheet, plugins: [{ header, prefix,
86
+ * source, params }] }` rendered by openmixer's own renderer, for the round-trip proof (spec §4.4 step 1).
87
+ */
88
+ export function renderSheetC(fixture) {
89
+ const s = fixture.sheet;
90
+ const sheet = { ...s, CORE_LIMITS: new Map(Object.entries(s.CORE_LIMITS)) };
91
+ const files = [{ path: 'omxcontract/omx_contract_limits.h', text: renderContractLimits(sheet) }];
92
+ for (const p of fixture.plugins) files.push({ path: `omxcontract/params/${p.header}`, text: generate(p, p.params) });
93
+ return files;
94
+ }
@@ -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 resolved data cut into the C renderer's doors, as openmixer's discovery cut its barrels
5
+ * (packages/core/src/native-limit-tables.ts, declared-scalars.ts, declared-lists.ts): a `travels`
6
+ * table named <PREFIX>_LIMITS is a native table; every scalar, sheet and single travel is scalars
7
+ * (its finite-number leaves, a travel's `unit`/`defaultFrom` labels skipped); a list is a declared
8
+ * list unless its item says `"c": { "render": false }`; a `c.alias` is the core-limit spelling.
9
+ */
10
+
11
+ const constantCase = (key) => key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
12
+ const isSheet = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;
13
+ const isTravel = (v) => typeof v.min === 'number' && typeof v.max === 'number';
14
+ const TRAVEL_LABELS = new Set(['unit', 'defaultFrom']);
15
+
16
+ /** Every leaf of `v` when ALL its leaves are finite numbers; `undefined` when any is not (declared-scalars.ts). */
17
+ function numberLeaves(v, path, macro) {
18
+ if (typeof v === 'number') return Number.isFinite(v) ? [{ path, macro, value: v }] : undefined;
19
+ if (!isSheet(v)) return undefined;
20
+ const out = [];
21
+ for (const [key, leaf] of Object.entries(v)) {
22
+ if (TRAVEL_LABELS.has(key) && typeof leaf === 'string' && !path.includes('.') && isTravel(v)) continue;
23
+ const leaves = numberLeaves(leaf, `${path}.${key}`, `${macro}_${constantCase(key)}`);
24
+ if (leaves === undefined) return undefined;
25
+ out.push(...leaves);
26
+ }
27
+ return out;
28
+ }
29
+
30
+ /** True for a `travels` table that is a native limit table (its name ends `_LIMITS`). */
31
+ export const isNativeTable = (r) => r.kind === 'travels' && r.shape === 'table' && r.name.endsWith('_LIMITS');
32
+
33
+ /** The doors of the resolved items (a Map name -> resolved item). */
34
+ export function doors(resolved) {
35
+ const items = [...resolved.values()];
36
+ const NATIVE_LIMIT_TABLES = items.filter(isNativeTable)
37
+ .map((r) => [r.name.slice(0, -'_LIMITS'.length), r.value])
38
+ .sort(([a], [b]) => a.localeCompare(b));
39
+ const byMacro = new Map();
40
+ for (const r of items) {
41
+ if (isNativeTable(r) || r.kind === 'list') continue;
42
+ const leaves = numberLeaves(r.value, r.name, r.name.replace(/_(KERNEL|LIMITS)$/, ''));
43
+ if (leaves === undefined) continue;
44
+ for (const s of leaves) {
45
+ const seen = byMacro.get(s.macro);
46
+ if (seen) throw new Error(`${seen.path} and ${s.path} both render as OMX_${s.macro} — rename one`);
47
+ byMacro.set(s.macro, s);
48
+ }
49
+ }
50
+ const DECLARED_SCALARS = [...byMacro.values()].sort((a, b) => (a.macro < b.macro ? -1 : a.macro > b.macro ? 1 : 0));
51
+ const DECLARED_LISTS = items.filter((r) => r.kind === 'list' && r.c?.render !== false)
52
+ .map((r) => ({ name: r.name, values: r.value }))
53
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
54
+ const ALIASES = new Map();
55
+ for (const r of items) {
56
+ if (!r.c?.alias) continue;
57
+ const { fact, field } = r.c.alias;
58
+ if (!ALIASES.has(fact)) ALIASES.set(fact, {});
59
+ ALIASES.get(fact)[field] = r.value;
60
+ }
61
+ const STANDARD_SAMPLE_RATES = resolved.get('STANDARD_SAMPLE_RATES')?.value;
62
+ return { NATIVE_LIMIT_TABLES, DECLARED_SCALARS, DECLARED_LISTS, ALIASES, STANDARD_SAMPLE_RATES };
63
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The `json` target: the resolved data (every reference and derivation evaluated) for readers in
5
+ * other languages, which need no resolver of their own.
6
+ */
7
+
8
+ /** Every file of the `json` target: `[{ path, text }]`. */
9
+ export function renderJson(resolved, version) {
10
+ const items = {};
11
+ for (const r of resolved.values()) {
12
+ const { name, ...rest } = r;
13
+ items[name] = rest;
14
+ }
15
+ return [{ path: 'omx-contract.json', text: `${JSON.stringify({ name: '@openmixer/omx-contract', version, items }, null, 2)}\n` }];
16
+ }
package/render/ts.mjs ADDED
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The `ts` target: one ESM module and its type declarations, every item exported under its own
5
+ * name with its literal type (`as const`), so a re-export in openmixer keeps names, values and types.
6
+ */
7
+
8
+ /** The value a TypeScript reader sees for one resolved item. */
9
+ export function tsValue(r) {
10
+ return r.value;
11
+ }
12
+
13
+ /** The literal type of a JSON value, every member readonly. */
14
+ export function tsType(v, indent = '') {
15
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
16
+ if (typeof v === 'string') return JSON.stringify(v);
17
+ const inner = `${indent} `;
18
+ if (Array.isArray(v)) return v.length ? `readonly [${v.map((x) => tsType(x, indent)).join(', ')}]` : 'readonly []';
19
+ const keys = Object.keys(v);
20
+ if (!keys.length) return '{}';
21
+ const key = (k) => (/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k));
22
+ return `{\n${keys.map((k) => `${inner}readonly ${key(k)}: ${tsType(v[k], inner)};`).join('\n')}\n${indent}}`;
23
+ }
24
+
25
+ const docComment = (r) => {
26
+ const text = [r.doc, r.why && `Why: ${r.why}`].filter(Boolean).join('\n\n').replace(/\*\//g, '*\\/');
27
+ return `/**\n${text.split('\n').map((l) => (l ? ` * ${l}` : ' *')).join('\n')}\n * @see ${r.rel}\n */`;
28
+ };
29
+
30
+ const BANNER = `// SPDX-License-Identifier: GPL-3.0-or-later
31
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
32
+ // GENERATED by \`omx-contract render --target ts\` from FreeMixer/omx-contract's data/ — do not edit.
33
+ `;
34
+
35
+ /** Every file of the `ts` target: `[{ path, text }]`. */
36
+ export function renderTs(resolved) {
37
+ const items = [...resolved.values()];
38
+ const js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('');
39
+ const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('');
40
+ return [{ path: 'omx-contract.js', text: js }, { path: 'omx-contract.d.ts', text: dts }];
41
+ }
@@ -0,0 +1,164 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/FreeMixer/omx-contract/raw/main/schema/omx-contract.schema.json",
4
+ "title": "omx-contract data file",
5
+ "description": "A map from an export name to one item: a scalar, a travels table or travel, a sheet or a list. GPL-3.0-or-later.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": { "type": "string" }
9
+ },
10
+ "propertyNames": { "anyOf": [{ "const": "$schema" }, { "pattern": "^[A-Z][A-Z0-9_]*$" }] },
11
+ "additionalProperties": { "$ref": "#/$defs/item" },
12
+ "$defs": {
13
+ "unit": {
14
+ "description": "The closed list of units the items use; \"x\" is a multiplier, \"\" is a ratio or a fraction.",
15
+ "enum": ["dB", "dBFS", "ms", "s", "min", "Hz", "oct", "cents", "%", "x", ""]
16
+ },
17
+ "text": { "type": "string", "minLength": 1 },
18
+ "name": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
19
+ "ref": {
20
+ "type": "object",
21
+ "properties": { "ref": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*(\\.[A-Za-z][A-Za-z0-9]*)*$" } },
22
+ "required": ["ref"],
23
+ "additionalProperties": false
24
+ },
25
+ "derive": {
26
+ "description": "The closed list of derivations; a new one is a renderer change with its own test.",
27
+ "oneOf": [
28
+ {
29
+ "type": "object",
30
+ "properties": { "derive": { "const": "count" }, "of": { "$ref": "#/$defs/name" } },
31
+ "required": ["derive", "of"],
32
+ "additionalProperties": false
33
+ },
34
+ {
35
+ "type": "object",
36
+ "properties": { "derive": { "const": "sqrt" }, "of": { "$ref": "#/$defs/number" } },
37
+ "required": ["derive", "of"],
38
+ "additionalProperties": false
39
+ },
40
+ {
41
+ "type": "object",
42
+ "properties": {
43
+ "derive": { "const": "lagrangeReadL1" },
44
+ "order": { "oneOf": [{ "type": "integer", "minimum": 1 }, { "$ref": "#/$defs/ref" }] }
45
+ },
46
+ "required": ["derive", "order"],
47
+ "additionalProperties": false
48
+ }
49
+ ]
50
+ },
51
+ "number": { "oneOf": [{ "type": "number" }, { "$ref": "#/$defs/ref" }, { "$ref": "#/$defs/derive" }] },
52
+ "travel": {
53
+ "type": "object",
54
+ "properties": {
55
+ "min": { "$ref": "#/$defs/number" },
56
+ "max": { "$ref": "#/$defs/number" },
57
+ "step": { "$ref": "#/$defs/number" },
58
+ "unit": { "$ref": "#/$defs/unit" },
59
+ "default": { "$ref": "#/$defs/number" },
60
+ "defaultFrom": { "enum": ["desk"] },
61
+ "byKind": {
62
+ "type": "object",
63
+ "propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
64
+ "additionalProperties": { "$ref": "#/$defs/number" },
65
+ "minProperties": 1
66
+ },
67
+ "doc": { "$ref": "#/$defs/text" },
68
+ "why": { "$ref": "#/$defs/text" }
69
+ },
70
+ "required": ["min", "max"],
71
+ "additionalProperties": false
72
+ },
73
+ "sheetValue": {
74
+ "type": "object",
75
+ "propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
76
+ "additionalProperties": { "oneOf": [{ "$ref": "#/$defs/number" }, { "$ref": "#/$defs/sheetValue" }] },
77
+ "minProperties": 1
78
+ },
79
+ "scalar": {
80
+ "type": "object",
81
+ "properties": {
82
+ "kind": { "const": "scalar" },
83
+ "doc": { "$ref": "#/$defs/text" },
84
+ "why": { "$ref": "#/$defs/text" },
85
+ "unit": { "$ref": "#/$defs/unit" },
86
+ "value": { "oneOf": [{ "$ref": "#/$defs/number" }, { "type": "boolean" }] }
87
+ },
88
+ "required": ["kind", "doc", "unit", "value"],
89
+ "additionalProperties": false
90
+ },
91
+ "travels": {
92
+ "type": "object",
93
+ "properties": {
94
+ "kind": { "const": "travels" },
95
+ "doc": { "$ref": "#/$defs/text" },
96
+ "why": { "$ref": "#/$defs/text" },
97
+ "fields": {
98
+ "type": "object",
99
+ "propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
100
+ "additionalProperties": { "oneOf": [{ "$ref": "#/$defs/travel" }, { "$ref": "#/$defs/ref" }] },
101
+ "minProperties": 1
102
+ },
103
+ "travel": { "$ref": "#/$defs/travel" },
104
+ "c": {
105
+ "type": "object",
106
+ "properties": {
107
+ "alias": {
108
+ "type": "object",
109
+ "properties": {
110
+ "fact": { "type": "string", "pattern": "^[a-z][A-Za-z0-9]*$" },
111
+ "field": { "type": "string", "pattern": "^[a-z][A-Za-z0-9]*$" }
112
+ },
113
+ "required": ["fact", "field"],
114
+ "additionalProperties": false
115
+ }
116
+ },
117
+ "additionalProperties": false
118
+ }
119
+ },
120
+ "required": ["kind", "doc"],
121
+ "oneOf": [{ "required": ["fields"] }, { "required": ["travel"] }],
122
+ "additionalProperties": false
123
+ },
124
+ "sheet": {
125
+ "type": "object",
126
+ "properties": {
127
+ "kind": { "const": "sheet" },
128
+ "doc": { "$ref": "#/$defs/text" },
129
+ "why": { "$ref": "#/$defs/text" },
130
+ "value": { "$ref": "#/$defs/sheetValue" }
131
+ },
132
+ "required": ["kind", "doc", "value"],
133
+ "additionalProperties": false
134
+ },
135
+ "list": {
136
+ "type": "object",
137
+ "properties": {
138
+ "kind": { "const": "list" },
139
+ "doc": { "$ref": "#/$defs/text" },
140
+ "why": { "$ref": "#/$defs/text" },
141
+ "unit": { "$ref": "#/$defs/unit" },
142
+ "values": { "type": "array", "items": { "type": "number" }, "minItems": 1 },
143
+ "c": {
144
+ "type": "object",
145
+ "properties": { "render": { "type": "boolean" } },
146
+ "additionalProperties": false
147
+ }
148
+ },
149
+ "required": ["kind", "doc", "unit", "values"],
150
+ "additionalProperties": false
151
+ },
152
+ "item": {
153
+ "type": "object",
154
+ "required": ["kind"],
155
+ "properties": { "kind": { "enum": ["scalar", "travels", "sheet", "list"] } },
156
+ "allOf": [
157
+ { "if": { "properties": { "kind": { "const": "scalar" } } }, "then": { "$ref": "#/$defs/scalar" } },
158
+ { "if": { "properties": { "kind": { "const": "travels" } } }, "then": { "$ref": "#/$defs/travels" } },
159
+ { "if": { "properties": { "kind": { "const": "sheet" } } }, "then": { "$ref": "#/$defs/sheet" } },
160
+ { "if": { "properties": { "kind": { "const": "list" } } }, "then": { "$ref": "#/$defs/list" } }
161
+ ]
162
+ }
163
+ }
164
+ }