@camstack/types 1.2.268 → 1.2.269

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.
Files changed (33) hide show
  1. package/dist/addon.js +1 -1
  2. package/dist/addon.mjs +1 -1
  3. package/dist/capabilities/capability-definition.d.ts +8 -1
  4. package/dist/capabilities/composer.cap.d.ts +6 -0
  5. package/dist/capabilities/core-blocks.cap.d.ts +54 -3
  6. package/dist/capabilities/device-manager.cap.d.ts +4 -4
  7. package/dist/capabilities/doorbell.cap.d.ts +4 -0
  8. package/dist/capabilities/media-player.cap.d.ts +4 -4
  9. package/dist/capabilities/recording-archive.cap.d.ts +3 -3
  10. package/dist/catalogs/sensor-active-state.d.ts +25 -1
  11. package/dist/composition/composition-events.d.ts +42 -0
  12. package/dist/composition/composition-report.d.ts +22 -10
  13. package/dist/composition/composition.d.ts +8 -0
  14. package/dist/composition/evaluate-field.d.ts +8 -0
  15. package/dist/composition/expression-kinds.d.ts +1 -0
  16. package/dist/composition/index.d.ts +1 -0
  17. package/dist/composition/plan-result.d.ts +2 -0
  18. package/dist/{composition-QlOB7QTG.js → composition-BAH3n-zq.js} +412 -14
  19. package/dist/{composition-CPFIlFfw.mjs → composition-D5BQywfe.mjs} +311 -15
  20. package/dist/expression/ast.d.ts +2 -0
  21. package/dist/expression/builtins.d.ts +9 -0
  22. package/dist/expression/evaluator.d.ts +8 -2
  23. package/dist/expression/expression-source.d.ts +26 -4
  24. package/dist/expression/index.d.ts +6 -6
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +321 -324
  27. package/dist/index.mjs +299 -314
  28. package/dist/interfaces/recording-config.d.ts +3 -3
  29. package/dist/node.js +1 -1
  30. package/dist/node.mjs +1 -1
  31. package/dist/{sleep-BSzia-xT.js → sleep-Bg0IB7gg.js} +4 -15
  32. package/dist/{sleep-jkRQgtSc.mjs → sleep-E6eD4yQS.mjs} +4 -15
  33. package/package.json +1 -1
@@ -714,6 +714,197 @@ function replaceWithSortedKeys(_key, value) {
714
714
  return value;
715
715
  }
716
716
  //#endregion
