@openmixer/plugin-qualify 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/bin/plugin-qualify.mjs +1 -1
- package/dist/clap-qualify.d.ts +29 -6
- package/dist/clap-qualify.js +77 -13
- package/dist/clap-qualify.js.map +1 -1
- package/dist/host-profiles.d.ts +14 -0
- package/dist/host-profiles.js +27 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.d.ts +5 -2
- package/dist/hosting-codes.js +8 -4
- package/dist/hosting-codes.js.map +1 -1
- package/dist/hosting-suitability.d.ts +32 -3
- package/dist/hosting-suitability.js +35 -6
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/measure-host.js +2 -2
- package/dist/measurement/format.d.ts +1 -1
- package/dist/measurement/vocabulary.d.ts +1 -1
- package/dist/measurement/vocabulary.js +1 -1
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/qualify-declarations.d.ts +15 -4
- package/dist/qualify-declarations.js +11 -3
- package/dist/qualify-declarations.js.map +1 -1
- package/package.json +3 -2
- package/src/clap-qualify.ts +89 -12
- package/src/host-profiles.ts +29 -1
- package/src/hosting-codes.ts +8 -4
- package/src/hosting-suitability.ts +61 -8
- package/src/measure-host.ts +2 -2
- package/src/measurement/format.ts +1 -1
- package/src/measurement/vocabulary.ts +1 -1
- package/src/qualify-declarations.ts +12 -4
- package/tools/ai-trace-commit-check.sh +114 -0
- package/tools/ai-trace-patterns.json +33 -0
- package/tools/benchmark.py +1 -1
- package/tools/private-refs-check.sh +90 -0
package/src/host-profiles.ts
CHANGED
|
@@ -54,6 +54,17 @@ export const OMX_CLAP_HOST_EXTENSIONS: readonly string[] = [
|
|
|
54
54
|
"clap.state",
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The three host extensions omx-clap-host turns on beyond the library's own (omx-clap-core
|
|
59
|
+
* `src/effects.c` `configure_core`: `preset_load`, `track_info`, `remote_controls`), by the ids
|
|
60
|
+
* its host object answers (`src/clap_host.c`); the prober offers them with `--offer`.
|
|
61
|
+
*/
|
|
62
|
+
export const OMX_CLAP_HOST_CONFIGURED_EXTENSIONS: readonly string[] = [
|
|
63
|
+
"clap.preset-load/2",
|
|
64
|
+
"clap.track-info/1",
|
|
65
|
+
"clap.remote-controls/2",
|
|
66
|
+
];
|
|
67
|
+
|
|
57
68
|
/** The LV2 extension namespace the jalv and mod-host lists are spelled in. */
|
|
58
69
|
const LV2_EXT = "http://lv2plug.in/ns/ext/";
|
|
59
70
|
const BUF_SIZE_ALL = ["powerOf2BlockLength", "fixedBlockLength", "boundedBlockLength"].map((f) => `${LV2_EXT}buf-size#${f}`);
|
|
@@ -113,8 +124,25 @@ export const MOD_HOST_PROFILE: HostProfile = {
|
|
|
113
124
|
isolation: "shared-process",
|
|
114
125
|
};
|
|
115
126
|
|
|
127
|
+
/**
|
|
128
|
+
* omx-clap-host, alone or as plugin-hostd's CLAP worker: one JACK client per plugin in a host
|
|
129
|
+
* process that several plugins may share (a plugin-hostd pool), instruments fed through each
|
|
130
|
+
* client's `midi_in`, latency published on the JACK ports. Its CLAP host object is the library's
|
|
131
|
+
* plus the three it configures; its LV2 plugins run through the same feature provider as the
|
|
132
|
+
* console's in-process stage.
|
|
133
|
+
*/
|
|
134
|
+
export const OMX_CLAP_HOST_PROFILE: HostProfile = {
|
|
135
|
+
id: "omx-clap-host",
|
|
136
|
+
lv2Features: OMX_INPROCESS_FEATURES,
|
|
137
|
+
clapExtensions: [...OMX_CLAP_HOST_EXTENSIONS, ...OMX_CLAP_HOST_CONFIGURED_EXTENSIONS],
|
|
138
|
+
instruments: true,
|
|
139
|
+
cost: HOST_BUDGETS["omx-clap-host"],
|
|
140
|
+
latency: "uncompensated",
|
|
141
|
+
isolation: "shared-process",
|
|
142
|
+
};
|
|
143
|
+
|
|
116
144
|
/** Every profile the tool ships, by id. */
|
|
117
|
-
export const HOST_PROFILES: readonly HostProfile[] = [OPENMIXER_CONSOLE_PROFILE, JALV_PROFILE, MOD_HOST_PROFILE];
|
|
145
|
+
export const HOST_PROFILES: readonly HostProfile[] = [OPENMIXER_CONSOLE_PROFILE, JALV_PROFILE, MOD_HOST_PROFILE, OMX_CLAP_HOST_PROFILE];
|
|
118
146
|
|
|
119
147
|
/** A shipped profile by id, or `undefined`. */
|
|
120
148
|
export function shippedHostProfile(id: string): HostProfile | undefined {
|
package/src/hosting-codes.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* The hosting verdict's message codes
|
|
4
|
+
* The hosting verdict's message codes. Codes, never prose: the
|
|
5
5
|
* surface renders the operator's locale.
|
|
6
6
|
*
|
|
7
|
-
* A UNION list file
|
|
7
|
+
* A UNION list file: `.gitattributes` marks it
|
|
8
8
|
* `merge=union`, so two lanes that each add a code merge with both kept. Only the table lives
|
|
9
9
|
* here, one `key: 'code',` per line, with keys and codes unique; the registry-hotspots gate
|
|
10
10
|
* refuses anything else.
|
|
@@ -24,6 +24,10 @@ export const HOSTING_CODE = {
|
|
|
24
24
|
rtSafetyViolations: 'hosting.rt-safety.violations-in-run',
|
|
25
25
|
rtSafetyUnmeasured: 'hosting.rt-safety.unmeasured',
|
|
26
26
|
rtSafetyNotReproducible: 'hosting.rt-safety.not-reproducible',
|
|
27
|
+
// The note sweep: an instrument played under the interposer, a count of its own.
|
|
28
|
+
rtSafetyNotesClean: 'hosting.rt-safety.clean-under-notes',
|
|
29
|
+
rtSafetyNotesViolations: 'hosting.rt-safety.violations-under-notes',
|
|
30
|
+
rtSafetyNotesUnmeasured: 'hosting.rt-safety.notes-unmeasured',
|
|
27
31
|
featuresProvided: 'hosting.features.all-provided',
|
|
28
32
|
featuresMissing: 'hosting.features.required-not-provided',
|
|
29
33
|
featuresDisqualifying: 'hosting.features.host-not-provided',
|
|
@@ -44,7 +48,7 @@ export const HOSTING_CODE = {
|
|
|
44
48
|
topologyNoAudioOutput: 'hosting.topology.no-audio-output',
|
|
45
49
|
topologyExtraInputs: 'hosting.topology.extra-inputs-fed-silence',
|
|
46
50
|
topologyInstrument: 'hosting.topology.instrument',
|
|
47
|
-
// The CLAP arm
|
|
51
|
+
// The CLAP arm: the SAME dimensions fed from CLAP facts,
|
|
48
52
|
// minted here where the LV2 table has no code for the fact.
|
|
49
53
|
topologyWiderThanStrip: 'hosting.topology.wider-than-strip',
|
|
50
54
|
clapLatencyMismatch: 'hosting.clap.latency-mismatch',
|
|
@@ -72,6 +76,6 @@ export const HOSTING_CODE = {
|
|
|
72
76
|
noRealisation: 'hosting.no-realisation',
|
|
73
77
|
realisedInProcess: 'hosting.realised.in-process',
|
|
74
78
|
// A plugin LINKED into the engine (the static-link registry) runs in-process without a
|
|
75
|
-
// verdict: the library's oracles are its proof
|
|
79
|
+
// verdict: the library's oracles are its proof.
|
|
76
80
|
linked: 'hosting.linked',
|
|
77
81
|
} as const;
|
|
@@ -199,12 +199,12 @@ export interface CrashSignature {
|
|
|
199
199
|
/** The top plugin-owned stack frame, or a host frame that exonerates the plugin. */
|
|
200
200
|
readonly topFrame: string;
|
|
201
201
|
readonly signal?: "SIGSEGV" | "SIGABRT" | "SIGBUS";
|
|
202
|
-
/**
|
|
202
|
+
/** What was found about this cluster. Provenance, so a row is never folklore. */
|
|
203
203
|
readonly note: string;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* Seeded from the
|
|
207
|
+
* Seeded from the crashes investigated on 2026-09-03/04. A raw crash matching NO row is
|
|
208
208
|
* `unknown` — the table blames a plugin only where a named investigation did.
|
|
209
209
|
*/
|
|
210
210
|
export const CRASH_SIGNATURES: readonly CrashSignature[] = [
|
|
@@ -213,26 +213,26 @@ export const CRASH_SIGNATURES: readonly CrashSignature[] = [
|
|
|
213
213
|
attribution: "host",
|
|
214
214
|
topFrame: "pw_memmap_free",
|
|
215
215
|
signal: "SIGSEGV",
|
|
216
|
-
note: "
|
|
216
|
+
note: "closing a PipeWire JACK client frees a memory map twice; the host crashes, not the plugin",
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
219
|
id: "pipewire-jack-close-double-free-abort",
|
|
220
220
|
attribution: "host",
|
|
221
221
|
topFrame: "tcache_double_free_verify",
|
|
222
222
|
signal: "SIGABRT",
|
|
223
|
-
note: "
|
|
223
|
+
note: "the same double free in PipeWire's JACK client close, caught by glibc's tcache check as an abort",
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
id: "mod-host-effects-remove",
|
|
227
227
|
attribution: "host",
|
|
228
228
|
topFrame: "effects_remove",
|
|
229
|
-
note: "
|
|
229
|
+
note: "mod-host crashes in effects_remove while removing an effect; the host is at fault",
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
id: "plugin-instantiate-layout",
|
|
233
233
|
attribution: "plugin",
|
|
234
234
|
topFrame: "lilv_instance_instantiate",
|
|
235
|
-
note: "
|
|
235
|
+
note: "the plugin crashes while lilv instantiates it, on a port layout it cannot host",
|
|
236
236
|
},
|
|
237
237
|
];
|
|
238
238
|
|
|
@@ -342,6 +342,28 @@ export interface PluginClapMeasurement {
|
|
|
342
342
|
readonly processCalls: number;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* The note sweep of a CLAP plugin with a note input (plugin-qualify's prober): the plugin played at
|
|
347
|
+
* every rate — a chord, bend and a controller, pressure, the chord let go, a burst of note ons and
|
|
348
|
+
* their offs, all notes off — in the dialect its first note input reads, counted under the
|
|
349
|
+
* interposer apart from the parameter sweep. `applicable: false` is a plugin the prober could not
|
|
350
|
+
* play: no note input, or none in a dialect it speaks.
|
|
351
|
+
*/
|
|
352
|
+
export interface PluginNoteSweepMeasurement {
|
|
353
|
+
readonly applicable: boolean;
|
|
354
|
+
readonly dialect?: "clap" | "midi";
|
|
355
|
+
readonly blocks?: number;
|
|
356
|
+
readonly blockFrames?: number;
|
|
357
|
+
/** Note, expression and MIDI events the sweep sent. */
|
|
358
|
+
readonly noteEvents?: number;
|
|
359
|
+
readonly processErrors?: number;
|
|
360
|
+
readonly nonFinite?: boolean;
|
|
361
|
+
readonly interposer?: boolean;
|
|
362
|
+
readonly allocationsInRun?: number;
|
|
363
|
+
readonly locksInRun?: number;
|
|
364
|
+
readonly syscallsInRun?: number;
|
|
365
|
+
}
|
|
366
|
+
|
|
345
367
|
/** What the classifier is told about the console asking. */
|
|
346
368
|
export interface HostingClassifyOptions {
|
|
347
369
|
/** The LIVE rate. Cost is read here and nowhere else — never projected. */
|
|
@@ -888,6 +910,37 @@ export function clapRtReasons(clap: PluginClapMeasurement | undefined): readonly
|
|
|
888
910
|
return out;
|
|
889
911
|
}
|
|
890
912
|
|
|
913
|
+
/**
|
|
914
|
+
* The rtSafety reasons of the note sweep, for a plugin with `noteInputs` note inputs: none for a
|
|
915
|
+
* plugin that takes no notes; `unknown` when it takes notes and was not played under the
|
|
916
|
+
* interposer (an instrument is cheap and clean at rest, so the parameter sweep alone says nothing
|
|
917
|
+
* about a voice); `unsuitable` on any allocation, lock or syscall while it was played, or a process
|
|
918
|
+
* error; `suitable` on a clean count.
|
|
919
|
+
*/
|
|
920
|
+
export function noteSweepReasons(sweep: PluginNoteSweepMeasurement | undefined, noteInputs: number): readonly HostingReason[] {
|
|
921
|
+
if (noteInputs === 0) return [];
|
|
922
|
+
if (sweep === undefined || !sweep.applicable || sweep.interposer !== true || !sweep.blocks ||
|
|
923
|
+
sweep.allocationsInRun === undefined || sweep.locksInRun === undefined || sweep.syscallsInRun === undefined) {
|
|
924
|
+
return [reason("rtSafety", "objective", "unknown", HOSTING_CODE.rtSafetyNotesUnmeasured)];
|
|
925
|
+
}
|
|
926
|
+
const out: HostingReason[] = [];
|
|
927
|
+
if (sweep.allocationsInRun + sweep.locksInRun + sweep.syscallsInRun > 0) {
|
|
928
|
+
out.push(
|
|
929
|
+
reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.rtSafetyNotesViolations, {
|
|
930
|
+
allocations: sweep.allocationsInRun,
|
|
931
|
+
syscalls: sweep.syscallsInRun,
|
|
932
|
+
locks: sweep.locksInRun,
|
|
933
|
+
}),
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if ((sweep.processErrors ?? 0) > 0) {
|
|
937
|
+
out.push(reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.clapProcessError, { errors: sweep.processErrors ?? 0 }));
|
|
938
|
+
}
|
|
939
|
+
return out.length > 0
|
|
940
|
+
? out
|
|
941
|
+
: [reason("rtSafety", "objective", "suitable", HOSTING_CODE.rtSafetyNotesClean, { blocks: sweep.blocks, events: sweep.noteEvents ?? 0 })];
|
|
942
|
+
}
|
|
943
|
+
|
|
891
944
|
/**
|
|
892
945
|
* The verdict. Worst-wins across every dimension, exactly as `classifyForRole` folds — with
|
|
893
946
|
* three gates that can poison the whole result before the fold is even read: a curator
|
|
@@ -905,7 +958,7 @@ export function classifyPluginHosting(
|
|
|
905
958
|
// interchange's one door (`measurement-catalog.ts`), whose per-dimension precedence is the
|
|
906
959
|
// only precedence there is. A second "passed measurements win" seam here was how an offline
|
|
907
960
|
// report could agree with itself while every served descriptor read unknown.
|
|
908
|
-
// The CLAP arm
|
|
961
|
+
// The CLAP arm: the SAME six dimensions, fed from the
|
|
909
962
|
// qualifier's CLAP facts where the LV2 scan's facts do not exist for the format. Stability,
|
|
910
963
|
// the interposer's counts and the cost are format-blind and read unchanged.
|
|
911
964
|
const reasons: HostingReason[] = descriptor.format === "clap"
|
|
@@ -988,7 +1041,7 @@ export interface HostingRealisation {
|
|
|
988
1041
|
readonly inProcessAvailable: boolean;
|
|
989
1042
|
/**
|
|
990
1043
|
* Whether the plugin is LINKED into the engine (its static-link registry): it needs no verdict
|
|
991
|
-
* to run in-process
|
|
1044
|
+
* to run in-process. Absent reads as not linked.
|
|
992
1045
|
*/
|
|
993
1046
|
readonly linked?: boolean;
|
|
994
1047
|
}
|
package/src/measure-host.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* Reading what the measuring host (`lv2_measure`) prints and supervising how long it may take
|
|
5
|
-
*
|
|
4
|
+
* Reading what the measuring host (`lv2_measure`) prints and supervising how long it may take.
|
|
5
|
+
* Pure functions: the runner spawns, these decide.
|
|
6
6
|
*/
|
|
7
7
|
import { isRecord } from "./measurement/guards.js";
|
|
8
8
|
import { SOAK_SWEEP_DB, SOAK_SWEEP_RATIO, SOAK_TIMEOUT_FLOOR_S, SOAK_TIMEOUT_MARGIN } from "./qualify-declarations.js";
|
|
@@ -464,7 +464,7 @@ export interface HostBudget {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
/**
|
|
467
|
-
* A HOST PROFILE
|
|
467
|
+
* A HOST PROFILE: the host a verdict was judged
|
|
468
468
|
* against — only what differs between hosts. A document carries every profile its verdicts name,
|
|
469
469
|
* in full, so a verdict against a project's own profile is reproducible from the document alone.
|
|
470
470
|
*/
|
|
@@ -302,7 +302,7 @@ export function readDimension(value: unknown): MeasurementDimension | undefined
|
|
|
302
302
|
export const VERDICT_RATINGS = ["suitable", "conditional", "unknown", "unsuitable"] as const;
|
|
303
303
|
export type VerdictRating = (typeof VERDICT_RATINGS)[number];
|
|
304
304
|
|
|
305
|
-
/** How a host runs its plugins
|
|
305
|
+
/** How a host runs its plugins. */
|
|
306
306
|
export const HOST_ISOLATIONS = ["in-process", "per-process", "shared-process"] as const;
|
|
307
307
|
export type HostIsolation = (typeof HOST_ISOLATIONS)[number];
|
|
308
308
|
|
|
@@ -82,18 +82,22 @@ export const CLAP_SYSTEM_DIR = "/usr/lib/clap";
|
|
|
82
82
|
export const CLAP_SCAN_TIMEOUT_S = 300;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* The CLAP prober (`native/clap-qualify.c`), the LV2 measurer (`native/lv2-measure.c`)
|
|
86
|
-
* interposer both run under (`native/rt-interpose.c`)
|
|
87
|
-
*
|
|
85
|
+
* The CLAP prober (`native/clap-qualify.c`), the LV2 measurer (`native/lv2-measure.c`), the
|
|
86
|
+
* interposer both run under (`native/rt-interpose.c`) and the LV2 host probe
|
|
87
|
+
* (`native/lv2-host-probe.c`, a worker plugin driven through lilv as a host drives it, for a console
|
|
88
|
+
* that exports an LV2 plugin to test its host-facing half): each located by one environment variable,
|
|
89
|
+
* else the package's own build in `native/build/`, else the first of `NATIVE_TOOL_DIRS` that holds it.
|
|
88
90
|
*/
|
|
89
91
|
export const CLAP_QUALIFY_TOOL_ENV = "OMX_CLAP_QUALIFY";
|
|
90
92
|
export const LV2_MEASURE_TOOL_ENV = "OMX_LV2_MEASURE";
|
|
91
93
|
export const RT_INTERPOSE_ENV = "OMX_RT_INTERPOSE";
|
|
94
|
+
export const LV2_HOST_PROBE_TOOL_ENV = "OMX_LV2_HOST_PROBE";
|
|
92
95
|
|
|
93
96
|
/** The file names of the native tools, in every place they are looked for. */
|
|
94
97
|
export const CLAP_QUALIFY_TOOL_NAME = "clap-qualify";
|
|
95
98
|
export const LV2_MEASURE_TOOL_NAME = "lv2_measure";
|
|
96
99
|
export const RT_INTERPOSE_NAME = "rt_interpose.so";
|
|
100
|
+
export const LV2_HOST_PROBE_TOOL_NAME = "lv2_host_probe";
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
103
|
* The ONE list of the native tools the packages install, each with the setting that names it.
|
|
@@ -103,6 +107,7 @@ export const NATIVE_TOOLS: readonly { readonly name: string; readonly env: strin
|
|
|
103
107
|
{ name: CLAP_QUALIFY_TOOL_NAME, env: CLAP_QUALIFY_TOOL_ENV },
|
|
104
108
|
{ name: LV2_MEASURE_TOOL_NAME, env: LV2_MEASURE_TOOL_ENV },
|
|
105
109
|
{ name: RT_INTERPOSE_NAME, env: RT_INTERPOSE_ENV },
|
|
110
|
+
{ name: LV2_HOST_PROBE_TOOL_NAME, env: LV2_HOST_PROBE_TOOL_ENV },
|
|
106
111
|
];
|
|
107
112
|
|
|
108
113
|
/**
|
|
@@ -162,7 +167,7 @@ export const DEFAULT_HOSTING_POLICY: HostingPolicy = {
|
|
|
162
167
|
soakDeadDropDb: 40,
|
|
163
168
|
};
|
|
164
169
|
|
|
165
|
-
/** What a host spends on one plugin instance
|
|
170
|
+
/** What a host spends on one plugin instance — the
|
|
166
171
|
* interchange's shape, since a document carries each profile's budget. */
|
|
167
172
|
import type { HostBudget } from "./measurement/format.js";
|
|
168
173
|
export type { HostBudget };
|
|
@@ -173,10 +178,13 @@ export type { HostBudget };
|
|
|
173
178
|
* so it gets the same per-instance share, at 48 kHz / 128 frames. `jalv` gives each plugin its own
|
|
174
179
|
* process and client, so a quarter core per instance, at 48 kHz / 256 frames. The last two are
|
|
175
180
|
* declared defaults, not a measurement of anyone's board — a project brings its own profile file.
|
|
181
|
+
* `omx-clap-host` runs its plugins one JACK client each, called in turn on the server's one cycle,
|
|
182
|
+
* so it gets mod-host's share and pair.
|
|
176
183
|
*/
|
|
177
184
|
export const HOST_BUDGETS = {
|
|
178
185
|
"openmixer-console": { coreFractionCeiling: 0.05, rate: COST_REFERENCE_RATE, quantum: QUANTUM_FRAMES },
|
|
179
186
|
jalv: { coreFractionCeiling: 0.25, rate: MEASURE_BASE_RATE, quantum: 256 },
|
|
180
187
|
"mod-host": { coreFractionCeiling: 0.05, rate: MEASURE_BASE_RATE, quantum: 128 },
|
|
188
|
+
"omx-clap-host": { coreFractionCeiling: 0.05, rate: MEASURE_BASE_RATE, quantum: 128 },
|
|
181
189
|
} as const satisfies Readonly<Record<string, HostBudget>>;
|
|
182
190
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# ai-trace-commit-check.sh: fail when a commit message carries an AI-attribution line.
|
|
6
|
+
#
|
|
7
|
+
# ai-trace-commit-check.sh <range> every commit in the range (e.g. origin/main..HEAD)
|
|
8
|
+
# ai-trace-commit-check.sh --msg-file FILE one message (the commit-msg hook; # lines are git comments, skipped)
|
|
9
|
+
# ai-trace-commit-check.sh --text-file FILE any text, every line judged (pull request titles and bodies)
|
|
10
|
+
# ai-trace-commit-check.sh --selftest fixtures plus a sabotage run
|
|
11
|
+
#
|
|
12
|
+
# Exit 0 clean, 1 a hit (each commit and offending line is named), 2 a range, file or pattern list that cannot be read
|
|
13
|
+
# (any crash is 2). A caller refuses on 2: a checker that cannot judge is never a pass.
|
|
14
|
+
# The patterns are ai-trace-patterns.json beside this script; AI_TRACE_PATTERNS names another JSON (self-tests).
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
export AI_TRACE_SELF="${BASH_SOURCE[0]}"
|
|
17
|
+
exec python3 -I - "$@" <<'PY'
|
|
18
|
+
import json, os, re, subprocess, sys
|
|
19
|
+
|
|
20
|
+
here = os.path.dirname(os.path.realpath(os.environ["AI_TRACE_SELF"]))
|
|
21
|
+
pfile = os.environ.get("AI_TRACE_PATTERNS") or os.path.join(here, "ai-trace-patterns.json")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load():
|
|
25
|
+
with open(pfile, encoding="utf-8") as f:
|
|
26
|
+
return [(m["name"], re.compile(m["pattern"], re.I | re.M)) for m in json.load(f)["markers"]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hits(text, markers):
|
|
30
|
+
out = []
|
|
31
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
32
|
+
for name, rx in markers:
|
|
33
|
+
if rx.search(line):
|
|
34
|
+
out.append((n, name, line.strip()))
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def report(label, text, markers):
|
|
39
|
+
h = hits(text, markers)
|
|
40
|
+
for n, name, line in h:
|
|
41
|
+
print(f"{label}: line {n} [{name}]: {line}")
|
|
42
|
+
return len(h)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def git(*a):
|
|
46
|
+
p = subprocess.run(["git", *a], capture_output=True, text=True)
|
|
47
|
+
if p.returncode:
|
|
48
|
+
print(f"ai-trace-commit-check: git {' '.join(a[:3])}: {p.stderr.strip()}", file=sys.stderr)
|
|
49
|
+
sys.exit(2)
|
|
50
|
+
return p.stdout
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def selftest():
|
|
54
|
+
# built from fragments: this file must not itself carry an attribution line
|
|
55
|
+
co, by, ai = "Co-Authored-", "By: ", "Cl" + "aude"
|
|
56
|
+
sess = "Cl" + "aude-Session: https://cl" + "aude.ai/code/session_01"
|
|
57
|
+
fx = {
|
|
58
|
+
"co-authored-by": f"Fix x\n\n{co}{by}{ai} Opus <noreply@anthro" + "pic.com>\n",
|
|
59
|
+
"claude-session": f"Fix x\n\n{sess}\n",
|
|
60
|
+
"generated-footer": f"Fix x\n\n\U0001F916 Generated with [{ai} Code](https://{ai.lower()}.com/{ai.lower()}-code)\n",
|
|
61
|
+
"claude.ai/code": f"Fix x\n\nsee https://{ai.lower()}.ai/code/session_9\n",
|
|
62
|
+
}
|
|
63
|
+
clean = f"Fix x\n\nGenerated by the build; mentions a hidden directory and a {co}{by}Pau line.\n"
|
|
64
|
+
def run(markers):
|
|
65
|
+
bad = [k for k, v in fx.items() if not hits(v, markers)]
|
|
66
|
+
if hits(clean, markers):
|
|
67
|
+
bad.append("clean-flagged")
|
|
68
|
+
return bad
|
|
69
|
+
m = load()
|
|
70
|
+
first = run(m)
|
|
71
|
+
print("selftest:", "GREEN" if not first else f"RED {first}")
|
|
72
|
+
sab = run([x for x in m if x[0] not in ("co-authored-by", "ai-co-author", "anthropic-noreply", "claude-session")])
|
|
73
|
+
print("sabotage (the co-author, email and session patterns removed):", "RED as expected" if sab else "still GREEN: blind")
|
|
74
|
+
return 0 if not first and sab else 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main(a):
|
|
78
|
+
if a == ["--selftest"]:
|
|
79
|
+
return selftest()
|
|
80
|
+
markers = load()
|
|
81
|
+
if len(a) == 2 and a[0] == "--msg-file":
|
|
82
|
+
try:
|
|
83
|
+
text = open(a[1], encoding="utf-8", errors="replace").read()
|
|
84
|
+
except OSError as e:
|
|
85
|
+
print(f"ai-trace-commit-check: {e}", file=sys.stderr)
|
|
86
|
+
return 2
|
|
87
|
+
# lines git adds as comments are not part of the message
|
|
88
|
+
text = "\n".join(l for l in text.splitlines() if not l.startswith("#"))
|
|
89
|
+
return 1 if report("commit message", text, markers) else 0
|
|
90
|
+
if len(a) == 2 and a[0] == "--text-file":
|
|
91
|
+
try:
|
|
92
|
+
text = open(a[1], encoding="utf-8", errors="replace").read()
|
|
93
|
+
except OSError as e:
|
|
94
|
+
print(f"ai-trace-commit-check: {e}", file=sys.stderr)
|
|
95
|
+
return 2
|
|
96
|
+
return 1 if report("text", text, markers) else 0
|
|
97
|
+
if len(a) == 1 and not a[0].startswith("-"):
|
|
98
|
+
shas = git("rev-list", a[0]).split()
|
|
99
|
+
bad = 0
|
|
100
|
+
for sha in shas:
|
|
101
|
+
bad += report(sha[:12], git("log", "-1", "--format=%B", sha), markers)
|
|
102
|
+
print(f"ai-trace-commit-check: {len(shas)} commits, {bad} offending lines")
|
|
103
|
+
return 1 if bad else 0
|
|
104
|
+
print(__doc__ if False else "usage: ai-trace-commit-check.sh <range> | --msg-file FILE | --text-file FILE | --selftest", file=sys.stderr)
|
|
105
|
+
return 2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
rc = main(sys.argv[1:])
|
|
110
|
+
except Exception as e: # a bad or missing pattern file, a decode error: unreadable, never "a hit"
|
|
111
|
+
print(f"ai-trace-commit-check: {type(e).__name__}: {e}", file=sys.stderr)
|
|
112
|
+
rc = 2
|
|
113
|
+
sys.exit(rc)
|
|
114
|
+
PY
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "SPDX-License-Identifier: GPL-3.0-or-later. The AI-attribution patterns (Python re, flags IGNORECASE|MULTILINE) read by ai-trace-commit-check.sh. Edit here only.",
|
|
3
|
+
"markers": [
|
|
4
|
+
{
|
|
5
|
+
"name": "robot-footer",
|
|
6
|
+
"pattern": "^[^\\S\\r\\n]*[_*>]*[^\\S\\r\\n]*🤖[^\\r\\n]*?(?:Claude|claude\\.(?:ai|com))[^\\r\\n]*"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "generated-footer",
|
|
10
|
+
"pattern": "(?:🤖[^\\S\\r\\n]*)?Generated[^\\S\\r\\n]+(?:with|by)[^\\S\\r\\n]+\\[?Claude[^\\S\\r\\n]+Code\\]?(?:\\([^)\\s]*\\))?"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "co-authored-by",
|
|
14
|
+
"pattern": "Co-Authored-By:[^\\S\\r\\n]*Claude\\b[^\\r\\n<`]*(?:<[^>\\r\\n]*>)?"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "ai-co-author",
|
|
18
|
+
"pattern": "Co-Authored-By:[^\\S\\r\\n]*(?:Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Codex|Cursor|Aider|Devin)\\b[^\\r\\n<`]*(?:<[^>\\r\\n]*>)?"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "anthropic-noreply",
|
|
22
|
+
"pattern": "noreply@anthropic\\.com"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "claude-session",
|
|
26
|
+
"pattern": "Claude-Session:[^\\S\\r\\n]*[^\\s`]*"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "claude.ai/code",
|
|
30
|
+
"pattern": "\\[[^\\]\\r\\n]*\\]\\([^\\S\\r\\n]*<?https?://claude\\.ai/code[^)\\s]*>?[^\\S\\r\\n]*\\)|<?https?://claude\\.ai/code\\b[^\\s)>`]*>?"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
package/tools/benchmark.py
CHANGED
|
@@ -1095,7 +1095,7 @@ LV2_WORKER_ERR_NO_SPACE = 3
|
|
|
1095
1095
|
class WorkerQueue:
|
|
1096
1096
|
"""The host half of `worker:schedule`: jobs are QUEUED, and run between blocks, off `run()`.
|
|
1097
1097
|
|
|
1098
|
-
|
|
1098
|
+
A schedule that accepts a job and
|
|
1099
1099
|
drops it measures an UNBUILT plugin — the Hex Chain amp read 0.9 us/sample that way against 5.4
|
|
1100
1100
|
built. After every block the host calls `pump(interface)`: the plugin's `work()` for each
|
|
1101
1101
|
queued job, then `work_response()` with whatever it answered, then `end_run()` once every answer
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# private-refs-check.sh — this repository is public, so its tracked files state facts and never point
|
|
6
|
+
# into a private repository: no private repository name, no private issue or pull request number, no
|
|
7
|
+
# private branch name or file path, no name of a private specification file. Say what the value is and
|
|
8
|
+
# why instead, and cite only public repositories (omx-dsp, omx-contract, omx-plugins, omx-clap-core, plugin-qualify).
|
|
9
|
+
# Product-name mentions of OpenMixer, and values such as the org.openmixer.* CLAP ids, the
|
|
10
|
+
# urn:openmixer:* LV2 URIs and the @openmixer npm scope, are not pointers and are not matched.
|
|
11
|
+
#
|
|
12
|
+
# tools/private-refs-check.sh the check, over every tracked file (generated ones included)
|
|
13
|
+
# tools/private-refs-check.sh --self-test each sabotage, in every kind of file, must go red
|
|
14
|
+
#
|
|
15
|
+
# ALLOW lists the tracked files exempt from the check, each with the reason; nothing else is.
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
|
18
|
+
|
|
19
|
+
# <file> — <why it may name them>
|
|
20
|
+
ALLOW=(
|
|
21
|
+
"tools/private-refs-check.sh" # it spells the patterns it looks for
|
|
22
|
+
)
|
|
23
|
+
EXTRA_PATHSPEC=() # the self-test sets a bad one, to prove a git grep error is red
|
|
24
|
+
|
|
25
|
+
# Extended regexps, matched case-insensitively. A new kind of pointer is one line here.
|
|
26
|
+
PATTERNS=(
|
|
27
|
+
'FreeMixer/openmixer([^A-Za-z0-9_-]|$)' # the private repository
|
|
28
|
+
'openmixer-ops' # the private operations repository
|
|
29
|
+
'openmixer ?#[0-9]' # one of its issues or pull requests
|
|
30
|
+
'[A-Za-z0-9_.]-dev#[0-9]' # an issue of a private *-dev repository
|
|
31
|
+
'integration/[A-Za-z0-9]' # a private integration branch
|
|
32
|
+
'packages/[a-z][a-z0-9-]+' # a path into the private repository's packages
|
|
33
|
+
'\.vue([^A-Za-z0-9_]|$)' # its web UI components
|
|
34
|
+
"openmixer's spec" # its specification files
|
|
35
|
+
'docs/design/' # its design documents
|
|
36
|
+
'harness/' # its test harness
|
|
37
|
+
'20[0-9]{2}-[0-9]{2}-[0-9]{2}-[a-z][a-z0-9-]+\.md' # a dated specification file name
|
|
38
|
+
'spec 20[0-9]{2}-[0-9]{2}-[0-9]{2}' # a citation of one
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
check() { # <root>
|
|
42
|
+
local root="$1" fail=0 args=() p a hits
|
|
43
|
+
for p in "${PATTERNS[@]}"; do args+=(-e "$p"); done
|
|
44
|
+
local excl=()
|
|
45
|
+
for a in "${ALLOW[@]}"; do excl+=(":(exclude)${a%% *}"); done
|
|
46
|
+
[ -n "$(git -C "$root" ls-files)" ] || { echo "private-refs-check: git lists no file" >&2; return 1; }
|
|
47
|
+
# git grep exits 1 for no match and 128 or more for an error; an error must not read as "no hits"
|
|
48
|
+
hits=$(git -C "$root" grep -nIiE "${args[@]}" -- . "${excl[@]}" "${EXTRA_PATHSPEC[@]}") || rc=$?
|
|
49
|
+
case "${rc:-0}" in 0|1) ;; *) echo "private-refs-check: git grep failed (exit $rc)" >&2; return 1 ;; esac
|
|
50
|
+
hits=$(printf '%s' "$hits" | cut -c1-200)
|
|
51
|
+
[ -z "$hits" ] || { printf '%s\n' "$hits" >&2; fail=1; }
|
|
52
|
+
[ "$fail" = 0 ] || { echo "private-refs-check: the lines above point into private repositories; state the fact instead" >&2; return 1; }
|
|
53
|
+
echo "private-refs-check: no tracked file points into a private repository"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if [ "${1:-}" = --self-test ]; then
|
|
57
|
+
scratch="$(mktemp -d)"; trap 'rm -rf "$scratch"' EXIT
|
|
58
|
+
fail=0
|
|
59
|
+
# a clean tree with one file of each kind the repository tracks; every sabotage writes one private pointer into one
|
|
60
|
+
mk() { rm -rf "$scratch/t"; mkdir -p "$scratch/t/src" "$scratch/t/tools" "$scratch/t/docs"
|
|
61
|
+
git -C "$scratch" init -q t
|
|
62
|
+
printf 'rdfs:comment "The OpenMixer console x." ;\nlv2:binary <x.so> ; # urn:openmixer:x\n' > "$scratch/t/src/x.ttl"
|
|
63
|
+
printf '// org.openmixer.x, @openmixer/x\n' > "$scratch/t/tools/gen.mjs"
|
|
64
|
+
printf '# Docs\n\nOpenMixer plugins.\n' > "$scratch/t/docs/a.md"
|
|
65
|
+
printf '/* x */\n' > "$scratch/t/src/x.h"; printf '#!/bin/sh\n# x\n' > "$scratch/t/tools/x.sh"; }
|
|
66
|
+
sab() { # <name> <file> <line>
|
|
67
|
+
mk; printf '%s\n' "$3" >> "$scratch/t/$2"; git -C "$scratch/t" add -A
|
|
68
|
+
if check "$scratch/t" >/dev/null 2>&1; then echo "SABOTAGE $1: STAYED GREEN"; fail=1; else echo "SABOTAGE $1: red"; fi; }
|
|
69
|
+
mk; git -C "$scratch/t" add -A
|
|
70
|
+
check "$scratch/t" >/dev/null || { echo "self-test: the clean tree is not green" >&2; exit 1; }
|
|
71
|
+
sab repo-in-header src/x.h '/* as FreeMixer/openmixer draws it */'
|
|
72
|
+
sab branch-in-sh tools/x.sh '# drawn on integration/lean-engine'
|
|
73
|
+
sab issue-in-md docs/a.md 'see openmixer#1580'
|
|
74
|
+
sab path-in-md docs/a.md 'packages/web-ui/Foo.vue'
|
|
75
|
+
sab default-path tools/x.sh 'plugin=${P:-$root/../openmixer/packages/omx-plugins/bin/omx-delay.clap}'
|
|
76
|
+
sab ops-in-mjs tools/gen.mjs '// see openmixer-ops'
|
|
77
|
+
sab dev-in-mjs tools/gen.mjs '// omx-dsp-dev#32'
|
|
78
|
+
sab spec-in-mjs tools/gen.mjs "// openmixer's spec 2026-10-09-plugin-from-contract.md"
|
|
79
|
+
sab ttl src/x.ttl 'rdfs:comment "see FreeMixer/openmixer#12" ;'
|
|
80
|
+
sab header src/x.h '/* packages/core/src/x.ts */'
|
|
81
|
+
sab design-dir docs/a.md 'the notes in docs/design/specs'
|
|
82
|
+
sab harness-dir tools/gen.mjs '// harness/dsp-check.sh'
|
|
83
|
+
mk; git -C "$scratch/t" add -A
|
|
84
|
+
if ( EXTRA_PATHSPEC=(":(bogus)x"); check "$scratch/t" >/dev/null 2>&1 ); then echo "SABOTAGE git-grep-error: STAYED GREEN"; fail=1; else echo "SABOTAGE git-grep-error: red"; fi
|
|
85
|
+
[ "$fail" = 0 ] || exit 1
|
|
86
|
+
check "$HERE" >/dev/null || { echo "self-test: the repository is not green" >&2; exit 1; }
|
|
87
|
+
echo "private-refs-check self-test: every sabotage red"
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
check "$HERE"
|