@camstack/system 1.2.314 → 1.2.316

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 (68) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/composer/composer-plan.d.ts +8 -1
  14. package/dist/builtins/composer/composer.addon.js +191 -29
  15. package/dist/builtins/composer/composer.addon.mjs +195 -33
  16. package/dist/builtins/composer/composer.d.ts +5 -1
  17. package/dist/builtins/composer/composition-runtime.d.ts +10 -1
  18. package/dist/builtins/composer/existing-target.d.ts +9 -0
  19. package/dist/builtins/composer/field-record.d.ts +12 -3
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/composition-sources.d.ts +3 -1
  23. package/dist/builtins/core-blocks/core-blocks.addon.js +3 -103
  24. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -102
  25. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  26. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  27. package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
  28. package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
  29. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -8
  31. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -8
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  38. package/dist/builtins/local-network/local-network.addon.js +1 -1
  39. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  40. package/dist/builtins/loki-logging/index.js +1 -1
  41. package/dist/builtins/loki-logging/index.mjs +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  44. package/dist/builtins/platform-probe/index.js +1 -1
  45. package/dist/builtins/platform-probe/index.mjs +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/snapshot/index.js +1 -1
  49. package/dist/builtins/snapshot/index.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  53. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  55. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/system-config/system-config.addon.js +1 -1
  57. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  58. package/dist/builtins/winston-logging/index.js +1 -1
  59. package/dist/builtins/winston-logging/index.mjs +1 -1
  60. package/dist/composition-sources-BwE1POnF.js +122 -0
  61. package/dist/composition-sources-fUyF4z39.mjs +111 -0
  62. package/dist/{dist-cH4t1ZEm.mjs → dist-BPvku43i.mjs} +596 -305
  63. package/dist/{dist-BtNkk3Qz.js → dist-RAl10EfG.js} +625 -304
  64. package/dist/index.js +1 -1
  65. package/dist/index.mjs +1 -1
  66. package/dist/{retired-settings-keys-B7imb-qH.js → retired-settings-keys-D3rGTGq5.js} +1 -1
  67. package/dist/{retired-settings-keys-DQYHfZ0S.mjs → retired-settings-keys-DqVWSrVw.mjs} +1 -1
  68. package/package.json +1 -1
@@ -701,7 +701,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
701
701
  return EventCategory;
702
702
  }({});
703
703
  //#endregion