717
+ //#region src/catalogs/sensor-active-state.ts
718
+ /**
719
+ * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
720
+ * valutatore puro del suo FRONTE.
721
+ *
722
+ * Viveva dentro il builtin virtual-doorbell
723
+ * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
724
+ * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
725
+ * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
726
+ * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
727
+ * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
728
+ * SPOSTA qui e il doorbell la ri-esporta.
729
+ *
730
+ * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
731
+ * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
732
+ * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
733
+ * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
734
+ */
735
+ /**
736
+ * Known binary / switch source caps → the boolean slice field whose
737
+ * false→true rise counts as ACTIVE. Every entry is "fire on active".
738
+ * Sensors whose "active" reading is not a plain boolean (presence's string
739
+ * state, connectivity's connected flag) are deliberately excluded — a
740
+ * reconnect is not a doorbell press, and it is not a recording either.
741
+ */
742
+ var SOURCE_CAP_ACTIVE_FIELD = {
743
+ contact: "entryOpen",
744
+ binary: "on",
745
+ switch: "on",
746
+ motion: "detected",
747
+ flood: "flooded",
748
+ gas: "detected",
749
+ smoke: "detected",
750
+ "carbon-monoxide": "detected",
751
+ vibration: "detected",
752
+ tamper: "tampered"
753
+ };
754
+ /**
755
+ * The same caps → the slice field carrying the ms-epoch timestamp of the
756
+ * last transition. Every source cap MUST appear here (guarded by a spec):
757
+ * without a transition timestamp the evaluator cannot tell a genuine rise
758
+ * from a boot-time hydration when the FIRST slice it ever sees is already
759
+ * active, and errs towards silence — swallowing the rise.
760
+ *
761
+ * These timestamps are UPSTREAM ones, not ingest ones: the Home Assistant
762
+ * provider derives them from `state.last_changed`, so they survive our own
763
+ * restarts and correctly read as "hours ago" for a state that has been
764
+ * active for hours. `motion` names its rise timestamp `lastDetectedAt`.
765
+ */
766
+ var SOURCE_CAP_CHANGED_AT_FIELD = {
767
+ contact: "lastChangedAt",
768
+ binary: "lastChangedAt",
769
+ switch: "lastChangedAt",
770
+ motion: "lastDetectedAt",
771
+ flood: "lastChangedAt",
772
+ gas: "lastChangedAt",
773
+ smoke: "lastChangedAt",
774
+ "carbon-monoxide": "lastChangedAt",
775
+ vibration: "lastChangedAt",
776
+ tamper: "lastChangedAt"
777
+ };
778
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
779
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
780
+ /**
781
+ * Device `type` values (from `DeviceType`) that can host a binary/switch
782
+ * source cap. Used by the camera's `device-multiselect` picker as the
783
+ * CLIENT-SIDE filter, alongside `SOURCE_CAPS`.
784
+ *
785
+ * Why types and not caps alone: the shared picker filters
786
+ * `deviceManager.listAll` rows client-side, and those rows carry only the
787
+ * device's advertised `features` — NOT its registered cap list. On the live
788
+ * cluster binary sensors and switches advertise EMPTY features (features
789
+ * mirror only a handful of caps like `motion-trigger`), so a caps-only
790
+ * filter matched against `features` would list nothing (the very bug this
791
+ * replaced, which relied on the now-empty `getAllBindings`). Matching by
792
+ * `type` is the reliable client-side signal; the union with `SOURCE_CAPS`
793
+ * still captures any device that DOES advertise a source-cap feature.
794
+ * `sensor` covers contact/motion/flood/gas/smoke/CO/vibration/tamper,
795
+ * `switch` covers switches, `control` covers generic binary actuators.
796
+ */
797
+ var SOURCE_DEVICE_TYPES = [
798
+ "sensor",
799
+ "switch",
800
+ "control"
801
+ ];
802
+ /** True when a cap is a recognised binary/switch source. */
803
+ function isSourceCap(capName) {
804
+ return Object.prototype.hasOwnProperty.call(SOURCE_CAP_ACTIVE_FIELD, capName);
805
+ }
806
+ /** Extract the "active" boolean a source cap's slice carries, or null when
807
+ * the cap is unknown or the field is missing / non-boolean. */
808
+ function sliceActiveValue(capName, slice) {
809
+ const field = SOURCE_CAP_ACTIVE_FIELD[capName];
810
+ if (field === void 0) return null;
811
+ const raw = slice[field];
812
+ return typeof raw === "boolean" ? raw : null;
813
+ }
814
+ /** Ms-epoch transition timestamp a source cap's slice carries, or null when
815
+ * it is absent, non-numeric or the zero "never observed" sentinel. */
816
+ function sliceChangedAt(capName, slice) {
817
+ const field = SOURCE_CAP_CHANGED_AT_FIELD[capName];
818
+ if (field === void 0) return null;
819
+ const raw = slice[field];
820
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return null;
821
+ return raw;
822
+ }
823
+ /**
824
+ * How recent a source's own transition timestamp must be for a FIRST
825
+ * sighting that is already active to count as a genuine rise rather than a
826
+ * hydration of long-standing state.
827
+ */
828
+ var DEFAULT_FIRST_SIGHTING_FRESHNESS_MS = 3e4;
829
+ /**
830
+ * The source-independent edge rule. The caller supplies an already extracted
831
+ * boolean level and source timestamp; this function owns first-sighting
832
+ * freshness and false-to-true detection.
833
+ */
834
+ function evaluateSensorLevelEdge(input) {
835
+ const timestamp = input.changedAt;
836
+ const freshFloor = Math.max(input.startedAtMs, input.nowMs - input.firstSightingFreshnessMs);
837
+ const firstRise = input.priorLevel === void 0 && input.level && timestamp !== null && Number.isFinite(timestamp) && timestamp >= freshFloor;
838
+ const transitionRise = input.priorLevel === false && input.level && timestamp !== null && Number.isFinite(timestamp);
839
+ if ((firstRise || transitionRise) && timestamp !== null) return {
840
+ kind: "rise",
841
+ timestamp
842
+ };
843
+ return {
844
+ kind: "none",
845
+ nextPriorLevel: input.level
846
+ };
847
+ }
848
+ /**
849
+ * IL fronte. Puro: nessun orologio proprio, nessuna memoria — il chiamante
850
+ * porta `prior`, `nowMs` e il proprio `startedAtMs`.
851
+ *
852
+ * Il caso della PRIMA slice già attiva è trattato esplicitamente e vale come
853
+ * fronte solo se il timestamp UPSTREAM della transizione è posteriore a
854
+ * `startedAtMs` **e** entro `firstSightingFreshnessMs`. Entrambe le metà
855
+ * servono: la sola freschezza scatterebbe su un'idratazione al boot di uno
856
+ * stato flippato pochi secondi prima del riavvio, e il solo "dopo che abbiamo
857
+ * iniziato" scatterebbe, su un processo di lunga vita, per una sorgente
858
+ * adottata oggi il cui stato è cambiato ieri. Il caso ambiguo ERRA VERSO IL
859
+ * SILENZIO e lo dichiara (`baseline-seeded-stale-active`).
860
+ */
861
+ function evaluateSensorEdge(input) {
862
+ const value = sliceActiveValue(input.capName, input.slice);
863
+ if (value === null) return {
864
+ edge: "none",
865
+ value: null,
866
+ reason: isSourceCap(input.capName) ? "non-boolean-value" : "unknown-cap"
867
+ };
868
+ if (evaluateSensorLevelEdge({
869
+ level: value,
870
+ changedAt: sliceChangedAt(input.capName, input.slice),
871
+ priorLevel: input.prior,
872
+ startedAtMs: input.startedAtMs,
873
+ nowMs: input.nowMs,
874
+ firstSightingFreshnessMs: input.firstSightingFreshnessMs
875
+ }).kind === "rise") return {
876
+ edge: "rising",
877
+ value: true
878
+ };
879
+ if (input.prior === void 0) {
880
+ if (!value) return {
881
+ edge: "none",
882
+ value,
883
+ reason: "baseline-seeded-inactive"
884
+ };
885
+ return {
886
+ edge: "none",
887
+ value,
888
+ reason: "baseline-seeded-stale-active"
889
+ };
890
+ }
891
+ if (input.prior === value) return {
892
+ edge: "none",
893
+ value,
894
+ reason: "no-change"
895
+ };
896
+ if (!value) return {
897
+ edge: "none",
898
+ value,
899
+ reason: "falling-edge"
900
+ };
901
+ return {
902
+ edge: "none",
903
+ value,
904
+ reason: "missing-timestamp"
905
+ };
906
+ }
907
+ //#endregion
717
908
  //#region src/expression/errors.ts
