@camstack/addon-post-analysis 1.2.219 → 1.2.221

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,4 +1,4 @@
1
- import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-D0l3xZqa.mjs";
1
+ import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as FailureCounters, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FULL_IMAGE_BBOX, h as FIRST_LEVEL_MACRO_CLASSES, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as LabelAttributionSchema, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-Clp3AeIp.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -19980,6 +19980,14 @@ function checkTimelapsePacing(input) {
19980
19980
  unservedWindows: []
19981
19981
  };
19982
19982
  }
19983
+ //#endregion
19984
+ //#region src/notification-center/timelapse/timelapse-renderer.ts
19985
+ /** `part / whole` to two decimals — the shape a log line is read in. An empty
19986
+ * whole is 0, never NaN: a ratio nobody can compare is not a measurement. */
19987
+ function ratio(part, whole) {
19988
+ if (whole <= 0) return 0;
19989
+ return Math.round(part / whole * 100) / 100;
19990
+ }
19983
19991
  /** Bound on the post-render pacing check. The check is best-effort by charter:
19984
19992
  * a byte read that hangs must delay delivery by at most this, never sink it. */
19985
19993
  var PACING_CHECK_TIMEOUT_MS = 2e4;
@@ -20003,25 +20011,6 @@ function profileCost(profile) {
20003
20011
  const i = PROFILE_BY_COST.indexOf(profile);
20004
20012
  return i === -1 ? PROFILE_BY_COST.length : i;
20005
20013
  }
