@camstack/system 1.2.476 → 1.2.477

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.
@@ -1,5 +1,6 @@
1
1
  import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-B_V9r61Z.mjs";
2
2
  import { n as normalizedNativeOwner } from "../../composition-sources-B5nK7pHt.mjs";
3
+ import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-lPgwLane.mjs";
3
4
  import { z } from "zod";
4
5
  //#region src/builtins/shared/device-state-changed.ts
5
6
  /** Structural narrowing helper for untrusted bus payloads. */
@@ -1196,9 +1197,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
1196
1197
  hasWritten: false,
1197
1198
  written: void 0,
1198
1199
  expressionMemory: createStatefulExpressionMemory(),
1199
- eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
1200
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
1201
+ pulseHold: false
1200
1202
  };
1201
1203
  }
1204
+ /** Apply a pulse start seed (D938). `keep` leaves the slice seed in place. */
1205
+ function applyPulseSeed(record, seed) {
1206
+ if (seed === void 0 || seed.kind === "keep") return record;
1207
+ if (seed.kind === "hold") return {
1208
+ ...record,
1209
+ initial: void 0,
1210
+ eventHighWater: null,
1211
+ pulseHold: true
1212
+ };
1213
+ const value = seed.value;
1214
+ return {
1215
+ ...record,
1216
+ initial: value,
1217
+ eventHighWater: typeof value === "number" && Number.isFinite(value) ? value : null,
1218
+ pulseHold: false
1219
+ };
1220
+ }
1221
+ function fieldRecords(fields, byCap, lookupCap, pulseSeeds) {
1222
+ return new Map(fields.map((plan) => {
1223
+ const key = fieldKey(plan);
1224
+ return [key, applyPulseSeed(createFieldRecord(plan, byCap, lookupCap), pulseSeeds?.get(key))];
1225
+ }));
1226
+ }
1202
1227
  function currentOutput(record) {
1203
1228
  return record.hasWritten ? record.written : record.initial;
1204
1229
  }