718
909
  /**
719
910
  * Error types for the safe expression engine. Two distinct classes so callers
@@ -740,6 +931,9 @@ var ExpressionEvalError = class extends Error {
740
931
  };
741
932
  //#endregion
742
933
  //#region src/expression/builtins.ts
934
+ function isStatefulBuiltin(name) {
935
+ return name === "rose" || name === "count";
936
+ }
743
937
  function asFiniteNumber(value, name, index) {
744
938
  if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
745
939
  return value;
@@ -775,6 +969,37 @@ function toNumberOrNull(value) {
775
969
  const parsed = Number(trimmed);
776
970
  return Number.isFinite(parsed) ? parsed : null;
777
971
  }
972
+ var DEFAULT_ROSE_DEBOUNCE_MS = 2e3;
973
+ function evaluateRose(level, changedAt, debounceMs, call, field, env) {
974
+ const active = asBoolean(level, "rose", 0);
975
+ const timestamp = typeof changedAt === "number" && Number.isFinite(changedAt) ? changedAt : null;
976
+ const debounce = debounceMs === void 0 ? DEFAULT_ROSE_DEBOUNCE_MS : asFiniteNumber(debounceMs, "rose", 2);
977
+ if (debounce < 0) throw new ExpressionEvalError("rose: debounce must be a non-negative finite number");
978
+ const verdict = evaluateSensorLevelEdge({
979
+ level: active,
980
+ changedAt: timestamp,
981
+ priorLevel: call.priorLevel,
982
+ startedAtMs: env.startedAtMs,
983
+ nowMs: env.nowMs,
984
+ firstSightingFreshnessMs: DEFAULT_FIRST_SIGHTING_FRESHNESS_MS
985
+ });
986
+ call.priorLevel = verdict.kind === "rise" ? true : verdict.nextPriorLevel;
987
+ if (verdict.kind === "none") return env.currentOutput;
988
+ if (field.acceptedEdgeAt !== null && verdict.timestamp - field.acceptedEdgeAt < debounce) return env.currentOutput;
989
+ field.acceptedEdgeAt = verdict.timestamp;
990
+ return verdict.timestamp;
991
+ }
992
+ function evaluateCount(input, call, currentOutput) {
993
+ const currentCount = typeof currentOutput === "number" && Number.isFinite(currentOutput) && currentOutput >= 0 ? currentOutput : 0;
994
+ if (typeof input !== "number" || !Number.isFinite(input)) return currentCount;
995
+ if (call.highWater === null) {
996
+ call.highWater = input;
997
+ return currentCount;
998
+ }
999
+ if (input <= call.highWater) return currentCount;
1000
+ call.highWater = input;
1001
+ return currentCount + 1;
1002
+ }
778
1003
  var INF = Number.POSITIVE_INFINITY;
779
1004
  var table = {
780
1005
  min: {
@@ -914,6 +1139,20 @@ var table = {
914
1139
  const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
915
1140
  return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
916
1141
  }
1142
+ },
1143
+ rose: {
1144
+ minArgs: 2,
1145
+ maxArgs: 3,
1146
+ apply: () => {
1147
+ throw new ExpressionEvalError("rose: stateful evaluation context is required");
1148
+ }
1149
+ },
1150
+ count: {
1151
+ minArgs: 1,
1152
+ maxArgs: 1,
1153
+ apply: () => {
1154
+ throw new ExpressionEvalError("count: stateful evaluation context is required");
1155
+ }
917
1156
  }
918
1157
  };
919
1158
  /** Frozen, null-prototype builtin table. */
@@ -1397,22 +1636,28 @@ function step(ctx) {
1397
1636
  ctx.steps += 1;
1398
1637
  if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
1399
1638
  }
1400
- function evalNode(node, ctx) {
1639
+ function childPath(path, child) {
1640
+ return `${path}/${child}`;
1641
+ }
1642
+ function evalNode(node, ctx, path) {
1401
1643
  step(ctx);
1402
1644
  switch (node.kind) {
1403
1645
  case "literal": return node.value;
1404
1646
  case "identifier":
1405
1647
  if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
1406
1648
  return ctx.scope[node.name];
1407
- case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
1408
- case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
1649
+ case "unary": return evalUnary(node.op, evalNode(node.operand, ctx, childPath(path, "operand")));
1650
+ case "binary": return evalBinary(node.op, evalNode(node.left, ctx, childPath(path, "left")), evalNode(node.right, ctx, childPath(path, "right")));
1409
1651
  case "logical": {
1410
- const left = evalNode(node.left, ctx);
1411
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
1412
- return truthy(left) ? left : evalNode(node.right, ctx);
1652
+ const left = evalNode(node.left, ctx, childPath(path, "left"));
1653
+ if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx, childPath(path, "right")) : left;
1654
+ return truthy(left) ? left : evalNode(node.right, ctx, childPath(path, "right"));
1655
+ }
1656
+ case "conditional": return truthy(evalNode(node.test, ctx, childPath(path, "test"))) ? evalNode(node.consequent, ctx, childPath(path, "consequent")) : evalNode(node.alternate, ctx, childPath(path, "alternate"));
1657
+ case "call": {
1658
+ const args = node.args.map((arg, index) => evalNode(arg, ctx, childPath(path, `arg:${index}`)));
1659
+ return invokeCall(node.callee, args, path, ctx);
1413
1660
  }
1414
- case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
1415
- case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
1416
1661
  }
1417
1662
  }
