@camstack/addon-post-analysis 1.2.308 → 1.2.309

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 NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, $n as ruleDeviceCompatCapQueries, $t as TimelapseRuleInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as isDetectionMacroClass, Ar as ZodObject, At as NcTemplateVarDescriptorSchema, B as ListTrackRelationsInputSchema, Bn as memoizePinnedFfmpeg, Br as record, Bt as SCENE_DEFAULT_ANCHOR_THRESHOLD, C as DEFAULT_EVENT_COLOR, Cn as encodeVectorBase64, Cr as customAction, Ct as NcSnoozeInputSchema, D as DeclaredDevices, Dn as gifDeriveSpeed, Dr as nodePin, Dt as NcTaxonomySchema, E as DETECTION_PIPELINE_CAP_NAME, En as failureContributionCapability, Er as isDeviceScopedCap, Et as NcSystemEventKindSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as isSystemDelivery, Fr as literal, Ft as RECORDING_SOURCE_CAMSTACK_ADDON, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as plateGalleryCapability, Gr as unknown, Gt as SCENE_DIVERGED, H as LockStateSchema, Hn as occupancyScope, Hr as string, Ht as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, I as FIRST_LEVEL_MACRO_CLASSES, In as isSystemEventSummaryRule, Ir as looseObject, It as RebuildObjectEmbeddingsRefusalSchema, J as MOTION_CLOSE_AFTER_MS, Jn as readTimelapseGeneratedAt, Jt as SetTrackPurityInputSchema, K as MAX_TRACK_PURITIES, Kn as readDeviceStateFrom, Kr as EventCategory, Kt as SCENE_RESET_RECAPTURES, L as FULL_IMAGE_BBOX, Ln as isVectorIndexDimConflict, Lr as number, Lt as RetrainStatusSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as isKnownTemplateVar, Mr as array, Mt as OpsLogEntrySchema, N as EVENT_KIND_BY_CAP, Nn as isMediaOwnerType, Nr as boolean, O as DetectionSourceSchema, On as gifFromMp4Args, Ot as NcTemplatePreviewInputSchema, P as EVENT_OWNER_TYPES, Pn as isScheduleActive, Pr as discriminatedUnion, Pt as RECORDING_EXPORT_MAX_READ_BYTES, Q as NC_AUDIO_CONFIRM_HITS_DEFAULT, Qn as ruleBelongsToAlarm$1, Qt as TRACK_RELATIONS_DEFAULT_LIMIT, R as FailureCounters, Rn as kebabToCamel, Rr as object, Rt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, S as CoverStateSchema, Sr as createEvent, St as NcScheduleSchema, T as DETECTION_MACRO_CLASSES, Tn as faceGalleryCapability, Tr as hydrateSchema, Tt as NcSnoozeSuppressedSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Ur as tuple, Ut as SCENE_DEFAULT_QUIET_SECONDS, V as ListTrackRelationsResultSchema, Vn as notificationRulesCapability, Vr as strictObject, Vt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, W as MAX_BIRTH_DECISION_RECORDS, Wn as pipelineAnalyticsCapability, Wr as union, Wt as SCENE_DEFAULT_UNCOVERED_POLICY, X as MotionTriggeredBySchema, Xn as resolveLocationMode, Xt as SetTrackRelationInputSchema, Y as MediaFileKindEnum, Yn as resolveClipSearchMinScore, Yt as SetTrackPurityResultSchema, Z as NC_ALARM_SYSTEM_EVENT_KINDS, Zt as TIMELAPSE_DENSE_FLOOR_SEC, _ as BaseDevice, _n as describeSystemEventFamilyProblem, _r as isSourceCap, _t as NcRuleInputSchema, a as CLIP_EMBEDDING_STEP_ID, an as TrackSideSchema, ar as subKindsOf, at as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, b as ClearTrackRelationInputSchema, bn as embeddingRefusalOf, br as BaseAddon, bt as NcRuleTargetSchema, c as resolveClusterModelPin, cn as alarmPanelCapability, cr as systemEventFilterApplies, ct as NC_SNOOZE_MAX_MINUTES, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as audioModeOf, dr as templateVarsFor, dt as NC_TAXONOMY, en as TimelapseRulePatchSchema, er as ruleKindOf, et as NC_CLIP_POST_ROLL_DEFAULT_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as buildEventKindDescriptor, fr as vectorDimFromBase64, ft as NC_TEMPLATE_VARS, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as describeSystemEventDeliveryProblem, gr as evaluateSensorEdge, gt as NcConditionsSchema, h as ArchivedDebugNoteSchema, hn as deriveRecordingMode, hr as canonicalHash, ht as NcConditionDescriptorSchema, in as TrackRelationSchema, ir as storageOccupancyCapability, it as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as isEventOwnerType, jr as _enum, jt as OPS_LOG_DEFAULT_LIMIT, k as EMBEDDING_MODEL_FAILED, kn as isAudioClassName, kr as sleep$1, kt as NcTemplatePreviewSchema, l as APPEARANCE_BIN_COUNT, ln as assertTimelapseCadences, lr as systemEventSummaryScheduleOf, lt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, m as AlarmArmModeSchema, mn as cosineSimilarity$1, mr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, mt as NcAlarmSettingsPatchSchema, nn as TrackPuritySchema, nr as sensorRaisedOf, nt as NC_CLIP_RETENTION_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, on as TrackSourceSchema, or as systemEventDeliveryProblems, ot as NC_DEFAULT_SNOOZE_MINUTES, p as AUDIO_MACRO_LABELS, pn as consumableLowPercentOf, pr as zoneAnalyticsCapability, pt as NcAlarmConfigSchema, q as MAX_TRACK_RELATIONS, qn as readMotionTriggeredBy, qt as SceneMonitorSchema, rn as TrackRelationKindSchema, rr as servedTemplateVars, rt as NC_CONDITION_CATALOG, s as readClusterModelPinStrict, sn as addonWidgetsSourceCapability, sr as systemEventFamilyProblems, st as NC_SENSOR_TRANSITION_KINDS, t as BUILTIN_CLIP_MODELS, tn as TimelapseRuleSchema, tr as sceneMonitorCapability, tt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, u as APPEARANCE_BLOCKS, un as audioMetricsCapability, ur as templateCarriesSummary, ut as NC_TANK_SYSTEM_EVENT_KINDS, v as BirthDecisionRecordSchema, vr as DeviceType, vt as NcRulePatchSchema, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as evictionPolicyOfLocation, wr as defineCustomActions, wt as NcSnoozeSchema, x as ClearTrackRelationResultSchema, xr as CamProfileSchema, xt as NcRuleUpdateInputSchema, y as COCO_TO_MACRO, yr as errMsg, yt as NcRuleSchema, z as LabelAttributionSchema, zn as mayWriteToLocation, zr as partialRecord, zt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS } from "../clip-model-registry-FGOIz3PZ.mjs";
1
+ import { $ as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, $n as ruleDeviceCompatCapQueries, $t as TimelapseRuleInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as isDetectionMacroClass, Ar as sleep$1, At as NcTemplateVarDescriptorSchema, B as ListTrackRelationsInputSchema, Bn as memoizePinnedFfmpeg, Br as partialRecord, Bt as SCENE_DEFAULT_ANCHOR_THRESHOLD, C as DEFAULT_EVENT_COLOR, Cn as encodeVectorBase64, Cr as createEvent, Ct as NcSnoozeInputSchema, D as DeclaredDevices, Dn as gifDeriveSpeed, Dr as isDeviceScopedCap, Dt as NcTaxonomySchema, E as DETECTION_PIPELINE_CAP_NAME, En as failureContributionCapability, Er as hydrateSchema, Et as NcSystemEventKindSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as isSystemDelivery, Fr as discriminatedUnion, Ft as RECORDING_SOURCE_CAMSTACK_ADDON, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as plateGalleryCapability, Gr as union, Gt as SCENE_DIVERGED, H as LockStateSchema, Hn as occupancyScope, Hr as strictObject, Ht as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, I as FIRST_LEVEL_MACRO_CLASSES, In as isSystemEventSummaryRule, Ir as literal, It as RebuildObjectEmbeddingsRefusalSchema, J as MOTION_CLOSE_AFTER_MS, Jn as readTimelapseGeneratedAt, Jt as SetTrackPurityInputSchema, K as MAX_TRACK_PURITIES, Kn as readDeviceStateFrom, Kr as unknown, Kt as SCENE_RESET_RECAPTURES, L as FULL_IMAGE_BBOX, Ln as isVectorIndexDimConflict, Lr as looseObject, Lt as RetrainStatusSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as isKnownTemplateVar, Mr as _enum, Mt as OpsLogEntrySchema, N as EVENT_KIND_BY_CAP, Nn as isMediaOwnerType, Nr as array, O as DetectionSourceSchema, On as gifFromMp4Args, Or as nodePin, Ot as NcTemplatePreviewInputSchema, P as EVENT_OWNER_TYPES, Pn as isScheduleActive, Pr as boolean, Pt as RECORDING_EXPORT_MAX_READ_BYTES, Q as NC_AUDIO_CONFIRM_HITS_DEFAULT, Qn as ruleBelongsToAlarm$1, Qt as TRACK_RELATIONS_DEFAULT_LIMIT, R as FailureCounters, Rn as kebabToCamel, Rr as number, Rt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, S as CoverStateSchema, Sr as CamProfileSchema, St as NcScheduleSchema, T as DETECTION_MACRO_CLASSES, Tn as faceGalleryCapability, Tr as defineCustomActions, Tt as NcSnoozeSuppressedSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Ur as string, Ut as SCENE_DEFAULT_QUIET_SECONDS, V as ListTrackRelationsResultSchema, Vn as notificationRulesCapability, Vr as record, Vt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, W as MAX_BIRTH_DECISION_RECORDS, Wn as pipelineAnalyticsCapability, Wr as tuple, Wt as SCENE_DEFAULT_UNCOVERED_POLICY, X as MotionTriggeredBySchema, Xn as resolveLocationMode, Xt as SetTrackRelationInputSchema, Y as MediaFileKindEnum, Yn as resolveClipSearchMinScore, Yt as SetTrackPurityResultSchema, Z as NC_ALARM_SYSTEM_EVENT_KINDS, Zt as TIMELAPSE_DENSE_FLOOR_SEC, _ as BaseDevice, _n as describeSystemEventFamilyProblem, _r as evaluateSensorEdge, _t as NcRuleInputSchema, a as CLIP_EMBEDDING_STEP_ID, an as TrackSideSchema, ar as subKindsOf, at as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, b as ClearTrackRelationInputSchema, bn as embeddingRefusalOf, br as errMsg, bt as NcRuleTargetSchema, c as resolveClusterModelPin, cn as alarmPanelCapability, cr as systemEventFilterApplies, ct as NC_SNOOZE_MAX_MINUTES, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as audioModeOf, dr as templateCarriesSummary, dt as NC_TAXONOMY, en as TimelapseRulePatchSchema, er as ruleKindOf, et as NC_CLIP_POST_ROLL_DEFAULT_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as buildEventKindDescriptor, fr as templateVarsFor, ft as NC_TEMPLATE_VARS, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as describeSystemEventDeliveryProblem, gr as canonicalHash, gt as NcConditionsSchema, h as ArchivedDebugNoteSchema, hn as deriveRecordingMode, hr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, ht as NcConditionDescriptorSchema, in as TrackRelationSchema, ir as storageOccupancyCapability, it as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as isEventOwnerType, jr as ZodObject, jt as OPS_LOG_DEFAULT_LIMIT, k as EMBEDDING_MODEL_FAILED, kn as isAudioClassName, kt as NcTemplatePreviewSchema, l as APPEARANCE_BIN_COUNT, ln as assertTimelapseCadences, lr as systemEventSummaryScheduleOf, lt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, m as AlarmArmModeSchema, mn as cosineSimilarity$1, mr as zoneAnalyticsCapability, mt as NcAlarmSettingsPatchSchema, nn as TrackPuritySchema, nr as sensorRaisedOf, nt as NC_CLIP_RETENTION_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, on as TrackSourceSchema, or as systemEventDeliveryProblems, ot as NC_DEFAULT_SNOOZE_MINUTES, p as AUDIO_MACRO_LABELS, pn as consumableLowPercentOf, pr as vectorDimFromBase64, pt as NcAlarmConfigSchema, q as MAX_TRACK_RELATIONS, qn as readMotionTriggeredBy, qr as EventCategory, qt as SceneMonitorSchema, rn as TrackRelationKindSchema, rr as servedTemplateVars, rt as NC_CONDITION_CATALOG, s as readClusterModelPinStrict, sn as addonWidgetsSourceCapability, sr as systemEventFamilyProblems, st as NC_SENSOR_TRANSITION_KINDS, t as BUILTIN_CLIP_MODELS, tn as TimelapseRuleSchema, tr as sceneMonitorCapability, tt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, u as APPEARANCE_BLOCKS, un as audioMetricsCapability, ur as systemEventSummarySplitOf, ut as NC_TANK_SYSTEM_EVENT_KINDS, v as BirthDecisionRecordSchema, vr as isSourceCap, vt as NcRulePatchSchema, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as evictionPolicyOfLocation, wr as customAction, wt as NcSnoozeSchema, x as ClearTrackRelationResultSchema, xr as BaseAddon, xt as NcRuleUpdateInputSchema, y as COCO_TO_MACRO, yr as DeviceType, yt as NcRuleSchema, z as LabelAttributionSchema, zn as mayWriteToLocation, zr as object, zt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS } from "../clip-model-registry-CrBn6Qvt.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -13484,6 +13484,8 @@ var en_default = {
13484
13484
  "summary.body.empty": "nothing was seen in this window",
