@camstack/addon-pipeline-orchestrator 1.1.17 → 1.1.19

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/dist/index.mjs CHANGED
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-MHm--th-.mjs
4630
+ //#region ../types/dist/sleep-BO1nweKv.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -7832,6 +7832,601 @@ var _macroLookup = /* @__PURE__ */ new Map();
7832
7832
  for (const [k, v] of Object.entries(YAMNET_TO_MACRO.mapping)) _macroLookup.set(k.toLowerCase(), v);
7833
7833
  for (const [k, v] of Object.entries(APPLE_SA_TO_MACRO.mapping)) _macroLookup.set(k.toLowerCase(), v);
7834
7834
  /**
7835
+ * Error types for the safe expression engine. Two distinct classes so callers
7836
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
7837
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
7838
+ */
7839
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7840
+ * the failure is anchored to a character (author-facing inline feedback). */
7841
+ var ExpressionParseError = class extends Error {
7842
+ position;
7843
+ constructor(message, position) {
7844
+ super(message);
7845
+ this.name = "ExpressionParseError";
7846
+ this.position = position;
7847
+ }
7848
+ };
7849
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7850
+ * result, unknown builtin, step-budget exceeded). */
7851
+ var ExpressionEvalError = class extends Error {
7852
+ constructor(message) {
7853
+ super(message);
7854
+ this.name = "ExpressionEvalError";
7855
+ }
7856
+ };
7857
+ /**
7858
+ * Resource-bound constants for the safe expression engine.
7859
+ *
7860
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
7861
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
7862
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
7863
+ * work a single author-supplied expression can request, so a hostile or
7864
+ * accidental pathological string can never spend unbounded CPU/memory.
7865
+ */
7866
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7867
+ * rejected without allocation. */
7868
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7869
+ /** A legal binding / identifier name. */
7870
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7871
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
7872
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
7873
+ var RESERVED_BINDING_NAMES = new Set([
7874
+ "now",
7875
+ "true",
7876
+ "false",
7877
+ "null"
7878
+ ]);
7879
+ /**
7880
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
7881
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
7882
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
7883
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
7884
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
7885
+ * is a parse error with a source position, so member access / assignment /
7886
+ * template literals are lexically impossible.
7887
+ */
7888
+ var KEYWORDS = new Set([
7889
+ "true",
7890
+ "false",
7891
+ "null"
7892
+ ]);
7893
+ function isDigit(ch) {
7894
+ return ch >= "0" && ch <= "9";
7895
+ }
7896
+ function isIdentStart(ch) {
7897
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
7898
+ }
7899
+ function isIdentPart(ch) {
7900
+ return isIdentStart(ch) || isDigit(ch);
7901
+ }
7902
+ function isWhitespace(ch) {
7903
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
7904
+ }
7905
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
7906
+ * Throws `ExpressionParseError` on any illegal character or unterminated
7907
+ * string. */
7908
+ function tokenize(source) {
7909
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
7910
+ const tokens = [];
7911
+ let i = 0;
7912
+ const n = source.length;
7913
+ while (i < n) {
7914
+ const ch = source[i];
7915
+ if (isWhitespace(ch)) {
7916
+ i += 1;
7917
+ continue;
7918
+ }
7919
+ if (isDigit(ch)) {
7920
+ const start = i;
7921
+ while (i < n && isDigit(source[i])) i += 1;
7922
+ if (i < n && source[i] === ".") {
7923
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
7924
+ i += 1;
7925
+ while (i < n && isDigit(source[i])) i += 1;
7926
+ }
7927
+ const text = source.slice(start, i);
7928
+ const value = Number(text);
7929
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
7930
+ tokens.push({
7931
+ type: "number",
7932
+ value,
7933
+ pos: start
7934
+ });
7935
+ continue;
7936
+ }
7937
+ if (ch === "'" || ch === "\"") {
7938
+ const quote = ch;
7939
+ const start = i;
7940
+ i += 1;
7941
+ let out = "";
7942
+ let closed = false;
7943
+ while (i < n) {
7944
+ const c = source[i];
7945
+ if (c === "\\") {
7946
+ const next = i + 1 < n ? source[i + 1] : "";
7947
+ if (next === "\\" || next === "'" || next === "\"") {
7948
+ out += next;
7949
+ i += 2;
7950
+ continue;
7951
+ }
7952
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
7953
+ }
7954
+ if (c === quote) {
7955
+ closed = true;
7956
+ i += 1;
7957
+ break;
7958
+ }
7959
+ out += c;
7960
+ i += 1;
7961
+ }
7962
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
7963
+ tokens.push({
7964
+ type: "string",
7965
+ value: out,
7966
+ pos: start
7967
+ });
7968
+ continue;
7969
+ }
7970
+ if (isIdentStart(ch)) {
7971
+ const start = i;
7972
+ while (i < n && isIdentPart(source[i])) i += 1;
7973
+ const text = source.slice(start, i);
7974
+ if (KEYWORDS.has(text)) tokens.push({
7975
+ type: "keyword",
7976
+ keyword: keywordOf(text),
7977
+ pos: start
7978
+ });
7979
+ else tokens.push({
7980
+ type: "identifier",
7981
+ name: text,
7982
+ pos: start
7983
+ });
7984
+ continue;
7985
+ }
7986
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
7987
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
7988
+ tokens.push({
7989
+ type: "punct",
7990
+ punct: two,
7991
+ pos: i
7992
+ });
7993
+ i += 2;
7994
+ continue;
7995
+ }
7996
+ if (isSinglePunct(ch)) {
7997
+ tokens.push({
7998
+ type: "punct",
7999
+ punct: ch,
8000
+ pos: i
8001
+ });
8002
+ i += 1;
8003
+ continue;
8004
+ }
8005
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
8006
+ }
8007
+ tokens.push({
8008
+ type: "eof",
8009
+ pos: n
8010
+ });
8011
+ return tokens;
8012
+ }
8013
+ function keywordOf(text) {
8014
+ if (text === "true") return "true";
8015
+ if (text === "false") return "false";
8016
+ return "null";
8017
+ }
8018
+ function isSinglePunct(ch) {
8019
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
8020
+ }
8021
+ /**
8022
+ * Frozen, null-prototype builtin function table for the expression engine
8023
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
8024
+ * parser rejects any callee not in it, and the evaluator gates each call on an
8025
+ * own-property check against it.
8026
+ *
8027
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
8028
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
8029
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
8030
+ * (there is no `Object.prototype` in the chain), so those names are not
8031
+ * callable — they are simply "unknown function" at parse time.
8032
+ *
8033
+ * Every numeric argument is validated as a finite number and every numeric
8034
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
8035
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
8036
+ * closed rather than emitting a garbage value.
8037
+ */
8038
+ function asFiniteNumber(value, name, index) {
8039
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
8040
+ return value;
8041
+ }
8042
+ function asString$1(value, name, index) {
8043
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
8044
+ return value;
8045
+ }
8046
+ function finiteResult(value, name) {
8047
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
8048
+ return value;
8049
+ }
8050
+ function allFiniteNumbers(args, name) {
8051
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
8052
+ }
8053
+ var INF = Number.POSITIVE_INFINITY;
8054
+ var table = {
8055
+ min: {
8056
+ minArgs: 1,
8057
+ maxArgs: INF,
8058
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
8059
+ },
8060
+ max: {
8061
+ minArgs: 1,
8062
+ maxArgs: INF,
8063
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
8064
+ },
8065
+ abs: {
8066
+ minArgs: 1,
8067
+ maxArgs: 1,
8068
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
8069
+ },
8070
+ floor: {
8071
+ minArgs: 1,
8072
+ maxArgs: 1,
8073
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
8074
+ },
8075
+ ceil: {
8076
+ minArgs: 1,
8077
+ maxArgs: 1,
8078
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
8079
+ },
8080
+ sqrt: {
8081
+ minArgs: 1,
8082
+ maxArgs: 1,
8083
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
8084
+ },
8085
+ round: {
8086
+ minArgs: 1,
8087
+ maxArgs: 2,
8088
+ apply: (args) => {
8089
+ const x = asFiniteNumber(args[0], "round", 0);
8090
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
8091
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
8092
+ const factor = 10 ** digits;
8093
+ return finiteResult(Math.round(x * factor) / factor, "round");
8094
+ }
8095
+ },
8096
+ pow: {
8097
+ minArgs: 2,
8098
+ maxArgs: 2,
8099
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
8100
+ },
8101
+ clamp: {
8102
+ minArgs: 3,
8103
+ maxArgs: 3,
8104
+ apply: (args) => {
8105
+ const x = asFiniteNumber(args[0], "clamp", 0);
8106
+ const lo = asFiniteNumber(args[1], "clamp", 1);
8107
+ const hi = asFiniteNumber(args[2], "clamp", 2);
8108
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
8109
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
8110
+ }
8111
+ },
8112
+ avg: {
8113
+ minArgs: 1,
8114
+ maxArgs: INF,
8115
+ apply: (args) => {
8116
+ const nums = allFiniteNumbers(args, "avg");
8117
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
8118
+ }
8119
+ },
8120
+ sum: {
8121
+ minArgs: 1,
8122
+ maxArgs: INF,
8123
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
8124
+ },
8125
+ coalesce: {
8126
+ minArgs: 1,
8127
+ maxArgs: INF,
8128
+ apply: (args) => {
8129
+ for (const a of args) if (a !== null) return a;
8130
+ return null;
8131
+ }
8132
+ },
8133
+ age: {
8134
+ minArgs: 2,
8135
+ maxArgs: 2,
8136
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
8137
+ },
8138
+ convert: {
8139
+ minArgs: 3,
8140
+ maxArgs: 3,
8141
+ apply: (args, hooks) => {
8142
+ const x = asFiniteNumber(args[0], "convert", 0);
8143
+ const from = asString$1(args[1], "convert", 1).trim();
8144
+ const to = asString$1(args[2], "convert", 2).trim();
8145
+ if (hooks.convert) {
8146
+ const out = hooks.convert(x, from, to);
8147
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
8148
+ return finiteResult(out, "convert");
8149
+ }
8150
+ if (from === to) return x;
8151
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
8152
+ }
8153
+ }
8154
+ };
8155
+ Object.freeze(Object.assign(Object.create(null), table));
8156
+ /** The set of valid builtin names — used by the parser to reject unknown
8157
+ * callees at parse time (immediate author feedback). */
8158
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
8159
+ /**
8160
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
8161
+ *
8162
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
8163
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
8164
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
8165
+ * string validated against the builtin table at parse time, so an unknown
8166
+ * function is rejected immediately (author feedback) and a persisted expression
8167
+ * that references a since-removed builtin degrades at read.
8168
+ *
8169
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
8170
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
8171
+ */
8172
+ /** Binary/logical operator precedence (higher binds tighter). */
8173
+ var BINARY_PRECEDENCE = {
8174
+ "||": 1,
8175
+ "&&": 2,
8176
+ "==": 3,
8177
+ "!=": 3,
8178
+ "<": 4,
8179
+ "<=": 4,
8180
+ ">": 4,
8181
+ ">=": 4,
8182
+ "+": 5,
8183
+ "-": 5,
8184
+ "*": 6,
8185
+ "/": 6,
8186
+ "%": 6
8187
+ };
8188
+ function isLogicalOp(op) {
8189
+ return op === "&&" || op === "||";
8190
+ }
8191
+ function isBinaryOp(op) {
8192
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
8193
+ }
8194
+ var Parser = class {
8195
+ tokens;
8196
+ pos = 0;
8197
+ nodeCount = 0;
8198
+ identifiers = /* @__PURE__ */ new Set();
8199
+ callees = /* @__PURE__ */ new Set();
8200
+ constructor(tokens) {
8201
+ this.tokens = tokens;
8202
+ }
8203
+ parse() {
8204
+ const ast = this.parseTernary();
8205
+ const tok = this.peek();
8206
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
8207
+ return {
8208
+ ast,
8209
+ identifiers: this.identifiers,
8210
+ callees: this.callees,
8211
+ nodeCount: this.nodeCount
8212
+ };
8213
+ }
8214
+ peek() {
8215
+ return this.tokens[this.pos];
8216
+ }
8217
+ next() {
8218
+ return this.tokens[this.pos++];
8219
+ }
8220
+ /** Consume a punctuator token, erroring if the next token isn't it. */
8221
+ expectPunct(punct) {
8222
+ const tok = this.peek();
8223
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
8224
+ this.pos += 1;
8225
+ }
8226
+ matchPunct(punct) {
8227
+ const tok = this.peek();
8228
+ if (tok.type === "punct" && tok.punct === punct) {
8229
+ this.pos += 1;
8230
+ return true;
8231
+ }
8232
+ return false;
8233
+ }
8234
+ countNode() {
8235
+ this.nodeCount += 1;
8236
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
8237
+ }
8238
+ parseTernary() {
8239
+ const test = this.parseBinary(1);
8240
+ if (this.matchPunct("?")) {
8241
+ const consequent = this.parseTernary();
8242
+ this.expectPunct(":");
8243
+ const alternate = this.parseTernary();
8244
+ this.countNode();
8245
+ return {
8246
+ kind: "conditional",
8247
+ test,
8248
+ consequent,
8249
+ alternate
8250
+ };
8251
+ }
8252
+ return test;
8253
+ }
8254
+ parseBinary(minPrec) {
8255
+ let left = this.parseUnary();
8256
+ for (;;) {
8257
+ const tok = this.peek();
8258
+ if (tok.type !== "punct") break;
8259
+ const prec = BINARY_PRECEDENCE[tok.punct];
8260
+ if (prec === void 0 || prec < minPrec) break;
8261
+ const op = tok.punct;
8262
+ this.pos += 1;
8263
+ const right = this.parseBinary(prec + 1);
8264
+ this.countNode();
8265
+ if (isLogicalOp(op)) left = {
8266
+ kind: "logical",
8267
+ op,
8268
+ left,
8269
+ right
8270
+ };
8271
+ else if (isBinaryOp(op)) left = {
8272
+ kind: "binary",
8273
+ op,
8274
+ left,
8275
+ right
8276
+ };
8277
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
8278
+ }
8279
+ return left;
8280
+ }
8281
+ parseUnary() {
8282
+ const tok = this.peek();
8283
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
8284
+ const op = tok.punct;
8285
+ this.pos += 1;
8286
+ const operand = this.parseUnary();
8287
+ this.countNode();
8288
+ return {
8289
+ kind: "unary",
8290
+ op,
8291
+ operand
8292
+ };
8293
+ }
8294
+ return this.parsePrimary();
8295
+ }
8296
+ parsePrimary() {
8297
+ const tok = this.next();
8298
+ switch (tok.type) {
8299
+ case "number":
8300
+ this.countNode();
8301
+ return {
8302
+ kind: "literal",
8303
+ value: tok.value
8304
+ };
8305
+ case "string":
8306
+ this.countNode();
8307
+ return {
8308
+ kind: "literal",
8309
+ value: tok.value
8310
+ };
8311
+ case "keyword":
8312
+ this.countNode();
8313
+ return {
8314
+ kind: "literal",
8315
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
8316
+ };
8317
+ case "identifier": {
8318
+ const nextTok = this.peek();
8319
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
8320
+ this.identifiers.add(tok.name);
8321
+ this.countNode();
8322
+ return {
8323
+ kind: "identifier",
8324
+ name: tok.name
8325
+ };
8326
+ }
8327
+ case "punct":
8328
+ if (tok.punct === "(") {
8329
+ const inner = this.parseTernary();
8330
+ this.expectPunct(")");
8331
+ return inner;
8332
+ }
8333
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
8334
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
8335
+ }
8336
+ }
8337
+ parseCall(callee, pos) {
8338
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
8339
+ this.expectPunct("(");
8340
+ const args = [];
8341
+ if (!this.matchPunct(")")) for (;;) {
8342
+ args.push(this.parseTernary());
8343
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
8344
+ if (this.matchPunct(",")) continue;
8345
+ this.expectPunct(")");
8346
+ break;
8347
+ }
8348
+ this.callees.add(callee);
8349
+ this.countNode();
8350
+ return {
8351
+ kind: "call",
8352
+ callee,
8353
+ args
8354
+ };
8355
+ }
8356
+ };
8357
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
8358
+ * `ExpressionParseError` on any lexical or grammatical failure. */
8359
+ function parseExpression(source) {
8360
+ return new Parser(tokenize(source)).parse();
8361
+ }
8362
+ Object.freeze({});
8363
+ /**
8364
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
8365
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
8366
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
8367
+ * one per read on a hot resolve path.
8368
+ *
8369
+ * The cache is a module-level singleton: entries are pure, content-addressed
8370
+ * ASTs keyed by the raw source string, so sharing one instance across all
8371
+ * callers is safe and maximises hit rate.
8372
+ */
8373
+ var cache = /* @__PURE__ */ new Map();
8374
+ function getCached(source) {
8375
+ const hit = cache.get(source);
8376
+ if (hit !== void 0) {
8377
+ cache.delete(source);
8378
+ cache.set(source, hit);
8379
+ return hit;
8380
+ }
8381
+ let result;
8382
+ try {
8383
+ result = {
8384
+ ok: true,
8385
+ parsed: parseExpression(source)
8386
+ };
8387
+ } catch (err) {
8388
+ result = {
8389
+ ok: false,
8390
+ error: err instanceof ExpressionParseError ? err.message : String(err)
8391
+ };
8392
+ }
8393
+ cache.set(source, result);
8394
+ if (cache.size > 256) {
8395
+ const oldest = cache.keys().next().value;
8396
+ if (oldest !== void 0) cache.delete(oldest);
8397
+ }
8398
+ return result;
8399
+ }
8400
+ /** Compile `source`, returning a discriminated result instead of throwing.
8401
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
8402
+ function compileExpressionSafe(source) {
8403
+ return getCached(source);
8404
+ }
8405
+ /**
8406
+ * Author-time validation. Returns `null` when the source is valid, else a
8407
+ * human-readable error message. Checks: the expression compiles; binding count
8408
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
8409
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
8410
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
8411
+ */
8412
+ function validateExpressionSource(src) {
8413
+ const names = Object.keys(src.bindings);
8414
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
8415
+ for (const name of names) {
8416
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
8417
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
8418
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
8419
+ }
8420
+ const compiled = compileExpressionSafe(src.expr);
8421
+ if (!compiled.ok) return compiled.error;
8422
+ const bound = new Set(names);
8423
+ for (const id of compiled.parsed.identifiers) {
8424
+ if (id === "now") continue;
8425
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
8426
+ }
8427
+ return null;
8428
+ }
8429
+ /**
7835
8430
  * Accessory device helpers — shared across drivers.
7836
8431
  *
7837
8432
  * Many vendor-specific drivers register accessory child devices on
@@ -13200,14 +13795,63 @@ var ChildLayoutEntrySchema = object({
13200
13795
  collapsed: boolean().optional()
13201
13796
  });
13202
13797
  /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
13203
- * `device-management.ts`. */
13798
+ * `device-management.ts`. Source is a union: a FIELD source copies a sibling
13799
+ * accessory's status field (`kind` optional/absent for wire compat); a
13800
+ * LITERAL source carries a per-device constant (no sibling is read); a
13801
+ * GLOBAL source (P2e) copies ANY device's status field, addressed by the
13802
+ * source device's full re-sync-stable `stableId`. */
13803
+ var DeviceLinkFieldSourceSchema = object({
13804
+ kind: literal("field").optional(),
13805
+ sourceKey: string(),
13806
+ cap: string(),
13807
+ fieldPath: string()
13808
+ });
13809
+ var DeviceLinkLiteralSourceSchema = object({
13810
+ kind: literal("literal"),
13811
+ value: union([
13812
+ string(),
13813
+ number(),
13814
+ boolean(),
13815
+ _null()
13816
+ ])
13817
+ });
13818
+ var DeviceLinkGlobalSourceSchema = object({
13819
+ kind: literal("global"),
13820
+ sourceStableId: string(),
13821
+ cap: string(),
13822
+ fieldPath: string()
13823
+ });
13824
+ /** Expression source (Stage X): compute the target field from N named bindings
13825
+ * via the safe expression engine. Bindings are field | literal | global — never
13826
+ * another expression (no nesting). The `superRefine` runs the SAME author-time
13827
+ * validation as `validateExpressionSource` (compiles the expr, checks binding
13828
+ * names + identifier coverage) so every wire boundary that parses a DeviceLink
13829
+ * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
13830
+ * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
13831
+ var DeviceLinkExpressionSourceSchema = object({
13832
+ kind: literal("expression"),
13833
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
13834
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
13835
+ DeviceLinkFieldSourceSchema,
13836
+ DeviceLinkLiteralSourceSchema,
13837
+ DeviceLinkGlobalSourceSchema
13838
+ ]))
13839
+ }).superRefine((src, ctx) => {
13840
+ const err = validateExpressionSource(src);
13841
+ if (err !== null) ctx.addIssue({
13842
+ code: "custom",
13843
+ message: err,
13844
+ path: ["expr"]
13845
+ });
13846
+ });
13204
13847
  var DeviceLinkSchema = object({
13205
13848
  id: string(),
13206
- source: object({
13207
- sourceKey: string(),
13208
- cap: string(),
13209
- fieldPath: string()
13210
- }),
13849
+ source: union([
13850
+ DeviceLinkFieldSourceSchema,
13851
+ DeviceLinkLiteralSourceSchema,
13852
+ DeviceLinkGlobalSourceSchema,
13853
+ DeviceLinkExpressionSourceSchema
13854
+ ]),
13211
13855
  target: object({
13212
13856
  cap: string(),
13213
13857
  fieldPath: string(),
@@ -13236,6 +13880,31 @@ var DeviceLinkSchema = object({
13236
13880
  })
13237
13881
  ]).optional()
13238
13882
  });
13883
+ /** Cap-wire shape of a per-cap display refinement — mirrors
13884
+ * `DeviceCapDisplayOverride` in `device-management.ts`. */
13885
+ var DeviceCapDisplayOverrideSchema = object({
13886
+ unit: string().min(1).optional(),
13887
+ precision: number().int().min(0).max(10).optional()
13888
+ });
13889
+ /** Cap-wire shape of an operator-authored per-device display override —
13890
+ * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
13891
+ * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
13892
+ var DeviceDisplayOverrideSchema = object({
13893
+ icon: string().min(1).optional(),
13894
+ label: string().min(1).optional(),
13895
+ unit: string().min(1).optional(),
13896
+ precision: number().int().min(0).max(10).optional(),
13897
+ hidden: boolean().optional(),
13898
+ perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
13899
+ });
13900
+ /** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
13901
+ * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
13902
+ * the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
13903
+ var RoleDisplayDefaultSchema = object({
13904
+ unit: string().min(1).optional(),
13905
+ precision: number().int().min(0).max(10).optional(),
13906
+ icon: string().min(1).optional()
13907
+ });
13239
13908
  /**
13240
13909
  * Serializable projection of a live IDevice.
13241
13910
  * Returned by listAll, getDevice, getChildren.
@@ -13291,7 +13960,9 @@ var DeviceInfoSchema = object({
13291
13960
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
13292
13961
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
13293
13962
  /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
13294
- deviceLinks: array(DeviceLinkSchema).readonly().optional()
13963
+ deviceLinks: array(DeviceLinkSchema).readonly().optional(),
13964
+ /** Operator-authored per-device display override. See `DeviceMeta.display`. */
13965
+ display: DeviceDisplayOverrideSchema.optional()
13295
13966
  });
