@camstack/addon-post-analysis 1.2.302 → 1.2.304

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.
@@ -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-CQIBimj5.js");
5
+ const require_clip_model_registry = require("../clip-model-registry--qJe8fTH.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");
@@ -14209,11 +14209,26 @@ function sortedSystemEvent(condition) {
14209
14209
  ...packageNames !== void 0 ? { packageNames: packageNames.toSorted(byValue) } : {}
14210
14210
  };
14211
14211
  }
14212
+ /**
14213
+ * `zones` with its redundant quantifier dropped: `match: 'any'` says exactly
14214
+ * what an absent `match` says (D690 — the editors write `{ ids }`, the store
14215
+ * still holds `match: 'any'` on every rule written before). `'all'` is kept:
14216
+ * it IS a different rule.
14217
+ */
14218
+ function canonicalZones(conditions) {
14219
+ const zones = conditions.zones;
14220
+ if (zones?.match !== "any") return conditions;
14221
+ return {
14222
+ ...conditions,
14223
+ zones: { ids: zones.ids }
14224
+ };
14225
+ }
14212
14226
  /** The canonical form two conditions are compared in. */
14213
14227
  function conditionsFingerprint(conditions) {
14214
- const { systemEvent } = conditions;
14215
- return require_clip_model_registry.canonicalHash(systemEvent === void 0 ? conditions : {
14216
- ...conditions,
14228
+ const canonical = canonicalZones(conditions);
14229
+ const { systemEvent } = canonical;
14230
+ return require_clip_model_registry.canonicalHash(systemEvent === void 0 ? canonical : {
14231
+ ...canonical,
14217
14232
  systemEvent: sortedSystemEvent(systemEvent)
14218
14233
  });
14219
14234
  }
@@ -14222,7 +14237,8 @@ function conditionsFingerprint(conditions) {
14222
14237
  * (the normalisation the store applies on every write) and are compared
14223
14238
  * canonically: key order is irrelevant, an `undefined` key equals an absent
14224
14239
  * one, and the system-event `kinds` and id-list filters are compared as SETS.
14225
- * Every other array keeps its order. A side that does not parse is treated as
14240
+ * Every other array keeps its order, and `zones.match: 'any'` equals an absent
14241
+ * `match`. A side that does not parse is treated as
14226
14242
  * a change, so it is judged rather than waved through.
14227
14243
  */
14228
14244
  function sameConditions(a, b) {
@@ -14267,7 +14283,7 @@ function assertPatchKeepsFamily(existing, patch) {
14267
14283
  * the offending kind or filter). Then rethrow. A refused save is dropped work,
14268
14284
  * and dropped work is never silent. Any other error passes through unlogged.
14269
14285
  */
14270
- function logRefusal$3(check, ctx) {
14286
+ function logRefusal$4(check, ctx) {
14271
14287
  try {
14272
14288
  check();
14273
14289
  } catch (err) {
@@ -14283,11 +14299,11 @@ function logRefusal$3(check, ctx) {
14283
14299
  }
14284
14300
  /** {@link assertRuleFamily}, logged. The create path of both routes. */
14285
14301
  function guardRuleFamily(rule, ctx) {
14286
- logRefusal$3(() => assertRuleFamily(rule), ctx);
14302
+ logRefusal$4(() => assertRuleFamily(rule), ctx);
14287
14303
  }
14288
14304
  /** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
14289
14305
  function guardPatchFamily(existing, patch, ctx) {
14290
- logRefusal$3(() => assertPatchKeepsFamily(existing, patch), ctx);
14306
+ logRefusal$4(() => assertPatchKeepsFamily(existing, patch), ctx);
14291
14307
  }
14292
14308
  //#endregion
14293
14309
  //#region src/notification-center/device-scope-guard.ts
@@ -14312,15 +14328,15 @@ function assertDevicesExcludeDelivery(rule) {
14312
14328
  throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
14313
14329
  }
14314
14330
  /** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
14315
- function logRefusal$2(check, ctx) {
14331
+ function logRefusal$3(check, ctx) {
14316
14332
  try {
14317
14333
  check();
14318
14334
  } catch (err) {
14319
- if (err instanceof NcDevicesExcludeRefusal) warnRefusal$1(ctx.logger, err, ctx);
14335
+ if (err instanceof NcDevicesExcludeRefusal) warnRefusal$2(ctx.logger, err, ctx);
14320
14336
  throw err;
14321
14337
  }
14322
14338
  }
14323
- function warnRefusal$1(logger, err, ctx) {
14339
+ function warnRefusal$2(logger, err, ctx) {
14324
14340
  logger.warn("nc rule refused: devicesExclude on a system rule", { meta: {
14325
14341
  ...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
14326
14342
  by: ctx.by,
@@ -14330,7 +14346,7 @@ function warnRefusal$1(logger, err, ctx) {
14330
14346
  }
14331
14347
  /** The create path of both routes. */
14332
14348
  function guardRuleDevicesExclude(rule, ctx) {
14333
- logRefusal$2(() => assertDevicesExcludeDelivery(rule), ctx);
14349
+ logRefusal$3(() => assertDevicesExcludeDelivery(rule), ctx);
14334
14350
  }
14335
14351
  /**
14336
14352
  * The update path of both routes: the MERGED rule is judged, and only when the
@@ -14340,7 +14356,7 @@ function guardPatchDevicesExclude(existing, patch, ctx) {
14340
14356
  if (existing === null) return;
14341
14357
  const merged = mergedFamilySubject(existing, patch);
14342
14358
  if (merged === null) return;
14343
- logRefusal$2(() => assertDevicesExcludeDelivery(merged), ctx);
14359
+ logRefusal$3(() => assertDevicesExcludeDelivery(merged), ctx);
14344
14360
  }
14345
14361
  //#endregion
14346
14362
  //#region src/notification-center/rule-summary-guard.ts
@@ -14398,7 +14414,7 @@ function mergedDeliverySubject(existing, patch, clear) {
14398
14414
  };
14399
14415
  return JUDGED_KEYS.some((key) => require_clip_model_registry.canonicalHash(merged[key] ?? null) !== require_clip_model_registry.canonicalHash(existing[key] ?? null)) ? merged : null;
14400
14416
  }
14401
- function logRefusal$1(check, ctx) {
14417
+ function logRefusal$2(check, ctx) {
14402
14418
  try {
14403
14419
  check();
14404
14420
  } catch (err) {
@@ -14412,14 +14428,14 @@ function logRefusal$1(check, ctx) {
14412
14428
  }
14413
14429
  /** The create path of both routes. */
14414
14430
  function guardRuleSystemEventDelivery(rule, ctx) {
14415
- logRefusal$1(() => assertSystemEventDelivery(rule), ctx);
14431
+ logRefusal$2(() => assertSystemEventDelivery(rule), ctx);
14416
14432
  }
14417
14433
  /** The update path of both routes: the MERGED rule, when the patch changes what is judged. */
14418
14434
  function guardPatchSystemEventDelivery(existing, patch, clear, ctx) {
14419
14435
  if (existing === null) return;
14420
14436
  const merged = mergedDeliverySubject(existing, patch, clear);
14421
14437
  if (merged === null) return;
14422
- logRefusal$1(() => assertSystemEventDelivery(merged), ctx);
14438
+ logRefusal$2(() => assertSystemEventDelivery(merged), ctx);
14423
14439
  }
14424
14440
  //#endregion
14425
14441
  //#region src/notification-center/sensor-transition-guard.ts
@@ -14441,15 +14457,15 @@ function assertSensorTransitionDelivery(rule) {
14441
14457
  throw new NcSensorTransitionRefusal(rule.delivery, transition);
14442
14458
  }
14443
14459
  /** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
14444
- function logRefusal(check, ctx) {
14460
+ function logRefusal$1(check, ctx) {
14445
14461
  try {
14446
14462
  check();
14447
14463
  } catch (err) {
14448
- if (err instanceof NcSensorTransitionRefusal) warnRefusal(ctx.logger, err, ctx);
14464
+ if (err instanceof NcSensorTransitionRefusal) warnRefusal$1(ctx.logger, err, ctx);
14449
14465
  throw err;
14450
14466
  }
14451
14467
  }
14452
- function warnRefusal(logger, err, ctx) {
14468
+ function warnRefusal$1(logger, err, ctx) {
14453
14469
  logger.warn("nc rule refused: sensorTransition off a device-event rule", { meta: {
14454
14470
  ...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
14455
14471
  by: ctx.by,
@@ -14459,7 +14475,7 @@ function warnRefusal(logger, err, ctx) {
14459
14475
  }
14460
14476
  /** The create path of both routes. */
14461
14477
  function guardRuleSensorTransition(rule, ctx) {
14462
- logRefusal(() => assertSensorTransitionDelivery(rule), ctx);
14478
+ logRefusal$1(() => assertSensorTransitionDelivery(rule), ctx);
14463
14479
  }
14464
14480
  /**
14465
14481
  * The update path of both routes: the MERGED rule is judged, and only when the
@@ -14470,7 +14486,92 @@ function guardPatchSensorTransition(existing, patch, ctx) {
14470
14486
  if (existing === null) return;
14471
14487
  const merged = mergedFamilySubject(existing, patch);
14472
14488
  if (merged === null) return;
14473
- logRefusal(() => assertSensorTransitionDelivery(merged), ctx);
14489
+ logRefusal$1(() => assertSensorTransitionDelivery(merged), ctx);
14490
+ }
14491
+ //#endregion
14492
+ //#region src/notification-center/detection-audio-guard.ts
14493
+ var NcDetectionAudioRefusal = class extends Error {
14494
+ audioClasses;
14495
+ constructor(audioClasses) {
14496
+ super(`rule refused: a detection rule cannot name sounds (${audioClasses.join(", ")}) — create a Sound rule for them`);
14497
+ this.name = "NcDetectionAudioRefusal";
14498
+ this.audioClasses = audioClasses;
14499
+ }
14500
+ };
14501
+ /**
14502
+ * The audio classes a DETECTION rule carries (include and exclude), in stored
14503
+ * order. Empty for a Sound rule (its `audio` condition owns its sounds) and for
14504
+ * a system rule (it has no class scope).
14505
+ */
14506
+ function audioClassesOnDetectionRule(rule) {
14507
+ if (require_clip_model_registry.isSystemDelivery(rule.delivery)) return [];
14508
+ if (rule.conditions.audio !== void 0) return [];
14509
+ return [...rule.conditions.classes ?? [], ...rule.conditions.classesExclude ?? []].filter(require_clip_model_registry.isAudioClassName);
14510
+ }
14511
+ function assertNoAudioClassOnDetectionRule(rule) {
14512
+ const audioClasses = audioClassesOnDetectionRule(rule);
14513
+ if (audioClasses.length > 0) throw new NcDetectionAudioRefusal(audioClasses);
14514
+ }
14515
+ /** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
14516
+ function logRefusal(check, ctx) {
14517
+ try {
14518
+ check();
14519
+ } catch (err) {
14520
+ if (err instanceof NcDetectionAudioRefusal) warnRefusal(ctx.logger, err, ctx);
14521
+ throw err;
14522
+ }
14523
+ }
14524
+ function warnRefusal(logger, err, ctx) {
14525
+ logger.warn("nc rule refused: a detection rule names sounds", { meta: {
14526
+ ...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
14527
+ by: ctx.by,
14528
+ audioClasses: err.audioClasses.join(", ")
14529
+ } });
14530
+ }
14531
+ /** The create path of both routes. */
14532
+ function guardRuleDetectionAudio(rule, ctx) {
14533
+ logRefusal(() => assertNoAudioClassOnDetectionRule(rule), ctx);
14534
+ }
14535
+ /**
14536
+ * The update path of both routes: the MERGED rule, only when the patch changes
14537
+ * it — and only the sounds the save ADDS. A legacy rule that already names a
14538
+ * sound stays editable (a threshold, a schedule, a zone); what is refused is
14539
+ * naming a new one.
14540
+ */
14541
+ function guardPatchDetectionAudio(existing, patch, ctx) {
14542
+ if (existing === null) return;
14543
+ const merged = mergedFamilySubject(existing, patch);
14544
+ if (merged === null) return;
14545
+ const stored = new Set(audioClassesOnDetectionRule(existing).map(normalisedClass));
14546
+ const added = audioClassesOnDetectionRule(merged).filter((name) => !stored.has(normalisedClass(name)));
14547
+ if (added.length === 0) return;
14548
+ logRefusal(() => {
14549
+ throw new NcDetectionAudioRefusal(added);
14550
+ }, ctx);
14551
+ }
14552
+ function normalisedClass(name) {
14553
+ return name.trim().toLowerCase();
14554
+ }
14555
+ /**
14556
+ * The loader's half: every stored detection rule that names a sound is KEPT
14557
+ * (it loads and fires as it always did) and reported at WARN with its id —
14558
+ * once per rule per process, never on every reload tick. Returns the ids it
14559
+ * reported this time, for the caller's bookkeeping.
14560
+ */
14561
+ function warnLegacyAudioClassRules(rules, alreadyWarned, logger) {
14562
+ const reported = [];
14563
+ for (const rule of rules) {
14564
+ if (alreadyWarned.has(rule.id)) continue;
14565
+ const audioClasses = audioClassesOnDetectionRule(rule);
14566
+ if (audioClasses.length === 0) continue;
14567
+ logger.warn("notification rule is a detection rule naming sounds — kept as it is; recreate it as a Sound rule", { meta: {
14568
+ ruleId: rule.id,
14569
+ name: rule.name,
14570
+ audioClasses: audioClasses.join(", ")
14571
+ } });
14572
+ reported.push(rule.id);
14573
+ }
14574
+ return reported;
14474
14575
  }
14475
14576
  //#endregion
14476
14577
  //#region src/notification-center/occupancy-watcher.ts
@@ -17221,6 +17322,10 @@ function makeNcActionHandlers(deps) {
17221
17322
  logger: deps.logger,
17222
17323
  by: c.userId
17223
17324
  });
17325
+ guardRuleDetectionAudio(parsed, {
17326
+ logger: deps.logger,
17327
+ by: c.userId
17328
+ });
17224
17329
  guardRuleDevicesExclude(parsed, {
17225
17330
  logger: deps.logger,
17226
17331
  by: c.userId
@@ -17257,6 +17362,11 @@ function makeNcActionHandlers(deps) {
17257
17362
  by: c.userId,
17258
17363
  ruleId: input.ruleId
17259
17364
  });
17365
+ guardPatchDetectionAudio(existing, patch, {
17366
+ logger: deps.logger,
17367
+ by: c.userId,
17368
+ ruleId: input.ruleId
17369
+ });
17260
17370
  guardPatchDevicesExclude(existing, patch, {
17261
17371
  logger: deps.logger,
17262
17372
  by: c.userId,
@@ -25420,6 +25530,8 @@ var NcRuleStore = class {
25420
25530
  identityIdsByName;
25421
25531
  /** Has a {@link load} ever completed? See the note there. */
25422
25532
  loaded = false;
25533
+ /** Legacy detection rules naming sounds already reported (D690) — once each. */
25534
+ audioClassWarned = /* @__PURE__ */ new Set();
25423
25535
  constructor(deps) {
25424
25536
  this.store = deps.store;
25425
25537
  this.ledger = new DurableLedger({
@@ -25454,6 +25566,7 @@ var NcRuleStore = class {
25454
25566
  async load() {
25455
25567
  await this.ledger.load();
25456
25568
  if (this.ledger.hasAnswered()) this.loaded = true;
25569
+ for (const id of warnLegacyAudioClassRules(this.ledger.snapshot(), this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
25457
25570
  this.logger.debug("notification rules loaded", { meta: { rules: this.ledger.size } });
25458
25571
  }
25459
25572
  /**
@@ -33685,6 +33798,10 @@ var NotificationCenter = class NotificationCenter {
33685
33798
  logger: this.logger,
33686
33799
  by: caller.userId
33687
33800
  });
33801
+ guardRuleDetectionAudio(rule, {
33802
+ logger: this.logger,
33803
+ by: caller.userId
33804
+ });
33688
33805
  guardRuleDevicesExclude(rule, {
33689
33806
  logger: this.logger,
33690
33807
  by: caller.userId
@@ -33716,6 +33833,11 @@ var NotificationCenter = class NotificationCenter {
33716
33833
  by: caller.userId,
33717
33834
  ruleId
33718
33835
  });
33836
+ guardPatchDetectionAudio(existing, patch, {
33837
+ logger: this.logger,
33838
+ by: caller.userId,
33839
+ ruleId
33840
+ });
33719
33841
  guardPatchDevicesExclude(existing, patch, {
33720
33842
  logger: this.logger,
33721
33843
  by: caller.userId,
@@ -67320,6 +67442,22 @@ function messageOf(err) {
67320
67442
  function isDetailNotAttachedRefusal(err) {
67321
67443
  return messageOf(err).includes(DETAIL_NOT_ATTACHED_MARKER);
67322
67444
  }
67445
+ /**
67446
+ * The runner's TYPED handle-expired refusal (D688): the frame handle a dispatch
67447
+ * named is past the native lease age bound, and the registry will refuse it on
67448
+ * every later ask. TWIN of `addon-pipeline/src/pipeline-runner/detail-park.ts`
67449
+ * `DETAIL_HANDLE_EXPIRED_MARKER` — keep the two identical.
67450
+ *
67451
+ * Why the dispatcher cares: unlike not-attached this is about ONE handle, not
67452
+ * the session — the parked frame still serves — but the handle itself is
67453
+ * finished. Naming it again (next tick, or through the native-strict crop rung)
67454
+ * can only be refused again; on the live fleet that was ~7 refusals per handle.
67455
+ */
67456
+ var DETAIL_HANDLE_EXPIRED_MARKER = "runDetailSubtree-refused:handle-expired";
67457
+ /** Did this `runDetailSubtree` error carry the runner's handle-expired marker? */
67458
+ function isDetailHandleExpiredRefusal(err) {
67459
+ return messageOf(err).includes(DETAIL_HANDLE_EXPIRED_MARKER);
67460
+ }
67323
67461
  //#endregion
67324
67462
  //#region src/pipeline-analytics/detail-scheduler.ts
67325
67463
  /** Default backoff/period when a step's cadence omits `minIntervalMs`. */
@@ -67776,6 +67914,7 @@ var TrackDetailDispatcher = class {
67776
67914
  className: frame.className
67777
67915
  };
67778
67916
  const steps = composeDetailSteps(req, (trackId) => this.deps.hasTrackLabel?.(trackId) ?? false);
67917
+ let handleRefused = false;
67779
67918
  if (frame.frameHandle !== void 0) try {
67780
67919
  const primary = await this.deps.runDetailSubtree({
67781
67920
  deviceId,
@@ -67789,7 +67928,10 @@ var TrackDetailDispatcher = class {
67789
67928
  this.warnNotAttachedThrottled(deviceId, dev, req);
67790
67929
  return null;
67791
67930
  }
67792
- this.deps.logger.debug("detail primary call failed — trying frame fallback", {
67931
+ if (isDetailHandleExpiredRefusal(err)) {
67932
+ handleRefused = true;
67933
+ this.retireRefusedHandle(deviceId, dev, req, frame);
67934
+ } else this.deps.logger.debug("detail primary call failed — trying frame fallback", {
67793
67935
  tags: { deviceId },
67794
67936
  meta: {
67795
67937
  trackId: req.trackId,
@@ -67839,7 +67981,7 @@ var TrackDetailDispatcher = class {
67839
67981
  }
67840
67982
  });
67841
67983
  }
67842
- if (this.deps.captureCropBase64 !== void 0) try {
67984
+ if (this.deps.captureCropBase64 !== void 0 && frame.frameHandle !== void 0 && !handleRefused) try {
67843
67985
  const cropJpeg = await this.deps.captureCropBase64(frame);
67844
67986
  if (cropJpeg !== null) {
67845
67987
  const retry = await this.deps.runDetailSubtree({
@@ -67868,6 +68010,35 @@ var TrackDetailDispatcher = class {
67868
68010
  return null;
67869
68011
  }
67870
68012
  /**
68013
+ * The runner refused `frame`'s handle as past the lease age bound (D688).
68014
+ * That verdict is final for the handle, so the track's stored association
68015
+ * loses it: later dispatches skip rung 1 and the native-strict crop rung
68016
+ * (both would name the same handle and be refused again — ~7 times per
68017
+ * handle on the live fleet) and go straight to the parked frame, until a
68018
+ * newly OBSERVED frame brings a fresh handle via {@link onFrame}.
68019
+ *
68020
+ * Only the refused handle is dropped: if a newer frame replaced it while the
68021
+ * dispatch was in flight, that one stands. The line is therefore emitted once
68022
+ * per (track, handle) — a concurrent dispatch refused on the same handle
68023
+ * finds it already gone and says nothing.
68024
+ */
68025
+ retireRefusedHandle(deviceId, dev, req, frame) {
68026
+ const current = dev.tracks.get(req.trackId);
68027
+ if (current === void 0 || current.frameHandle !== frame.frameHandle) return;
68028
+ const { frameHandle: refused, ...withoutHandle } = current;
68029
+ dev.tracks.set(req.trackId, withoutHandle);
68030
+ this.deps.logger.info("detail dispatch: frame handle refused as expired — retired for this track, parked frame only until a fresh observation", {
68031
+ tags: { deviceId },
68032
+ meta: {
68033
+ trackId: req.trackId,
68034
+ stepId: req.stepId,
68035
+ reason: "handle-expired",
68036
+ handle: refused === void 0 ? null : `${refused.shmId}#${refused.slot}#${refused.seq}`,
68037
+ parkedFrameRung: this.deps.captureParkedFrame !== void 0
68038
+ }
68039
+ });
68040
+ }
68041
+ /**
67871
68042
  * The typed refusal is EXPECTED for a track that outlives the runner's
67872
68043
  * detail park grace (a closing track's last dispatches), so it logs its own
67873
68044
  * line — the reason is knowable, unlike the every-rung-missed warn — and