@@ -1252,6 +1277,7 @@ function namedField(r) {
1252
1277
  function writable(record) {
1253
1278
  const outcome = record.outcome;
1254
1279
  if (outcome === null) return void 0;
1280
+ if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
1255
1281
  if (outcome.state !== "unavailable") return outcome.value;
1256
1282
  switch (record.plan.unavailable) {
1257
1283
  case "null": return null;
@@ -1689,7 +1715,7 @@ var CompositionRuntime = class {
1689
1715
  constructor(deps) {
1690
1716
  this.deps = deps;
1691
1717
  this.startedAtMs = deps.now();
1692
- this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
1718
+ this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
1693
1719
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1694
1720
  const eventContext = {
1695
1721
  deviceId: deps.target.id,
@@ -2536,7 +2562,7 @@ function writerFor(deps, p, device, onClaimEvent) {
2536
2562
  onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
2537
2563
  });
2538
2564
  }
2539
- function newCompositionRuntime(deps, p, target, seed) {
2565
+ function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
2540
2566
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2541
2567
  const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
2542
2568
  const nativeCaps = new Set(self?.native ?? []);
@@ -2549,6 +2575,7 @@ function newCompositionRuntime(deps, p, target, seed) {
2549
2575
  target,
2550
2576
  lookupCap: deps.lookupCap,
2551
2577
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2578
+ ...pulseSeeds === void 0 ? {} : { pulseSeeds },
2552
2579
  read: (read) => deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2553
2580
  isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2554
2581
  now: deps.now,
@@ -2754,6 +2781,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2754
2781
  };
2755
2782
  }
2756
2783
  //#endregion
2784
+ //#region src/builtins/composer/pulse-seed.ts
2785
+ /** The last false→true in time order. A series that opens already on is not a transition. */
2786
+ function lastOffToOn(points, beforeOn) {
2787
+ const ordered = [...points].toSorted((a, b) => a.t - b.t);
2788
+ let previous = beforeOn;
2789
+ let rise = null;
2790
+ for (const point of ordered) {
2791
+ if (previous === false && point.on && Number.isFinite(point.t)) rise = point.t;
2792
+ previous = point.on;
2793
+ }
2794
+ return rise;
2795
+ }
2796
+ function finite(value) {
2797
+ return value !== null && Number.isFinite(value) ? value : null;
2798
+ }
2799
+ /**
2800
+ * What the source proves. A failed read is `hold`, never `none`.
2801
+ * When the source records no history and it is off, `lastChangedAt` is the
2802
+ * end of the last ring: it bounds the ring from above (the press was at or
2803
+ * before that instant). It is used only when it is newer than the target's
2804
+ * native stamp — an older bound does not prove the native value.
2805
+ */
2806
+ function proof(facts) {
2807
+ if (facts.history.kind === "unread") return {
2808
+ kind: "hold",
2809
+ reason: "history-unread"
2810
+ };
2811
+ if (facts.history.kind === "series") {
2812
+ const at = lastOffToOn(facts.history.points, facts.history.beforeOn);
2813
+ return at === null ? { kind: "none" } : {
2814
+ kind: "rise",
2815
+ at,
2816
+ via: "history"
2817
+ };
2818
+ }
2819
+ if (facts.level.kind === "unread") return {
2820
+ kind: "hold",
2821
+ reason: "level-unread"
2822
+ };
2823
+ const changedAt = facts.level.kind === "off" ? finite(facts.level.changedAt) : null;
2824
+ const newerThanNative = changedAt !== null && (facts.nativeStamp === null || changedAt > facts.nativeStamp);
2825
+ if (changedAt !== null && newerThanNative) return {
2826
+ kind: "rise",
2827
+ at: changedAt,
2828
+ via: "changed-at-bound"
2829
+ };
2830
+ return { kind: "none" };
2831
+ }
2832
+ function claimed(owned) {
2833
+ return owned.kind === "stamp" || owned.kind === "never";
2834
+ }
2835
+ function decidePulseSeed(facts) {
2836
+ const proved = proof(facts);
2837
+ if (proved.kind === "hold") return claimed(facts.owned) ? { kind: "keep" } : proved;
2838
+ if (proved.kind === "rise") {
2839
+ if (facts.owned.kind === "stamp" && facts.owned.at >= proved.at) return { kind: "keep" };
2840
+ const count = facts.owned.kind === "stamp" ? facts.owned.count : 0;
2841
+ return {
2842
+ kind: "stamp",
2843
+ at: proved.at,
2844
+ count,
2845
+ via: proved.via
2846
+ };
2847
+ }
2848
+ return claimed(facts.owned) ? { kind: "keep" } : {
2849
+ kind: "none",
2850
+ count: 0
2851
+ };
2852
+ }
2853
+ /** One failed source blocks "no ring". A rise from another source still stands. */
2854
+ function mergePulseDecisions(decisions) {
2855
+ if (decisions.length === 0) return {
2856
+ kind: "none",
2857
+ count: 0
2858
+ };
2859
+ const stamps = decisions.flatMap((decision) => decision.kind === "stamp" ? [decision] : []);
2860
+ if (stamps.length > 0) return stamps.reduce((best, next) => next.at > best.at ? next : best);
2861
+ const hold = decisions.find((decision) => decision.kind === "hold");
2862
+ if (hold !== void 0) return hold;
2863
+ if (decisions.some((decision) => decision.kind === "keep")) return { kind: "keep" };
2864
+ return {
2865
+ kind: "none",
2866
+ count: 0
2867
+ };
2868
+ }
2869
+ //#endregion
2870
+ //#region src/builtins/composer/pulse-seed-resolve.ts
2871
+ /**
2872
+ * Gather a pulse override's start seed (D938). The stored expression is not
2873
+ * changed. A failed read holds the field: no seed, and a claimed stamp is
2874
+ * not cleared (D49).
2875
+ */
2876
+ var ROSE_CALL = String.raw`rose\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)`;
2877
+ function historySeriesKey(cap, fieldPath) {
2878
+ return capSeriesKey(cap, fieldPath);
2879
+ }
2880
+ async function resolvePulseSeeds(input) {
2881
+ try {
2882
+ return await resolveNow(input);
2883
+ } catch (err) {
2884
+ input.logger.warn("pulse override seed held — gathering the source facts threw, so nothing is seeded and nothing is cleared", {
2885
+ tags: { deviceId: input.deviceId },
2886
+ meta: {
2887
+ blockId: input.blockId,
2888
+ error: errMsg(err)
2889
+ }
2890
+ });
2891
+ return /* @__PURE__ */ new Map();
2892
+ }
2893
+ }
2894
+ async function resolveNow(input) {
2895
+ const roses = input.fields.filter(isPulseTimestamp);
2896
+ if (roses.length === 0) return /* @__PURE__ */ new Map();
2897
+ const listing = await readClaims(input);
2898
+ if (listing === null) return /* @__PURE__ */ new Map();
2899
+ const seeds = /* @__PURE__ */ new Map();
2900
+ for (const field of roses) {
2901
+ const decision = await decideField(input, field, listing);
2902
+ logDecision(input, field, decision);
2903
+ for (const [key, seed] of seedsFor(input.fields, field, decision)) seeds.set(key, seed);
2904
+ }
2905
+ return seeds;
2906
+ }
2907
+ function isPulseTimestamp(field) {
2908
+ return field.source.kind === "expression" && expressionContainsBuiltinCall(field.source.expr, "rose");
2909
+ }
2910
+ async function readClaims(input) {
2911
+ try {
2912
+ const listing = await input.claims.listClaims({ ownerPrefix: input.blockId });
2913
+ if (listing.state === "loaded") return listing;
2914
+ input.logger.warn("pulse override seed held — the claims index has not loaded, so nothing is seeded and nothing is cleared", {
2915
+ tags: { deviceId: input.deviceId },
2916
+ meta: {
2917
+ blockId: input.blockId,
2918
+ reason: listing.reason
2919
+ }
2920
+ });
2921
+ return null;
2922
+ } catch (err) {
2923
+ input.logger.warn("pulse override seed held — the claims index could not be read, so nothing is seeded and nothing is cleared", {
2924
+ tags: { deviceId: input.deviceId },
2925
+ meta: {
2926
+ blockId: input.blockId,
2927
+ error: errMsg(err)
2928
+ }
2929
+ });
2930
+ return null;
2931
+ }
2932
+ }
2933
+ async function decideField(input, field, listing) {
2934
+ if (listing.state !== "loaded") return {
2935
+ kind: "hold",
2936
+ reason: "claims-unread"
2937
+ };
2938
+ const claim = listing.claims.find((row) => row.owner === input.blockId && row.deviceId === input.deviceId && row.capName === field.cap);
2939
+ const slice = input.targetSeed.byCap.get(field.cap) ?? null;
2940
+ const counter = counterField(input.fields, field);
2941
+ const owned = ownedOf(claim, slice, field.path, counter?.path ?? null);
2942
+ const nativeStamp = stampOf(slice, field.path);
2943
+ const pairs = rosePairs(field);
2944
+ if (pairs.length === 0) return { kind: "keep" };
2945
+ const decisions = [];
2946
+ for (const pair of pairs) decisions.push(await decidePair(input, pair, owned, nativeStamp));
2947
+ return mergePulseDecisions(decisions);
2948
+ }
2949
+ async function decidePair(input, pair, owned, nativeStamp) {
2950
+ const resolution = await input.sources.resolve(pair.source, false);
2951
+ if (resolution.kind === "missing") return { kind: "keep" };
2952
+ if (resolution.kind !== "found") return decidePulseSeed({
2953
+ owned,
2954
+ nativeStamp,
2955
+ history: { kind: "unread" },
2956
+ level: { kind: "unread" }
2957
+ });
2958
+ return decidePulseSeed({
2959
+ owned,
2960
+ nativeStamp,
2961
+ history: await historyOf(input, resolution.deviceId, pair),
2962
+ level: await levelOf(input, resolution.deviceId, pair)
2963
+ });
2964
+ }
2965
+ async function historyOf(input, deviceId, pair) {
2966
+ if (!recordsField(input.lookupCap, pair.cap, pair.activeField)) return { kind: "absent" };
2967
+ const now = input.now();
2968
+ const fromMs = now - DEFAULT_RETENTION.rawDays * DAY_MS;
2969
+ try {
2970
+ const answer = await input.readPulseHistory(deviceId, pair.cap, pair.activeField, fromMs, now);
2971
+ if (answer.kind === "unread") return { kind: "unread" };
2972
+ return {
2973
+ kind: "series",
2974
+ beforeOn: answer.series.beforeOn,
2975
+ points: answer.series.points
2976
+ };
2977
+ } catch (err) {
2978
+ input.logger.warn("pulse override history read threw — treated as unread", {
2979
+ tags: { deviceId: input.deviceId },
2980
+ meta: {
2981
+ blockId: input.blockId,
2982
+ sourceDeviceId: deviceId,
2983
+ cap: pair.cap,
2984
+ field: pair.activeField,
2985
+ error: errMsg(err)
2986
+ }
2987
+ });
2988
+ return { kind: "unread" };
2989
+ }
2990
+ }
2991
+ async function levelOf(input, deviceId, pair) {
2992
+ try {
2993
+ return levelFromSlice(await input.readCap(deviceId, pair.cap), pair);
2994
+ } catch (err) {
2995
+ input.logger.warn("pulse override source slice could not be read — treated as unread", {
2996
+ tags: { deviceId: input.deviceId },
2997
+ meta: {
2998
+ blockId: input.blockId,
2999
+ sourceDeviceId: deviceId,
3000
+ cap: pair.cap,
3001
+ error: errMsg(err)
3002
+ }
3003
+ });
3004
+ return { kind: "unread" };
3005
+ }
3006
+ }
3007
+ function levelFromSlice(slice, pair) {
3008
+ if (slice === null) return {
3009
+ kind: "off",
3010
+ changedAt: null
3011
+ };
3012
+ const active = slice[pair.activeField];
3013
+ if (typeof active !== "boolean") return { kind: "unread" };
3014
+ const changed = slice[pair.changedAtField];
3015
+ const changedAt = typeof changed === "number" && Number.isFinite(changed) ? changed : null;
3016
+ return active ? {
3017
+ kind: "on",
3018
+ changedAt
3019
+ } : {
3020
+ kind: "off",
3021
+ changedAt
3022
+ };
3023
+ }
3024
+ function recordsField(lookup, cap, fieldPath) {
3025
+ const history = lookup(cap)?.history;
3026
+ if (history === void 0 || history === "none") return false;
3027
+ return Object.hasOwn(history.fields, fieldPath);
3028
+ }
3029
+ function ownedOf(claim, slice, timestampPath, counterPath) {
3030
+ if (claim === void 0 || slice === null || !claim.fields.includes(timestampPath)) return { kind: "none" };
3031
+ const at = slice[timestampPath];
3032
+ if (at === null) return { kind: "never" };
3033
+ if (typeof at !== "number" || !Number.isFinite(at)) return { kind: "none" };
3034
+ const rawCount = counterPath === null ? void 0 : slice[counterPath];
3035
+ return {
3036
+ kind: "stamp",
3037
+ at,
3038
+ count: typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount >= 0 ? rawCount : 0
3039
+ };
3040
+ }
3041
+ function stampOf(slice, path) {
3042
+ const at = slice?.[path];
3043
+ return typeof at === "number" && Number.isFinite(at) ? at : null;
3044
+ }
3045
+ function counterField(fields, timestamp) {
3046
+ return fields.find((field) => {
3047
+ if (field.cap !== timestamp.cap || field.path === timestamp.path) return false;
3048
+ return field.source.kind === "expression" && field.source.expr === `count(${timestamp.path})`;
3049
+ });
3050
+ }
3051
+ function rosePairs(field) {
3052
+ if (field.source.kind !== "expression") return [];
3053
+ const found = [];
3054
+ for (const match of field.source.expr.matchAll(new RegExp(ROSE_CALL, "g"))) {
3055
+ const pair = rosePair(field, match[1], match[2]);
3056
+ if (pair !== null) found.push(pair);
3057
+ }
3058
+ return found;
3059
+ }
3060
+ function rosePair(field, activeName, changedName) {
3061
+ if (field.source.kind !== "expression") return null;
3062
+ if (activeName === void 0 || changedName === void 0) return null;
3063
+ const active = field.source.bindings[activeName];
3064
+ const changed = field.source.bindings[changedName];
3065
+ if (active === void 0 || changed === void 0) return null;
3066
+ if (active.kind !== "from" || changed.kind !== "from") return null;
3067
+ if (active.cap !== changed.cap) return null;
3068
+ if (active.source.addonId !== changed.source.addonId) return null;
3069
+ if (active.source.stableId !== changed.source.stableId) return null;
3070
+ return {
3071
+ source: active.source,
3072
+ cap: active.cap,
3073
+ activeField: active.fieldPath,
3074
+ changedAtField: changed.fieldPath
3075
+ };
3076
+ }
3077
+ function seedsFor(fields, timestamp, decision) {
3078
+ const seeds = /* @__PURE__ */ new Map();
3079
+ if (decision.kind === "keep") return seeds;
3080
+ const counter = counterField(fields, timestamp);
3081
+ if (decision.kind === "hold") {
3082
+ seeds.set(fieldKey(timestamp), { kind: "hold" });
3083
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3084
+ kind: "value",
3085
+ value: 0
3086
+ });
3087
+ return seeds;
3088
+ }
3089
+ if (decision.kind === "none") {
3090
+ seeds.set(fieldKey(timestamp), {
3091
+ kind: "value",
3092
+ value: null
3093
+ });
3094
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3095
+ kind: "value",
3096
+ value: 0
3097
+ });
3098
+ return seeds;
3099
+ }
3100
+ seeds.set(fieldKey(timestamp), {
3101
+ kind: "value",
3102
+ value: decision.at
3103
+ });
3104
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3105
+ kind: "value",
3106
+ value: decision.count
3107
+ });
3108
+ return seeds;
3109
+ }
3110
+ /** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
3111
+ function pulseHistoryFromQuery(series) {
3112
+ const row = series.find((item) => item.scope === "");
3113
+ if (row === void 0) return {
3114
+ beforeOn: null,
3115
+ points: []
3116
+ };
3117
+ return {
3118
+ beforeOn: levelBit(row.before?.value ?? null),
3119
+ points: row.points.flatMap((point) => {
3120
+ const on = levelBit(point.last);
3121
+ return on === null ? [] : [{
3122
+ t: point.t,
3123
+ on
3124
+ }];
3125
+ })
3126
+ };
3127
+ }
3128
+ function levelBit(value) {
3129
+ if (value === 1) return true;
3130
+ if (value === 0) return false;
3131
+ return null;
3132
+ }
3133
+ function logDecision(input, field, decision) {
3134
+ const meta = {
3135
+ blockId: input.blockId,
3136
+ cap: field.cap,
3137
+ field: field.path
3138
+ };
3139
+ if (decision.kind === "stamp") {
3140
+ input.logger.info("pulse override seeded lastPressedAt from its source", {
3141
+ tags: { deviceId: input.deviceId },
3142
+ meta: {
3143
+ ...meta,
3144
+ at: decision.at,
3145
+ via: decision.via,
3146
+ count: decision.count
3147
+ }
3148
+ });
3149
+ return;
3150
+ }
3151
+ if (decision.kind === "none") {
3152
+ input.logger.info("pulse override seeded no ring — the source proved none, so the native stamp is not used", {
3153
+ tags: { deviceId: input.deviceId },
3154
+ meta
3155
+ });
3156
+ return;
3157
+ }
3158
+ if (decision.kind === "hold") input.logger.warn("pulse override seed held — a source read failed, so nothing is seeded and nothing is cleared", {
3159
+ tags: { deviceId: input.deviceId },
3160
+ meta: {
3161
+ ...meta,
3162
+ reason: decision.reason
3163
+ }
3164
+ });
3165
+ }
3166
+ //#endregion
2757
3167
  //#region src/builtins/composer/composer.ts
2758
3168
  /**
2759
3169
  * The composer: every declarative composition on this hub, in one runner
@@ -2991,6 +3401,7 @@ var Composer = class {
2991
3401
  continue;
2992
3402
  }
2993
3403
  let targetSeed;
3404
+ let pulseSeeds;
2994
3405
  if (p.target.kind === "existing" && p.target.deviceId !== null && p.target.waiting === null && p.target.gone === null && p.block.enabled && p.fields.length > 0) try {
2995
3406
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2996
3407
  reportSeedlessRestoredCaps({
@@ -3001,6 +3412,20 @@ var Composer = class {
3001
3412
  seed: targetSeed,
3002
3413
  lookupCap: this.deps.lookupCap
3003
3414
  });
3415
+ const readHistory = this.deps.readPulseHistory;
3416
+ if (readHistory !== void 0) pulseSeeds = await resolvePulseSeeds({
3417
+ blockId: p.block.id,
3418
+ deviceId: p.target.deviceId,
3419
+ fields: p.fields,
3420
+ targetSeed,
3421
+ lookupCap: this.deps.lookupCap,
3422
+ claims: this.deps.claims,
3423
+ sources: this.deps.tracker,
3424
+ readCap: this.deps.readExistingTargetCap,
3425
+ readPulseHistory: readHistory,
3426
+ now: this.deps.now,
3427
+ logger: this.deps.logger
3428
+ });
3004
3429
  } catch (err) {
3005
3430
  const reason = `the target device state could not be read before ownership (${errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
3006
3431
  this.deps.logger.warn(previous?.runtime === null || previous === void 0 ? "customization target state could not be read — ownership is held" : "customization edit target state could not be read — the previous runtime stays installed", {
@@ -3026,7 +3451,7 @@ var Composer = class {
3026
3451
  const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
3027
3452
  this.onClaimEvent(deviceId, cap, owner, event);
3028
3453
  });
3029
- const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed);
3454
+ const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed, pulseSeeds);
3030
3455
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
3031
3456
  deviceId: p.target.deviceId,
3032
3457
  ref: p.target.ref,
@@ -4159,6 +4584,8 @@ var SourceTracker = class {
4159
4584
  */