1418
1663
  function evalUnary(op, operand) {
@@ -1443,11 +1688,19 @@ function evalBinary(op, left, right) {
1443
1688
  throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
1444
1689
  }
1445
1690
  }
1446
- function evalCall(callee, args, hooks) {
1691
+ function invokeCall(callee, args, path, ctx) {
1447
1692
  if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
1448
1693
  const builtin = EXPRESSION_BUILTINS[callee];
1449
1694
  if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
1450
- return builtin.apply(args, hooks);
1695
+ if (isStatefulBuiltin(callee)) {
1696
+ if (ctx.invokeStateful === void 0) throw new ExpressionEvalError(`${callee}: stateful evaluation context is required`);
1697
+ return ctx.invokeStateful({
1698
+ name: callee,
1699
+ path,
1700
+ args
1701
+ });
1702
+ }
1703
+ return builtin.apply(args, ctx.hooks);
1451
1704
  }
1452
1705
  /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
1453
1706
  * runtime failure (unknown identifier, type mismatch, non-finite result,
@@ -1457,11 +1710,47 @@ function evaluateAst(node, scope, opts) {
1457
1710
  scope,
1458
1711
  hooks: opts?.hooks ?? EMPTY_HOOKS,
1459
1712
  maxSteps: opts?.maxSteps ?? 4096,
1713
+ ...opts?.invokeStateful === void 0 ? {} : { invokeStateful: opts.invokeStateful },
1460
1714
  steps: 0
1461
- });
1715
+ }, "root");
1462
1716
  }
1463
1717
  //#endregion
1464
1718
  //#region src/expression/expression-source.ts
1719
+ function createStatefulExpressionMemory() {
1720
+ return {
1721
+ calls: /* @__PURE__ */ new Map(),
1722
+ acceptedEdgeAt: null
1723
+ };
1724
+ }
1725
+ function cloneStatefulExpressionMemory(memory) {
1726
+ return {
1727
+ calls: new Map([...memory.calls].map(([path, call]) => [path, { ...call }])),
1728
+ acceptedEdgeAt: memory.acceptedEdgeAt
1729
+ };
1730
+ }
1731
+ function evaluateStatefulExpressionCall(invocation, memory, env) {
1732
+ const existing = memory.calls.get(invocation.path);
1733
+ switch (invocation.name) {
1734
+ case "rose": {
1735
+ const call = existing?.kind === "rose" ? existing : { kind: "rose" };
1736
+ memory.calls.set(invocation.path, call);
1737
+ const currentOutput = env.currentOutput === null || typeof env.currentOutput === "number" && Number.isFinite(env.currentOutput) ? env.currentOutput : null;
1738
+ return evaluateRose(invocation.args[0], invocation.args[1], invocation.args[2], call, memory, {
1739
+ currentOutput,
1740
+ startedAtMs: env.startedAtMs,
1741
+ nowMs: env.nowMs
1742
+ });
1743
+ }
1744
+ case "count": {
1745
+ const call = existing?.kind === "count" ? existing : {
1746
+ kind: "count",
1747
+ highWater: null
1748
+ };
1749
+ memory.calls.set(invocation.path, call);
1750
+ return evaluateCount(invocation.args[0], call, env.currentOutput);
1751
+ }
1752
+ }
1753
+ }
1465
1754
  /** The `now` epoch-ms binding is auto-injected into every evaluation and is a
1466
1755
  * reserved binding name (authors may not rebind it). */
1467
1756
  var EXPRESSION_INJECTED_NOW = "now";