704
- //#region ../types/dist/sleep-jkRQgtSc.mjs
704
+ //#region ../types/dist/sleep-E6eD4yQS.mjs
705
705
  /**
706
706
  * The audio chunk plane's byte format, and the ONE expansion from a coded
707
707
  * window to float samples (D455).
@@ -2777,6 +2777,10 @@ function toNodeId(idOrRunnerId) {
2777
2777
  const head = idOrRunnerId.split("/")[0];
2778
2778
  return head === void 0 || head.length === 0 ? idOrRunnerId : head;
2779
2779
  }
2780
+ zod.z.object({
2781
+ field: zod.z.string().min(1),
2782
+ on: zod.z.literal("advance")
2783
+ });
2780
2784
  /**
2781
2785
  * Resolve the EFFECTIVE mount hint for a cap — the explicit `mount` when
2782
2786
  * present, else the scope/mode/deviceNative-derived default. Single source
@@ -3508,7 +3512,7 @@ function errMsg(err) {
3508
3512
  return String(err);
3509
3513
  }
3510
3514
  //#endregion
3511
- //#region ../types/dist/composition-CPFIlFfw.mjs
3515
+ //#region ../types/dist/composition-D5BQywfe.mjs
3512
3516
  var AUDIO_ENCODER_BY_CODEC = {
3513
3517
  opus: "libopus",
3514
3518
  aac: "aac",
@@ -3902,6 +3906,173 @@ function buildFfmpegArgs(inv) {
3902
3906
  ];
3903
3907
  }
3904
3908
  /**
3909
+ * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
3910
+ * valutatore puro del suo FRONTE.
3911
+ *
3912
+ * Viveva dentro il builtin virtual-doorbell
3913
+ * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
3914
+ * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
3915
+ * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
3916
+ * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
3917
+ * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
3918
+ * SPOSTA qui e il doorbell la ri-esporta.
3919
+ *
3920
+ * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
3921
+ * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
3922
+ * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
3923
+ * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
3924
+ */
3925
+ /**
3926
+ * Known binary / switch source caps → the boolean slice field whose
3927
+ * false→true rise counts as ACTIVE. Every entry is "fire on active".
3928
+ * Sensors whose "active" reading is not a plain boolean (presence's string
3929
+ * state, connectivity's connected flag) are deliberately excluded — a
3930
+ * reconnect is not a doorbell press, and it is not a recording either.
3931
+ */
3932
+ var SOURCE_CAP_ACTIVE_FIELD = {
3933
+ contact: "entryOpen",
3934
+ binary: "on",
3935
+ switch: "on",
3936
+ motion: "detected",
3937
+ flood: "flooded",
3938
+ gas: "detected",
3939
+ smoke: "detected",
3940
+ "carbon-monoxide": "detected",
3941
+ vibration: "detected",
3942
+ tamper: "tampered"
3943
+ };
3944
+ /**
3945
+ * The same caps → the slice field carrying the ms-epoch timestamp of the
3946
+ * last transition. Every source cap MUST appear here (guarded by a spec):
3947
+ * without a transition timestamp the evaluator cannot tell a genuine rise
3948
+ * from a boot-time hydration when the FIRST slice it ever sees is already
3949
+ * active, and errs towards silence — swallowing the rise.
3950
+ *
3951
+ * These timestamps are UPSTREAM ones, not ingest ones: the Home Assistant
3952
+ * provider derives them from `state.last_changed`, so they survive our own
3953
+ * restarts and correctly read as "hours ago" for a state that has been
3954
+ * active for hours. `motion` names its rise timestamp `lastDetectedAt`.
3955
+ */
3956
+ var SOURCE_CAP_CHANGED_AT_FIELD = {
3957
+ contact: "lastChangedAt",
3958
+ binary: "lastChangedAt",
3959
+ switch: "lastChangedAt",
3960
+ motion: "lastDetectedAt",
3961
+ flood: "lastChangedAt",
3962
+ gas: "lastChangedAt",
3963
+ smoke: "lastChangedAt",
3964
+ "carbon-monoxide": "lastChangedAt",
3965
+ vibration: "lastChangedAt",
3966
+ tamper: "lastChangedAt"
3967
+ };
3968
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
3969
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
3970
+ /** True when a cap is a recognised binary/switch source. */
3971
+ function isSourceCap(capName) {
3972
+ return Object.prototype.hasOwnProperty.call(SOURCE_CAP_ACTIVE_FIELD, capName);
3973
+ }
3974
+ /** Extract the "active" boolean a source cap's slice carries, or null when
3975
+ * the cap is unknown or the field is missing / non-boolean. */
3976
+ function sliceActiveValue(capName, slice) {
3977
+ const field = SOURCE_CAP_ACTIVE_FIELD[capName];
3978
+ if (field === void 0) return null;
3979
+ const raw = slice[field];
3980
+ return typeof raw === "boolean" ? raw : null;
3981
+ }
3982
+ /** Ms-epoch transition timestamp a source cap's slice carries, or null when
3983
+ * it is absent, non-numeric or the zero "never observed" sentinel. */
3984
+ function sliceChangedAt(capName, slice) {
3985
+ const field = SOURCE_CAP_CHANGED_AT_FIELD[capName];
3986
+ if (field === void 0) return null;
3987
+ const raw = slice[field];
3988
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return null;
3989
+ return raw;
3990
+ }
3991
+ /**
3992
+ * How recent a source's own transition timestamp must be for a FIRST
3993
+ * sighting that is already active to count as a genuine rise rather than a
3994
+ * hydration of long-standing state.
3995
+ */
3996
+ var DEFAULT_FIRST_SIGHTING_FRESHNESS_MS = 3e4;
3997
+ /**
3998
+ * The source-independent edge rule. The caller supplies an already extracted
3999
+ * boolean level and source timestamp; this function owns first-sighting
4000
+ * freshness and false-to-true detection.
4001
+ */
4002
+ function evaluateSensorLevelEdge(input) {
4003
+ const timestamp = input.changedAt;
4004
+ const freshFloor = Math.max(input.startedAtMs, input.nowMs - input.firstSightingFreshnessMs);
4005
+ const firstRise = input.priorLevel === void 0 && input.level && timestamp !== null && Number.isFinite(timestamp) && timestamp >= freshFloor;
4006
+ const transitionRise = input.priorLevel === false && input.level && timestamp !== null && Number.isFinite(timestamp);
4007
+ if ((firstRise || transitionRise) && timestamp !== null) return {
4008
+ kind: "rise",
4009
+ timestamp
4010
+ };
4011
+ return {
4012
+ kind: "none",
4013
+ nextPriorLevel: input.level
4014
+ };
4015
+ }
4016
+ /**
4017
+ * IL fronte. Puro: nessun orologio proprio, nessuna memoria — il chiamante
4018
+ * porta `prior`, `nowMs` e il proprio `startedAtMs`.
4019
+ *
4020
+ * Il caso della PRIMA slice già attiva è trattato esplicitamente e vale come
4021
+ * fronte solo se il timestamp UPSTREAM della transizione è posteriore a
4022
+ * `startedAtMs` **e** entro `firstSightingFreshnessMs`. Entrambe le metà
4023
+ * servono: la sola freschezza scatterebbe su un'idratazione al boot di uno
4024
+ * stato flippato pochi secondi prima del riavvio, e il solo "dopo che abbiamo
4025
+ * iniziato" scatterebbe, su un processo di lunga vita, per una sorgente
4026
+ * adottata oggi il cui stato è cambiato ieri. Il caso ambiguo ERRA VERSO IL
4027
+ * SILENZIO e lo dichiara (`baseline-seeded-stale-active`).
4028
+ */
4029
+ function evaluateSensorEdge(input) {
4030
+ const value = sliceActiveValue(input.capName, input.slice);
4031
+ if (value === null) return {
4032
+ edge: "none",
4033
+ value: null,
4034
+ reason: isSourceCap(input.capName) ? "non-boolean-value" : "unknown-cap"
4035
+ };
4036
+ if (evaluateSensorLevelEdge({
4037
+ level: value,
4038
+ changedAt: sliceChangedAt(input.capName, input.slice),
4039
+ priorLevel: input.prior,
4040
+ startedAtMs: input.startedAtMs,
4041
+ nowMs: input.nowMs,
4042
+ firstSightingFreshnessMs: input.firstSightingFreshnessMs
4043
+ }).kind === "rise") return {
4044
+ edge: "rising",
4045
+ value: true
4046
+ };
4047
+ if (input.prior === void 0) {
4048
+ if (!value) return {
4049
+ edge: "none",
4050
+ value,
4051
+ reason: "baseline-seeded-inactive"
4052
+ };
4053
+ return {
4054
+ edge: "none",
4055
+ value,
4056
+ reason: "baseline-seeded-stale-active"
4057
+ };
4058
+ }
4059
+ if (input.prior === value) return {
4060
+ edge: "none",
4061
+ value,
4062
+ reason: "no-change"
4063
+ };
4064
+ if (!value) return {
4065
+ edge: "none",
4066
+ value,
4067
+ reason: "falling-edge"
4068
+ };
4069
+ return {
4070
+ edge: "none",
4071
+ value,
4072
+ reason: "missing-timestamp"
4073
+ };
4074
+ }
4075
+ /**
3905
4076
  * Error types for the safe expression engine. Two distinct classes so callers
3906
4077
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
3907
4078
  * failure — both are non-fatal to the host: read paths degrade to "skip link".
@@ -3924,6 +4095,9 @@ var ExpressionEvalError = class extends Error {
3924
4095
  this.name = "ExpressionEvalError";
3925
4096
  }
3926
4097
  };
4098
+ function isStatefulBuiltin(name) {
4099
+ return name === "rose" || name === "count";
4100
+ }
3927
4101
  function asFiniteNumber(value, name, index) {
3928
4102
  if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
3929
4103
  return value;
@@ -3959,6 +4133,37 @@ function toNumberOrNull(value) {
3959
4133
  const parsed = Number(trimmed);
3960
4134
  return Number.isFinite(parsed) ? parsed : null;
3961
4135
  }
4136
+ var DEFAULT_ROSE_DEBOUNCE_MS = 2e3;
4137
+ function evaluateRose(level, changedAt, debounceMs, call, field, env) {
4138
+ const active = asBoolean(level, "rose", 0);
4139
+ const timestamp = typeof changedAt === "number" && Number.isFinite(changedAt) ? changedAt : null;
4140
+ const debounce = debounceMs === void 0 ? DEFAULT_ROSE_DEBOUNCE_MS : asFiniteNumber(debounceMs, "rose", 2);
4141
+ if (debounce < 0) throw new ExpressionEvalError("rose: debounce must be a non-negative finite number");
4142
+ const verdict = evaluateSensorLevelEdge({
4143
+ level: active,
4144
+ changedAt: timestamp,
4145
+ priorLevel: call.priorLevel,
4146
+ startedAtMs: env.startedAtMs,
4147
+ nowMs: env.nowMs,
4148
+ firstSightingFreshnessMs: DEFAULT_FIRST_SIGHTING_FRESHNESS_MS
4149
+ });
4150
+ call.priorLevel = verdict.kind === "rise" ? true : verdict.nextPriorLevel;
4151
+ if (verdict.kind === "none") return env.currentOutput;
4152
+ if (field.acceptedEdgeAt !== null && verdict.timestamp - field.acceptedEdgeAt < debounce) return env.currentOutput;
4153
+ field.acceptedEdgeAt = verdict.timestamp;
4154
+ return verdict.timestamp;
4155
+ }
4156
+ function evaluateCount(input, call, currentOutput) {
4157
+ const currentCount = typeof currentOutput === "number" && Number.isFinite(currentOutput) && currentOutput >= 0 ? currentOutput : 0;
4158
+ if (typeof input !== "number" || !Number.isFinite(input)) return currentCount;
4159
+ if (call.highWater === null) {
4160
+ call.highWater = input;
4161
+ return currentCount;
4162
+ }
4163
+ if (input <= call.highWater) return currentCount;
4164
+ call.highWater = input;
4165
+ return currentCount + 1;
4166
+ }
3962
4167
  var INF = Number.POSITIVE_INFINITY;
3963
4168
  var table = {
3964
4169
  min: {
@@ -4098,6 +4303,20 @@ var table = {
4098
4303
  const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
4099
4304
  return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
4100
4305
  }
4306
+ },
4307
+ rose: {
4308
+ minArgs: 2,
4309
+ maxArgs: 3,
4310
+ apply: () => {
4311
+ throw new ExpressionEvalError("rose: stateful evaluation context is required");
4312
+ }
4313
+ },
4314
+ count: {
4315
+ minArgs: 1,
4316
+ maxArgs: 1,
4317
+ apply: () => {
4318
+ throw new ExpressionEvalError("count: stateful evaluation context is required");
4319
+ }
4101
4320
  }
4102
4321
  };
4103
4322
  /** Frozen, null-prototype builtin table. */
