@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.
@@ -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 {
@@ -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 (2026-09-04-lv2-hosting-path.md). Codes, never prose: the
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 (2026-09-15-no-stored-derivations.md §9): `.gitattributes` marks it
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 (2026-09-26-clap-hosting-path.md §3): the SAME dimensions fed from CLAP facts,
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 (`2026-09-26-clap-hosting-path.md` §7).
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
- /** The note that recorded this cluster. Provenance, so a row is never folklore. */
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 crash notes of 2026-09-03/04. A raw crash matching NO row is
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: "2026-09-04-pipewire-jack-client-close-double-free.md",
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: "2026-09-04-pipewire-jack-client-close-double-free.md",
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: "2026-09-03-mod-host-crash-cluster2-3-investigation.md",
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: "2026-09-03-mod-host-crash-cluster2-3-investigation.md",
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 (2026-09-26-clap-hosting-path.md §3): the SAME six dimensions, fed from the
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 (`2026-09-26-clap-hosting-path.md` §7). Absent reads as not linked.
1044
+ * to run in-process. Absent reads as not linked.
992
1045
  */
993
1046
  readonly linked?: boolean;
994
1047
  }
@@ -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
- * (`2026-09-25-plugin-qualify.md` §2a). Pure functions: the runner spawns, these decide.
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 (`2026-09-25-plugin-qualify.md` §3a; §3b here): the host a verdict was judged
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 (`2026-09-25-plugin-qualify.md` §3a). */
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`) and the
86
- * interposer both run under (`native/rt-interpose.c`): each located by one environment variable, else
87
- * the package's own build in `native/build/`, else the first of `NATIVE_TOOL_DIRS` that holds it.
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 (`2026-09-25-plugin-qualify.md` §3a) — the
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
+ }
@@ -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
- Spec 2a (`2026-09-25-plugin-qualify.md`): a schedule that accepts a job and
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"