13485
13485
  "system.state-summary.title": "{{rule}}",
13486
13486
  "system.state-summary.body": "{{summaryText}}",
13487
+ "system.state-summary.section.title": "{{rule}} — {{summarySection}}",
13488
+ "system.state-summary.section.body": "{{summaryText}}",
13487
13489
  "summary.section.device-state": "Devices offline or switched off",
13488
13490
  "summary.section.streams": "Streams offline",
13489
13491
  "summary.section.battery": "Low batteries",
@@ -13812,6 +13814,8 @@ var it_default = {
13812
13814
  "summary.body.empty": "nessun evento in questa finestra",
13813
13815
  "system.state-summary.title": "{{rule}}",
13814
13816
  "system.state-summary.body": "{{summaryText}}",
13817
+ "system.state-summary.section.title": "{{rule}} — {{summarySection}}",
13818
+ "system.state-summary.section.body": "{{summaryText}}",
13815
13819
  "summary.section.device-state": "Dispositivi offline o spenti",
13816
13820
  "summary.section.streams": "Stream offline",
13817
13821
  "summary.section.battery": "Batterie scariche",
@@ -15757,6 +15761,38 @@ function planRuleTest(input) {
15757
15761
  };
15758
15762
  }
15759
15763
  //#endregion
15764
+ //#region src/notification-center/state-summary/state-summary-test.ts
15765
+ /**
15766
+ * What TESTING a state-summary rule answers (D698, `nc.testRule`).
15767
+ *
15768
+ * A summary rule sends nothing per event (D647), so the per-event test the
15769
+ * other rules get — one synthetic event, injected — can never deliver for it.
15770
+ * Its test builds the summary NOW and enqueues it; this is the report: the
15771
+ * rows it enqueued (one, or one per section) and every section's read, so
15772
+ * "nothing to report" is an answer with its reason per section, never a
15773
+ * silence.
15774
+ */
15775
+ var NcStateSummaryTestSchema = object({
15776
+ /** The test's record id — a per-section row appends `:<sectionId>`. */
15777
+ recordId: string(),
15778
+ split: _enum(["rule", "section"]),
15779
+ /** Nothing open and every section read: nothing was enqueued. */
15780
+ nothingToReport: boolean(),
15781
+ sections: array(object({
15782
+ id: string(),
15783
+ status: _enum(["read", "unavailable"]),
15784
+ items: number().int().nonnegative(),
15785
+ reason: string().optional()
15786
+ })),
15787
+ rows: array(object({
15788
+ recordId: string(),
15789
+ /** The section of a per-section row. */
15790
+ section: string().optional(),
15791
+ enqueued: number().int().nonnegative(),
15792
+ targetIds: array(string())
15793
+ }))
15794
+ });
15795
+ //#endregion
15760
15796
  //#region src/notification-center/clock-text.ts
15761
15797
  /**
15762
15798
  * `{{time}}` — hours and minutes in the NOTIFICATION language.
@@ -17109,13 +17145,18 @@ var ncActions = defineCustomActions({
17109
17145
  deviceId: number().int().optional()
17110
17146
  }), NcTestEventResultSchema.extend({
17111
17147
  /** The subject kind the rule was tested with — the answer to "why was my
17112
- * rule not a candidate". */
17148
+ * rule not a candidate". `state-summary` for a summary rule (D698). */
17113
17149
  trigger: string(),
17114
17150
  /** What the plan could not reproduce (a device-state gate, a polygon). */
