@camstack/addon-post-analysis 1.2.638 → 1.2.640

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 (15) hide show
  1. package/dist/{clip-model-registry-B-Dv5y3Z.mjs → clip-model-registry-B1HxbGuP.mjs} +120 -8
  2. package/dist/{clip-model-registry-DEcrrqRz.js → clip-model-registry-C8xdbChV.js} +120 -8
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-LOOCKNGE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CO5qrMxU.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CENVyAAQ.mjs +26 -0
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DIbQ2FJ4.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-C2kfB4nI.mjs → hostInit-BN2pgAbz.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +216 -85
  11. package/dist/pipeline-analytics/index.mjs +216 -85
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D4TYbuPV.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DXlqUmY1.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-DEcrrqRz.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-C8xdbChV.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -8995,6 +8995,7 @@ var en_default = {
8995
8995
  "system.device-battery-low.title": "Battery low — {{deviceName}}",
8996
8996
  "system.device-battery-low.body": "{{deviceName}} is down to {{percentage}}% battery",
8997
8997
  "system.device-battery-low.body.binary": "{{deviceName}} reports a low battery",
8998
+ "system.device-battery-low.body.unmeasured": "{{deviceName}} has a low battery",
8998
8999
  "system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
8999
9000
  "system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
9000
9001
  "system.device-battery-normal.body.binary": "{{deviceName}} no longer reports a low battery",
@@ -9395,6 +9396,7 @@ var it_default = {
9395
9396
  "system.device-battery-low.title": "Batteria scarica — {{deviceName}}",
9396
9397
  "system.device-battery-low.body": "{{deviceName}} è al {{percentage}}% di batteria",
9397
9398
  "system.device-battery-low.body.binary": "{{deviceName}} segnala la batteria scarica",
9399
+ "system.device-battery-low.body.unmeasured": "{{deviceName}} ha la batteria scarica",
9398
9400
  "system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
9399
9401
  "system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
9400
9402
  "system.device-battery-normal.body.binary": "{{deviceName}} non segnala più la batteria scarica",
@@ -78853,6 +78855,97 @@ function alertSubjectDeviceId(subject) {
78853
78855
  return Number.isInteger(parsed) ? parsed : void 0;
78854
78856
  }
78855
78857
  //#endregion
78858
+ //#region src/notification-center/alerts/consumable-subject.ts
78859
+ /**
78860
+ * A consumable EPISODE's name, and the thresholds the rule set asks for (D642).
78861
+ *
78862
+ * Before D642 one cluster-wide threshold judged every item, so an item had one
78863
+ * episode: `consumable:<deviceId>:<key>`. The threshold is now the RULE's, and
78864
+ * the ledger keeps one episode per threshold a rule asks for — each with its
78865
+ * own hysteresis, pending raise and reminder — named
78866
+ * `consumable@<threshold>:<deviceId>:<key>`.
78867
+ *
78868
+ * The threshold rides the FAMILY segment on purpose: the device id stays the
78869
+ * second segment (`alertSubjectDeviceId`, D636 — the log tag and the indexed
78870
+ * column read it), and the item key stays last and whole, whatever it holds.
78871
+ *
78872
+ * Pure. No clock, no I/O.
78873
+ */
78874
+ var FAMILY = "consumable";
78875
+ /** The episode of `key` on `deviceId`, judged at `thresholdPercent`. */
78876
+ function consumableSubject(thresholdPercent, deviceId, key) {
78877
+ return `${FAMILY}@${String(thresholdPercent)}:${String(deviceId)}:${key}`;
78878
+ }
78879
+ /** The pre-D642 subject of the same item — read only to carry it over. */
78880
+ function legacyConsumableSubject(deviceId, key) {
78881
+ return `${FAMILY}:${String(deviceId)}:${key}`;
78882
+ }
78883
+ /** `null` for a subject that is not a consumable's. */
78884
+ function parseConsumableSubject(subject) {
78885
+ const first = subject.indexOf(":");
78886
+ const second = first < 0 ? -1 : subject.indexOf(":", first + 1);
78887
+ if (first < 0 || second < 0) return null;
78888
+ const family = subject.slice(0, first);
78889
+ const deviceId = Number(subject.slice(first + 1, second));
78890
+ const key = subject.slice(second + 1);
78891
+ if (!Number.isInteger(deviceId) || key.length === 0) return null;
78892
+ if (family === FAMILY) return {
78893
+ thresholdPercent: require_clip_model_registry.NC_CONSUMABLE_LOW_PERCENT_DEFAULT,
78894
+ deviceId,
78895
+ key,
78896
+ legacy: true
78897
+ };
78898
+ if (!family.startsWith(`${FAMILY}@`)) return null;
78899
+ const thresholdPercent = Number(family.slice(11));
78900
+ if (!Number.isInteger(thresholdPercent)) return null;
78901
+ return {
78902
+ thresholdPercent,
78903
+ deviceId,
78904
+ key,
78905
+ legacy: false
78906
+ };
78907
+ }
78908
+ /**
78909
+ * Every consumable threshold the rule set asks for, ascending.
78910
+ *
78911
+ * A rule asks when it is a system-event rule whose kinds include a consumable
78912
+ * kind — enabled or not: a disabled rule's episode stays PENDING (no rule
78913
+ * matched it) and is re-offered the moment the rule is switched back on
78914
+ * (D636), which is what that rule would have said had it been on. With no such
78915
+ * rule at all the default is still judged, so an episode exists for the first
78916
+ * rule an operator writes to hear (D636's "offered once per rule change").
78917
+ */
78918
+ function consumableThresholdCensus(rules) {
78919
+ const thresholds = /* @__PURE__ */ new Set();
78920
+ for (const rule of rules) {
78921
+ const condition = rule.conditions.systemEvent;
78922
+ if (rule.delivery !== "system-event" || condition === void 0) continue;
78923
+ if (!condition.kinds.some((kind) => require_clip_model_registry.NC_CONSUMABLE_SYSTEM_EVENT_KINDS.includes(kind))) continue;
78924
+ thresholds.add(require_clip_model_registry.consumableLowPercentOf(condition));
78925
+ }
78926
+ if (thresholds.size === 0) thresholds.add(require_clip_model_registry.NC_CONSUMABLE_LOW_PERCENT_DEFAULT);
78927
+ return [...thresholds].toSorted((a, b) => a - b);
78928
+ }
78929
+ //#endregion
78930
+ //#region src/notification-center/alerts/tank-subject.ts
78931
+ function tankSubject(deviceId, key) {
78932
+ return `tank:${String(deviceId)}:${key}`;
78933
+ }
78934
+ /** `null` for a subject that is not a leftover tank episode. */
78935
+ function parseTankSubject(subject) {
78936
+ const first = subject.indexOf(":");
78937
+ const second = first < 0 ? -1 : subject.indexOf(":", first + 1);
78938
+ if (first < 0 || second < 0) return null;
78939
+ if (subject.slice(0, first) !== "tank") return null;
78940
+ const deviceId = Number(subject.slice(first + 1, second));
78941
+ const key = subject.slice(second + 1);
78942
+ if (!Number.isInteger(deviceId) || key.length === 0) return null;
78943
+ return {
78944
+ deviceId,
78945
+ key
78946
+ };
78947
+ }
78948
+ //#endregion
78856
78949
  //#region src/notification-center/alerts/alert-store.ts
78857
78950
  /**
78858
78951
  * @durable class=ledger owner=notification-center
@@ -78973,6 +79066,7 @@ function mergeBootWindowAlertRow(stored, provisional) {
78973
79066
  }
78974
79067
  var NcAlertStore = class {
78975
79068
  ledger;
79069
+ logger;
78976
79070
  /**
78977
79071
  * THE BOOT WINDOW (D698). Battery slices arrive from the moment the centre
78978
79072
  * starts, and the ledger is read only in the back-fill. A row written in that
@@ -78984,6 +79078,7 @@ var NcAlertStore = class {
78984
79078
  */
78985
79079
  bootWindow;
78986
79080
  constructor(deps) {
79081
+ this.logger = deps.logger;
78987
79082
  this.ledger = new DurableLedger({
78988
79083
  spec: NC_ALERTS_SPEC,
78989
79084
  kind: deps.records.kind(ncDurableRecordKinds["device-alerts"]),
@@ -79003,6 +79098,50 @@ var NcAlertStore = class {
79003
79098
  this.bootWindow.settle();
79004
79099
  return loaded;
79005
79100
  }
79101
+ /**
79102
+ * Close open `tank:<id>:<key>` leftovers once (D934). Idempotent.
79103
+ *
79104
+ * A row closes when the same key is already a consumable episode in this
79105
+ * ledger, or when an answered directory has no device for it. A live device
79106
+ * whose key has not been seen yet is kept — the next consumable observation
79107
+ * closes it. `battery:` rows are not tank subjects and are never touched.
79108
+ * Deletes go through this ledger (D679).
79109
+ */
79110
+ async sweepOpenTankLeftovers(input) {
79111
+ const known = consumableKeysOf(this.ledger.snapshot());
79112
+ const closing = [];
79113
+ let kept = 0;
79114
+ for (const row of this.ledger.snapshot()) {
79115
+ if (!row.alerting) continue;
79116
+ const tank = parseTankSubject(row.subject);
79117
+ if (tank === null) continue;
79118
+ const reason = tankCloseReason(tank, known, input.liveOwner(tank.deviceId));
79119
+ if (reason === "keep") kept += 1;
79120
+ else closing.push({
79121
+ row,
79122
+ reason
79123
+ });
79124
+ }
79125
+ for (const item of closing) {
79126
+ const deviceId = alertSubjectDeviceId(item.row.subject);
79127
+ await this.ledger.forget(item.row.subject);
79128
+ this.logger.info("leftover tank alert closed", {
79129
+ ...deviceId !== void 0 ? { tags: { deviceId } } : {},
79130
+ meta: {
79131
+ subject: item.row.subject,
79132
+ reason: item.reason
79133
+ }
79134
+ });
79135
+ }
79136
+ this.logger.info("leftover tank alert sweep", { meta: {
79137
+ closed: closing.length,
79138
+ kept
79139
+ } });
79140
+ return {
79141
+ closed: closing.length,
79142
+ kept
79143
+ };
79144
+ }
79006
79145
  /** Drop a deleted device's mirrored rows (the durable ones go by `deleteWhere`, D871). */
79007
79146
  evictDevice(deviceId) {
79008
79147
  return this.ledger.evictDevice(deviceId);
@@ -79030,77 +79169,21 @@ var NcAlertStore = class {
79030
79169
  };
79031
79170
  }
79032
79171
  };
79033
- //#endregion
79034
- //#region src/notification-center/alerts/consumable-subject.ts
79035
- /**
79036
- * A consumable EPISODE's name, and the thresholds the rule set asks for (D642).
79037
- *
79038
- * Before D642 one cluster-wide threshold judged every item, so an item had one
79039
- * episode: `consumable:<deviceId>:<key>`. The threshold is now the RULE's, and
79040
- * the ledger keeps one episode per threshold a rule asks for — each with its
79041
- * own hysteresis, pending raise and reminder — named
79042
- * `consumable@<threshold>:<deviceId>:<key>`.
79043
- *
79044
- * The threshold rides the FAMILY segment on purpose: the device id stays the
79045
- * second segment (`alertSubjectDeviceId`, D636 — the log tag and the indexed
79046
- * column read it), and the item key stays last and whole, whatever it holds.
79047
- *
79048
- * Pure. No clock, no I/O.
79049
- */
79050
- var FAMILY = "consumable";
79051
- /** The episode of `key` on `deviceId`, judged at `thresholdPercent`. */
79052
- function consumableSubject(thresholdPercent, deviceId, key) {
79053
- return `${FAMILY}@${String(thresholdPercent)}:${String(deviceId)}:${key}`;
79054
- }
79055
- /** The pre-D642 subject of the same item — read only to carry it over. */
79056
- function legacyConsumableSubject(deviceId, key) {
79057
- return `${FAMILY}:${String(deviceId)}:${key}`;
79058
- }
79059
- /** `null` for a subject that is not a consumable's. */
79060
- function parseConsumableSubject(subject) {
79061
- const first = subject.indexOf(":");
79062
- const second = first < 0 ? -1 : subject.indexOf(":", first + 1);
79063
- if (first < 0 || second < 0) return null;
79064
- const family = subject.slice(0, first);
79065
- const deviceId = Number(subject.slice(first + 1, second));
79066
- const key = subject.slice(second + 1);
79067
- if (!Number.isInteger(deviceId) || key.length === 0) return null;
79068
- if (family === FAMILY) return {
79069
- thresholdPercent: require_clip_model_registry.NC_CONSUMABLE_LOW_PERCENT_DEFAULT,
79070
- deviceId,
79071
- key,
79072
- legacy: true
79073
- };
79074
- if (!family.startsWith(`${FAMILY}@`)) return null;
79075
- const thresholdPercent = Number(family.slice(11));
79076
- if (!Number.isInteger(thresholdPercent)) return null;
79077
- return {
79078
- thresholdPercent,
79079
- deviceId,
79080
- key,
79081
- legacy: false
79082
- };
79172
+ function consumableKey(deviceId, key) {
79173
+ return `${String(deviceId)}:${key}`;
79083
79174
  }
79084
- /**
79085
- * Every consumable threshold the rule set asks for, ascending.
79086
- *
79087
- * A rule asks when it is a system-event rule whose kinds include a consumable
79088
- * kind — enabled or not: a disabled rule's episode stays PENDING (no rule
79089
- * matched it) and is re-offered the moment the rule is switched back on
79090
- * (D636), which is what that rule would have said had it been on. With no such
79091
- * rule at all the default is still judged, so an episode exists for the first
79092
- * rule an operator writes to hear (D636's "offered once per rule change").
79093
- */
79094
- function consumableThresholdCensus(rules) {
79095
- const thresholds = /* @__PURE__ */ new Set();
79096
- for (const rule of rules) {
79097
- const condition = rule.conditions.systemEvent;
79098
- if (rule.delivery !== "system-event" || condition === void 0) continue;
79099
- if (!condition.kinds.some((kind) => require_clip_model_registry.NC_CONSUMABLE_SYSTEM_EVENT_KINDS.includes(kind))) continue;
79100
- thresholds.add(require_clip_model_registry.consumableLowPercentOf(condition));
79175
+ function consumableKeysOf(rows) {
79176
+ const keys = /* @__PURE__ */ new Set();
79177
+ for (const row of rows) {
79178
+ const parts = parseConsumableSubject(row.subject);
79179
+ if (parts !== null) keys.add(consumableKey(parts.deviceId, parts.key));
79101
79180
  }
79102
- if (thresholds.size === 0) thresholds.add(require_clip_model_registry.NC_CONSUMABLE_LOW_PERCENT_DEFAULT);
79103
- return [...thresholds].toSorted((a, b) => a - b);
79181
+ return keys;
79182
+ }
79183
+ function tankCloseReason(tank, known, owner) {
79184
+ if (known.has(consumableKey(tank.deviceId, tank.key))) return "consumable key already known";
79185
+ if (owner === false) return "no live owner";
79186
+ return "keep";
79104
79187
  }
79105
79188
  //#endregion
79106
79189
  //#region src/notification-center/alerts/consumable-band.ts
@@ -79853,6 +79936,24 @@ function incomingFromBatteryBand(input) {
79853
79936
  });
79854
79937
  }
79855
79938
  /**
79939
+ * The summary sentence for a battery episode whose slice has not been read
79940
+ * since boot. It names the device and nothing else: a missing level is not
79941
+ * 0%, and it is not the binary indicator's sentence.
79942
+ */
79943
+ function incomingFromUnmeasuredBattery(input) {
79944
+ const kind = "device-battery-low";
79945
+ return systemSubject({
79946
+ facts: input.facts,
79947
+ kind,
79948
+ subject: `battery:${input.deviceId}`,
79949
+ titleKey: `system.${kind}.title`,
79950
+ bodyKey: `system.${kind}.body.unmeasured`,
79951
+ textParams: deviceTextParams(input.deviceId, input.identity),
79952
+ deviceId: input.deviceId,
79953
+ ...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
79954
+ });
79955
+ }
79956
+ /**
79856
79957
  * A consumable BAND, as a system-event subject.
79857
79958
  *
79858
79959
  * Built exactly like {@link incomingFromBatteryBand} — the operator asked for
@@ -80828,22 +80929,24 @@ function deliveryForKind(kind) {
80828
80929
  }
80829
80930
  //#endregion
80830
80931
  //#region src/notification-center/alerts/open-alert.ts
80932
+ /** `battery:<deviceId>` — nothing else. A `tank:` subject is not one of these. */
80933
+ function isBatterySubject(subject) {
80934
+ const deviceId = alertSubjectDeviceId(subject);
80935
+ return deviceId !== void 0 && subject === `battery:${String(deviceId)}`;
80936
+ }
80831
80937
  function unmeasuredEpisode(input) {
80832
80938
  const deviceId = alertSubjectDeviceId(input.subject);
80833
80939
  if (deviceId === void 0) return null;
80834
80940
  const identity = input.lookup(deviceId);
80835
80941
  const facts = input.facts;
80836
- if (input.family === "battery") return incomingFromBatteryBand({
80837
- facts,
80838
- deviceId,
80839
- band: "low",
80840
- reading: {
80841
- percentage: 0,
80842
- lastUpdated: 0,
80843
- binary: true
80844
- },
80845
- identity
80846
- });
80942
+ if (input.family === "battery") {
80943
+ if (!isBatterySubject(input.subject)) return null;
80944
+ return incomingFromUnmeasuredBattery({
80945
+ facts,
80946
+ deviceId,
80947
+ identity
80948
+ });
80949
+ }
80847
80950
  const parts = parseConsumableSubject(input.subject);
80848
80951
  if (parts !== null && parts.legacy) {
80849
80952
  const parsed = parseProviderVerdictDetail(input.detail);
@@ -80917,6 +81020,15 @@ function isProviderVerdictDetail(detail) {
80917
81020
  }
80918
81021
  function openAlertOf(row, input) {
80919
81022
  const parts = parseConsumableSubject(row.subject);
81023
+ const battery = isBatterySubject(row.subject);
81024
+ if (parts === null && !battery) {
81025
+ const deviceId = alertSubjectDeviceId(row.subject);
81026
+ input.logger?.warn("open alert skipped — subject is neither a battery nor a consumable", {
81027
+ ...deviceId !== void 0 ? { tags: { deviceId } } : {},
81028
+ meta: { subject: row.subject }
81029
+ });
81030
+ return null;
81031
+ }
80920
81032
  const family = parts !== null ? "consumable" : "battery";
80921
81033
  const subject = (input.restated ?? unmeasuredEpisode({
80922
81034
  subject: row.subject,
@@ -81137,6 +81249,7 @@ var NcDeviceAlerts = class {
81137
81249
  */
81138
81250
  /** One item from the consumables slice — every form (D934). */
81139
81251
  observeConsumableItem(deviceId, sliceItem) {
81252
+ this.closeTankLeftover(deviceId, sliceItem.key);
81140
81253
  if (sliceItem.wear !== null) {
81141
81254
  this.observeConsumable(deviceId, sliceItem.wear);
81142
81255
  return;
@@ -81317,13 +81430,15 @@ var NcDeviceAlerts = class {
81317
81430
  for (const row of this.deps.rows.all()) {
81318
81431
  if (!row.alerting) continue;
81319
81432
  const restated = this.restatementOf(row.subject);
81433
+ const logger = this.logger();
81320
81434
  const open = openAlertOf(row, {
81321
81435
  facts,
81322
81436
  restated: restated === null ? null : restated(facts),
81323
- lookup: this.deps.lookup
81437
+ lookup: this.deps.lookup,
81438
+ ...logger !== void 0 ? { logger } : {}
81324
81439
  });
81325
81440
  if (open !== null) out.push(open);
81326
- else this.logger()?.debug("open alert left out of the state summary — it cannot be worded", {
81441
+ else if (parseConsumableSubject(row.subject) !== null || isBatterySubject(row.subject)) this.logger()?.debug("open alert left out of the state summary — it cannot be worded", {
81327
81442
  ...tagsOf(row.subject),
81328
81443
  meta: { subject: row.subject }
81329
81444
  });
@@ -81388,6 +81503,16 @@ var NcDeviceAlerts = class {
81388
81503
  }
81389
81504
  this.absentThresholds = absent;
81390
81505
  }
81506
+ /**
81507
+ * A leftover `tank:<id>:<key>` ends when that key is seen on the consumables
81508
+ * slice (D934). Silent: no recovery. A real `battery:` row is a different
81509
+ * subject and is not touched.
81510
+ */
81511
+ closeTankLeftover(deviceId, key) {
81512
+ const subject = tankSubject(deviceId, key);
81513
+ if (this.deps.rows.get(subject) === void 0) return;
81514
+ this.dropRow(subject, "closed — the same key is a consumable (D934)");
81515
+ }
81391
81516
  /** Remove one row, mirror and durable, with a line that names why. */
81392
81517
  dropRow(subject, reason) {
81393
81518
  this.offeredAt.delete(subject);
@@ -101584,7 +101709,13 @@ async function runBootBackfill(host) {
101584
101709
  if (!await phase("alertEpisodes", () => host.hydrateAlertEpisodes())) return;
101585
101710
  if (!await phase("deviceAlerts", async () => {
101586
101711
  const rows = await host.alertStore.load();
101587
- host.logger.info("device alert ledger reseeded", { meta: { rows } });
101712
+ const directoryAnswered = host.deviceDirectory.answeredGeneration() > 0;
101713
+ const tankLeftovers = await host.alertStore.sweepOpenTankLeftovers({ liveOwner: (deviceId) => directoryAnswered ? host.deviceDirectory.get(deviceId) !== void 0 : void 0 });
101714
+ host.logger.info("device alert ledger reseeded", { meta: {
101715
+ rows,
101716
+ tankLeftoversClosed: tankLeftovers.closed,
101717
+ tankLeftoversKept: tankLeftovers.kept
101718
+ } });
101588
101719
  })) return;
101589
101720
  if (!await phase("stateSummary", async () => {
101590
101721
  const loaded = await host.stateSummary.load();
@@ -112207,7 +112338,7 @@ var MotionEpisodeTracker = class {
112207
112338
  *
112208
112339
  * A single timeout is the call's bound doing its job. Three uncertain confirms
112209
112340
  * in a row, with no yes and no no between them, is the provider not answering
112210
- * — the shape camera 615 showed on 2026-10-26 (nine timeouts, zero answers).
112341
+ * — the shape camera 102 showed on 2026-10-26 (nine timeouts, zero answers).
112211
112342
  * A time window would also count a timeout that a later answer already closed.
112212
112343
  * The streak resets on any answer. A further uncertain while the edge is
112213
112344
  * open does not raise again.