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