13296
13967
  var ConfigEntrySchema = object({
13297
13968
  key: string(),
@@ -13356,7 +14027,9 @@ var DeviceMetaSchema = object({
13356
14027
  deviceLinks: array(DeviceLinkSchema).readonly().optional(),
13357
14028
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
13358
14029
  * Optional: only present for accessory children that carry a known role. */
13359
- role: string().nullable().optional()
14030
+ role: string().nullable().optional(),
14031
+ /** Operator-authored per-device display override. See `DeviceMeta.display`. */
14032
+ display: DeviceDisplayOverrideSchema.optional()
13360
14033
  });
13361
14034
  /** ConfigUISchema passed through as unknown — mirrors device-provider's CreationSchemaOutputSchema */
13362
14035
  var ConfigUISchemaOutput = unknown().nullable();
@@ -13450,7 +14123,19 @@ method(object({
13450
14123
  }), _void(), {
13451
14124
  kind: "mutation",
13452
14125
  auth: "admin"
13453
- }), method(object({ deviceId: number() }), object({ caps: array(object({
14126
+ }), method(object({
14127
+ deviceId: number(),
14128
+ display: DeviceDisplayOverrideSchema.nullable()
14129
+ }), _void(), {
14130
+ kind: "mutation",
14131
+ auth: "admin"
14132
+ }), method(object({}), object({ defaults: record(string(), RoleDisplayDefaultSchema) }), { kind: "query" }), method(object({ defaults: record(string(), RoleDisplayDefaultSchema) }), _void(), {
14133
+ kind: "mutation",
14134
+ auth: "admin"
14135
+ }), method(object({
14136
+ deviceId: number(),
14137
+ includeSynthesizable: boolean().optional()
14138
+ }), object({ caps: array(object({
13454
14139
  cap: string(),
13455
14140
  fields: array(object({
13456
14141
  path: string(),
@@ -13460,8 +14145,13 @@ method(object({
13460
14145
  "boolean",
13461
14146
  "enum"
13462
14147
  ]),
13463
- enumValues: array(string()).optional()
13464
- })).readonly()
14148
+ enumValues: array(string()).optional(),
14149
+ item: boolean().optional()
14150
+ })).readonly(),
14151
+ itemArray: object({
14152
+ path: string(),
14153
+ keyField: string()
14154
+ }).optional()
13465
14155
  })).readonly() }), { kind: "query" }), method(object({
13466
14156
  deviceId: number(),
13467
14157
  role: string().nullable()
@@ -13531,7 +14221,11 @@ method(object({
13531
14221
  deviceId: number(),
13532
14222
  entries: array(object({
13533
14223
  capName: string(),
13534
- kind: _enum(["native", "wrapped"]),
14224
+ kind: _enum([
14225
+ "native",
14226
+ "wrapped",
14227
+ "linked"
14228
+ ]),
13535
14229
  providerAddonId: string(),
13536
14230
  providerNodeId: string(),
13537
14231
  nativeAddonId: string()
@@ -13540,7 +14234,11 @@ method(object({
13540
14234
  deviceId: number(),
13541
14235
  entries: array(object({
13542
14236
  capName: string(),
13543
- kind: _enum(["native", "wrapped"]),
14237
+ kind: _enum([
14238
+ "native",
14239
+ "wrapped",
14240
+ "linked"
14241
+ ]),
13544
14242
  providerAddonId: string(),
13545
14243
  providerNodeId: string(),
13546
14244
  nativeAddonId: string()
@@ -14958,7 +15656,10 @@ var pipelineOrchestratorCapability = {
14958
15656
  methods: {
14959
15657
  /**
14960
15658
  * Pin a camera's pipeline to a specific agent (L1 affinity).
14961
- * The orchestrator re-evaluates the assignment immediately.
15659
+ * The orchestrator re-evaluates the assignment immediately and persists
15660
+ * the pin under the canonical `pipelineNodeId` device-store key (the
15661
+ * legacy `preferredAgent` key is nulled on write and kept only as a
15662
+ * read-only fallback for stores written before the unification).
14962
15663
  */
14963
15664
  assignPipeline: method(object({
14964
15665
  deviceId: number(),
@@ -14969,8 +15670,9 @@ var pipelineOrchestratorCapability = {
14969
15670
  }),
14970
15671
  /**
14971
15672
  * Clear a camera's pipeline pin and let the auto-balancer re-pick
14972
- * the optimal agent. The orchestrator persists `preferredAgent=null`
14973
- * (and `pipelineNodeId='auto'`), then re-runs the balancer with the
15673
+ * the optimal agent. The orchestrator persists the canonical
15674
+ * `pipelineNodeId='auto'` (and nulls the legacy `preferredAgent`),
15675
+ * then re-runs the balancer with the
14974
15676
  * cached `RunnerCameraConfig` and migrates only when the chosen
14975
15677
  * node differs. The camera stays in `getPipelineAssignments()` —
14976
15678
  * just with `pinned=false`. If no runner is currently available
@@ -19284,6 +19986,12 @@ Object.freeze({
19284
19986
  addonId: null,
19285
19987
  access: "view"
19286
19988
  },
19989
+ "deviceManager.getRoleDisplayDefaults": {
19990
+ capName: "device-manager",
19991
+ capScope: "system",
19992
+ addonId: null,
19993
+ access: "view"
19994
+ },
19287
19995
  "deviceManager.getSettingsSchema": {
19288
19996
  capName: "device-manager",
19289
19997
  capScope: "system",
@@ -19434,6 +20142,12 @@ Object.freeze({
19434
20142
  addonId: null,
19435
20143
  access: "create"
19436
20144
  },
20145
+ "deviceManager.setDisplay": {
20146
+ capName: "device-manager",
20147
+ capScope: "system",
20148
+ addonId: null,
20149
+ access: "create"
20150
+ },
19437
20151
  "deviceManager.setIntegrationId": {
19438
20152
  capName: "device-manager",
19439
20153
  capScope: "system",
@@ -19476,6 +20190,12 @@ Object.freeze({
19476
20190
  addonId: null,
19477
20191
  access: "create"
19478
20192
  },
20193
+ "deviceManager.setRoleDisplayDefaults": {
20194
+ capName: "device-manager",
20195
+ capScope: "system",
20196
+ addonId: null,
20197
+ access: "create"
20198
+ },
19479
20199
  "deviceManager.setStreamProfileMap": {
19480
20200
  capName: "device-manager",
19481
20201
  capScope: "system",
@@ -24619,9 +25339,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24619
25339
  }
24620
25340
  }
24621
25341
  }
24622
- const pipelinePin = (await this.ctx.settings?.readDeviceStore(runnerConfig.deviceId) ?? {})["pipelineNodeId"];
24623
- const legacyPreferred = await this.readPreferredAgent(runnerConfig.deviceId);
24624
- const preferredAgent = typeof pipelinePin === "string" && pipelinePin !== "auto" ? pipelinePin : legacyPreferred;
25342
+ const preferredAgent = await this.readPipelinePin(runnerConfig.deviceId);
24625
25343
  const decision = balance({
24626
25344
  nodes: await this.collectAgentLoad({ onlyEnabled: true }),
24627
25345
  preferredAgent,
@@ -24693,7 +25411,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24693
25411
  if (!this.ctx) throw new Error("PipelineOrchestrator: assignPipeline called before initialize");
24694
25412
  const eligible = this.detectionEligibleNodes(input.deviceId);
24695
25413
  if (!eligible.includes(input.agentNodeId)) throw new Error(`Cannot pin camera ${input.deviceId} detection to '${input.agentNodeId}': the node cannot obtain this camera's decoded frames (frame-source nodes: ${eligible.join(", ") || "none"}). Add it to Enabled Decoder Nodes first.`);
24696
- await this.ctx.settings?.writeDeviceStore(input.deviceId, { [PREFERRED_AGENT_SETTING]: input.agentNodeId }).catch((err) => {
25414
+ await this.ctx.settings?.writeDeviceStore(input.deviceId, {
25415
+ pipelineNodeId: input.agentNodeId,
25416
+ [PREFERRED_AGENT_SETTING]: null
25417
+ }).catch((err) => {
24697
25418
  const msg = errMsg(err);
24698
25419
  this.ctx.logger.warn("assignPipeline: failed to persist pin", {
24699
25420
  tags: { deviceId: input.deviceId },
@@ -24787,13 +25508,26 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24787
25508
  if (!this.ctx) throw new Error("PipelineOrchestrator: rebalance called before initialize");
24788
25509
  const loads = await this.collectAgentLoad({ onlyEnabled: true });
24789
25510
  const nodeCaps = await this.buildNodeCaps();
25511
+ const attachedDelta = /* @__PURE__ */ new Map();
25512
+ const bumpAttached = (nodeId, by) => {
25513
+ attachedDelta.set(nodeId, (attachedDelta.get(nodeId) ?? 0) + by);
25514
+ };
25515
+ const talliedLoads = () => loads.map((load) => {
25516
+ const delta = attachedDelta.get(load.nodeId) ?? 0;
25517
+ if (delta === 0) return load;
25518
+ return {
25519
+ ...load,
25520
+ attachedCameras: Math.max(0, load.attachedCameras + delta)
25521
+ };
25522
+ });
24790
25523
  let migrated = 0;
24791
25524
  for (const [deviceId, config] of this.cameraConfigs) {
24792
25525
  const current = this.assignments.get(deviceId);
24793
25526
  if (current?.pinned) continue;
25527
+ const preferredAgent = await this.readPipelinePin(deviceId);
24794
25528
  const decision = balance({
24795
- nodes: loads,
24796
- preferredAgent: await this.readPreferredAgent(deviceId),
25529
+ nodes: talliedLoads(),
25530
+ preferredAgent,
24797
25531
  nodeCaps,
24798
25532
  eligibleNodes: this.detectionEligibleNodes(deviceId)
24799
25533
  });
@@ -24807,14 +25541,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24807
25541
  continue;
24808
25542
  }
24809
25543
  if (current && current.agentNodeId === decision.agentNodeId) continue;
24810
- if (current) await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
24811
- const msg = errMsg(err);
24812
- this.ctx.logger.debug("rebalance detach-old failed", {
24813
- tags: { deviceId },
24814
- meta: { error: msg }
25544
+ if (current) {
25545
+ await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
25546
+ const msg = errMsg(err);
25547
+ this.ctx.logger.debug("rebalance detach-old failed", {
25548
+ tags: { deviceId },
25549
+ meta: { error: msg }
25550
+ });
24815
25551
  });
24816
- });
25552
+ bumpAttached(current.agentNodeId, -1);
25553
+ }
24817
25554
  await this.attachOn(decision.agentNodeId, config);
25555
+ bumpAttached(decision.agentNodeId, 1);
24818
25556
  this.recordAssignment(deviceId, decision.agentNodeId, "rebalance", false);
24819
25557
  migrated++;
24820
25558
  }
@@ -25176,11 +25914,22 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25176
25914
  if (timer) clearTimeout(timer);
25177
25915
  }
25178
25916
  }
25179
- async readPreferredAgent(deviceId) {
25917
+ /**
25918
+ * Resolve the persisted pipeline-node pin for a device. `pipelineNodeId` is
25919
+ * the canonical key (matching the sibling `decoderNodeId` / `audioNodeId`
25920
+ * `'auto'`-sentinel pattern); the legacy `preferredAgent` key is a read-only
25921
+ * fallback kept for backward compatibility with stores written before the
25922
+ * unification. A single device-store read serves both. Returns a concrete
25923
+ * node id, or `null` when neither key holds a pin (auto-balance).
25924
+ */
25925
+ async readPipelinePin(deviceId) {
25180
25926
  if (!this.ctx?.settings) return null;
25181
25927
  try {
25182
- const value = (await this.ctx.settings.readDeviceStore(deviceId))[PREFERRED_AGENT_SETTING];
25183
- return typeof value === "string" && value.length > 0 ? value : null;
25928
+ const settings = await this.ctx.settings.readDeviceStore(deviceId);
25929
+ const pipelinePin = settings["pipelineNodeId"];
25930
+ if (typeof pipelinePin === "string" && pipelinePin.length > 0 && pipelinePin !== "auto") return pipelinePin;
25931
+ const legacy = settings[PREFERRED_AGENT_SETTING];
25932
+ return typeof legacy === "string" && legacy.length > 0 ? legacy : null;
25184
25933
  } catch {
25185
25934
  return null;
25186
25935
  }
@@ -25321,7 +26070,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25321
26070
  } });
25322
26071
  continue;
25323
26072
  }
25324
- if (assignment.pinned && this.failoverPolicy.pinnedOnDisconnect === "unpin-and-migrate") await this.ctx.settings?.writeDeviceStore(deviceId, { [PREFERRED_AGENT_SETTING]: null }).catch(() => {});
26073
+ if (assignment.pinned && this.failoverPolicy.pinnedOnDisconnect === "unpin-and-migrate") await this.ctx.settings?.writeDeviceStore(deviceId, {
26074
+ [PREFERRED_AGENT_SETTING]: null,
26075
+ pipelineNodeId: "auto"
26076
+ }).catch(() => {});
25325
26077
  affected.push({
25326
26078
  deviceId,
25327
26079
  config