20006
- /**
20007
- * The CHEAPEST profile that has footage over the window.
20008
- *
20009
- * It used to be the profile with the MOST coverage, ties broken by name — and
20010
- * that is a rule which picks `high` almost every time, because the high band is
20011
- * the one that records continuously. On 2026-09-03 it did: a test render of a
20012
- * ten-hour window chose `profile=high` at 97% coverage, which on this hub means
20013
- * 4K read from a USB spinning disk. Decoding it made the machine stop answering
20014
- * SSH and its own web UI for half an hour, and it had to be power-cycled.
20015
- *
20016
- * A timelapse is watched accelerated. Resolution buys nothing at 12 fps over a
20017
- * night, and the cost difference between the bands is one to two orders of
20018
- * magnitude in both bytes read and frames decoded. So there is no trade to
20019
- * weigh: take the cheapest band that has anything, every time.
20020
- *
20021
- * Coverage no longer decides WHICH — only whether a band qualifies at all. A
20022
- * band with less footage is still the right source; the gaps show as a shorter
20023
- * video, not as a reason to read twenty times the bytes.
20024
- */
20025
20014
  function pickProfile(ranges, window) {
20026
20015
  const byProfile = /* @__PURE__ */ new Map();
20027
20016
  for (const r of ranges) {
@@ -20268,18 +20257,20 @@ var TimelapseRenderer = class {
20268
20257
  const finished = await this.awaitReady(queued, tags);
20269
20258
  const published = await this.publish(finished.id, request, tags);
20270
20259
  const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
20260
+ const archive = await this.measureArchive(finished.id, request, chosen, tags);
20261
+ const deliveredRows = archive?.rows ?? availability.filter((r) => r.profile === chosen.profile);
20271
20262
  await this.verifyPacing({
20272
20263
  exportId: finished.id,
20273
20264
  request,
20274
20265
  dense,
20275
20266
  subjects,
20276
- availability: availability.filter((r) => r.profile === chosen.profile),
20267
+ availability: deliveredRows,
20277
20268
  tags
20278
20269
  });
20279
20270
  return {
20280
20271
  exportId: finished.id,
20281
20272
  profile: chosen.profile,
20282
- coverage: chosen.coverage,
20273
+ coverage: archive?.coverage ?? chosen.coverage,
20283
20274
  plan,
20284
20275
  detections,
20285
20276
  artifact: published.video,
@@ -20387,6 +20378,96 @@ var TimelapseRenderer = class {
20387
20378
  throw new TimelapseRenderError("export-timeout", `export ${queued.id} timed out after ${this.timeoutMs}ms` + (pollFailures > 0 ? ` (${pollFailures} poll failures, last: ${lastPollError})` : ""));
20388
20379
  }
20389
20380
  /**
20381
+ * The window against the archive, measured AFTER the export and always said.
20382
+ *
20383
+ * Three figures, one line: the window's length, what the PLAN was cut from
20384
+ * (the availability read before the export) and what the ARCHIVE holds (the
20385
+ * same read after it — the export's verified walk has repaired the index the
20386
+ * calendar serves from by then). `plannedOfArchive` is the ratio D445 found
20387
+ * at 0.20 on camera 4374 and nothing reported; a healthy night reads 1.00
20388
+ * because both reads return the same rows.
20389
+ *
20390
+ * WHY THE THRESHOLD IS A SLACK AND NOT A RATIO. The two reads are the SAME
20391
+ * query against the SAME index, so on a healthy night they return the same
20392
+ * rows and the shortfall is exactly 0 — there is no distribution to pick a
20393
+ * percentile out of. It is non-zero only when something CHANGED the index
20394
+ * between them, and the export's verified walk is the only thing that does.
20395
+ * So the bar is not "how short is too short" (the robot's night was 0.20 and
20396
+ * a milder one would be just as wrong); it is only "is this a real change or
20397
+ * a boundary artefact". One artefact exists: the window is closed, but a
20398
+ * segment straddling its end can finalize while the export runs and extend
20399
+ * the last range by its own length. {@link COVERAGE_COMPLETE_SLACK_MS} (30 s)
20400
+ * is the slack the coverage figure already allows at exactly those edges,
20401
+ * and it is several segments wide. No second constant, and nothing between
20402
+ * "unchanged" and "the planner rendered from a short answer".
20403
+ *
20404
+ * Never throws. A re-read that fails is SAID, the plan's figure stands, and
20405
+ * every figure that needed the archive is `null` rather than a stand-in.
20406
+ */
20407
+ async measureArchive(exportId, request, chosen, tags) {
20408
+ const { window } = request;
20409
+ const windowSec = Math.round((window.endMs - window.startMs) / 1e3);
20410
+ const plannedSec = Math.round(chosen.coverage.coveredMs / 1e3);
20411
+ let rows;
20412
+ try {
20413
+ rows = (await this.ports.getAvailability({
20414
+ deviceId: request.deviceId,
20415
+ fromMs: window.startMs,
20416
+ toMs: window.endMs,
20417
+ profile: chosen.profile
20418
+ })).ranges.filter((r) => r.profile === chosen.profile);
20419
+ } catch (err) {
20420
+ this.ports.logger.warn("timelapse coverage: archive re-read failed — the window is reported against the plan only", {
20421
+ tags,
20422
+ meta: {
20423
+ exportId,
20424
+ ruleId: request.ruleId,
20425
+ profile: chosen.profile,
20426
+ windowSec,
20427
+ plannedSec,
20428
+ plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
20429
+ archiveSec: null,
20430
+ plannedOfArchive: null,
20431
+ archiveOfWindow: null,
20432
+ error: String(err)
20433
+ }
20434
+ });
20435
+ return null;
20436
+ }
20437
+ const coverage = footageCoverage(rows, window.startMs, window.endMs);
20438
+ const shortfallMs = coverage.coveredMs - chosen.coverage.coveredMs;
20439
+ const meta = {
20440
+ exportId,
20441
+ ruleId: request.ruleId,
20442
+ profile: chosen.profile,
20443
+ windowSec,
20444
+ plannedSec,
20445
+ archiveSec: Math.round(coverage.coveredMs / 1e3),
20446
+ /** What the operator ASKED for against what the file HOLDS — the ratio
20447
+ * the robot's night made someone file a report over: ten hours in,
20448
+ * 6.4 s of video out. Low is not by itself a defect (a lazy source is
20449
+ * legitimately idle most of a night), which is why it never gates. */
20450
+ plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
20451
+ archiveOfWindow: ratio(coverage.coveredMs, window.endMs - window.startMs),
20452
+ /** The one that IS a defect: what the planner saw against what the
20453
+ * export's verified walk found. 0.20 on camera 4374, 1.00 when healthy. */
20454
+ plannedOfArchive: ratio(chosen.coverage.coveredMs, coverage.coveredMs),
20455
+ shortfallSec: Math.round(shortfallMs / 1e3)
20456
+ };
20457
+ if (shortfallMs > 3e4) this.ports.logger.warn("timelapse coverage: the plan was cut from less footage than the archive holds", {
20458
+ tags,
20459
+ meta
20460
+ });
20461
+ else this.ports.logger.info("timelapse coverage: the window against the archive", {
20462
+ tags,
20463
+ meta
20464
+ });
20465
+ return {
20466
+ rows,
20467
+ coverage
20468
+ };
20469
+ }
20470
+ /**
20390
20471
  * Run the pacing check against the finished file, and SAY what it found.
20391
20472
  *
20392
20473
  * Never throws and never blocks past its bound: the check is telemetry about
@@ -28409,6 +28490,34 @@ var MEDIA_INDEXES = [
28409
28490
  "deviceId",
28410
28491
  "sizeBytes"
28411
28492
  ]
28493
+ }),
28494
+ (
28495
+ /**
28496
+ * The per-LOCATION footprint — `COUNT(*)`/`SUM(sizeBytes) WHERE locationId
28497
+ * = ?`, and the legacy `locationId IS NULL` half of the same question. See
28498
+ * {@link MediaStore.footprintByLocation} (D388) and D447.
28499
+ *
28500
+ * **Why not `idx_media_location`.** It leads with `locationId`, so it finds
28501
+ * the range — and then, exactly as the `idx_media_device_size` note above
28502
+ * describes for `deviceId`, fetches the table row behind every entry to read
28503
+ * `sizeBytes`. Every row of the store is on SOME location, so the range is
28504
+ * the table: measured 2026-09-10/11 on `hub/sqlite-settings`, 4 128–6 962 ms
28505
+ * per statement, every five minutes (the occupancy refresh), all night —
28506
+ * `plan=["SEARCH … USING INDEX idx_media_location (locationId=?)"]` on every
28507
+ * WARN. That thread is synchronous, so every configuration read in the
28508
+ * cluster queued behind it: the orchestrator's `configMs=6640` at 23:26:11
28509
+ * is this statement's 6 002 ms at 23:26:10.
28510
+ *
28511
+ * With `sizeBytes` in the index the planner never touches the table
28512
+ * (`USING COVERING INDEX idx_media_location_size`), and the seal gate keeps
28513
+ * its own narrower index. Same shape, same one-off caveat as its two
28514
+ * siblings: ~30 bytes a row, one more b-tree per insert, and the first
28515
+ * `declareCollection` after this ships builds it over every existing row —
28516
+ * do not deploy it beside a running drain or seal.
28517
+ */
28518
+ {
28519
+ name: "idx_media_location_size",
28520
+ columns: ["locationId", "sizeBytes"]
28412
28521
  })
28413
28522
  ];
28414
28523
  /** The storage location of a media row/record: its stamped `locationId`, or
@@ -46396,6 +46505,17 @@ var AudioDetectionSettingsSchema = object({
46396
46505
  audioConfirmHits: number().int().min(1).default(2),
46397
46506
  /** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
46398
46507
  audioConfirmWindowSec: number().positive().default(10),
46508
+ /**
46509
+ * How far above the camera's own 10 s mean a sample must sit to be a peak.
46510
+ *
46511
+ * A DIFFERENCE between two readings on the same camera, so a uniform scale
46512
+ * offset cancels exactly and the D459 correction did not touch it. Proved,
46513
+ * not assumed — D459 enumerates this alongside `audioMarkerMinDeviationDb`,
46514
+ * `audioMarkerMinDeviations` and the EWMA/MAD pair. "Correcting" it by
46515
+ * 12 dB would be introducing the very error the epoch removed.
46516
+ *
46517
+ * @dbfs-era relative
46518
+ */
46399
46519
  levelDeviationDb: number().positive().default(10),
46400
46520
  levelWindowSec: number().positive().default(10),
46401
46521
  levelMaxWaitSec: number().positive().default(20),
@@ -46405,14 +46525,44 @@ var AudioDetectionSettingsSchema = object({
46405
46525
  /** EWMA time constant for the camera's "normal" loudness, in seconds. Also
46406
46526
  * the warm-up: no marker until one time constant of audio has been heard. */
46407
46527
  audioMarkerBaselineSec: number().positive().default(300),
46408
- /** "High volume", absolute. A sample quieter than this is never a marker
46409
- * however surprising it is for the camera. */
46528
+ /**
46529
+ * "High volume", absolute. A sample quieter than this is never a marker
46530
+ * however surprising it is for the camera.
46531
+ *
46532
+ * NOT shifted at the D459 epoch, for the same reason as `SILENCE_FLOOR_DBFS`
46533
+ * and with one extra argument of its own. `-25 dBFS is loud` is a statement
46534
+ * about the scale, not a measurement taken through the old decoder: nobody
46535
+ * read it off a meter, it is what "loud" means on a 16-bit full-scale range.
46536
+ *
46537
+ * The extra argument is that on the old scale this default was effectively
46538
+ * DEAD on the 19 mis-decoded cameras — a reading of -25 there required a
46539
+ * true level of -13 dBFS, which is a sound close to clipping. Bar 1 almost
46540
+ * never bound on them, so the feature has been running on two of its three
46541
+ * bars. At the epoch it starts working as designed rather than becoming too
46542
+ * permissive.
46543
+ *
46544
+ * That it also ships OFF by default (`audioMarkerEnabled`) is what makes
46545
+ * this safe to leave alone: the operator sizes the rate from the `wouldFire`
46546
+ * counter before paying for it, and that counter is now measuring the real
46547
+ * thing for the first time.
46548
+ *
46549
+ * @dbfs-era itu
46550
+ */
46410
46551
  audioMarkerMinDbfs: number().max(0).default(-25),
46411
46552
  /** "Out of the ordinary": how many mean-absolute-deviations of the camera's
46412
46553
  * OWN jitter the sample must sit above its own mean. */
46413
46554
  audioMarkerMinDeviations: number().positive().default(4),
46414
- /** Floor in dB on the distance above the camera's own mean — what holds
46415
- * when a dead-steady camera's deviation unit collapses toward zero. */
46555
+ /**
46556
+ * Floor in dB on the distance above the camera's own mean what holds when
46557
+ * a dead-steady camera's deviation unit collapses toward zero.
46558
+ *
46559
+ * A distance between two readings on the same camera: scale-invariant, and
46560
+ * untouched by D459. The numerical coincidence with the PCMU correction
46561
+ * (12 dB) is exactly that — a coincidence, and the reason this note exists
46562
+ * is so nobody reads it as one of the shifted numbers.
46563
+ *
46564
+ * @dbfs-era relative
46565
+ */
46416
46566
  audioMarkerMinDeviationDb: number().positive().default(12),
46417
46567
  /** Audio matters most when nothing visual is happening. A marker is
46418
46568
  * suppressed while a track is alive on the camera, or motion was reported,
@@ -64926,6 +65076,10 @@ var STALE_BASELINE_FACTOR = 5;
64926
65076
  * has MAD 0 and would make every deviation infinite. 1 dB is below the
64927
65077
  * quantisation of any real level meter, so it only ever binds on a synthetic
64928
65078
  * or a genuinely dead-steady signal — where bar 2 is what should decide.
65079
+ *
65080
+ * A floor on a DEVIATION, not on a level: scale-invariant, untouched by D459.
65081
+ *
65082
+ * @dbfs-era relative
64929
65083
  */
64930
65084
  var MIN_MAD_DB = 1;
64931
65085
  function emptyAudioBaseline() {
@@ -65209,6 +65363,44 @@ function buildAudioLabels(input) {
65209
65363
  }];
65210
65364
  }
65211
65365
  //#endregion
65366
+ //#region src/pipeline-analytics/services/event-kinds-config.ts
65367
+ /** The honest answer for a pipeline that could not be read: nothing advertised. */
65368
+ var NO_EMITTABLE = {
65369
+ macroClasses: [],
65370
+ audioEnabled: false
65371
+ };
65372
+ /** Depth-first walk of the step tree; the detector is a root today, but the
65373
+ * question is "is it enabled anywhere", not "is it a root". */
65374
+ function* walkSteps(steps) {
65375
+ for (const step of steps) {
65376
+ yield step;
65377
+ if (step.children !== void 0) yield* walkSteps(step.children);
65378
+ }
65379
+ }
65380
+ /**
65381
+ * The macro classes an enabled `object-detection` step emits. Order follows
65382
+ * the authority, not the stored array, so two cameras narrowed to the same
65383
+ * set render the same lanes in the same order.
65384
+ */
65385
+ function macroClassesOf(step) {
65386
+ const stored = step.settings?.["enabledMacroClasses"];
65387
+ if (!Array.isArray(stored) || stored.length === 0) return [...FIRST_LEVEL_MACRO_CLASSES];
65388
+ const chosen = new Set(stored.filter((v) => typeof v === "string"));
65389
+ return FIRST_LEVEL_MACRO_CLASSES.filter((macro) => chosen.has(macro));
65390
+ }
65391
+ function emittableFromPipelineConfig(config) {
65392
+ let macroClasses = [];
65393
+ for (const step of walkSteps(config.steps)) {
65394
+ if (step.addonId !== "object-detection" || step.enabled === false) continue;
65395
+ macroClasses = macroClassesOf(step);
65396
+ break;
65397
+ }
65398
+ return {
65399
+ macroClasses,
65400
+ audioEnabled: config.audio?.enabled === true
65401
+ };
65402
+ }
65403
+ //#endregion
65212
65404
  //#region src/pipeline-analytics/services/event-kinds.ts
65213
65405
  /**
65214
65406
  * Extensible per-device event kinds (Part B).
@@ -78116,28 +78308,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78116
78308
  };
78117
78309
  }
78118
78310
  /**
78119
- * Every event kind the device can produce: built-ins (motion + audio),
78120
- * detection classes actually observed on the device, and sensor kinds
78121
- * from LINKED devices (device-manager `getLinkedDevices`, binding-driven
78122
- * per linked device). Degrades to the built-ins on error.
78311
+ * Every event kind the device can produce: built-ins (motion + audio), the
78312
+ * detection classes its CONFIGURED pipeline can emit, and sensor kinds from
78313
+ * LINKED devices (device-manager `getLinkedDevices`, binding-driven per
78314
+ * linked device). Degrades to the built-ins on error.
78315
+ *
78316
+ * The detection and audio facts are read from `resolvePipeline` — the
78317
+ * configuration — and never from `getCameraStatus`, which is RUNTIME: an
78318
+ * on-motion camera holds no session between motion events
78319
+ * (`reasons.detection: 'armed:on-motion'`, `detection: null`) and was being
78320
+ * advertised as detecting nothing while its tracks said person / vehicle /
78321
+ * animal ([D449](../../../../docs/decisions/adr-0449-the-timeline-taxonomy-is-the-configured-one-raw-forward-has-no-switch-and-a-one-row-write-waits-on-the-disk.md)).
78123
78322
  */
78124
78323
  async listEventKinds(input) {
78125
78324
  const api = this.ctx.api;
78126
- const status = await api.pipelineOrchestrator.getCameraStatus.query({ deviceId: input.deviceId }).catch((err) => {
78127
- this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
78128
- tags: { deviceId: input.deviceId },
78129
- meta: { error: errMsg$1(err) }
78130
- });
78131
- return null;
78132
- });
78133
- const detectionOn = status?.detection != null;
78134
- const audioOn = status?.audio?.enabled === true;
78135
- const detectionMacros = MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
78325
+ const emittable = await this.emittableFromConfig(input.deviceId, "listEventKinds");
78136
78326
  return composeEventKinds({
78137
78327
  linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
78138
78328
  bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
78139
- enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
78140
- audioEnabled: async () => audioOn,
78329
+ enabledMacroClasses: async () => emittable.macroClasses,
78330
+ audioEnabled: async () => emittable.audioEnabled,
78141
78331
  packageZonesEnabled: async (deviceId) => {
78142
78332
  return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
78143
78333
  },
@@ -78151,6 +78341,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78151
78341
  }, input.deviceId);
78152
78342
  }
78153
78343
  /**
78344
+ * What one camera's configured pipeline can emit (`resolvePipeline`), or
78345
+ * the honest fallback when the orchestrator could not answer: nothing
78346
+ * advertised, and a WARN saying so. The cap's answer is a plain list, so it
78347
+ * has no room for "unknown" — that residual of D315 is named in D449; the
78348
+ * steady state (an armed camera with no session) is no longer in it.
78349
+ */
78350
+ async emittableFromConfig(deviceId, scope) {
78351
+ try {
78352
+ return emittableFromPipelineConfig(await this.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }));
78353
+ } catch (err) {
78354
+ this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
78355
+ tags: { deviceId },
78356
+ meta: { error: errMsg$1(err) }
78357
+ });
78358
+ return NO_EMITTABLE;
78359
+ }
78360
+ }
78361
+ /**
78154
78362
  * `listEventKinds` for many cameras, in one call.
78155
78363
  *
78156
78364
  * The saving is not the round-trips the CALLER avoids — it is the fan-out
@@ -78162,11 +78370,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78162
78370
  * `getLinkedDevices` alone took 4110 ms — each one runs a full-fleet
78163
78371
  * `listAll` inside device-manager and thirty of them QUEUE.
78164
78372
  *
78165
- * Now three calls for the whole set, whatever N is: one
78166
- * `getCameraStatuses`, then `prefetchDeviceTopology`'s
78167
- * `getLinkedDevicesBatch` + `getBindingsBatch`. `composeEventKinds` is
78168
- * unchanged it is handed the same per-device client interfaces, backed by
78169
- * maps instead of RPC.
78373
+ * Now two calls for the whole topology, whatever N is
78374
+ * `prefetchDeviceTopology`'s `getLinkedDevicesBatch` + `getBindingsBatch`
78375
+ * plus one `resolvePipeline` per camera, all in flight at once (D449; there
78376
+ * is no batch form of it). `composeEventKinds` is unchanged it is handed
78377
+ * the same per-device client interfaces, backed by maps instead of RPC.
78170
78378
  *
78171
78379
  * A camera whose composition fails yields an EMPTY list rather than dropping
78172
78380
  * out of the response: a caller that asked for twelve and got eleven cannot
@@ -78175,15 +78383,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78175
78383
  async listEventKindsBatch(input) {
78176
78384
  const api = this.ctx.api;
78177
78385
  const deviceIds = [...new Set(input.deviceIds)];
78178
- const statuses = await api.pipelineOrchestrator.getCameraStatuses.query({ deviceIds }).catch((err) => {
78179
- this.ctx.logger.warn("listEventKindsBatch: getCameraStatuses failed", { meta: {
78180
- deviceCount: deviceIds.length,
78181
- error: errMsg$1(err)
78182
- } });
78183
- return [];
78184
- });
78185
- const statusByDevice = new Map(statuses.map((st) => [st.deviceId, st]));
78186
- const detectionMacros = MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
78386
+ const emittableByDevice = new Map(await Promise.all(deviceIds.map(async (deviceId) => [deviceId, await this.emittableFromConfig(deviceId, "listEventKindsBatch")])));
78187
78387
  const topology = await prefetchDeviceTopology({
78188
78388
  getLinkedDevicesBatch: (i) => api.deviceManager.getLinkedDevicesBatch.query(i),
78189
78389
  getBindingsBatch: (i) => api.deviceManager.getBindingsBatch.query(i)
@@ -78195,14 +78395,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78195
78395
  return null;
78196
78396
  });
78197
78397
  return composeEventKindsForDevices(deviceIds, async (deviceId) => {
78198
- const status = statusByDevice.get(deviceId) ?? null;
78199
- const detectionOn = status?.detection != null;
78200
- const audioOn = status?.audio?.enabled === true;
78398
+ const emittable = emittableByDevice.get(deviceId) ?? NO_EMITTABLE;
78201
78399
  return composeEventKinds({
78202
78400
  linkedDevices: topology?.linkedDevices ?? { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
78203
78401
  bindings: topology?.bindings ?? { getBindings: (i) => api.deviceManager.getBindings.query(i) },
78204
- enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
78205
- audioEnabled: async () => audioOn,
78402
+ enabledMacroClasses: async () => emittable.macroClasses,
78403
+ audioEnabled: async () => emittable.audioEnabled,
78206
78404
  packageZonesEnabled: async (id) => {
78207
78405
  return (await this.resolveDevicePackageRules(id)).some((r) => r.enabled !== false);
78208
78406
  },
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C8d8YCvw.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BVv-ddKp.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.219",
3
+ "version": "1.2.221",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",