17115
17151
  planNotes: array(object({
17116
17152
  conditionId: string(),
17117
17153
  note: string()
17118
- }))
17154
+ })),
17155
+ /**
17156
+ * A STATE-SUMMARY rule's test (D698): the summary built now and the rows
17157
+ * it enqueued, or why nothing was — absent on every other rule.
17158
+ */
17159
+ stateSummary: NcStateSummaryTestSchema.optional()
17119
17160
  }), {
17120
17161
  kind: "mutation",
17121
17162
  auth: "admin",
@@ -17474,6 +17515,48 @@ function makeNcActionHandlers(deps) {
17474
17515
  if (rule.ownerUserId !== void 0 && rule.ownerUserId !== caller.userId) throw new Error(`forbidden: rule not owned: ${ruleId}`);
17475
17516
  return rule;
17476
17517
  };
17518
+ /**
17519
+ * TEST a STATE-SUMMARY rule (D698). The per-event injection cannot deliver
17520
+ * for it — a summary rule sends nothing per event (D647), and that test used
17521
+ * to be dropped without a word — so the summary is built NOW and enqueued.
17522
+ * The answer is shaped like every rule test's, plus the summary's own report.
17523
+ */
17524
+ const testSummaryRule = async (rule, by) => {
17525
+ const test = deps.testStateSummary;
17526
+ if (test === void 0) throw new Error("this node does not host the notification centre — test on the hub");
17527
+ deps.logger.info("rule test requested", { meta: {
17528
+ by,
17529
+ ruleId: rule.id,
17530
+ ruleName: rule.name,
17531
+ trigger: "state-summary",
17532
+ bypassMute: true
17533
+ } });
17534
+ const report = await test(rule);
17535
+ const enqueued = report.rows.reduce((sum, row) => sum + row.enqueued, 0);
17536
+ const notes = ["per-camera notification mute bypassed"];
17537
+ if (report.nothingToReport) notes.push(`nothing to report — ${report.sections.map((section) => `${section.id}: ${section.status === "read" ? `${String(section.items)} open` : "unavailable"}`).join(", ")} — nothing was sent, as the scheduled summary would send nothing`);
17538
+ else if (enqueued === 0) notes.push("the summary was built but no row was enqueued — the rule reaches no target");
17539
+ return {
17540
+ recordId: report.recordId,
17541
+ origin: "synthetic",
17542
+ delivery: rule.delivery,
17543
+ candidates: 1,
17544
+ outcomes: [{
17545
+ ruleId: rule.id,
17546
+ ruleName: rule.name,
17547
+ matched: !report.nothingToReport,
17548
+ cooldown: "bypassed",
17549
+ enqueued,
17550
+ targetIds: [...new Set(report.rows.flatMap((row) => row.targetIds))]
17551
+ }],
17552
+ still: "unattachable",
17553
+ notes,
17554
+ ...report.nothingToReport ? { droppedBy: "nothing-to-report" } : {},
17555
+ trigger: "state-summary",
17556
+ planNotes: [],
17557
+ stateSummary: report
17558
+ };
17559
+ };
17477
17560
  /** The producer half, or an honest refusal. */
17478
17561
  const requireTimelapse = () => {
17479
17562
  const tl = deps.timelapse;
@@ -17808,6 +17891,7 @@ function makeNcActionHandlers(deps) {
17808
17891
  const inject = deps.injectTestEvent;
17809
17892
  if (inject === void 0) throw new Error("this node does not host the notification centre — test on the hub");
17810
17893
  const rule = assertRuleVisible(input.ruleId, c);
17894
+ if (isSystemEventSummaryRule(rule)) return testSummaryRule(rule, c.userId);
17811
17895
  const plan = planRuleTest({
17812
17896
  rule,
17813
17897
  now: Date.now(),
@@ -19642,6 +19726,77 @@ var NcDeviceStateCache = class {
19642
19726
  }
19643
19727
  };
19644
19728
  //#endregion
19729
+ //#region src/notification-center/state-summary/state-model.ts
19730
+ /**
19731
+ * The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
19732
+ * item is.
19733
+ *
19734
+ * A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
19735
+ * — the same grouping both editors list kinds under — so "the rule selected a
19736
+ * battery kind" and "the summary has a battery section" are the same fact.
19737
+ * `backups-exports` is the one group with no section: a backup or an export is
19738
+ * a job that COMPLETED, and the state it leaves behind is not a condition that
19739
+ * is still true. The ADR records why it is excluded rather than approximated.
19740
+ */
19741
+ var NC_STATE_SUMMARY_SECTION_IDS = [
19742
+ "device-state",
19743
+ "streams",
19744
+ "battery",
19745
+ "consumables",
19746
+ "tanks",
19747
+ "detection-blind",
19748
+ "nodes",
19749
+ "addons",
19750
+ "updates"
19751
+ ];
19752
+ /** The kind groups that have no section, and why — said on the preview. */
19753
+ var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
19754
+ function isStateSectionId(value) {
19755
+ return NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
19756
+ }
19757
+ /** The template variable that carries one section's lines (`template-vars.ts`). */
19758
+ var NC_STATE_SECTION_VAR = {
19759
+ "device-state": "summaryDevices",
19760
+ streams: "summaryStreams",
19761
+ battery: "summaryBattery",
19762
+ consumables: "summaryConsumables",
19763
+ tanks: "summaryTanks",
19764
+ "detection-blind": "summaryDetection",
19765
+ nodes: "summaryNodes",
19766
+ addons: "summaryAddons",
19767
+ updates: "summaryUpdates"
19768
+ };
19769
+ /** The kinds of a section — its kind group. */
19770
+ function sectionKinds(section) {
19771
+ return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
19772
+ }
19773
+ /**
19774
+ * The sections a rule reports, in section order: every section whose kind
19775
+ * group shares a kind with the rule. A rule that selected only
19776
+ * `device-battery-low` still reports the battery STATE — the state has no
19777
+ * direction, only the event did.
19778
+ */
19779
+ function sectionsOfRule(condition) {
19780
+ return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
19781
+ }
19782
+ /** The kind groups the rule selected that have no section. */
19783
+ function excludedGroupsOfRule(condition) {
19784
+ return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
19785
+ }
19786
+ /**
19787
+ * The condition the summary judges its items with: the rule's own, with its
19788
+ * kinds widened to every kind of every section it reports — so a rule that
19789
+ * named the raise is not told "not my kind" about the state the raise left
19790
+ * behind. Every FILTER is the rule's, untouched.
19791
+ */
19792
+ function summaryCondition(condition) {
19793
+ const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
19794
+ return {
19795
+ ...condition,
19796
+ kinds: [...new Set([...condition.kinds, ...kinds])]
19797
+ };
19798
+ }
19799
+ //#endregion
19645
19800
  //#region src/notification-center/state-summary/state-summary-delivery.ts
19646
19801
  /**
19647
19802
  * A state summary as an ORDINARY system-event subject (D647), so it reaches the
@@ -19650,7 +19805,9 @@ var NcDeviceStateCache = class {
19650
19805
  * which renders the rule's own template with the summary's variables.
19651
19806
  *
19652
19807
  * - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
19653
- * key, so a restart can never enqueue one window twice.
19808
+ * key, so a restart can never enqueue one window twice. Split per SECTION
19809
+ * (D698), each row is `state-summary:<ruleId>:<windowEndMs>:<sectionId>`:
19810
+ * the same guarantee, per section.
19654
19811
  * - The row files under the system device (0): a summary spans devices.
19655
19812
  * `targetUsers` therefore reach admins only — a user whose grant is
19656
19813
  * narrower than the whole installation must not be sent the names of
@@ -19664,20 +19821,37 @@ var NcDeviceStateCache = class {
19664
19821
  */
19665
19822
  var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
19666
19823
  var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
19824
+ /** A per-section row's title and body (D698): the rule's name and the section's. */
19825
+ var NC_STATE_SUMMARY_SECTION_TITLE_KEY = "system.state-summary.section.title";
19826
+ var NC_STATE_SUMMARY_SECTION_BODY_KEY = "system.state-summary.section.body";
19667
19827
  function stateSummaryRecordId(ruleId, windowEndMs) {
19668
19828
  return `state-summary:${ruleId}:${String(windowEndMs)}`;
19669
19829
  }
19830
+ /**
19831
+ * The record id of a TEST of the rule (D698, `nc.testRule`): its own prefix,
19832
+ * so a test can never take — or be swallowed by — a real window's dedup key.
19833
+ */
19834
+ function stateSummaryTestRecordId(ruleId, nowMs) {
19835
+ return `state-summary-test:${ruleId}:${String(nowMs)}`;
19836
+ }
19837
+ /** One section's row: the record id of the whole, plus `:<sectionId>` — the per-section dedup key. */
19838
+ function sectionRecordId(base, sectionId) {
19839
+ return `${base}:${sectionId}`;
19840
+ }
19670
19841
  /** Is this system-event data the data of a state-summary row? */
19671
19842
  function isStateSummaryData(data) {
19672
19843
  return data["stateSummary"] === true;
19673
19844
  }
19845
+ function recordBaseOf(input) {
19846
+ return input.recordBase ?? stateSummaryRecordId(input.rule.id, input.window.endMs);
19847
+ }
19674
19848
  /** `null` when the rule has no system-event condition (the guard refuses that). */
19675
19849
  function stateSummarySubject(input) {
19676
19850
  const kind = input.rule.conditions.systemEvent?.kinds[0];
19677
19851
  if (kind === void 0) return null;
19678
19852
  return {
19679
19853
  kind: "system-event",
19680
- recordId: stateSummaryRecordId(input.rule.id, input.window.endMs),
19854
+ recordId: recordBaseOf(input),
19681
19855
  deviceId: NC_SYSTEM_DEVICE_ID$1,
19682
19856
  timestamp: input.window.endMs,
19683
19857
  classNames: [],
@@ -19690,6 +19864,7 @@ function stateSummarySubject(input) {
19690
19864
  textParams: {
19691
19865
  summaryText: input.text.text,
19692
19866
  summaryCount: String(input.text.count),
19867
+ summarySection: input.text.sections.map((section) => section.title).join(", "),
19693
19868
  ...input.text.sectionVars
19694
19869
  },
19695
19870
  textCount: input.text.count,
@@ -19707,13 +19882,82 @@ function stateSummarySubject(input) {
19707
19882
  };
19708
19883
  }
19709
19884
  /**
19885
+ * The rows one summary window sends. `rule`: the one D647 row. `section`: one
19886
+ * row per section with something to say — its items, or the sentence that it
19887
+ * is UNAVAILABLE, exactly as the combined text words it. A section with
19888
+ * nothing open sends nothing; neither does a summary with nothing at all.
19889
+ */
19890
+ function stateSummarySubjects(input) {
19891
+ if (input.split === "rule") {
19892
+ const subject = stateSummarySubject(input);
19893
+ return subject === null ? [] : [subject];
19894
+ }
19895
+ const kind = input.rule.conditions.systemEvent?.kinds[0];
19896
+ if (kind === void 0) return [];
19897
+ return input.text.sections.map((section) => sectionSubject(input, kind, section));
19898
+ }
19899
+ function sectionSubject(input, kind, section) {
19900
+ const sectionVars = {};
19901
+ for (const name of Object.keys(input.text.sectionVars)) sectionVars[name] = "";
19902
+ sectionVars[NC_STATE_SECTION_VAR[section.id]] = section.text;
19903
+ return {
19904
+ kind: "system-event",
19905
+ recordId: sectionRecordId(recordBaseOf(input), section.id),
19906
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
19907
+ timestamp: input.window.endMs,
19908
+ classNames: [],
19909
+ zones: [],
19910
+ systemEvent: {
19911
+ kind,
19912
+ subject: `state-summary:${input.rule.id}:${section.id}`,
19913
+ titleKey: NC_STATE_SUMMARY_SECTION_TITLE_KEY,
19914
+ bodyKey: NC_STATE_SUMMARY_SECTION_BODY_KEY,
19915
+ textParams: {
19916
+ summaryText: section.body,
19917
+ summaryCount: String(section.count),
19918
+ summarySection: section.title,
19919
+ ...sectionVars
19920
+ },
19921
+ textCount: section.count,
19922
+ data: {
19923
+ stateSummary: true,
19924
+ section: section.id,
19925
+ windowStartMs: input.window.startMs,
19926
+ windowEndMs: input.window.endMs,
19927
+ sections: [{
19928
+ id: section.id,
19929
+ status: section.status,
19930
+ items: section.count
19931
+ }]
19932
+ }
19933
+ }
19934
+ };
19935
+ }
19936
+ /** The section a per-section row is about (`data.section`), or `undefined` on a combined row. */
19937
+ function stateSummarySectionOf(subject) {
19938
+ const section = subject.systemEvent?.data["section"];
19939
+ return typeof section === "string" && isStateSectionId(section) ? section : void 0;
19940
+ }
19941
+ /** Does a body name `{{summaryText}}` or this section's own variable? */
19942
+ function bodyCarriesSection(body, section) {
19943
+ return new RegExp(`\\{\\{\\s*(?:summaryText|${NC_STATE_SECTION_VAR[section]})\\s*\\}\\}`).test(body);
19944
+ }
19945
+ /**
19710
19946
  * The rule as its summary is ENQUEUED: a custom body that carries none of the
19711
19947
  * summary's content gets `{{summaryText}}` appended on its own line, so an
19712
19948
  * operator's body never sends an empty summary (review M3). `null` = unchanged.
19949
+ *
19950
+ * On a per-SECTION row (D698 review M3) the content is THIS section's: a body
19951
+ * naming only `{{summaryBattery}}` carries the battery row and nothing on the
19952
+ * streams row, so every other section's row gets `{{summaryText}}` — its own
19953
+ * lines — appended.
19713
19954
  */
19714
- function withSummaryBody(rule) {
19955
+ function withSummaryBody(rule, section) {
19715
19956
  const body = rule.template?.body;
19716
- if (templateCarriesSummary(body)) return null;
19957
+ if (templateCarriesSummary(body)) {
19958
+ if (section === void 0 || body === void 0 || body.trim().length === 0) return null;
19959
+ if (bodyCarriesSection(body, section)) return null;
19960
+ }
19717
19961
  return {
19718
19962
  ...rule,
19719
19963
  template: {
@@ -23762,6 +24006,120 @@ function withTimeout$3(promise, ms, what) {
23762
24006
  });
23763
24007
  }
23764
24008
  //#endregion
24009
+ //#region src/notification-center/boot-window-rows.ts
24010
+ var BootWindowRows = class {
24011
+ deps;
24012
+ window = /* @__PURE__ */ new Map();
24013
+ /** Deferred closes, keyed so a repeat (one per detection) replaces rather than grows. */
24014
+ deferred = /* @__PURE__ */ new Map();
24015
+ settled = false;
24016
+ constructor(deps) {
24017
+ this.deps = deps;
24018
+ }
24019
+ /** Is the ledger still inside its boot window (not settled after an answered load)? */
24020
+ open() {
24021
+ return !this.settled;
24022
+ }
24023
+ /** Stage a row that would be CREATED inside the window — mirror only, never persisted. */
24024
+ stageProvisional(row) {
24025
+ this.deps.ledger.stage(row);
24026
+ this.window.set(this.deps.keyOf(row), {
24027
+ kind: "provisional",
24028
+ row
24029
+ });
24030
+ }
24031
+ /** A row the ledger persisted inside the window: the load must not regress it. */
24032
+ noteWritten(row) {
24033
+ this.window.set(this.deps.keyOf(row), {
24034
+ kind: "written",
24035
+ row
24036
+ });
24037
+ }
24038
+ /** A key the ledger removed inside the window: the load must not resurrect it. */
24039
+ noteRemoved(key) {
24040
+ this.window.set(key, { kind: "removed" });
24041
+ }
24042
+ /**
24043
+ * While the window is open, keep `replay` to run against the LOADED rows at
24044
+ * the settle; once settled, nothing — the caller's own pass already saw every
24045
+ * row (review LOW-5: no allocation per detection after boot). `key` names the
24046
+ * close: the same key deferred twice runs once, at the place it was first
24047
+ * deferred. `replay` answers how many rows it closed.
24048
+ */
24049
+ deferWhileOpen(key, replay) {
24050
+ if (this.open()) this.deferred.set(key, replay);
24051
+ }
24052
+ /**
24053
+ * Close the window — call right after the ledger's `load`. A load that did
24054
+ * not answer leaves the window open and returns `null`.
24055
+ */
24056
+ settle() {
24057
+ if (this.settled) return null;
24058
+ if (!this.deps.ledger.hasAnswered()) {
24059
+ this.deps.logger.warn("boot window NOT settled — the load did not answer", { meta: {
24060
+ collection: this.deps.ledger.collection,
24061
+ pendingCloses: this.deferred.size,
24062
+ provisionalRows: this.window.size
24063
+ } });
24064
+ return null;
24065
+ }
24066
+ this.settled = true;
24067
+ const ledger = this.deps.ledger;
24068
+ let replayed = 0;
24069
+ for (const replay of this.deferred.values()) replayed += replay();
24070
+ this.deferred.clear();
24071
+ let keptStored = 0;
24072
+ let written = 0;
24073
+ for (const [key, entry] of this.window) {
24074
+ if (entry.kind === "removed") {
24075
+ ledger.forget(key);
24076
+ continue;
24077
+ }
24078
+ if (entry.kind === "written") {
24079
+ ledger.stage(entry.row);
24080
+ continue;
24081
+ }
24082
+ const mirrored = ledger.get(key);
24083
+ const stored = mirrored === entry.row ? void 0 : mirrored;
24084
+ const held = this.deps.merge(stored, entry.row);
24085
+ const deviceId = this.deps.deviceIdOf(entry.row);
24086
+ const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
24087
+ if (stored !== void 0 && held === stored) {
24088
+ keptStored += 1;
24089
+ this.deps.logger.debug("boot-window row kept as stored — the restart learned nothing new", {
24090
+ ...tags,
24091
+ meta: {
24092
+ collection: ledger.collection,
24093
+ key
24094
+ }
24095
+ });
24096
+ continue;
24097
+ }
24098
+ ledger.put(held);
24099
+ written += 1;
24100
+ this.deps.logger.debug("boot-window row written at load", {
24101
+ ...tags,
24102
+ meta: {
24103
+ collection: ledger.collection,
24104
+ key,
24105
+ stored: stored !== void 0
24106
+ }
24107
+ });
24108
+ }
24109
+ this.window.clear();
24110
+ const merge = {
24111
+ keptStored,
24112
+ written,
24113
+ replayed
24114
+ };
24115
+ this.deps.logger.info("boot window settled", { meta: {
24116
+ collection: ledger.collection,
24117
+ ...merge
24118
+ } });
24119
+ return merge;
24120
+ }
24121
+ };
24122
+ //#endregion
23765
24123
  //#region src/notification-center/liveness-ledger.ts
23766
24124
  /**
23767
24125
  * @durable class=ledger owner=notification-center
@@ -23872,12 +24230,26 @@ var NC_LIVENESS_SPEC = {
23872
24230
  };
23873
24231
  var NcLivenessLedger = class {
23874
24232
  ledger;
24233
+ /**
24234
+ * What was observed before the boot load answered (D698) — see
24235
+ * `boot-window-rows.ts`. A stored row holding the same STATE keeps its own
24236
+ * `updatedAt`: a restart that re-hears "offline" learned nothing about WHEN
24237
+ * the device went offline, and `updatedAt` is the state summary's "since".
24238
+ */
24239
+ bootWindow;
23875
24240
  constructor(deps) {
23876
24241
  this.ledger = new DurableLedger({
23877
24242
  spec: NC_LIVENESS_SPEC,
23878
24243
  store: deps.store,
23879
24244
  logger: deps.logger
23880
24245
  });
24246
+ this.bootWindow = new BootWindowRows({
24247
+ ledger: this.ledger,
24248
+ keyOf: (row) => row.subject,
24249
+ deviceIdOf: (row) => row.deviceId,
24250
+ logger: deps.logger,
24251
+ merge: (stored, provisional) => stored !== void 0 && stored.state === provisional.state ? stored : provisional
24252
+ });
23881
24253
  }
23882
24254
  static declare(store) {
23883
24255
  return DurableLedger.declare(store, NC_LIVENESS_SPEC);
@@ -23892,7 +24264,9 @@ var NcLivenessLedger = class {
23892
24264
  * flood, so no error path may do it on purpose.
23893
24265
  */
23894
24266
  async load() {
23895
- return (await this.ledger.load()).length;
24267
+ const loaded = (await this.ledger.load()).length;
24268
+ this.bootWindow.settle();
24269
+ return loaded;
23896
24270
  }
23897
24271
  /** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
23898
24272
  hasAnswered() {
@@ -23985,13 +24359,21 @@ var NcLivenessLedger = class {
23985
24359
  * spoke". That is the timestamp anyone reading the table wants.
23986
24360
  */
23987
24361
  observe(observation, now) {
23988
- return this.ledger.observe({
24362
+ const row = {
23989
24363
  subject: observation.subject,
23990
24364
  state: observation.state,
23991
24365
  kind: observation.kind,
23992
24366
  ...observation.deviceId !== void 0 ? { deviceId: observation.deviceId } : {},
23993
24367
  updatedAt: now
23994
- });
24368
+ };
24369
+ if (!this.bootWindow.open()) return this.ledger.observe(row);
24370
+ if (this.ledger.get(row.subject) === void 0) {
24371
+ this.bootWindow.stageProvisional(row);
24372
+ return "seeded";
24373
+ }
24374
+ const verdict = this.ledger.observe(row);
24375
+ if (verdict === "flip") this.bootWindow.noteWritten(row);
24376
+ return verdict;
23995
24377
  }
23996
24378
  };
23997
24379
  /** The persisted column map (the `subject` PK is passed separately). */
@@ -30526,74 +30908,6 @@ function stateSources(ports) {
30526
30908
  return { read: (section, nowMs) => readSection(section, ports, nowMs) };
30527
30909
  }
30528
30910
  //#endregion
30529
- //#region src/notification-center/state-summary/state-model.ts
30530
- /**
30531
- * The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
30532
- * item is.
30533
- *
30534
- * A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
30535
- * — the same grouping both editors list kinds under — so "the rule selected a
30536
- * battery kind" and "the summary has a battery section" are the same fact.
30537
- * `backups-exports` is the one group with no section: a backup or an export is
30538
- * a job that COMPLETED, and the state it leaves behind is not a condition that
30539
- * is still true. The ADR records why it is excluded rather than approximated.
30540
- */
30541
- var NC_STATE_SUMMARY_SECTION_IDS = [
30542
- "device-state",
30543
- "streams",
30544
- "battery",
30545
- "consumables",
30546
- "tanks",
30547
- "detection-blind",
30548
- "nodes",
30549
- "addons",
30550
- "updates"
30551
- ];
30552
- /** The kind groups that have no section, and why — said on the preview. */
30553
- var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
30554
- /** The template variable that carries one section's lines (`template-vars.ts`). */
30555
- var NC_STATE_SECTION_VAR = {
30556
- "device-state": "summaryDevices",
30557
- streams: "summaryStreams",
30558
- battery: "summaryBattery",
30559
- consumables: "summaryConsumables",
30560
- tanks: "summaryTanks",
30561
- "detection-blind": "summaryDetection",
30562
- nodes: "summaryNodes",
30563
- addons: "summaryAddons",
30564
- updates: "summaryUpdates"
30565
- };
30566
- /** The kinds of a section — its kind group. */
30567
- function sectionKinds(section) {
30568
- return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
30569
- }
30570
- /**
30571
- * The sections a rule reports, in section order: every section whose kind
30572
- * group shares a kind with the rule. A rule that selected only
30573
- * `device-battery-low` still reports the battery STATE — the state has no
30574
- * direction, only the event did.
30575
- */
30576
- function sectionsOfRule(condition) {
30577
- return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
30578
- }
30579
- /** The kind groups the rule selected that have no section. */
30580
- function excludedGroupsOfRule(condition) {
30581
- return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
30582
- }
30583
- /**
30584
- * The condition the summary judges its items with: the rule's own, with its
30585
- * kinds widened to every kind of every section it reports — so a rule that
30586
- * named the raise is not told "not my kind" about the state the raise left
30587
- * behind. Every FILTER is the rule's, untouched.
30588
- */
30589
- function summaryCondition(condition) {
30590
- const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
30591
- return {
30592
- ...condition,
30593
- kinds: [...new Set([...condition.kinds, ...kinds])]
30594
- };
30595
- }
30596
- //#endregion
30597
30911
  //#region src/notification-center/state-summary/state-summary.ts
30598
30912
  /** Nothing to say: no item, and every section READ. Such a summary is not sent. */
30599
30913
  function isEmptyStateSummary(summary) {
@@ -30694,28 +31008,50 @@ function lineOf(input, item) {
30694
31008
  }
30695
31009
  });
30696
31010
  }
30697
- function blockOf(input, section) {
30698
- const title = input.texts.text({ key: `summary.section.${section.id}` });
30699
- if (section.status === "unavailable") return input.texts.text({
30700
- key: "summary.section.unavailable",
30701
- vars: { section: title }
30702
- });
31011
+ function sectionTitle(input, section) {
31012
+ return input.texts.text({ key: `summary.section.${section.id}` });
31013
+ }
31014
+ /** A section's words: its block, and the body a per-section row sends. `null` = nothing to say. */
31015
+ function wordsOf(input, section) {
31016
+ const title = sectionTitle(input, section);
31017
+ if (section.status === "unavailable") {
31018
+ const sentence = input.texts.text({
31019
+ key: "summary.section.unavailable",
31020
+ vars: { section: title }
31021
+ });
31022
+ return {
31023
+ block: sentence,
31024
+ body: sentence
31025
+ };
31026
+ }
30703
31027
  if (section.items.length === 0) return null;
30704
- const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
30705
- return [`${title}:`, ...lines].join("\n");
31028
+ const lines = section.items.map((item) => `• ${lineOf(input, item)}`).join("\n");
31029
+ return {
31030
+ block: `${title}:\n${lines}`,
31031
+ body: lines
31032
+ };
30706
31033
  }
30707
31034
  function stateSummaryText(input) {
30708
31035
  const sectionVars = {};
30709
- const blocks = [];
31036
+ const sections = [];
30710
31037
  for (const section of input.summary.sections) {
30711
- const block = blockOf(input, section);
30712
- sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
30713
- if (block !== null) blocks.push(block);
31038
+ const words = wordsOf(input, section);
31039
+ sectionVars[NC_STATE_SECTION_VAR[section.id]] = words?.block ?? "";
31040
+ if (words === null) continue;
31041
+ sections.push({
31042
+ id: section.id,
31043
+ title: sectionTitle(input, section),
31044
+ text: words.block,
31045
+ body: words.body,
31046
+ count: section.items.length,
31047
+ status: section.status
31048
+ });
30714
31049
  }
30715
31050
  return {
30716
- text: blocks.join("\n"),
31051
+ text: sections.map((section) => section.text).join("\n"),
30717
31052
  count: input.summary.itemCount,
30718
- sectionVars
31053
+ sectionVars,
31054
+ sections
30719
31055
  };
30720
31056
  }
30721
31057
  //#endregion
@@ -31152,6 +31488,40 @@ var NcStateSummaryStampStore = class {
31152
31488
  //#endregion
31153
31489
  //#region src/notification-center/state-summary/system-state-ledger.ts
31154
31490
  /**
31491
+ * NcSystemStateLedger — what is STILL TRUE among the event-driven system facts
31492
+ * no other ledger holds (D647).
31493
+ *
31494
+ * Battery, consumable and tank episodes already have a durable row
31495
+ * (`alert-store.ts`), and device/node liveness has its own
31496
+ * (`liveness-ledger.ts`). The rest of the summarisable kinds are EVENTS the
31497
+ * centre hears once — a stream going offline, a device switched off, a camera
31498
+ * detecting nothing, an update becoming available — and nothing remembered
31499
+ * that they were still true. A state summary is a claim about now, so this
31500
+ * ledger keeps one row per OPEN state, fed by the same intake the instant
31501
+ * rules read, and removes it when the closing event arrives:
31502
+ *
31503
+ * | opens | closes |
31504
+ * | --- | --- |
31505
+ * | `stream-offline` | `stream-online` (same subject) |
31506
+ * | `device-disabled` | `device-enabled` (same subject) |
31507
+ * | `detection-blind` | a DETECTION on that camera — the only evidence of recovery there is |
31508
+ * | `node-inference-unavailable`, `addon-crash-loop` | the node's next liveness edge, or a server update on it |
31509
+ * | `addon-update-available` (per package, per node) | `addon.updated` for that package on that node |
31510
+ * | `server-` / `wrapper-update-available` | `server-updated` on that node |
31511
+ *
31512
+ * Two kinds have no recovery event at all (`node-inference-unavailable`,
31513
+ * `addon-crash-loop`): the orchestrator drops such a node until it comes back,
31514
+ * and a crash-looped runner stays down until something respawns it — a node
31515
+ * restart or an update. The closing edges above are those respawns. A newer
31516
+ * `update-available` for the same package REPLACES the row.
31517
+ *
31518
+ * Durable (write-behind), so a restart does not forget an outage it heard
31519
+ * about. It is fed only on the evaluating node, like the liveness ledger. The
31520
+ * summary reads the RAM mirror only — never SQLite on its schedule (D447) —
31521
+ * and a mirror that has not loaded answers `null`: the sections it feeds say
31522
+ * "unavailable", never "none" (D49).
31523
+ */
31524
+ /**
31155
31525
  * @durable class=ledger owner=notification-center
31156
31526
  * write="a system state the centre accepted OPENING (a stream offline, a device switched off, a camera detecting nothing, an update available, a node that cannot infer, a crash-looped runner); its closing event deletes the row"
31157
31527
  * retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
@@ -31313,9 +31683,7 @@ var NcSystemStateLedger = class {
31313
31683
  }
31314
31684
  /** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
31315
31685
  observeDetection(deviceId) {
31316
- const key = `detection-blind:${String(deviceId)}`;
31317
- if (this.deps.rows.get(key) === void 0) return;
31318
- this.close(key, "detection");
31686
+ this.close(`detection-blind:${String(deviceId)}`, "detection");
31319
31687
  }
31320
31688
  /**
31321
31689
  * Any `stream.online`, whatever its reason (D647 review I1). The intake gate
@@ -31330,7 +31698,7 @@ var NcSystemStateLedger = class {
31330
31698
  const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
31331
31699
  for (const nodeId of list.nodeIds) {
31332
31700
  const prefix = `update:${list.target}:${nodeId}:`;
31333
- for (const row of this.deps.rows.all()) if (row.key.startsWith(prefix) && !keep.has(row.key)) this.close(row.key, "update-list");
31701
+ this.closeWhere(`update-list:${list.target}:${nodeId}`, (row) => row.key.startsWith(prefix) && !keep.has(row.key), "update-list");
31334
31702
  }
31335
31703
  for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
31336
31704
  }
@@ -31340,11 +31708,7 @@ var NcSystemStateLedger = class {
31340
31708
  * on that node.
31341
31709
  */
31342
31710
  observeAddonStarted(addonId, nodeId) {
31343
- for (const row of this.deps.rows.all()) {
31344
- if (row.kind !== "addon-crash-loop") continue;
31345
- if (row.nodeId !== nodeId) continue;
31346
- if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
31347
- }
31711
+ this.closeWhere(`addon-started:${nodeId}:${addonId}`, (row) => row.kind === "addon-crash-loop" && row.nodeId === nodeId && (row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId), "addon-started");
31348
31712
  }
31349
31713
  /**
31350
31714
  * The balancer placed a camera on a node (not pinned): placement skips a
@@ -31400,11 +31764,31 @@ var NcSystemStateLedger = class {
31400
31764
  }
31401
31765
  });
31402
31766
  }
31767
+ /**
31768
+ * Close one state: the row held now (a loaded one, or one opened in the boot
31769
+ * window), and — deferred while the boot window is open — the STORED row
31770
+ * the window cannot see yet (D698 review H1).
31771
+ */
31403
31772
  close(key, by) {
31773
+ this.closeIfHeld(key, by, false);
31774
+ this.deps.rows.deferWhileOpen(`close:${key}`, () => this.closeIfHeld(key, by, true) ? 1 : 0);
31775
+ }
31776
+ /** {@link close} for every row a predicate names — now, and against the loaded rows. */
31777
+ closeWhere(key, names, by) {
31778
+ const run = (replayed) => {
31779
+ let closed = 0;
31780
+ for (const row of this.deps.rows.all()) if (names(row) && this.closeIfHeld(row.key, by, replayed)) closed += 1;
31781
+ return closed;
31782
+ };
31783
+ run(false);
31784
+ this.deps.rows.deferWhileOpen(`where:${key}`, () => run(true));
31785
+ }
31786
+ /** Close the row if one is held; `true` when it was. */
31787
+ closeIfHeld(key, by, replayed) {
31404
31788
  const held = this.deps.rows.get(key);
31405
- if (held === void 0) return;
31789
+ if (held === void 0) return false;
31406
31790
  this.deps.rows.remove(key);
31407
- this.deps.logger?.debug("system state closed", {
31791
+ const line = {
31408
31792
  ...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
31409
31793
  meta: {
31410
31794
  key,
@@ -31412,49 +31796,78 @@ var NcSystemStateLedger = class {
31412
31796
  by,
31413
31797
  since: held.since
31414
31798
  }
31415
- });
31799
+ };
31800
+ if (replayed) {
31801
+ this.deps.logger?.info("system state closed by an event heard before the load", line);
31802
+ return true;
31803
+ }
31804
+ this.deps.logger?.debug("system state closed", line);
31805
+ return true;
31416
31806
  }
31417
31807
  closeNodeFaults(nodeId, by) {
31418
- for (const row of this.deps.rows.all()) {
31419
- if (row.nodeId !== nodeId) continue;
31420
- if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
31421
- }
31808
+ this.closeWhere(`node-faults:${nodeId}`, (row) => row.nodeId === nodeId && (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop"), by);
31422
31809
  }
31423
31810
  closeServerUpdate(nodeId) {
31424
- for (const row of this.deps.rows.all()) {
31425
- if ((row.nodeId ?? HUB_NODE) !== nodeId) continue;
31426
- if (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable") this.close(row.key, "server-updated");
31427
- }
31811
+ this.closeWhere(`server-update:${nodeId}`, (row) => (row.nodeId ?? HUB_NODE) === nodeId && (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable"), "server-updated");
31428
31812
  }
31429
31813
  };
31430
31814
  /** The durable home of the rows. */
31431
31815
  var NcSystemStateStore = class {
31432
31816
  ledger;
31817
+ /**
31818
+ * What the intake opened or closed before the boot load answered (D698,
31819
+ * `boot-window-rows.ts`). A stored row of the same KIND keeps its own
31820
+ * `since`: a stream re-announced offline at boot was not offline "since the
31821
+ * restart".
31822
+ */
31823
+ bootWindow;
31433
31824
  constructor(deps) {
31434
31825
  this.ledger = new DurableLedger({
31435
31826
  spec: NC_SYSTEM_STATE_SPEC,
31436
31827
  store: deps.store,
31437
31828
  logger: deps.logger
31438
31829
  });
31830
+ this.bootWindow = new BootWindowRows({
31831
+ ledger: this.ledger,
31832
+ keyOf: (row) => row.key,
31833
+ deviceIdOf: (row) => row.deviceId,
31834
+ logger: deps.logger,
31835
+ merge: (stored, provisional) => {
31836
+ if (stored === void 0 || stored.kind !== provisional.kind) return provisional;
31837
+ const merged = {
31838
+ ...provisional,
31839
+ since: stored.since
31840
+ };
31841
+ return isDeepStrictEqual(merged, stored) ? stored : merged;
31842
+ }
31843
+ });
31439
31844
  }
31440
31845
  static declare(store) {
31441
31846
  return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
31442
31847
  }
31443
31848
  /** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
31444
31849
  async load() {
31445
- return (await this.ledger.load()).length;
31850
+ const loaded = (await this.ledger.load()).length;
31851
+ this.bootWindow.settle();
31852
+ return loaded;
31446
31853
  }
31447
31854
  port() {
31448
31855
  return {
31449
31856
  get: (key) => this.ledger.get(key),
31450
31857
  put: (row) => {
31858
+ if (this.bootWindow.open()) {
31859
+ this.bootWindow.stageProvisional(row);
31860
+ return;
31861
+ }
31451
31862
  this.ledger.put(row).catch(() => void 0);
31452
31863
  },
31453
31864
  remove: (key) => {
31865
+ if (this.bootWindow.open()) this.bootWindow.noteRemoved(key);
31454
31866
  this.ledger.forget(key);
31455
31867
  },
31456
31868
  all: () => this.ledger.snapshot(),
31457
- answered: () => this.ledger.hasAnswered()
31869
+ answered: () => this.ledger.hasAnswered(),
31870
+ deferWhileOpen: (key, replay) => this.bootWindow.deferWhileOpen(key, replay)
31458
31871
  };
31459
31872
  }
31460
31873
  };
@@ -31579,8 +31992,21 @@ var NcStateSummaryService = class {
31579
31992
  const nowMs = this.deps.now();
31580
31993
  const summary = await this.collect(rule, nowMs);
31581
31994
  const text = this.word(summary, nowMs);
31995
+ const split = systemEventSummarySplitOf(rule);
31996
+ const empty = isEmptyStateSummary(summary);
31582
31997
  return {
31583
- empty: summary.itemCount === 0 && summary.unavailableCount === 0,
31998
+ empty,
31999
+ split,
32000
+ rows: empty ? [] : split === "section" ? text.sections.map((section) => ({
32001
+ section: section.id,
32002
+ title: section.title,
32003
+ text: section.body,
32004
+ count: section.count
32005
+ })) : [{
32006
+ title: text.sections.map((section) => section.title).join(", "),
32007
+ text: text.text,
32008
+ count: text.count
32009
+ }],
31584
32010
  count: text.count,
31585
32011
  text: text.text,
31586
32012
  sections: summary.sections.map((section) => ({
@@ -31593,12 +32019,84 @@ var NcStateSummaryService = class {
31593
32019
  excludedGroups: [...summary.excludedGroups]
31594
32020
  };
31595
32021
  }
31596
- async collect(rule, nowMs) {
32022
+ /**
32023
+ * TEST the rule (D698, `nc.testRule`): the summary it would send NOW,
32024
+ * ENQUEUED through the ordinary outbox under a test record id — split as the
32025
+ * rule is. The per-camera mute is bypassed, as every rule test bypasses it,
32026
+ * and a summary has no cooldown to bypass. Nothing to report sends nothing
32027
+ * and SAYS so, per section.
32028
+ */
32029
+ async test(rule) {
32030
+ const nowMs = this.deps.now();
32031
+ const summary = await this.collect(rule, nowMs, () => false);
32032
+ const split = systemEventSummarySplitOf(rule);
32033
+ const recordId = stateSummaryTestRecordId(rule.id, nowMs);
32034
+ const sections = summary.sections.map((section) => ({
32035
+ id: section.id,
32036
+ status: section.status,
32037
+ items: section.items.length,
32038
+ ...section.reason !== void 0 ? { reason: section.reason } : {}
32039
+ }));
32040
+ const meta = {
32041
+ ruleId: rule.id,
32042
+ rule: rule.name,
32043
+ split,
32044
+ bypassMute: true
32045
+ };
32046
+ if (isEmptyStateSummary(summary)) {
32047
+ this.deps.logger.info("state summary TEST has nothing to report — not sent", { meta: {
32048
+ ...meta,
32049
+ dropped: summary.dropped
32050
+ } });
32051
+ return {
32052
+ recordId,
32053
+ split,
32054
+ nothingToReport: true,
32055
+ sections,
32056
+ rows: []
32057
+ };
32058
+ }
32059
+ const subjects = stateSummarySubjects({
32060
+ rule,
32061
+ summary,
32062
+ text: this.word(summary, nowMs),
32063
+ window: {
32064
+ startMs: nowMs,
32065
+ endMs: nowMs
32066
+ },
32067
+ split,
32068
+ recordBase: recordId
32069
+ });
32070
+ const rows = [];
32071
+ for (const subject of subjects) {
32072
+ const enqueued = await this.deps.enqueue(rule, subject, "synthetic");
32073
+ const section = subject.systemEvent?.data["section"];
32074
+ rows.push({
32075
+ recordId: subject.recordId,
32076
+ ...typeof section === "string" ? { section } : {},
32077
+ enqueued: enqueued.inserted,
32078
+ targetIds: [...enqueued.targetIds]
32079
+ });
32080
+ }
32081
+ this.deps.logger.info("state summary TEST enqueued", { meta: {
32082
+ ...meta,
32083
+ items: summary.itemCount,
32084
+ rows: rows.map((row) => `${row.recordId}:${String(row.enqueued)}`)
32085
+ } });
32086
+ return {
32087
+ recordId,
32088
+ split,
32089
+ nothingToReport: false,
32090
+ sections,
32091
+ rows
32092
+ };
32093
+ }
32094
+ async collect(rule, nowMs, isMuted = this.deps.isMuted) {
31597
32095
  const nodes = await this.nodeView();
31598
32096
  return collectStateSummary({
31599
32097
  rule,
31600
32098
  nowMs,
31601
- isMuted: this.deps.isMuted,
32099
+ isMuted,
31602
32100
  sources: stateSources({
31603
32101
  alerts: this.deps.alerts,
31604
32102
  liveness: this.deps.liveness,
@@ -31634,17 +32132,24 @@ var NcStateSummaryService = class {
31634
32132
  });
31635
32133
  }
31636
32134
  async deliver(rule, summary, window) {
31637
- const subject = stateSummarySubject({
32135
+ const text = this.word(summary, window.endMs);
32136
+ const subjects = stateSummarySubjects({
31638
32137
  rule,
31639
32138
  summary,
31640
- text: this.word(summary, window.endMs),
31641
- window
32139
+ text,
32140
+ window,
32141
+ split: systemEventSummarySplitOf(rule)
31642
32142
  });
31643
- if (subject === null) {
31644
- this.deps.logger.warn("state summary rule has no system-event condition — not sent", { meta: { ruleId: rule.id } });
32143
+ if (subjects.length === 0) {
32144
+ this.deps.logger.warn("state summary produced no row — not sent", { meta: {
32145
+ ruleId: rule.id,
32146
+ sections: text.sections.length
32147
+ } });
31645
32148
  return 0;
31646
32149
  }
31647
- return this.deps.enqueue(rule, subject);
32150
+ let inserted = 0;
32151
+ for (const subject of subjects) inserted += (await this.deps.enqueue(rule, subject, "system")).inserted;
32152
+ return inserted;
31648
32153
  }
31649
32154
  };
31650
32155
  //#endregion
@@ -33370,7 +33875,7 @@ var NotificationCenter = class NotificationCenter {
33370
33875
  livenessHoldoffMs: deps.livenessHoldoffMs ?? DEVICE_LIVENESS_HOLDOFF_MS,
33371
33876
  isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
33372
33877
  texts: () => this.texts,
33373
- enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
33878
+ enqueue: (rule, subject, origin) => this.enqueueStateSummary(rule, subject, origin)
33374
33879
  });
33375
33880
  this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
33376
33881
  logger: this.logger.child("confirm"),
@@ -35911,30 +36416,42 @@ var NotificationCenter = class NotificationCenter {
35911
36416
  await this.enqueuePickerTargets(event, reached, outcomes);
35912
36417
  return report({ candidates: candidates.length });
35913
36418
  }
36419
+ /** TEST a state-summary rule (D698, `nc.testRule`) — see `NcStateSummaryService.test`. */
36420
+ testStateSummary(rule) {
36421
+ return this.stateSummary.test(rule);
36422
+ }
35914
36423
  /**
35915
- * Enqueue ONE state summary (D647) through the rule's ordinary rows: its
36424
+ * Enqueue ONE state summary row (D647) through the rule's ordinary rows: its
35916
36425
  * targets, its users (admins only — the row files under the system device),
35917
36426
  * its template, and the drain's snooze gate and retries. The record id is
35918
- * the dedup key, so a window is enqueued once whatever the restarts.
36427
+ * the dedup key, so a window — or each section of it (D698) — is enqueued
36428
+ * once whatever the restarts. A TEST's rows carry the `synthetic` origin, so
36429
+ * they are marked as a test exactly like every other rule test's.
35919
36430
  */
35920
- async enqueueStateSummary(rule, subject) {
35921
- const appended = withSummaryBody(rule);
36431
+ async enqueueStateSummary(rule, subject, origin) {
36432
+ const appended = withSummaryBody(rule, stateSummarySectionOf(subject));
35922
36433
  if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
35923
36434
  ruleId: rule.id,
35924
36435
  rule: rule.name
35925
36436
  } });
35926
36437
  const sent = appended ?? rule;
35927
36438
  const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
35928
- const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, "system");
36439
+ const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, origin);
35929
36440
  if (entries.length === 0) {
35930
36441
  this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
35931
36442
  ruleId: rule.id,
35932
36443
  rule: rule.name,
35933
36444
  targetUsers: rule.targetUsers ?? []
35934
36445
  } });
35935
- return 0;
36446
+ return {
36447
+ inserted: 0,
36448
+ targetIds: []
36449
+ };
35936
36450
  }
35937
- return this.outbox.enqueue(entries);
36451
+ return {
36452
+ inserted: await this.outbox.enqueue(entries),
36453
+ targetIds: entries.map((entry) => entry.targetId)
36454
+ };
35938
36455
  }
35939
36456
  /**
35940
36457
  * The rules a trigger may fire, which is `listEnabled(delivery)` plus and
@@ -36648,9 +37165,11 @@ var NotificationCenter = class NotificationCenter {
36648
37165
  *
36649
37166
  * A seed is INFO and a repeat is DEBUG on purpose: seeding happens once per
36650
37167
  * subject per lifetime of the ledger, and it is exactly what an operator
36651
- * reading the logs after a restart needs to see ("it recorded them, it did
36652
- * not announce them"). A no-flip is ordinary bus chatter — two categories
36653
- * describing one edge — and would be noise at info.
37168
+ * reading the logs after a restart needs to see ("it did not announce
37169
+ * them"). A seed heard BEFORE the boot load is provisional (D698): the load
37170
+ * keeps the stored row when it holds the same state, and its `boot window
37171
+ * settled` line says how many it kept and wrote. A no-flip is ordinary bus
37172
+ * chatter — two categories describing one edge — and would be noise at info.
36654
37173
  */
36655
37174
  reportLivenessDrop(incoming, verdict) {
36656
37175
  const dropped = discardedLivenessEvent(incoming, verdict);
@@ -88133,6 +88652,18 @@ function isRecord(x) {
88133
88652
  }
88134
88653
  //#endregion
88135
88654
  //#region src/pipeline-analytics/store/track-alias-store.ts
88655
+ /**
88656
+ * The alias collection.
88657
+ *
88658
+ * @durable class=ledger owner=pipeline-analytics
88659
+ * write="one row per fused piece (D683): aliasId = the absorbed provisional
88660
+ * id, trackId = its survivor; rewritten when the survivor is itself
88661
+ * absorbed by a later fusion"
88662
+ * retention="TRACK-OWNED by its survivor through the declared `trackId`
88663
+ * column: aliases leave with the survivor in the retention cascade, and
88664
+ * widen that cascade (`expandLeafIds`) to rows still keyed by an
88665
+ * absorbed id"
88666
+ */
88136
88667
  var TRACK_ALIASES_COLLECTION = "pipeline-analytics:track-aliases";
88137
88668
  var TRACK_ALIAS_COLUMNS = [
88138
88669
  {
@@ -88315,6 +88846,20 @@ var TrackAliasStore = class {
88315
88846
  dropped: 0
88316
88847
  }];
88317
88848
  }
88849
+ /**
88850
+ * The orphan audit's reclaim of ONE alias row: the row AND the `recent`
88851
+ * entry that would otherwise keep resolving it for up to an hour. `true`
88852
+ * only when a row was actually deleted. (`misses` needs nothing: a deleted
88853
+ * alias IS a non-alias, which is what a miss records.)
88854
+ */
88855
+ async deleteAlias(aliasId) {
88856
+ const { deleted } = await this.store.deleteWhere.mutate({
88857
+ collection: TRACK_ALIASES_COLLECTION,
88858
+ filter: { where: { id: aliasId } }
88859
+ });
88860
+ this.recent.delete(aliasId);
88861
+ return deleted > 0;
88862
+ }
88318
88863
  async deleteByTracks(trackIds) {
88319
88864
  const { deleted } = await this.store.deleteWhere.mutate({
88320
88865
  collection: TRACK_ALIASES_COLLECTION,
@@ -89241,7 +89786,8 @@ async function auditScope(scope, deps, limits, cursor) {
89241
89786
  stoppedBy,
89242
89787
  nextCursor: offset
89243
89788
  } });
89244
- if (limits.mode === "reclaim" && tally.orphans > 0 && tally.bytesReclaimed === 0) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
89789
+ const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
89790
+ if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
89245
89791
  collection: scope.collection,
89246
89792
  shard: scope.shard?.value ?? null,
89247
89793
  orphans: tally.orphans,
@@ -89355,6 +89901,40 @@ var ORPHAN_AUDIT_DEFAULTS = {
89355
89901
  pageSize: OrphanAuditPageSizeSchema.parse(void 0)
89356
89902
  };
89357
89903
  var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
89904
+ /**
89905
+ * The `deleteRow` port: ONE row by its primary key `id` (what every
89906
+ * raw-delete collection above declares), answering whether a row went.
89907
+ *
89908
+ * Through `deleteWhere`, because it is the delete that SAYS: `delete`'s output
89909
+ * is `z.void()`, and reading it as `!== undefined` answered `false` for every
89910
+ * row it really deleted — the audit counted every plates/object-events reclaim
89911
+ * as "found nothing", and the per-scope reclaim ceiling never tripped.
89912
+ */
89913
+ function settingsRowDeleter(store) {
89914
+ return async (collection, key) => {
89915
+ const { deleted } = await store.deleteWhere.mutate({
89916
+ collection,
89917
+ filter: { where: { id: key } }
89918
+ });
89919
+ return deleted > 0;
89920
+ };
89921
+ }
89922
+ /**
89923
+ * The `deleteRow` port AS PRODUCTION WIRES IT: {@link settingsRowDeleter} over
89924
+ * a store that may not be bound yet. Unbound THROWS — the engine counts that as
89925
+ * `reclaimFailed` and keeps the row — rather than answering "nothing deleted",
89926
+ * which is indistinguishable from a row already gone. A named factory so the
89927
+ * wiring itself is under test (D695 T7 review M1: the inline wiring could be
89928
+ * reverted to the original bug with every spec green);
89929
+ * `scripts/check-orphan-reclaimers.ts` checks `index.ts` uses it.
89930
+ */
89931
+ function settingsRowDeleterOf(getStore) {
89932
+ return async (collection, key) => {
89933
+ const store = getStore();
89934
+ if (store === null) throw new Error("orphan audit: settings store not bound");
89935
+ return await settingsRowDeleter(store)(collection, key);
89936
+ };
89937
+ }
89358
89938
  function buildOrphanReclaimer(ports) {
89359
89939
  return async (collection, key) => {
89360
89940
  if (collection === MEDIA_COLLECTION) {
@@ -89378,6 +89958,10 @@ function buildOrphanReclaimer(ports) {
89378
89958
  bytes: null
89379
89959
  };
89380
89960
  }
89961
+ if (collection === TRACK_ALIASES_COLLECTION) return {
89962
+ rows: await ports.deleteTrackAlias(key) ? 1 : 0,
89963
+ bytes: null
89964
+ };
89381
89965
  if (RAW_DELETE_COLLECTIONS.has(collection)) return {
89382
89966
  rows: await ports.deleteRow(collection, key) ? 1 : 0,
89383
89967
  bytes: null
@@ -99747,6 +100331,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99747
100331
  },
99748
100332
  listTrackGroups: (query) => center.listTrackGroups(query),
99749
100333
  injectTestEvent: (input) => center.injectTestEvent(input, { getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }) }),
100334
+ testStateSummary: (rule) => center.testStateSummary(rule),
99750
100335
  timelapse: {
99751
100336
  store: center.timelapseStore,
99752
100337
  getRecordingConfig: (deviceId) => api.recordingArchive.getDeviceConfig.query({ deviceId }).catch(() => null),
@@ -107505,10 +108090,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
107505
108090
  bytes: count.bytes
107506
108091
  };
107507
108092
  },
107508
- deleteRow: async (collection, key) => await this.settingsStore?.delete.mutate({
107509
- collection,
107510
- key
107511
- }) !== void 0,
108093
+ deleteTrackAlias: async (aliasId) => {
108094
+ const aliasStore = this.trackAliasStore;
108095
+ if (aliasStore === null) throw new Error("track alias store not bound");
108096
+ return await aliasStore.deleteAlias(aliasId);
108097
+ },
108098
+ deleteRow: settingsRowDeleterOf(() => this.settingsStore),
107512
108099
  logger: this.ctx.logger
107513
108100
  }),
107514
108101
  now: () => Date.now(),