@@ -1504,9 +1793,10 @@ function validateExpressionSource(src) {
1504
1793
  /**
1505
1794
  * Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
1506
1795
  * plus the injected `now` (supplied by the caller for determinism and
1507
- * testability), compiles via the LRU, and evaluates. Any failure (parse or
1508
- * eval) returns `{ ok: false }` — the caller treats that as "skip this
1509
- * derivation", never as a throw that takes the pass down.
1796
+ * testability), compiles via the LRU, and evaluates. Stateful calls are
1797
+ * delegated through `opts.invokeStateful` with their stable AST path. Any
1798
+ * failure (parse or eval) returns `{ ok: false }` — the caller treats that as
1799
+ * "skip this derivation", never as a throw that takes the pass down.
1510
1800
  */
1511
1801
  function evaluateExpressionSource(expr, bindingValues, now, opts) {
1512
1802
  const compiled = compileExpressionSafe(expr);
@@ -1650,6 +1940,7 @@ var CompositionItemEntrySchema = z.object({
1650
1940
  var CompositionFieldsFeatureSchema = z.object({
1651
1941
  kind: z.literal("fields"),
1652
1942
  cap: z.string().min(1),
1943
+ enabled: z.boolean().optional(),
1653
1944
  fields: z.record(z.string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
1654
1945
  /**
1655
1946
  * Items of the cap's item array, keyed by item key (D663). Naming `items`
@@ -1663,9 +1954,14 @@ var CompositionFieldsFeatureSchema = z.object({
1663
1954
  var CompositionPassthroughFeatureSchema = z.object({
1664
1955
  kind: z.literal("passthrough"),
1665
1956
  cap: z.string().min(1),
1957
+ enabled: z.boolean().optional(),
1666
1958
  source: CompositionSourceRefSchema
1667
1959
  });
1668
1960
  var CompositionFeatureSchema = z.discriminatedUnion("kind", [CompositionFieldsFeatureSchema, CompositionPassthroughFeatureSchema]);
1961
+ /** Only an explicit `false` disables a feature; omitted preserves the existing wire default. */
1962
+ function compositionFeatureEnabled(feature) {
1963
+ return feature.enabled !== false;
1964
+ }
1669
1965
  /**
1670
1966
  * A NEW device. It has no `name` of its own: the block's name IS the device's
1671
1967
  * name (2026-08-08). A second copy here would be a second writer of one string (D62).
@@ -1756,4 +2052,4 @@ function featureSources(feature) {
1756
2052
  return featureFieldEntries(feature).map((e) => e.source);
1757
2053
  }
1758
2054
  //#endregion
1759
- export { MAX_EXPRESSION_SOURCE_LENGTH as $, compositionItemFieldPath as A, validateExpressionSource as B, CompositionTargetSchema as C, MAX_COMPOSITION_ITEMS as D, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, featureSources as F, parseExpression as G, evaluateAst as H, fieldReads as I, EXPRESSION_IDENTIFIER_RE as J, tokenize as K, EXPRESSION_INJECTED_NOW as L, compositionSliceKey as M, compositionSourceKey as N, composedDeviceRef as O, featureFieldEntries as P, MAX_EXPRESSION_EVAL_STEPS as Q, evaluateExpressionSource as R, CompositionSourceRefSchema as S, MAX_COMPOSITION_FEATURES as T, compileExpression as U, createExpressionScope as V, compileExpressionSafe as W, MAX_EXPRESSION_BINDINGS as X, MAX_EXPRESSION_AST_NODES as Y, MAX_EXPRESSION_CALL_ARGS as Z, CompositionItemEntrySchema as _, resolveStreamMaps as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, canonicalHash as at, CompositionPassthroughFeatureSchema as b, CompositionCommandTargetSchema as c, audioPlanFromEncodeProfile as ct, CompositionFeatureSchema as d, buildInputArgs as dt, RESERVED_BINDING_NAMES as et, CompositionFieldReadSchema as f, buildVideoArgs as ft, CompositionFromSourceSchema as g, pickVideoEncoder as gt, CompositionForwardCommandSchema as h, logBannerArgs as ht, COMPOSITION_ITEM_KEY_RE as i, ExpressionParseError as it, compositionReadKey as j, composedDeviceStableId as k, CompositionExistingTargetSchema as l, buildAudioArgs as lt, CompositionFieldsFeatureSchema as m, isSoftwareDecode as mt, COMPOSER_ADDON_ID as n, EXPRESSION_BUILTIN_NAMES as nt, CompositionBindingSchema as o, Fmp4BoxSplitter as ot, CompositionFieldSourceSchema as p, invocationFromEncodeProfile as pt, EXPRESSION_COMPILE_CACHE_CAPACITY as q, COMPOSITION_ITEM_ARRAY_PATH as r, ExpressionEvalError as rt, CompositionCodeSourceSchema as s, AUDIO_PRESETS as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, EXPRESSION_BUILTINS as tt, CompositionExpressionSourceSchema as u, buildFfmpegArgs as ut, CompositionLiteralBindingSchema as v, MAX_COMPOSITION_CODE_LENGTH as w, CompositionSchema as x, CompositionNewTargetSchema as y, toExpressionValue as z };
2055
+ export { MAX_EXPRESSION_AST_NODES as $, compositionFeatureEnabled as A, invocationFromEncodeProfile as At, createStatefulExpressionMemory as B, CompositionTargetSchema as C, Fmp4BoxSplitter as Ct, MAX_COMPOSITION_ITEMS as D, buildFfmpegArgs as Dt, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, buildAudioArgs as Et, featureFieldEntries as F, createExpressionScope as G, evaluateStatefulExpressionCall as H, featureSources as I, compileExpressionSafe as J, evaluateAst as K, fieldReads as L, compositionReadKey as M, logBannerArgs as Mt, compositionSliceKey as N, pickVideoEncoder as Nt, composedDeviceRef as O, buildInputArgs as Ot, compositionSourceKey as P, resolveStreamMaps as Pt, EXPRESSION_IDENTIFIER_RE as Q, EXPRESSION_INJECTED_NOW as R, CompositionSourceRefSchema as S, canonicalHash as St, MAX_COMPOSITION_FEATURES as T, audioPlanFromEncodeProfile as Tt, toExpressionValue as U, evaluateExpressionSource as V, validateExpressionSource as W, tokenize as X, parseExpression as Y, EXPRESSION_COMPILE_CACHE_CAPACITY as Z, CompositionItemEntrySchema as _, evaluateSensorEdge as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, EXPRESSION_BUILTINS as at, CompositionPassthroughFeatureSchema as b, sliceActiveValue as bt, CompositionCommandTargetSchema as c, evaluateRose as ct, CompositionFeatureSchema as d, ExpressionParseError as dt, MAX_EXPRESSION_BINDINGS as et, CompositionFieldReadSchema as f, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as ft, CompositionFromSourceSchema as g, SOURCE_DEVICE_TYPES as gt, CompositionForwardCommandSchema as h, SOURCE_CAP_CHANGED_AT_FIELD as ht, COMPOSITION_ITEM_KEY_RE as i, RESERVED_BINDING_NAMES as it, compositionItemFieldPath as j, isSoftwareDecode as jt, composedDeviceStableId as k, buildVideoArgs as kt, CompositionExistingTargetSchema as l, isStatefulBuiltin as lt, CompositionFieldsFeatureSchema as m, SOURCE_CAP_ACTIVE_FIELD as mt, COMPOSER_ADDON_ID as n, MAX_EXPRESSION_EVAL_STEPS as nt, CompositionBindingSchema as o, EXPRESSION_BUILTIN_NAMES as ot, CompositionFieldSourceSchema as p, SOURCE_CAPS as pt, compileExpression as q, COMPOSITION_ITEM_ARRAY_PATH as r, MAX_EXPRESSION_SOURCE_LENGTH as rt, CompositionCodeSourceSchema as s, evaluateCount as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, MAX_EXPRESSION_CALL_ARGS as tt, CompositionExpressionSourceSchema as u, ExpressionEvalError as ut, CompositionLiteralBindingSchema as v, evaluateSensorLevelEdge as vt, MAX_COMPOSITION_CODE_LENGTH as w, AUDIO_PRESETS as wt, CompositionSchema as x, sliceChangedAt as xt, CompositionNewTargetSchema as y, isSourceCap as yt, cloneStatefulExpressionMemory as z };
@@ -14,6 +14,8 @@
14
14
  */
15
15
  /** The only value types the engine produces or consumes. */
16
16
  export type ExpressionValue = number | string | boolean | null;
17
+ /** Stable structural address of a node within one parsed expression AST. */
18
+ export type ExpressionNodePath = string;
17
19
  export type ExpressionBinaryOperator = '+' | '-' | '*' | '/' | '%' | '==' | '!=' | '<' | '<=' | '>' | '>=';
18
20
  export type ExpressionLogicalOperator = '&&' | '||';
19
21
  export type ExpressionUnaryOperator = '-' | '!';
@@ -16,6 +16,7 @@
16
16
  * closed rather than emitting a garbage value.
17
17
  */
18
18
  import type { ExpressionValue } from './ast.js';
19
+ import type { CountCallState, RoseCallState, StatefulExpressionMemory } from './expression-source.js';
19
20
  /** Optional runtime hooks the evaluator threads through to builtins. */
20
21
  export interface ExpressionEvalHooks {
21
22
  /** Convert `value` from unit `from` to unit `to`; `null` when the pair is
@@ -40,6 +41,14 @@ export interface ExpressionBuiltin {
40
41
  readonly maxArgs: number;
41
42
  readonly apply: (args: readonly ExpressionValue[], hooks: ExpressionEvalHooks) => ExpressionValue;
42
43
  }
44
+ export type StatefulExpressionBuiltin = 'rose' | 'count';
45
+ export declare function isStatefulBuiltin(name: string): name is StatefulExpressionBuiltin;
46
+ export declare function evaluateRose(level: unknown, changedAt: unknown, debounceMs: unknown, call: RoseCallState, field: StatefulExpressionMemory, env: {
47
+ readonly currentOutput: number | null;
48
+ readonly startedAtMs: number;
49
+ readonly nowMs: number;
50
+ }): number | null;
51
+ export declare function evaluateCount(input: unknown, call: CountCallState, currentOutput: unknown): number;
43
52
  /** Frozen, null-prototype builtin table. */
44
53
  export declare const EXPRESSION_BUILTINS: Readonly<Record<string, ExpressionBuiltin>>;
45
54
  /** The set of valid builtin names — used by the parser to reject unknown
@@ -1,8 +1,14 @@
1
- import { type ExpressionEvalHooks } from './builtins.js';
2
- import type { ExpressionNode, ExpressionValue } from './ast.js';
1
+ import { type ExpressionEvalHooks, type StatefulExpressionBuiltin } from './builtins.js';
2
+ import type { ExpressionNode, ExpressionNodePath, ExpressionValue } from './ast.js';
3
+ export interface StatefulCallInvocation {
4
+ readonly name: StatefulExpressionBuiltin;
5
+ readonly path: ExpressionNodePath;
6
+ readonly args: readonly ExpressionValue[];
7
+ }
3
8
  export interface ExpressionEvalOptions {
4
9
  readonly hooks?: ExpressionEvalHooks;
5
10
  readonly maxSteps?: number;
11
+ readonly invokeStateful?: (call: StatefulCallInvocation) => ExpressionValue;
6
12
  }
7
13
  /** Build a null-prototype scope from own-enumerable binding entries. Inherited
8
14
  * keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
@@ -9,7 +9,28 @@
9
9
  * positions (derive a value, gate a trigger, compute an argument).
10
10
  */
11
11
  import type { ExpressionValue } from './ast.js';
12
- import { type ExpressionEvalOptions } from './evaluator.js';
12
+ import { type ExpressionEvalOptions, type StatefulCallInvocation } from './evaluator.js';
13
+ export interface RoseCallState {
14
+ readonly kind: 'rose';
15
+ priorLevel?: boolean;
16
+ }
17
+ export interface CountCallState {
18
+ readonly kind: 'count';
19
+ highWater: number | null;
20
+ }
21
+ export type StatefulCallState = RoseCallState | CountCallState;
22
+ export interface StatefulExpressionMemory {
23
+ readonly calls: Map<string, StatefulCallState>;
24
+ acceptedEdgeAt: number | null;
25
+ }
26
+ export declare function createStatefulExpressionMemory(): StatefulExpressionMemory;
27
+ export declare function cloneStatefulExpressionMemory(memory: StatefulExpressionMemory): StatefulExpressionMemory;
28
+ export interface StatefulExpressionEnvironment {
29
+ readonly currentOutput: unknown;
30
+ readonly startedAtMs: number;
31
+ readonly nowMs: number;
32
+ }
33
+ export declare function evaluateStatefulExpressionCall(invocation: StatefulCallInvocation, memory: StatefulExpressionMemory, env: StatefulExpressionEnvironment): ExpressionValue;
13
34
  /** The `now` epoch-ms binding is auto-injected into every evaluation and is a
14
35
  * reserved binding name (authors may not rebind it). */
15
36
  export declare const EXPRESSION_INJECTED_NOW = "now";
@@ -46,8 +67,9 @@ export type EvaluateExpressionSourceResult = {
46
67
  /**
47
68
  * Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
48
69
  * plus the injected `now` (supplied by the caller for determinism and
49
- * testability), compiles via the LRU, and evaluates. Any failure (parse or
50
- * eval) returns `{ ok: false }` — the caller treats that as "skip this
51
- * derivation", never as a throw that takes the pass down.
70
+ * testability), compiles via the LRU, and evaluates. Stateful calls are
71
+ * delegated through `opts.invokeStateful` with their stable AST path. Any
72
+ * failure (parse or eval) returns `{ ok: false }` — the caller treats that as
73
+ * "skip this derivation", never as a throw that takes the pass down.
52
74
  */
53
75
  export declare function evaluateExpressionSource(expr: string, bindingValues: Readonly<Record<string, ExpressionValue>>, now: number, opts?: ExpressionEvalOptions): EvaluateExpressionSourceResult;
@@ -9,18 +9,18 @@
9
9
  * the deep modules (e.g. `../expression/compile.js`), never through this file
10
10
  * OR the root barrel (see biome-plugins/no-types-barrel-leaf-import.grit).
11
11
  */
12
- export type { ExpressionBinaryNode, ExpressionBinaryOperator, ExpressionCallNode, ExpressionConditionalNode, ExpressionIdentifierNode, ExpressionLiteralNode, ExpressionLogicalNode, ExpressionLogicalOperator, ExpressionNode, ExpressionUnaryNode, ExpressionUnaryOperator, ExpressionValue, } from './ast.js';
12
+ export type { ExpressionBinaryNode, ExpressionBinaryOperator, ExpressionCallNode, ExpressionConditionalNode, ExpressionIdentifierNode, ExpressionLiteralNode, ExpressionLogicalNode, ExpressionLogicalOperator, ExpressionNode, ExpressionNodePath, ExpressionUnaryNode, ExpressionUnaryOperator, ExpressionValue, } from './ast.js';
13
13
  export type { ExpressionBindingSource, ExpressionFieldBinding, ExpressionGlobalBinding, ExpressionLiteralBinding, ExpressionSource, } from './binding-source.js';
14
14
  export { ExpressionBindingSourceSchema, ExpressionFieldBindingSchema, ExpressionGlobalBindingSchema, ExpressionLiteralBindingSchema, ExpressionSourceSchema, } from './binding-source.js';
15
- export type { ExpressionBuiltin, ExpressionEvalHooks } from './builtins.js';
16
- export { EXPRESSION_BUILTIN_NAMES, EXPRESSION_BUILTINS, } from './builtins.js';
15
+ export type { ExpressionBuiltin, ExpressionEvalHooks, StatefulExpressionBuiltin, } from './builtins.js';
16
+ export { evaluateCount, evaluateRose, EXPRESSION_BUILTIN_NAMES, EXPRESSION_BUILTINS, isStatefulBuiltin, } from './builtins.js';
17
17
  export type { CompileResult } from './compile.js';
18
18
  export { compileExpression, compileExpressionSafe } from './compile.js';
19
19
  export { ExpressionEvalError, ExpressionParseError } from './errors.js';
20
- export type { ExpressionEvalOptions } from './evaluator.js';
20
+ export type { ExpressionEvalOptions, StatefulCallInvocation } from './evaluator.js';
21
21
  export { createExpressionScope, evaluateAst } from './evaluator.js';
22
- export type { EvaluateExpressionSourceResult, ExpressionSourceInput, } from './expression-source.js';
23
- export { EXPRESSION_INJECTED_NOW, evaluateExpressionSource, toExpressionValue, validateExpressionSource, } from './expression-source.js';
22
+ export type { CountCallState, EvaluateExpressionSourceResult, ExpressionSourceInput, RoseCallState, StatefulCallState, StatefulExpressionEnvironment, StatefulExpressionMemory, } from './expression-source.js';
23
+ export { cloneStatefulExpressionMemory, createStatefulExpressionMemory, EXPRESSION_INJECTED_NOW, evaluateExpressionSource, evaluateStatefulExpressionCall, toExpressionValue, validateExpressionSource, } from './expression-source.js';
24
24
  export { EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, RESERVED_BINDING_NAMES, } from './limits.js';
25
25
  export type { ParsedExpression } from './parser.js';
26
26
  export { parseExpression } from './parser.js';
package/dist/index.d.ts CHANGED
@@ -150,7 +150,7 @@ export { COCO_80_LABELS, COCO_TO_MACRO, DETECTION_MACRO_CLASSES, isDetectionMacr
150
150
  export { DEVICE_STATE_READERS, readDeviceStateFrom, stateVocabularyFor, } from './catalogs/device-state-vocabulary.js';
151
151
  export { colorForKind, DEFAULT_EVENT_COLOR, EVENT_TAXONOMY, type EventTaxonomyCategory, type EventTaxonomyEntry, type EventTaxonomyLevel, getTaxonomyEntry, subKindsOf, TAXONOMY_COLORS, } from './catalogs/event-taxonomy.js';
152
152
  export { buildNcTaxonomy, NC_TAXONOMY, type NcTaxonomy, type NcTaxonomyEntry, NcTaxonomyEntrySchema, NcTaxonomySchema, } from './catalogs/nc-taxonomy.js';
153
- export { DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, evaluateSensorEdge, isSourceCap, type SensorEdgeIgnoreReason, type SensorEdgeInput, type SensorEdgeVerdict, SOURCE_CAP_ACTIVE_FIELD, SOURCE_CAP_CHANGED_AT_FIELD, SOURCE_CAPS, SOURCE_DEVICE_TYPES, sliceActiveValue, sliceChangedAt, } from './catalogs/sensor-active-state.js';
153
+ export { DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, evaluateSensorEdge, evaluateSensorLevelEdge, isSourceCap, type SensorEdgeIgnoreReason, type SensorEdgeInput, type SensorEdgeVerdict, type SensorLevelEdgeInput, type SensorLevelEdgeVerdict, SOURCE_CAP_ACTIVE_FIELD, SOURCE_CAP_CHANGED_AT_FIELD, SOURCE_CAPS, SOURCE_DEVICE_TYPES, sliceActiveValue, sliceChangedAt, } from './catalogs/sensor-active-state.js';
154
154
  export * from './constants.js';
155
155
  export type { AccessoryKindValue } from './device/accessory.js';
156
156
  export { ACCESSORY_LABEL, AccessoryKind, accessoryStableId, } from './device/accessory.js';
@@ -201,8 +201,8 @@ export type { DeviceConfigEntry } from './device/zod-to-config-ui.js';
201
201
  export { zodEntriesToConfigUI } from './device/zod-to-config-ui.js';
202
202
  export { EventCategory, isOwnerOnlyEventCategory, OWNER_ONLY_EVENT_CATEGORIES, publicEventCategories, } from './enums/event-category.js';
203
203
  export * from './enums/index.js';
204
- export type { CompileResult, EvaluateExpressionSourceResult, ExpressionBinaryOperator, ExpressionBindingSource, ExpressionBuiltin, ExpressionEvalHooks, ExpressionEvalOptions, ExpressionFieldBinding, ExpressionGlobalBinding, ExpressionLiteralBinding, ExpressionLogicalOperator, ExpressionNode, ExpressionSource, ExpressionSourceInput, ExpressionUnaryOperator, ExpressionValue, Keyword, ParsedExpression, Punctuator, Token, } from './expression/index.js';
205
- export { compileExpression, compileExpressionSafe, createExpressionScope, EXPRESSION_BUILTIN_NAMES, EXPRESSION_BUILTINS, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, ExpressionBindingSourceSchema, ExpressionEvalError, ExpressionFieldBindingSchema, ExpressionGlobalBindingSchema, ExpressionLiteralBindingSchema, ExpressionParseError, ExpressionSourceSchema, evaluateAst, evaluateExpressionSource, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, parseExpression, RESERVED_BINDING_NAMES, toExpressionValue, tokenize, validateExpressionSource, } from './expression/index.js';
204
+ export type { CompileResult, EvaluateExpressionSourceResult, ExpressionBinaryOperator, ExpressionBindingSource, ExpressionBuiltin, ExpressionEvalHooks, ExpressionEvalOptions, ExpressionFieldBinding, ExpressionGlobalBinding, ExpressionLiteralBinding, ExpressionLogicalOperator, ExpressionNode, ExpressionNodePath, ExpressionSource, ExpressionSourceInput, ExpressionUnaryOperator, ExpressionValue, CountCallState, RoseCallState, StatefulCallState, StatefulCallInvocation, StatefulExpressionEnvironment, StatefulExpressionMemory, StatefulExpressionBuiltin, Keyword, ParsedExpression, Punctuator, Token, } from './expression/index.js';
205
+ export { compileExpression, compileExpressionSafe, cloneStatefulExpressionMemory, createExpressionScope, createStatefulExpressionMemory, EXPRESSION_BUILTIN_NAMES, EXPRESSION_BUILTINS, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, ExpressionBindingSourceSchema, ExpressionEvalError, ExpressionFieldBindingSchema, ExpressionGlobalBindingSchema, ExpressionLiteralBindingSchema, ExpressionParseError, ExpressionSourceSchema, evaluateAst, evaluateCount, evaluateExpressionSource, evaluateRose, evaluateStatefulExpressionCall, isStatefulBuiltin, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, parseExpression, RESERVED_BINDING_NAMES, toExpressionValue, tokenize, validateExpressionSource, } from './expression/index.js';
206
206
  export type { AddonApi, AppRouter } from './generated/addon-api.js';
207
207
  export type { CapNameWithStatus, CapStatusTypeMap } from './generated/cap-status-types.js';
208
208
  export { CAP_NAMES_WITH_STATUS } from './generated/cap-status-types.js';