@@ -4551,22 +4770,28 @@ function step(ctx) {
4551
4770
  ctx.steps += 1;
4552
4771
  if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
4553
4772
  }
4554
- function evalNode(node, ctx) {
4773
+ function childPath(path, child) {
4774
+ return `${path}/${child}`;
4775
+ }
4776
+ function evalNode(node, ctx, path) {
4555
4777
  step(ctx);
4556
4778
  switch (node.kind) {
4557
4779
  case "literal": return node.value;
4558
4780
  case "identifier":
4559
4781
  if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
4560
4782
  return ctx.scope[node.name];
4561
- case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
4562
- case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
4783
+ case "unary": return evalUnary(node.op, evalNode(node.operand, ctx, childPath(path, "operand")));
4784
+ case "binary": return evalBinary(node.op, evalNode(node.left, ctx, childPath(path, "left")), evalNode(node.right, ctx, childPath(path, "right")));
4563
4785
  case "logical": {
4564
- const left = evalNode(node.left, ctx);
4565
- if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
4566
- return truthy(left) ? left : evalNode(node.right, ctx);
4786
+ const left = evalNode(node.left, ctx, childPath(path, "left"));
4787
+ if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx, childPath(path, "right")) : left;
4788
+ return truthy(left) ? left : evalNode(node.right, ctx, childPath(path, "right"));
4789
+ }
4790
+ 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"));
4791
+ case "call": {
4792
+ const args = node.args.map((arg, index) => evalNode(arg, ctx, childPath(path, `arg:${index}`)));
4793
+ return invokeCall(node.callee, args, path, ctx);
4567
4794
  }
4568
- case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
4569
- case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
4570
4795
  }
4571
4796
  }
4572
4797
  function evalUnary(op, operand) {
@@ -4597,11 +4822,19 @@ function evalBinary(op, left, right) {
4597
4822
  throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
4598
4823
  }
4599
4824
  }
4600
- function evalCall(callee, args, hooks) {
4825
+ function invokeCall(callee, args, path, ctx) {
4601
4826
  if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
4602
4827
  const builtin = EXPRESSION_BUILTINS[callee];
4603
4828
  if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
4604
- return builtin.apply(args, hooks);
4829
+ if (isStatefulBuiltin(callee)) {
4830
+ if (ctx.invokeStateful === void 0) throw new ExpressionEvalError(`${callee}: stateful evaluation context is required`);
4831
+ return ctx.invokeStateful({
4832
+ name: callee,
4833
+ path,
4834
+ args
4835
+ });
4836
+ }
4837
+ return builtin.apply(args, ctx.hooks);
4605
4838
  }
4606
4839
  /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
4607
4840
  * runtime failure (unknown identifier, type mismatch, non-finite result,
@@ -4611,8 +4844,44 @@ function evaluateAst(node, scope, opts) {
4611
4844
  scope,
4612
4845
  hooks: opts?.hooks ?? EMPTY_HOOKS,
4613
4846
  maxSteps: opts?.maxSteps ?? 4096,
4847
+ ...opts?.invokeStateful === void 0 ? {} : { invokeStateful: opts.invokeStateful },
4614
4848
  steps: 0
4615
- });
4849
+ }, "root");
4850
+ }
4851
+ function createStatefulExpressionMemory() {
4852
+ return {
4853
+ calls: /* @__PURE__ */ new Map(),
4854
+ acceptedEdgeAt: null
4855
+ };
4856
+ }
4857
+ function cloneStatefulExpressionMemory(memory) {
4858
+ return {
4859
+ calls: new Map([...memory.calls].map(([path, call]) => [path, { ...call }])),
4860
+ acceptedEdgeAt: memory.acceptedEdgeAt
4861
+ };
4862
+ }
4863
+ function evaluateStatefulExpressionCall(invocation, memory, env) {
4864
+ const existing = memory.calls.get(invocation.path);
4865
+ switch (invocation.name) {
4866
+ case "rose": {
4867
+ const call = existing?.kind === "rose" ? existing : { kind: "rose" };
4868
+ memory.calls.set(invocation.path, call);
4869
+ const currentOutput = env.currentOutput === null || typeof env.currentOutput === "number" && Number.isFinite(env.currentOutput) ? env.currentOutput : null;
4870
+ return evaluateRose(invocation.args[0], invocation.args[1], invocation.args[2], call, memory, {
4871
+ currentOutput,
4872
+ startedAtMs: env.startedAtMs,
4873
+ nowMs: env.nowMs
4874
+ });
4875
+ }
4876
+ case "count": {
4877
+ const call = existing?.kind === "count" ? existing : {
4878
+ kind: "count",
4879
+ highWater: null
4880
+ };
4881
+ memory.calls.set(invocation.path, call);
4882
+ return evaluateCount(invocation.args[0], call, env.currentOutput);
4883
+ }
4884
+ }
4616
4885
  }
4617
4886
  /**
4618
4887
  * Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
@@ -4653,9 +4922,10 @@ function validateExpressionSource(src) {
4653
4922
  /**
4654
4923
  * Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
4655
4924
  * plus the injected `now` (supplied by the caller for determinism and
4656
- * testability), compiles via the LRU, and evaluates. Any failure (parse or
4657
- * eval) returns `{ ok: false }` — the caller treats that as "skip this
4658
- * derivation", never as a throw that takes the pass down.
4925
+ * testability), compiles via the LRU, and evaluates. Stateful calls are
4926
+ * delegated through `opts.invokeStateful` with their stable AST path. Any
4927
+ * failure (parse or eval) returns `{ ok: false }` — the caller treats that as
4928
+ * "skip this derivation", never as a throw that takes the pass down.
4659
4929
  */