4160
4585
  /** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
4161
4586
  var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
4587
+ /** The device-history query ceiling. A switch's raw tier is change-only, so this is the whole window. */
4588
+ var PULSE_HISTORY_MAX_POINTS = 5e3;
4162
4589
  /** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
4163
4590
  function parseNativeShadowChanged(data) {
4164
4591
  if (!isRecord$2(data)) return null;
@@ -4307,6 +4734,26 @@ var ComposerAddon = class extends BaseAddon {
4307
4734
  deviceId,
4308
4735
  capName
4309
4736
  }) ?? null,
4737
+ readPulseHistory: async (deviceId, cap, fieldPath, fromMs, toMs) => {
4738
+ try {
4739
+ return {
4740
+ kind: "series",
4741
+ series: pulseHistoryFromQuery((await api.deviceHistory.query.query({
4742
+ deviceId,
4743
+ key: historySeriesKey(cap, fieldPath),
4744
+ from: fromMs,
4745
+ to: toMs,
4746
+ resolution: "raw",
4747
+ maxPoints: PULSE_HISTORY_MAX_POINTS
4748
+ })).series)
4749
+ };
4750
+ } catch (err) {
4751
+ return {
4752
+ kind: "unread",
4753
+ error: errMsg(err)
4754
+ };
4755
+ }
4756
+ },
4310
4757
  describeTarget: readBoundCaps,
4311
4758
  grafts,
4312
4759
  now: () => Date.now(),
@@ -53,6 +53,7 @@ import { type ComposerDeclare, type ComposerDevicePorts } from "./composed-devic
53
53
  import { type PersistedComposedRow } from "./composition-declarations.js";
54
54
  import type { CompositionEventEmitter, DeadlineTimer } from "./composition-runtime.js";
55
55
  import { type ComposerClaimsApi, type ComposerGraftPorts, type DescribeTarget } from "./existing-target.js";
56
+ import { type ReadPulseHistory } from "./pulse-seed-resolve.js";
56
57
  import type { SourceBindingsChangeReason, SourceTracker } from "./source-tracker.js";
57
58
  export type { ComposerDerivedSpec } from "./composer-plan.js";
58
59
  export type { ComposerDeclareResult, ComposerDevicePorts } from "./composed-device-set.js";
@@ -84,6 +85,11 @@ export interface ComposerDeps {
84
85
  readonly events: CompositionEventEmitter;
85
86
  /** Effective target state, read before an existing-target runtime owns any field. */