4660
4930
  function evaluateExpressionSource(expr, bindingValues, now, opts) {
4661
4931
  const compiled = compileExpressionSafe(expr);
@@ -4787,6 +5057,7 @@ var CompositionItemEntrySchema = zod.z.object({
4787
5057
  var CompositionFieldsFeatureSchema = zod.z.object({
4788
5058
  kind: zod.z.literal("fields"),
4789
5059
  cap: zod.z.string().min(1),
5060
+ enabled: zod.z.boolean().optional(),
4790
5061
  fields: zod.z.record(zod.z.string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
4791
5062
  /**
4792
5063
  * Items of the cap's item array, keyed by item key (D663). Naming `items`
@@ -4800,9 +5071,14 @@ var CompositionFieldsFeatureSchema = zod.z.object({
4800
5071
  var CompositionPassthroughFeatureSchema = zod.z.object({
4801
5072
  kind: zod.z.literal("passthrough"),
4802
5073
  cap: zod.z.string().min(1),
5074
+ enabled: zod.z.boolean().optional(),
4803
5075
  source: CompositionSourceRefSchema
4804
5076
  });
4805
5077
  var CompositionFeatureSchema = zod.z.discriminatedUnion("kind", [CompositionFieldsFeatureSchema, CompositionPassthroughFeatureSchema]);
5078
+ /** Only an explicit `false` disables a feature; omitted preserves the existing wire default. */
5079
+ function compositionFeatureEnabled(feature) {
5080
+ return feature.enabled !== false;
5081
+ }
4806
5082
  /**
4807
5083
  * A NEW device. It has no `name` of its own: the block's name IS the device's
4808
5084
  * name (2026-08-08). A second copy here would be a second writer of one string (D62).
@@ -11009,6 +11285,190 @@ function procedureAuthKey(auth) {
11009
11285
  function kebabToCamel(s) {
11010
11286
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
11011
11287
  }
11288
+ /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
11289
+ * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
11290
+ function unwrapSchema(schema) {
11291
+ let s = schema;
11292
+ while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
11293
+ return s;
11294
+ }
11295
+ function leafKind(schema) {
11296
+ const s = unwrapSchema(schema);
11297
+ if (s instanceof zod.z.ZodEnum) return {
11298
+ kind: "enum",
11299
+ enumValues: s.options
11300
+ };
11301
+ if (s instanceof zod.z.ZodNumber) return { kind: "number" };
11302
+ if (s instanceof zod.z.ZodString) return { kind: "string" };
11303
+ if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
11304
+ return null;
11305
+ }
11306
+ /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
11307
+ * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
11308
+ * so fields with null live values are still offered. Arrays / unknown shapes
11309
+ * are skipped — never throws. */
11310
+ function enumerateSchemaFields(schema, prefix = "") {
11311
+ const root = unwrapSchema(schema);
11312
+ if (!(root instanceof zod.z.ZodObject)) return [];
11313
+ const out = [];
11314
+ const shape = root.shape;
11315
+ for (const [key, field] of Object.entries(shape)) {
11316
+ const path = prefix ? `${prefix}.${key}` : key;
11317
+ const inner = unwrapSchema(field);
11318
+ if (inner instanceof zod.z.ZodObject) {
11319
+ out.push(...enumerateSchemaFields(inner, path));
11320
+ continue;
11321
+ }
11322
+ const leaf = leafKind(field);
11323
+ if (leaf) out.push({
11324
+ path,
11325
+ kind: leaf.kind,
11326
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
11327
+ });
11328
+ }
11329
+ return out;
11330
+ }
11331
+ /** Enumerate the per-item wireable fields of an item-array cap (see
11332
+ * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
11333
+ * `keyField` (the key comes from the link's `itemKey`, never from a wired
11334
+ * source), each tagged `item: true` so the authoring UI collects an
11335
+ * `itemKey` alongside the field. Never throws. */
11336
+ function enumerateItemArrayFields(itemArray) {
11337
+ return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
11338
+ ...f,
11339
+ item: true
11340
+ }));
11341
+ }
11342
+ /**
11343
+ * What a capability's state field IS: kind, nullability, whether required. The
11344
+ * validator needs nullability, which `enumerateSchemaFields` does not report,
11345
+ * and that decides a field's unavailable mode (D393).
11346
+ */
11347
+ function fieldAt(schema, path) {
11348
+ let current = schema;
11349
+ for (const segment of path.split(".")) {
11350
+ const container = unwrapSchema(current);
11351
+ if (!(container instanceof zod.z.ZodObject)) return null;
11352
+ const next = container.shape[segment];
11353
+ if (!(next instanceof zod.z.ZodType)) return null;
11354
+ current = next;
11355
+ }
11356
+ return current;
11357
+ }
11358
+ function describeStateField(schema, path) {
11359
+ const leaf = enumerateSchemaFields(schema).find((f) => f.path === path);
11360
+ if (leaf === void 0) return null;
11361
+ const field = fieldAt(schema, path);
11362
+ if (field === null) return null;
11363
+ return {
11364
+ path,
11365
+ kind: leaf.kind,
11366
+ nullable: field.safeParse(null).success,
11367
+ required: !field.safeParse(void 0).success,
11368
+ ...leaf.enumValues === void 0 ? {} : { enumValues: leaf.enumValues }
11369
+ };
11370
+ }
11371
+ function describeTopLevelStateFields(schema) {
11372
+ return new Map(enumerateSchemaFields(schema).filter((f) => !f.path.includes(".")).flatMap((f) => {
11373
+ const described = describeStateField(schema, f.path);
11374
+ return described === null ? [] : [[f.path, described]];
11375
+ }));
11376
+ }
11377
+ /** Top-level keys a value MUST carry: those whose schema refuses `undefined`. */
11378
+ function requiredTopLevelKeys(schema) {
11379
+ const root = unwrapSchema(schema);
11380
+ if (!(root instanceof zod.z.ZodObject)) return [];
11381
+ return Object.entries(root.shape).flatMap(([key, field]) => field instanceof zod.z.ZodType && !field.safeParse(void 0).success ? [key] : []);
11382
+ }
11383
+ /**
11384
+ * The proper prefixes of `path` whose schema accepts `undefined`: a required
11385
+ * leaf under one of them is required only once something writes that parent
11386
+ * (`remaining.unit` is required only when `remaining` is present).
11387
+ */
11388
+ function optionalAncestors(schema, path) {
11389
+ const segments = path.split(".");
11390
+ return segments.slice(0, -1).flatMap((_, i) => {
11391
+ const prefix = segments.slice(0, i + 1).join(".");
11392
+ const field = fieldAt(schema, prefix);
11393
+ return field !== null && field.safeParse(void 0).success ? [prefix] : [];
11394
+ });
11395
+ }
11396
+ var EVENT_CATEGORIES = { "doorbell.onPressed": EventCategory.DoorbellOnPressed };
11397
+ var CompositionEventPlanSchema = zod.z.object({
11398
+ featureIndex: zod.z.number().int().nonnegative(),
11399
+ cap: zod.z.string(),
11400
+ eventName: zod.z.string(),
11401
+ category: zod.z.enum(EventCategory),
11402
+ stateField: zod.z.string(),
11403
+ fieldKey: zod.z.string()
11404
+ });
11405
+ function eventProblem(code, path, message) {
11406
+ return {
11407
+ code,
11408
+ path,
11409
+ message
11410
+ };
11411
+ }
11412
+ function payloadReason(error) {
11413
+ return error.issues.map((issue) => `${issue.path.length === 0 ? "payload" : issue.path.join(".")}: ${issue.message}`).join("; ");
11414
+ }
11415
+ /**
11416
+ * Plan one capability's state-derived events after its feature mode and fields
11417
+ * are known. A native replacement never becomes a second event authority.
11418
+ */
11419
+ function planCompositionEvents(def, feature, fields) {
11420
+ if (feature.mode === "replace") return {
11421
+ problems: [],
11422
+ events: []
11423
+ };
11424
+ const problems = [];
11425
+ const events = [];
11426
+ for (const [eventName, declaration] of Object.entries(def.events ?? {})) {
11427
+ const fromState = declaration.fromState;
11428
+ if (fromState === void 0) continue;
11429
+ const statePath = `features[${feature.featureIndex}].fields.${fromState.field}`;
11430
+ const state = def.runtimeState ? describeStateField(def.runtimeState, fromState.field) : null;
11431
+ if (state === null) {
11432
+ problems.push(eventProblem("unknown-field", statePath, `\`${def.name}.${eventName}\` derives from missing state field \`${fromState.field}\``));
11433
+ continue;
11434
+ }
11435
+ if (state.kind !== "number") {
11436
+ problems.push(eventProblem("kind-mismatch", statePath, `\`${def.name}.${eventName}\` needs a numeric state field; \`${fromState.field}\` is ${state.kind}`));
11437
+ continue;
11438
+ }
11439
+ const eventKey = `${def.name}.${eventName}`;
11440
+ const category = EVENT_CATEGORIES[eventKey];
11441
+ if (category === void 0) {
11442
+ problems.push(eventProblem("not-composable", `features[${feature.featureIndex}].cap`, `\`${eventKey}\` has no explicit composition event-category mapping`));
11443
+ continue;
11444
+ }
11445
+ const payload = declaration.data.safeParse({
11446
+ deviceId: 1,
11447
+ timestamp: 1
11448
+ });
11449
+ if (!payload.success) {
11450
+ problems.push(eventProblem("not-composable", `features[${feature.featureIndex}].cap`, `\`${eventKey}\` cannot be derived from { deviceId, timestamp }: ${payloadReason(payload.error)}`));
11451
+ continue;
11452
+ }
11453
+ const field = fields.find((candidate) => candidate.featureIndex === feature.featureIndex && candidate.cap === def.name && candidate.path === fromState.field && candidate.item === void 0);
11454
+ if (field === void 0) {
11455
+ problems.push(eventProblem("unknown-field", statePath, `\`${eventKey}\` derives from \`${fromState.field}\`, but that field is not composed`));
11456
+ continue;
11457
+ }
11458
+ events.push({
11459
+ featureIndex: feature.featureIndex,
11460
+ cap: def.name,
11461
+ eventName,
11462
+ category,
11463
+ stateField: fromState.field,
11464
+ fieldKey: `${field.cap}.${field.path}`
11465
+ });
11466
+ }
11467
+ return {
11468
+ problems,
11469
+ events
11470
+ };
11471
+ }
11012
11472
  /**
11013
11473
  * What the validator says about a composition and what the composer says about
11014
11474
  * a running one: the wire shapes the API returns (D659).
@@ -11063,12 +11523,14 @@ var CompositionUnavailableModeSchema = zod.z.enum([
11063
11523
  ]);
11064
11524
  /**
11065
11525
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
11066
- * field of its cap changes value · `constant` reads nothing, computed once.
11526
+ * field of its cap changes value · `constant` reads nothing, computed once ·
11527
+ * `accumulator` advances from the target's seed/current output.
11067
11528
  */
11068
11529
  var CompositionFieldRoleSchema = zod.z.enum([
11069
11530
  "derived",
11070
11531
  "clock",
11071
- "constant"
11532
+ "constant",
11533
+ "accumulator"
11072
11534
  ]);
11073
11535
  /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
11074
11536
  var CompositionFieldItemSchema = zod.z.object({
@@ -11109,6 +11571,7 @@ var CompositionFeatureModeSchema = zod.z.enum([
11109
11571
  "replace"
11110
11572
  ]);
11111
11573
  var CompositionFeaturePlanSchema = zod.z.object({
11574
+ /** Index in the stored feature array; disabled features are omitted rather than re-indexed. */
11112
11575
  featureIndex: zod.z.number().int().nonnegative(),
11113
11576
  cap: zod.z.string(),
11114
11577
  mode: CompositionFeatureModeSchema
@@ -11117,7 +11580,8 @@ var CompositionValidationSchema = zod.z.object({
11117
11580
  ok: zod.z.boolean(),
11118
11581
  problems: zod.z.array(CompositionProblemSchema),
11119
11582
  fields: zod.z.array(CompositionFieldPlanSchema),
11120
- features: zod.z.array(CompositionFeaturePlanSchema)
11583
+ features: zod.z.array(CompositionFeaturePlanSchema),
11584
+ events: zod.z.array(CompositionEventPlanSchema)
11121
11585
  });
11122
11586
  /**
11123
11587
  * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
@@ -29024,148 +29488,6 @@ var videoclipsCapability = {
29024
29488
  }
29025
29489
  };
29026
29490
  /**
29027
- * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
29028
- * valutatore puro del suo FRONTE.
29029
- *
29030
- * Viveva dentro il builtin virtual-doorbell
29031
- * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
29032
- * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
29033
- * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
29034
- * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
29035
- * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
29036
- * SPOSTA qui e il doorbell la ri-esporta.
29037
- *
29038
- * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
29039
- * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
29040
- * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
29041
- * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
29042
- */
29043
- /**
29044
- * Known binary / switch source caps → the boolean slice field whose
29045
- * false→true rise counts as ACTIVE. Every entry is "fire on active".
29046
- * Sensors whose "active" reading is not a plain boolean (presence's string
29047
- * state, connectivity's connected flag) are deliberately excluded — a
29048
- * reconnect is not a doorbell press, and it is not a recording either.
29049
- */
29050
- var SOURCE_CAP_ACTIVE_FIELD = {
29051
- contact: "entryOpen",
29052
- binary: "on",
29053
- switch: "on",
29054
- motion: "detected",
29055
- flood: "flooded",
29056
- gas: "detected",
29057
- smoke: "detected",
29058
- "carbon-monoxide": "detected",
29059
- vibration: "detected",
29060
- tamper: "tampered"
29061
- };
29062
- /**
29063
- * The same caps → the slice field carrying the ms-epoch timestamp of the
29064
- * last transition. Every source cap MUST appear here (guarded by a spec):
29065
- * without a transition timestamp the evaluator cannot tell a genuine rise
29066
- * from a boot-time hydration when the FIRST slice it ever sees is already
29067
- * active, and errs towards silence — swallowing the rise.
29068
- *
29069
- * These timestamps are UPSTREAM ones, not ingest ones: the Home Assistant
29070
- * provider derives them from `state.last_changed`, so they survive our own
29071
- * restarts and correctly read as "hours ago" for a state that has been
29072
- * active for hours. `motion` names its rise timestamp `lastDetectedAt`.
29073
- */
29074
- var SOURCE_CAP_CHANGED_AT_FIELD = {
29075
- contact: "lastChangedAt",
29076
- binary: "lastChangedAt",
29077
- switch: "lastChangedAt",
29078
- motion: "lastDetectedAt",
29079
- flood: "lastChangedAt",
29080
- gas: "lastChangedAt",
29081
- smoke: "lastChangedAt",
29082
- "carbon-monoxide": "lastChangedAt",
29083
- vibration: "lastChangedAt",
29084
- tamper: "lastChangedAt"
29085
- };
29086
- /** Cap names whose presence in a device's bindings qualify it as a source. */
29087
- var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
29088
- /** True when a cap is a recognised binary/switch source. */
29089
- function isSourceCap(capName) {
29090
- return Object.prototype.hasOwnProperty.call(SOURCE_CAP_ACTIVE_FIELD, capName);
29091
- }
29092
- /** Extract the "active" boolean a source cap's slice carries, or null when
29093
- * the cap is unknown or the field is missing / non-boolean. */
29094
- function sliceActiveValue(capName, slice) {
29095
- const field = SOURCE_CAP_ACTIVE_FIELD[capName];
29096
- if (field === void 0) return null;
29097
- const raw = slice[field];
29098
- return typeof raw === "boolean" ? raw : null;
29099
- }
29100
- /** Ms-epoch transition timestamp a source cap's slice carries, or null when
29101
- * it is absent, non-numeric or the zero "never observed" sentinel. */
29102
- function sliceChangedAt(capName, slice) {
29103
- const field = SOURCE_CAP_CHANGED_AT_FIELD[capName];
29104
- if (field === void 0) return null;
29105
- const raw = slice[field];
29106
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return null;
29107
- return raw;
29108
- }
29109
- /**
29110
- * How recent a source's own transition timestamp must be for a FIRST
29111
- * sighting that is already active to count as a genuine rise rather than a
29112
- * hydration of long-standing state.
29113
- */
29114
- var DEFAULT_FIRST_SIGHTING_FRESHNESS_MS = 3e4;
29115
- /**
29116
- * IL fronte. Puro: nessun orologio proprio, nessuna memoria — il chiamante
29117
- * porta `prior`, `nowMs` e il proprio `startedAtMs`.
29118
- *
29119
- * Il caso della PRIMA slice già attiva è trattato esplicitamente e vale come
29120
- * fronte solo se il timestamp UPSTREAM della transizione è posteriore a
29121
- * `startedAtMs` **e** entro `firstSightingFreshnessMs`. Entrambe le metà
29122
- * servono: la sola freschezza scatterebbe su un'idratazione al boot di uno
29123
- * stato flippato pochi secondi prima del riavvio, e il solo "dopo che abbiamo
29124
- * iniziato" scatterebbe, su un processo di lunga vita, per una sorgente
29125
- * adottata oggi il cui stato è cambiato ieri. Il caso ambiguo ERRA VERSO IL
29126
- * SILENZIO e lo dichiara (`baseline-seeded-stale-active`).
29127
- */
29128
- function evaluateSensorEdge(input) {
29129
- const value = sliceActiveValue(input.capName, input.slice);
29130
- if (value === null) return {
29131
- edge: "none",
29132
- value: null,
29133
- reason: isSourceCap(input.capName) ? "non-boolean-value" : "unknown-cap"
29134
- };
29135
- if (input.prior === void 0) {
29136
- if (!value) return {
29137
- edge: "none",
29138
- value,
29139
- reason: "baseline-seeded-inactive"
29140
- };
29141
- const changedAt = sliceChangedAt(input.capName, input.slice);
29142
- const floor = Math.max(input.startedAtMs, input.nowMs - input.firstSightingFreshnessMs);
29143
- if (changedAt === null || changedAt < floor) return {
29144
- edge: "none",
29145
- value,
29146
- reason: "baseline-seeded-stale-active"
29147
- };
29148
- return {
29149
- edge: "rising",
29150
- value: true
29151
- };
29152
- }
29153
- if (input.prior === value) return {
29154
- edge: "none",
29155
- value,
29156
- reason: "no-change"
29157
- };
29158
- if (!value) return {
29159
- edge: "none",
29160
- value,
29161
- reason: "falling-edge"
29162
- };
29163
- return {
29164
- edge: "rising",
29165
- value: true
29166
- };
29167
- }
29168
- /**
29169
29491
  * Doorbell button cap. Two kinds of providers coexist behind this cap
29170
29492
  * name (same pattern as `snapshot`):
29171
29493
  *
@@ -29238,7 +29560,13 @@ var doorbellCapability = {
29238
29560
  * push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
29239
29561
  * release/duration — it's a pulse.
29240
29562
  */
29241
- onPressed: { data: DoorbellPressEventSchema } },
29563
+ onPressed: {
29564
+ data: DoorbellPressEventSchema,
29565
+ fromState: {
29566
+ field: "lastPressedAt",
29567
+ on: "advance"
29568
+ }
29569
+ } },
29242
29570
  status: {
29243
29571
  schema: DoorbellStatusSchema,
29244
29572
  kind: "push"
@@ -38263,12 +38591,21 @@ var RecordingStorageUsageSchema = zod.z.object({
38263
38591
  */
38264
38592
  devicesRead: zod.z.enum(["archive", "unavailable"]).optional(),
38265
38593
  /**
38594
+ * Is the `archive` answer WHOLE (D674): the hour ledger loaded without
38595
+ * hitting its row cap, and at least one location resolved? Only then may a
38596
+ * camera ABSENT from `devices[]` be read as "no footage" — and only then is a
38597
+ * row's `oldestMs` the archive's oldest rather than the oldest of a fraction.
38598
+ * OPTIONAL: a recorder predating D674 omits it, and a consumer must read the
38599
+ * absence as `false`. A retention floor is a delete; it never guesses.
38600
+ */
38601
+ complete: zod.z.boolean().optional(),
38602
+ /**
38266
38603
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
38267
- * of the payload without it: the hub serves this method through a BAKED
38268
- * router, so a hub whose framework train predates a change to this array
38269
- * strips it out of an answer a newer recorder happily produced. A UI that
38270
- * dereferences it unconditionally crashes the whole Storage page over a
38271
- * missing disk bar.
38604
+ * of the payload without it: a recorder that could not build it omits it. A
38605
+ * UI that dereferences it unconditionally crashes the whole Storage page over
38606
+ * a missing disk bar. (The hub's cap routers pass output through unvalidated
38607
+ * since 2026-07-11 — `cap-router-builder.ts` — so a newer recorder's fields
38608
+ * are not stripped by an older hub.)
38272
38609
  */
38273
38610
  locations: zod.z.array(RecordingLocationUsageSchema).optional()
38274
38611
  });
@@ -42640,7 +42977,7 @@ function createDeviceExtensionProvider(options) {
42640
42977
  var CompositionGraphError = class extends Error {};
42641
42978
  /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
42642
42979
  function writtenFields(peer) {
42643
- return new Set(peer.composition.features.flatMap((feature) => feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
42980
+ return new Set(peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
42644
42981
  }
42645
42982
  /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
42646
42983
  function readsComposedValue(peer, written, read) {
@@ -42650,7 +42987,7 @@ function readsComposedValue(peer, written, read) {
42650
42987
  }
42651
42988
  function peerEdges(peer) {
42652
42989
  const written = writtenFields(peer);
42653
- return peer.composition.features.flatMap((feature) => feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
42990
+ return peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
42654
42991
  source: peer.target,
42655
42992
  cap: feature.cap,
42656
42993
  fieldPath
@@ -42755,114 +43092,6 @@ function findCandidateCycles(candidate, stored) {
42755
43092
  }];
42756
43093
  });
42757
43094
  }
42758
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
42759
- * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
42760
- function unwrapSchema(schema) {
42761
- let s = schema;
42762
- while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
42763
- return s;
42764
- }
42765
- function leafKind(schema) {
42766
- const s = unwrapSchema(schema);
42767
- if (s instanceof zod.z.ZodEnum) return {
42768
- kind: "enum",
42769
- enumValues: s.options
42770
- };
42771
- if (s instanceof zod.z.ZodNumber) return { kind: "number" };
42772
- if (s instanceof zod.z.ZodString) return { kind: "string" };
42773
- if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
42774
- return null;
42775
- }
42776
- /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
42777
- * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
42778
- * so fields with null live values are still offered. Arrays / unknown shapes
42779
- * are skipped — never throws. */
42780
- function enumerateSchemaFields(schema, prefix = "") {
42781
- const root = unwrapSchema(schema);
42782
- if (!(root instanceof zod.z.ZodObject)) return [];
42783
- const out = [];
42784
- const shape = root.shape;
42785
- for (const [key, field] of Object.entries(shape)) {
42786
- const path = prefix ? `${prefix}.${key}` : key;
42787
- const inner = unwrapSchema(field);
42788
- if (inner instanceof zod.z.ZodObject) {
42789
- out.push(...enumerateSchemaFields(inner, path));
42790
- continue;
42791
- }
42792
- const leaf = leafKind(field);
42793
- if (leaf) out.push({
42794
- path,
42795
- kind: leaf.kind,
42796
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
42797
- });
42798
- }
42799
- return out;
42800
- }
42801
- /** Enumerate the per-item wireable fields of an item-array cap (see
42802
- * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
42803
- * `keyField` (the key comes from the link's `itemKey`, never from a wired
42804
- * source), each tagged `item: true` so the authoring UI collects an
42805
- * `itemKey` alongside the field. Never throws. */
42806
- function enumerateItemArrayFields(itemArray) {
42807
- return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
42808
- ...f,
42809
- item: true
42810
- }));
42811
- }
42812
- /**
42813
- * What a capability's state field IS: kind, nullability, whether required. The
42814
- * validator needs nullability, which `enumerateSchemaFields` does not report,
42815
- * and that decides a field's unavailable mode (D393).
42816
- */
42817
- function fieldAt(schema, path) {
42818
- let current = schema;
42819
- for (const segment of path.split(".")) {
42820
- const container = unwrapSchema(current);
42821
- if (!(container instanceof zod.z.ZodObject)) return null;
42822
- const next = container.shape[segment];
42823
- if (!(next instanceof zod.z.ZodType)) return null;
42824
- current = next;
42825
- }
42826
- return current;
42827
- }
42828
- function describeStateField(schema, path) {
42829
- const leaf = enumerateSchemaFields(schema).find((f) => f.path === path);
42830
- if (leaf === void 0) return null;
42831
- const field = fieldAt(schema, path);
42832
- if (field === null) return null;
42833
- return {
42834
- path,
42835
- kind: leaf.kind,
42836
- nullable: field.safeParse(null).success,
42837
- required: !field.safeParse(void 0).success,
42838
- ...leaf.enumValues === void 0 ? {} : { enumValues: leaf.enumValues }
42839
- };
42840
- }
42841
- function describeTopLevelStateFields(schema) {
42842
- return new Map(enumerateSchemaFields(schema).filter((f) => !f.path.includes(".")).flatMap((f) => {
42843
- const described = describeStateField(schema, f.path);
42844
- return described === null ? [] : [[f.path, described]];
42845
- }));
42846
- }
42847
- /** Top-level keys a value MUST carry: those whose schema refuses `undefined`. */
42848
- function requiredTopLevelKeys(schema) {
42849
- const root = unwrapSchema(schema);
42850
- if (!(root instanceof zod.z.ZodObject)) return [];
42851
- return Object.entries(root.shape).flatMap(([key, field]) => field instanceof zod.z.ZodType && !field.safeParse(void 0).success ? [key] : []);
42852
- }
42853
- /**
42854
- * The proper prefixes of `path` whose schema accepts `undefined`: a required
42855
- * leaf under one of them is required only once something writes that parent
42856
- * (`remaining.unit` is required only when `remaining` is present).
42857
- */
42858
- function optionalAncestors(schema, path) {
42859
- const segments = path.split(".");
42860
- return segments.slice(0, -1).flatMap((_, i) => {
42861
- const prefix = segments.slice(0, i + 1).join(".");
42862
- const field = fieldAt(schema, prefix);
42863
- return field !== null && field.safeParse(void 0).success ? [prefix] : [];
42864
- });
42865
- }
42866
43095
  function problem(code, path, message) {
42867
43096
  return {
42868
43097
  code,
@@ -42874,7 +43103,8 @@ function refused(...problems) {
42874
43103
  return {
42875
43104
  problems,
42876
43105
  fields: [],
42877
- features: []
43106
+ features: [],
43107
+ events: []
42878
43108
  };
42879
43109
  }
42880
43110
  /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
@@ -42916,7 +43146,12 @@ var BUILTIN_RESULT_KINDS = Object.freeze({
42916
43146
  number: {
42917
43147
  kind: "fixed",
42918
43148
  result: ["number", "null"]
42919
- }
43149
+ },
43150
+ rose: {
43151
+ kind: "fixed",
43152
+ result: ["number", "null"]
43153
+ },
43154
+ count: NUMBER
42920
43155
  });
42921
43156
  var KindInferenceError = class extends Error {};
42922
43157
  function kindOfValue(value) {
@@ -42975,6 +43210,21 @@ function kindsOf(node, bindings) {
42975
43210
  case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
42976
43211
  }
42977
43212
  }
43213
+ function nodeContainsCall(node, callee) {
43214
+ switch (node.kind) {
43215
+ case "literal":
43216
+ case "identifier": return false;
43217
+ case "unary": return nodeContainsCall(node.operand, callee);
43218
+ case "binary":
43219
+ case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
43220
+ case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
43221
+ case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
43222
+ }
43223
+ }
43224
+ function expressionContainsBuiltinCall(expr, callee) {
43225
+ const compiled = compileExpressionSafe(expr);
43226
+ return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
43227
+ }
42978
43228
  function inferExpressionKinds(input) {
42979
43229
  const compiled = compileExpressionSafe(input.expr);
42980
43230
  if (!compiled.ok) return {
@@ -43095,6 +43345,7 @@ function enumLiteralProblem(cap, target, source, at) {
43095
43345
  }
43096
43346
  function roleOf(source) {
43097
43347
  if (source.kind !== "expression") return "derived";
43348
+ if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
43098
43349
  if (fieldReads(source).length > 0) return "derived";
43099
43350
  const compiled = compileExpressionSafe(source.expr);
43100
43351
  return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
@@ -43106,13 +43357,13 @@ function roleOf(source) {
43106
43357
  * field that READS a source never gets `offline`.
43107
43358
  */
43108
43359
  function unavailableFor(tf, role, mode, cap, at) {
43109
- if (mode === "new" || role !== "derived") return {
43360
+ if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
43110
43361
  ok: true,
43111
- unavailable: tf.nullable ? "null" : "offline"
43362
+ unavailable: "release"
43112
43363
  };
43113
- if (mode === "replace") return {
43364
+ if (mode === "new" || role !== "derived") return {
43114
43365
  ok: true,
43115
- unavailable: "release"
43366
+ unavailable: tf.nullable ? "null" : "offline"
43116
43367
  };
43117
43368
  if (tf.nullable) return {
43118
43369
  ok: true,
@@ -43185,7 +43436,8 @@ function planItemField(ctx, leaf) {
43185
43436
  return {
43186
43437
  problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
43187
43438
  fields: [plan],
43188
- features: []
43439
+ features: [],
43440
+ events: []
43189
43441
  };
43190
43442
  }
43191
43443
  function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
@@ -43204,7 +43456,8 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
43204
43456
  const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
43205
43457
  problems: missingLeaves(ctx, item, `${at}.${key}`),
43206
43458
  fields: [],
43207
- features: []
43459
+ features: [],
43460
+ events: []
43208
43461
  }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
43209
43462
  key,
43210
43463
  label: item.label,
@@ -43214,7 +43467,8 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
43214
43467
  return {
43215
43468
  problems: results.flatMap((r) => r.problems),
43216
43469
  fields: results.flatMap((r) => r.fields),
43217
- features: []
43470
+ features: [],
43471
+ events: []
43218
43472
  };
43219
43473
  }
43220
43474
  function describeRead(read) {
@@ -43238,7 +43492,7 @@ function fromReading(reading, read) {
43238
43492
  deadline: null
43239
43493
  };
43240
43494
  }
43241
- function expressionOutcome(source, read, now) {
43495
+ function expressionOutcome(source, read, now, stateful) {
43242
43496
  const values = {};
43243
43497
  const staleReasons = [];
43244
43498
  let fatalUnavailable = null;
@@ -43267,10 +43521,31 @@ function expressionOutcome(source, read, now) {
43267
43521
  values[name] = reading.value;
43268
43522
  if (reading.state === "stale") staleReasons.push(`\`${name}\`: ${reading.reason}`);
43269
43523
  }
43270
- if (fatalUnavailable !== null) return fatalUnavailable;
43271
- if (nonFatalUnavailable !== null) return nonFatalUnavailable;
43524
+ const unavailable = fatalUnavailable ?? nonFatalUnavailable;
43525
+ if (unavailable !== null) {
43526
+ if (stateful !== void 0 && stateful.currentOutput !== void 0 && expressionContainsBuiltinCall(source.expr, "count")) return {
43527
+ state: "stale",
43528
+ value: evaluateCount(null, {
43529
+ kind: "count",
43530
+ highWater: null
43531
+ }, stateful.currentOutput),
43532
+ reason: `${unavailable.reason}; holding the last accumulator value`,
43533
+ deadline: null
43534
+ };
43535
+ return unavailable;
43536
+ }
43272
43537
  const deadlines = [];
43273
- const result = evaluateExpressionSource(source.expr, values, now, { hooks: { noteDeadline: (at) => deadlines.push(at) } });
43538
+ const result = evaluateExpressionSource(source.expr, values, now, {
43539
+ hooks: { noteDeadline: (at) => deadlines.push(at) },
43540
+ ...stateful === void 0 ? {} : { invokeStateful: (invocation) => evaluateStatefulExpressionCall(stateful.callPathPrefix === void 0 ? invocation : {
43541
+ ...invocation,
43542
+ path: `${stateful.callPathPrefix}@${invocation.path}`
43543
+ }, stateful.expressionMemory, {
43544
+ currentOutput: stateful.currentOutput,
43545
+ startedAtMs: stateful.startedAtMs,
43546
+ nowMs: now
43547
+ }) }
43548
+ });
43274
43549
  if (!result.ok) return {
43275
43550
  state: "unavailable",
43276
43551
  value: null,
@@ -43289,7 +43564,7 @@ function evaluateCompositionField(input) {
43289
43564
  const { source } = input;
43290
43565
  switch (source.kind) {
43291
43566
  case "from": return fromReading(input.read(source), source);
43292
- case "expression": return expressionOutcome(source, input.read, input.now);
43567
+ case "expression": return expressionOutcome(source, input.read, input.now, input.stateful);
43293
43568
  case "code": return {
43294
43569
  state: "unavailable",
43295
43570
  value: null,
@@ -43367,7 +43642,8 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
43367
43642
  return {
43368
43643
  problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
43369
43644
  fields: [plan],
43370
- features: []
43645
+ features: [],
43646
+ events: []
43371
43647
  };
43372
43648
  }
43373
43649
  function planFeature(feature, featureIndex, target, lookupCap) {
@@ -43383,14 +43659,22 @@ function planFeature(feature, featureIndex, target, lookupCap) {
43383
43659
  }, target.mode, lookupCap));
43384
43660
  const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
43385
43661
  const all = [...perField, ...items];
43662
+ const fields = all.flatMap((result) => result.fields);
43663
+ const featurePlan = {
43664
+ featureIndex,
43665
+ cap: cap.def.name,
43666
+ mode: target.mode
43667
+ };
43668
+ const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
43386
43669
  return {
43387
- problems: [...capProblems, ...all.flatMap((r) => r.problems)],
43388
- fields: all.flatMap((r) => r.fields),
43389
- features: [{
43390
- featureIndex,
43391
- cap: cap.def.name,
43392
- mode: target.mode
43393
- }]
43670
+ problems: [
43671
+ ...capProblems,
43672
+ ...all.flatMap((r) => r.problems),
43673
+ ...eventPlan.problems
43674
+ ],
43675
+ fields,
43676
+ features: [featurePlan],
43677
+ events: eventPlan.events
43394
43678
  };
43395
43679
  }
43396
43680
  /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
@@ -43432,17 +43716,23 @@ function planComposition(composition, lookupCap, target) {
43432
43716
  const planTarget = planTargetOf(composition, target);
43433
43717
  if ("code" in planTarget) return refused(planTarget);
43434
43718
  const results = composition.features.map((feature, index) => {
43435
- if (feature.kind === "passthrough") return refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`));
43436
- return planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
43719
+ const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
43720
+ return compositionFeatureEnabled(feature) ? result : {
43721
+ problems: result.problems,
43722
+ fields: [],
43723
+ features: [],
43724
+ events: []
43725
+ };
43437
43726
  });
43438
43727
  return {
43439
43728
  problems: results.flatMap((r) => r.problems),
43440
43729
  fields: results.flatMap((r) => r.fields),
43441
- features: results.flatMap((r) => r.features)
43730
+ features: results.flatMap((r) => r.features),
43731
+ events: results.flatMap((r) => r.events)
43442
43732
  };
43443
43733
  }
43444
43734
  function compositionSourceRefs(composition) {
43445
- const refs = composition.features.flatMap((f) => f.kind === "fields" ? featureSources(f).flatMap(fieldReads).map((r) => r.source) : []);
43735
+ const refs = composition.features.flatMap((f) => compositionFeatureEnabled(f) && f.kind === "fields" ? featureSources(f).flatMap(fieldReads).map((r) => r.source) : []);
43446
43736
  return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
43447
43737
  }
43448
43738
  /**
@@ -43577,7 +43867,8 @@ function validateComposition(composition, ctx) {
43577
43867
  ok: problems.length === 0,
43578
43868
  problems,
43579
43869
  fields: [...plan.fields],
43580
- features: [...plan.features]
43870
+ features: [...plan.features],
43871
+ events: [...plan.events]
43581
43872
  };
43582
43873
  }
43583
43874
  /**
@@ -57364,6 +57655,18 @@ Object.defineProperty(exports, "SOURCE_CAPS", {
57364
57655
  return SOURCE_CAPS;
57365
57656
  }
57366
57657
  });
57658
+ Object.defineProperty(exports, "SOURCE_CAP_ACTIVE_FIELD", {
57659
+ enumerable: true,
57660
+ get: function() {
57661
+ return SOURCE_CAP_ACTIVE_FIELD;
57662
+ }
57663
+ });
57664
+ Object.defineProperty(exports, "SOURCE_CAP_CHANGED_AT_FIELD", {
57665
+ enumerable: true,
57666
+ get: function() {
57667
+ return SOURCE_CAP_CHANGED_AT_FIELD;
57668
+ }
57669
+ });
57367
57670
  Object.defineProperty(exports, "STORAGE_ACCESS_FALLBACK", {
57368
57671
  enumerable: true,
57369
57672
  get: function() {
@@ -57508,6 +57811,12 @@ Object.defineProperty(exports, "cameraGridLayoutCapability", {
57508
57811
  return cameraGridLayoutCapability;
57509
57812
  }
57510
57813
  });
57814
+ Object.defineProperty(exports, "cloneStatefulExpressionMemory", {
57815
+ enumerable: true,
57816
+ get: function() {
57817
+ return cloneStatefulExpressionMemory;
57818
+ }
57819
+ });
57511
57820
  Object.defineProperty(exports, "collectSecretConfigKeys", {
57512
57821
  enumerable: true,
57513
57822
  get: function() {
@@ -57532,6 +57841,12 @@ Object.defineProperty(exports, "composerCapability", {
57532
57841
  return composerCapability;
57533
57842
  }
57534
57843
  });
57844
+ Object.defineProperty(exports, "compositionFeatureEnabled", {
57845
+ enumerable: true,
57846
+ get: function() {
57847
+ return compositionFeatureEnabled;
57848
+ }
57849
+ });
57535
57850
  Object.defineProperty(exports, "compositionFieldKey", {
57536
57851
  enumerable: true,
57537
57852
  get: function() {
@@ -57604,6 +57919,12 @@ Object.defineProperty(exports, "createEvent", {
57604
57919
  return createEvent;
57605
57920
  }
57606
57921
  });
57922
+ Object.defineProperty(exports, "createStatefulExpressionMemory", {
57923
+ enumerable: true,
57924
+ get: function() {
57925
+ return createStatefulExpressionMemory;
57926
+ }
57927
+ });
57607
57928
  Object.defineProperty(exports, "dataStoreProviderCapability", {
57608
57929
  enumerable: true,
57609
57930
  get: function() {