86
87
  readonly readExistingTargetCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
88
+ /**
89
+ * Raw history of a source field, for a pulse override's start seed (D938).
90
+ * Absent (tests that predate the seed) leaves the boot slice as the initial.
91
+ */
92
+ readonly readPulseHistory?: ReadPulseHistory;
87
93
  /** ONE `getBindings` per target description read (rule 1). */
88
94
  readonly describeTarget: DescribeTarget;
89
95
  /** `GraftHost` (Task 10): a method-serving native for each ADDED cap. Absent, nothing is grafted. */
@@ -54,6 +54,7 @@
54
54
  import { type CapabilityLookup, type CompositionEventPlan, type CompositionFieldPlan, type CompositionFieldRead, type CompositionFieldState, type IScopedLogger, type SourceReader } from "@camstack/types";
55
55
  import type { ComposedWriteTarget, CompositionEventEmitter, DeadlineTimer } from "./composition-target.js";
56
56
  import { type ReleasedField } from "./field-record.js";
57
+ import type { PulseFieldSeed } from "./pulse-seed.js";
57
58
  export type { ReleasedField } from "./field-record.js";
58
59
  export type { ComposedWriteTarget, CompositionEventEmitter, DeadlineTimer, TargetAsyncFailure, WriteOk, WriteOutcome, WriteRefused } from "./composition-target.js";
59
60
  export interface CompositionRuntimeDeps {
@@ -65,6 +66,8 @@ export interface CompositionRuntimeDeps {
65
66
  readonly lookupCap: CapabilityLookup;
66
67
  /** Effective target slices captured before an existing-target runtime takes ownership. */
67
68
  readonly initialByCap?: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
69
+ /** Pulse start seed (D938). Absent keeps `initialByCap`. `hold` publishes nothing. */
70
+ readonly pulseSeeds?: ReadonlyMap<string, PulseFieldSeed>;
68
71
  readonly read: SourceReader;
69
72
  /**
70
73
  * Whether `read` is of this composition's OWN output (an added cap on its
@@ -6,6 +6,7 @@
6
6
  * behaviour.
7
7
  */
8
8
  import { type CapabilityLookup, type CompositionFieldItem, type CompositionFieldPlan, type CompositionFieldState, type ExpressionValue, type FieldOutcome, type StatefulExpressionMemory } from "@camstack/types";
9
+ import type { PulseFieldSeed } from "./pulse-seed.js";
9
10
  export declare const NO_FIELDS: ReadonlySet<string>;
10
11
  export interface FieldRecord {
11
12
  readonly plan: CompositionFieldPlan;
@@ -19,9 +20,15 @@ export interface FieldRecord {
19
20
  readonly expressionMemory: StatefulExpressionMemory;
20
21
  /** Last finite state value already represented by a derived event. */
21
22
  readonly eventHighWater: number | null;
23
+ /**
24
+ * A pulse timestamp whose start seed could not be read (D49, D938). A null
25
+ * result is not published, so a failed read is not "no ring".
26
+ */
27
+ readonly pulseHold: boolean;
22
28
  }
23
29
  export declare function fieldKey(plan: CompositionFieldPlan): string;
24
30
  export declare function createFieldRecord(plan: CompositionFieldPlan, byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup): FieldRecord;
31
+ export declare function fieldRecords(fields: readonly CompositionFieldPlan[], byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup, pulseSeeds: ReadonlyMap<string, PulseFieldSeed> | undefined): ReadonlyMap<string, FieldRecord>;
25
32
  export declare function currentOutput(record: FieldRecord): unknown;
26
33
  /** The top-level field a record writes: an item leaf writes its whole array. */
27
34
  export declare function topLevelField(plan: CompositionFieldPlan): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Gather a pulse override's start seed (D938). The stored expression is not
3
+ * changed. A failed read holds the field: no seed, and a claimed stamp is
4
+ * not cleared (D49).
5
+ */
6
+ import { type CapabilityLookup, type ClaimsListing, type CompositionFieldPlan, type CompositionSourceRef, type IScopedLogger } from "@camstack/types";
7
+ import type { ExistingTargetSeedRead } from "./existing-target.js";
8
+ import { type PulseFieldSeed, type PulseHistoryPoint } from "./pulse-seed.js";
9
+ import type { SourceResolution } from "./source-readings.js";
10
+ export type { PulseFieldSeed } from "./pulse-seed.js";
11
+ export interface PulseHistorySeries {
12
+ readonly beforeOn: boolean | null;
13
+ readonly points: readonly PulseHistoryPoint[];
14
+ }
15
+ export type PulseHistoryAnswer = {
16
+ readonly kind: "unread";
17
+ readonly error: string;
18
+ } | {
19
+ readonly kind: "series";
20
+ readonly series: PulseHistorySeries;
21
+ };
22
+ export type ReadPulseHistory = (deviceId: number, cap: string, fieldPath: string, fromMs: number, toMs: number) => Promise<PulseHistoryAnswer>;
23
+ export interface PulseClaimListing {
24
+ listClaims(input: {
25
+ readonly ownerPrefix?: string;
26
+ }): Promise<ClaimsListing>;
27
+ }
28
+ export interface PulseSourceLookup {
29
+ resolve(ref: CompositionSourceRef, reset: boolean): Promise<SourceResolution>;
30
+ }
31
+ export interface ResolvePulseSeedsInput {
32
+ readonly blockId: string;
33
+ readonly deviceId: number;
34
+ readonly fields: readonly CompositionFieldPlan[];
35
+ readonly targetSeed: ExistingTargetSeedRead;
36
+ readonly lookupCap: CapabilityLookup;
37
+ readonly claims: PulseClaimListing;
38
+ readonly sources: PulseSourceLookup;
39
+ readonly readCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
40
+ readonly readPulseHistory: ReadPulseHistory;
41
+ readonly now: () => number;
42
+ readonly logger: IScopedLogger;
43
+ }
44
+ export declare function historySeriesKey(cap: string, fieldPath: string): string;
45
+ export declare function resolvePulseSeeds(input: ResolvePulseSeedsInput): Promise<ReadonlyMap<string, PulseFieldSeed>>;
46
+ export interface PulseHistoryQueryPoint {
47
+ readonly t: number;
48
+ readonly last: number | null;
49
+ }
50
+ export interface PulseHistoryQueryBefore {
51
+ readonly value: number | null;
52
+ }
53
+ export interface PulseHistoryQuerySeries {
54
+ readonly scope: string;
55
+ readonly before: PulseHistoryQueryBefore | null;
56
+ readonly points: readonly PulseHistoryQueryPoint[];
57
+ }
58
+ /** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
59
+ export declare function pulseHistoryFromQuery(series: readonly PulseHistoryQuerySeries[]): PulseHistorySeries;