@camstack/addon-post-analysis 1.2.613 → 1.2.615

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 MediaFileKindEnum, $i as sleep$1, $n as coreBlockIdFromAddonId, $r as readDeviceStateFrom, $t as NcTemplateVarDescriptorSchema, A as DeclaredDevices, Ai as templateCarriesSummary, An as TrackSideSchema, Ar as isEventOwnerType, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as errMsg, Bn as applyModeDelaysEdits, Br as kebabToCamel, Bt as NcRuleSchema, C as ClearTrackRelationResultSchema, Ci as summarizeReview, Cn as TRACK_RELATIONS_DEFAULT_LIMIT, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as systemEventFilterApplies, Dn as TrackPuritySchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as systemEventFamilyProblems, En as TimelapseRuleSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Fn as alarmPanelCapability, Fr as isRootSuppliableStepId, Ft as NcLegacyRuleActionSequenceSchema, G as ListTrackRelationsResultSchema, Gi as createEvent, Gn as audioMetricsCapability, Gr as normalizeNcMediaPolicy, Gt as NcSnoozeInputSchema, H as LABEL_SUPPLY_MACRO, Hi as CamProfileSchema, Hn as applySecureOnArmEdit, Hr as mayWriteToLocation, Ht as NcRuleUpdateInputSchema, I as EVENT_KIND_BY_CAP, Ii as canonicalHash, In as alertEpisodeId, Ir as isScheduleActive, It as NcRuleActionSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as defineRecordKinds, Jn as buildViewerLink, Jr as parseAlertEpisodePayload, Jt as NcStateSummarySectionIdSchema, K as LockStateSchema, Ki as customAction, Kn as audioModeOf, Kr as notificationRulesCapability, Kt as NcSnoozeSchema, L as EVENT_OWNER_TYPES, Li as evaluateSensorEdge, Ln as alertEpisodeKind, Lr as isSystemDelivery, Lt as NcRuleActionsSchema, M as EMBEDDING_MODEL_FAILED, Mi as vectorDimFromBase64, Mn as activateAlarmProfile, Mr as isLabelSupplyStepId, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as viewerLinkBaseOf, Nn as addonWidgetsSourceCapability, Nr as isMediaOwnerType, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as systemEventSummaryScheduleOf, On as TrackRelationKindSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as zoneAnalyticsCapability, Pn as alarmDeviceSecureOnArm, Pr as isRetiredClearKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qn as consumableLowPercentOf, Qr as plateGalleryCapability, Qt as NcTemplatePreviewSchema, R as EXPORT_DENSE_MAX_RANGES, Ri as isSourceCap, Rn as alertEpisodePayload, Rr as isSystemEventSummaryRule, Rt as NcRuleInputSchema, S as ClearTrackRelationInputSchema, Si as subKindsOf, Sn as TIMELAPSE_DENSE_FLOOR_SEC, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as systemEventDeliveryProblems, Tn as TimelapseRulePatchSchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as RECORD_MAX_DATA_BYTES, Un as armRemovalReconcile, Ur as memoizePinnedFfmpeg, Ut as NcScheduleSchema, V as FailureCounters, Vi as BaseAddon, Vn as applyNonBlockingEdit, Vr as linkedStepOf, Vt as NcRuleTargetSchema, W as ListTrackRelationsInputSchema, Wi as RECORD_QUERY_MAX, Wn as assertTimelapseCadences, Wr as ncMediaPolicyError, Wt as NcSharedSequenceSchema, X as MAX_TRACK_PURITIES, Xi as isDeviceScopedCap, Xn as classifyRetrainReview, Xr as pickFaceRescoreIntervalMinutes, Xt as NcTaxonomySchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as hydrateSchema, Yn as calendarEventLocalSpan, Yt as NcSystemEventKindSchema, Z as MAX_TRACK_RELATIONS, Zi as nodePin, Zn as cloneAlarmSettings, Zr as pipelineAnalyticsCapability, Zt as NcTemplatePreviewInputSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as EventCategory, _i as secureActuationFor, _n as SCENE_RESET_RECAPTURES, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as literal, ai as resolveModeDelays, an as RebuildObjectEmbeddingsRefusalSchema, ar as deriveRecordingMode, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, bi as startMoverUnderClearance, bn as SetTrackPurityResultSchema, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as object, cn as RetrainStatusSchema, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as strictObject, di as ruleAutoTriggersAlarm, dn as SCENE_DEFAULT_ANCHOR_THRESHOLD, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as ZodObject, ei as readMotionTriggeredBy, en as OPS_LOG_DEFAULT_LIMIT, er as cosineSimilarity$1, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as string, fi as ruleBelongsToAlarm$1, fn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as unknown, gi as sceneMonitorCapability, gn as SCENE_DIVERGED, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as union, hi as ruleKindOf, hn as SCENE_DEFAULT_UNCOVERED_POLICY, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as discriminatedUnion, ii as resolveLocationMode, in as RECORDING_SOURCE_CAMSTACK_ADDON, ir as derivePostTriggeredAction, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as templateVarsFor, jn as TrackSourceSchema, jr as isKnownTemplateVar, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventSummarySplitOf, kn as TrackRelationSchema, kr as isEventKindLabelKey, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as partialRecord, li as retrainRevisionConflictMessage, ln as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as tuple, mi as ruleGroupingApplies, mn as SCENE_DEFAULT_QUIET_SECONDS, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as array, ni as renameAlarmProfile, nr as deleteAlarmProfile, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as looseObject, oi as resolveModeNotifyPolicy, on as RetrainDismissedSchema, or as describeSystemEventDeliveryProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as templateLiteral, pi as ruleDeviceCompatCapQueries, pn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as defineCustomActions, qn as buildEventKindDescriptor, qr as occupancyScope, qt as NcSnoozeSuppressedSchema, ra as boolean, ri as resolveClipSearchMinScore, rn as RECORDING_EXPORT_MAX_READ_BYTES, rr as deriveDetailCropRect, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as number, si as resolveNcMediaImage, sn as RetrainOriginSchema, sr as describeSystemEventFamilyProblem, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as _enum, ti as readTimelapseGeneratedAt, tn as OpsLogEntrySchema, tr as createAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as record, ui as rootSupplyServes, un as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, vi as sensorRaisedOf, vn as SceneMonitorSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as syncActiveAlarmProfileSettings, wn as TimelapseRuleInputSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xi as storageOccupancyCapability, xn as SetTrackRelationInputSchema, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, yi as servedTemplateVars, yn as SetTrackPurityInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as DeviceType, zn as applyBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRulePatchSchema } from "../clip-model-registry-D6tdLEFB.mjs";
1
+ import { $ as MediaFileKindEnum, $i as sleep$1, $n as coreBlockIdFromAddonId, $r as readDeviceStateFrom, $t as NcTemplateVarDescriptorSchema, A as DeclaredDevices, Ai as templateCarriesSummary, An as TrackSideSchema, Ar as isEventOwnerType, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as errMsg, Bn as applyModeDelaysEdits, Br as kebabToCamel, Bt as NcRuleSchema, C as ClearTrackRelationResultSchema, Ci as summarizeReview, Cn as TRACK_RELATIONS_DEFAULT_LIMIT, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as systemEventFilterApplies, Dn as TrackPuritySchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as systemEventFamilyProblems, En as TimelapseRuleSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Fn as alarmPanelCapability, Fr as isRootSuppliableStepId, Ft as NcLegacyRuleActionSequenceSchema, G as ListTrackRelationsResultSchema, Gi as createEvent, Gn as audioMetricsCapability, Gr as normalizeNcMediaPolicy, Gt as NcSnoozeInputSchema, H as LABEL_SUPPLY_MACRO, Hi as CamProfileSchema, Hn as applySecureOnArmEdit, Hr as mayWriteToLocation, Ht as NcRuleUpdateInputSchema, I as EVENT_KIND_BY_CAP, Ii as canonicalHash, In as alertEpisodeId, Ir as isScheduleActive, It as NcRuleActionSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as defineRecordKinds, Jn as buildViewerLink, Jr as parseAlertEpisodePayload, Jt as NcStateSummarySectionIdSchema, K as LockStateSchema, Ki as customAction, Kn as audioModeOf, Kr as notificationRulesCapability, Kt as NcSnoozeSchema, L as EVENT_OWNER_TYPES, Li as evaluateSensorEdge, Ln as alertEpisodeKind, Lr as isSystemDelivery, Lt as NcRuleActionsSchema, M as EMBEDDING_MODEL_FAILED, Mi as vectorDimFromBase64, Mn as activateAlarmProfile, Mr as isLabelSupplyStepId, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as viewerLinkBaseOf, Nn as addonWidgetsSourceCapability, Nr as isMediaOwnerType, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as systemEventSummaryScheduleOf, On as TrackRelationKindSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as zoneAnalyticsCapability, Pn as alarmDeviceSecureOnArm, Pr as isRetiredClearKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qn as consumableLowPercentOf, Qr as plateGalleryCapability, Qt as NcTemplatePreviewSchema, R as EXPORT_DENSE_MAX_RANGES, Ri as isSourceCap, Rn as alertEpisodePayload, Rr as isSystemEventSummaryRule, Rt as NcRuleInputSchema, S as ClearTrackRelationInputSchema, Si as subKindsOf, Sn as TIMELAPSE_DENSE_FLOOR_SEC, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as systemEventDeliveryProblems, Tn as TimelapseRulePatchSchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as RECORD_MAX_DATA_BYTES, Un as armRemovalReconcile, Ur as memoizePinnedFfmpeg, Ut as NcScheduleSchema, V as FailureCounters, Vi as BaseAddon, Vn as applyNonBlockingEdit, Vr as linkedStepOf, Vt as NcRuleTargetSchema, W as ListTrackRelationsInputSchema, Wi as RECORD_QUERY_MAX, Wn as assertTimelapseCadences, Wr as ncMediaPolicyError, Wt as NcSharedSequenceSchema, X as MAX_TRACK_PURITIES, Xi as isDeviceScopedCap, Xn as classifyRetrainReview, Xr as pickFaceRescoreIntervalMinutes, Xt as NcTaxonomySchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as hydrateSchema, Yn as calendarEventLocalSpan, Yt as NcSystemEventKindSchema, Z as MAX_TRACK_RELATIONS, Zi as nodePin, Zn as cloneAlarmSettings, Zr as pipelineAnalyticsCapability, Zt as NcTemplatePreviewInputSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as EventCategory, _i as secureActuationFor, _n as SCENE_RESET_RECAPTURES, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as literal, ai as resolveModeDelays, an as RebuildObjectEmbeddingsRefusalSchema, ar as deriveRecordingMode, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, bi as startMoverUnderClearance, bn as SetTrackPurityResultSchema, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as object, cn as RetrainStatusSchema, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as strictObject, di as ruleAutoTriggersAlarm, dn as SCENE_DEFAULT_ANCHOR_THRESHOLD, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as ZodObject, ei as readMotionTriggeredBy, en as OPS_LOG_DEFAULT_LIMIT, er as cosineSimilarity$1, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as string, fi as ruleBelongsToAlarm$1, fn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as unknown, gi as sceneMonitorCapability, gn as SCENE_DIVERGED, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as union, hi as ruleKindOf, hn as SCENE_DEFAULT_UNCOVERED_POLICY, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as discriminatedUnion, ii as resolveLocationMode, in as RECORDING_SOURCE_CAMSTACK_ADDON, ir as derivePostTriggeredAction, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as templateVarsFor, jn as TrackSourceSchema, jr as isKnownTemplateVar, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventSummarySplitOf, kn as TrackRelationSchema, kr as isEventKindLabelKey, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as partialRecord, li as retrainRevisionConflictMessage, ln as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as tuple, mi as ruleGroupingApplies, mn as SCENE_DEFAULT_QUIET_SECONDS, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as array, ni as renameAlarmProfile, nr as deleteAlarmProfile, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as looseObject, oi as resolveModeNotifyPolicy, on as RetrainDismissedSchema, or as describeSystemEventDeliveryProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as templateLiteral, pi as ruleDeviceCompatCapQueries, pn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as defineCustomActions, qn as buildEventKindDescriptor, qr as occupancyScope, qt as NcSnoozeSuppressedSchema, ra as boolean, ri as resolveClipSearchMinScore, rn as RECORDING_EXPORT_MAX_READ_BYTES, rr as deriveDetailCropRect, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as number, si as resolveNcMediaImage, sn as RetrainOriginSchema, sr as describeSystemEventFamilyProblem, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as _enum, ti as readTimelapseGeneratedAt, tn as OpsLogEntrySchema, tr as createAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as record, ui as rootSupplyServes, un as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, vi as sensorRaisedOf, vn as SceneMonitorSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as syncActiveAlarmProfileSettings, wn as TimelapseRuleInputSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xi as storageOccupancyCapability, xn as SetTrackRelationInputSchema, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, yi as servedTemplateVars, yn as SetTrackPurityInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as DeviceType, zn as applyBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRulePatchSchema } from "../clip-model-registry-BtK1fPgi.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";
@@ -4071,6 +4071,224 @@ function ncAlarmPanelPort(panel) {
4071
4071
  };
4072
4072
  }
4073
4073
  //#endregion
4074
+ //#region src/durable/durable-record-body.ts
4075
+ /**
4076
+ * Zod body for phase-3 durable ledger record kinds. The engine validates on
4077
+ * write; each owner's `fromRecord` / `toValue` remain authoritative on read.
4078
+ */
4079
+ var DurableLedgerBodySchema = record(string(), unknown());
4080
+ //#endregion
4081
+ //#region src/notification-center/nc-durable-record-kinds.ts
4082
+ /**
4083
+ * Record kinds for NC live-state ledgers (records plan phase 3, steps 25–36).
4084
+ *
4085
+ * Hosted in the pipeline-analytics addon (`pipeline-analytics` owner stamp).
4086
+ * Declared by the hub; every node reads and writes them through `ctx.records`.
4087
+ * The legacy settings-store handles (`durableLegacyKind`, `legacyCollectionKind`)
4088
+ * serve only a centre built WITHOUT `records` — the specs.
4089
+ */
4090
+ /** The settings-store tables these kinds replaced; read only WITHOUT `ctx.records` (the specs). */
4091
+ var NC_LIVENESS_LEGACY = "notification-center:liveness";
4092
+ var NC_DEVICE_ALERTS_LEGACY = "notification-center:device-alerts";
4093
+ var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
4094
+ var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
4095
+ var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
4096
+ var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
4097
+ var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
4098
+ var NC_META_LEGACY = "notification-center:meta";
4099
+ var NC_SNOOZES_LEGACY = "notification-center:snoozes";
4100
+ var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
4101
+ var NC_TRACK_GROUPS_LEGACY = "notification-center:track-groups";
4102
+ var NC_OUTBOX_LEGACY = "notification-center:outbox";
4103
+ var NC_ARTIFACTS_LEGACY = "notification-center:artifacts";
4104
+ var ledger = (writeTrigger, database = "settings", retention = { none: "mirror load-all at boot; rows drop only via owner prune or device-removal hook" }) => ({
4105
+ schema: DurableLedgerBodySchema,
4106
+ class: "ledger",
4107
+ database,
4108
+ writeTrigger,
4109
+ device: "optional",
4110
+ at: "none",
4111
+ retention
4112
+ });
4113
+ /**
4114
+ * The shape the four QUERIED kinds share with the mirrors. Each spells its own
4115
+ * file, trigger, indexes and retention as literals (the records guards read them).
4116
+ * `device` and `at` stay what the first (empty) declaration said — the engine
4117
+ * refuses to change either — so every read is served by a DATA-field index, and
4118
+ * the camera rides the record envelope (`deviceId`) for the D865 removal.
4119
+ */
4120
+ var QUERIED = {
4121
+ schema: DurableLedgerBodySchema,
4122
+ device: "optional",
4123
+ at: "none"
4124
+ };
4125
+ var ncDurableRecordKinds = defineRecordKinds({
4126
+ liveness: ledger("a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
4127
+ "device-alerts": ledger("a sustained device alert episode the centre confirmed or cleared"),
4128
+ "system-state": ledger("a system-state subject flip the state summary accepted"),
4129
+ "alert-episodes": ledger("an alert episode the centre notified (critical or error); its resolution deletes the row"),
4130
+ occupancy: ledger("a confirmed occupancy level write-through after the watcher decided an edge"),
4131
+ "device-prunes": ledger("one row recording what a deleted-device prune removed"),
4132
+ "state-summary-stamps": ledger("digest delivery stamps per rule kind"),
4133
+ meta: ledger("outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
4134
+ snoozes: ledger("operator opens or closes a snooze window (write-through)"),
4135
+ "snooze-suppressed": {
4136
+ ...QUERIED,
4137
+ class: "ledger",
4138
+ database: "events",
4139
+ writeTrigger: "aggregate count per (window, target, rule, camera) for suppressed notifications",
4140
+ indexes: [{
4141
+ name: "snooze",
4142
+ fields: ["snoozeId"]
4143
+ }],
4144
+ retention: {
4145
+ owner: "notification-center",
4146
+ reason: "deleted with its snooze window, 6 h after that window closes"
4147
+ }
4148
+ },
4149
+ "track-groups": {
4150
+ ...QUERIED,
4151
+ class: "ledger",
4152
+ database: "events",
4153
+ writeTrigger: "one row per grouped delivery window opened at admission time",
4154
+ indexes: [
4155
+ {
4156
+ name: "open",
4157
+ fields: ["sealed"]
4158
+ },
4159
+ {
4160
+ name: "opened",
4161
+ fields: ["openedAt"]
4162
+ },
4163
+ {
4164
+ name: "device_opened",
4165
+ fields: ["deviceId", "openedAt"]
4166
+ },
4167
+ {
4168
+ name: "rule_opened",
4169
+ fields: ["ruleId", "openedAt"]
4170
+ },
4171
+ {
4172
+ name: "rule_device_opened",
4173
+ fields: [
4174
+ "ruleId",
4175
+ "deviceId",
4176
+ "openedAt"
4177
+ ]
4178
+ }
4179
+ ],
4180
+ retention: {
4181
+ owner: "notification-center",
4182
+ reason: "ring: the newest NC_GROUP_MAX_ROWS_PER_DEVICE rows per camera, trimmed on write"
4183
+ }
4184
+ },
4185
+ outbox: {
4186
+ ...QUERIED,
4187
+ class: "ledger",
4188
+ database: "events",
4189
+ writeTrigger: "one row per (rule, dedupRef, target) enqueued for delivery",
4190
+ indexes: [
4191
+ {
4192
+ name: "created",
4193
+ fields: ["createdAt"]
4194
+ },
4195
+ {
4196
+ name: "rule_created",
4197
+ fields: ["ruleId", "createdAt"]
4198
+ },
4199
+ {
4200
+ name: "status_created",
4201
+ fields: ["status", "createdAt"]
4202
+ },
4203
+ {
4204
+ name: "device_created",
4205
+ fields: ["deviceId", "createdAt"]
4206
+ },
4207
+ {
4208
+ name: "rule_status_created",
4209
+ fields: [
4210
+ "ruleId",
4211
+ "status",
4212
+ "createdAt"
4213
+ ]
4214
+ },
4215
+ {
4216
+ name: "rule_device_created",
4217
+ fields: [
4218
+ "ruleId",
4219
+ "deviceId",
4220
+ "createdAt"
4221
+ ]
4222
+ },
4223
+ {
4224
+ name: "status_device_created",
4225
+ fields: [
4226
+ "status",
4227
+ "deviceId",
4228
+ "createdAt"
4229
+ ]
4230
+ },
4231
+ {
4232
+ name: "rule_status_device_created",
4233
+ fields: [
4234
+ "ruleId",
4235
+ "status",
4236
+ "deviceId",
4237
+ "createdAt"
4238
+ ]
4239
+ }
4240
+ ],
4241
+ retention: {
4242
+ owner: "notification-center",
4243
+ reason: "terminal (sent/dead) rows older than 7 days pruned at boot; a pending row retries to dead first"
4244
+ },
4245
+ maxDataBytes: RECORD_MAX_DATA_BYTES
4246
+ },
4247
+ artifacts: {
4248
+ ...QUERIED,
4249
+ class: "ledger",
4250
+ database: "events",
4251
+ writeTrigger: "retained notification artefact metadata indexed for lookup and expiry",
4252
+ indexes: [{
4253
+ name: "captured",
4254
+ fields: ["capturedAt"]
4255
+ }, {
4256
+ name: "expires",
4257
+ fields: ["expiresAt"]
4258
+ }],
4259
+ retention: {
4260
+ owner: "notification-center",
4261
+ reason: "falls with its bytes: expired rows on every node, orphans against the shelf on the producer node"
4262
+ }
4263
+ }
4264
+ });
4265
+ /**
4266
+ * The settings-store table each kind stands in for when a centre is built
4267
+ * WITHOUT `ctx.records` - the specs' fake store (`legacyCollectionKind`,
4268
+ * `durableLegacyKind`). Production never reads it: every node has
4269
+ * `ctx.records`, and the operator dropped these tables offline (D900).
4270
+ */
4271
+ var NC_DURABLE_LEGACY_TABLES = {
4272
+ liveness: NC_LIVENESS_LEGACY,
4273
+ "device-alerts": NC_DEVICE_ALERTS_LEGACY,
4274
+ "system-state": NC_SYSTEM_STATE_LEGACY,
4275
+ "alert-episodes": NC_ALERT_EPISODES_LEGACY,
4276
+ occupancy: NC_OCCUPANCY_LEGACY,
4277
+ "device-prunes": NC_DEVICE_PRUNES_LEGACY,
4278
+ "state-summary-stamps": NC_STATE_SUMMARY_STAMPS_LEGACY,
4279
+ meta: NC_META_LEGACY,
4280
+ snoozes: NC_SNOOZES_LEGACY,
4281
+ "snooze-suppressed": NC_SNOOZE_SUPPRESSED_LEGACY,
4282
+ "track-groups": NC_TRACK_GROUPS_LEGACY,
4283
+ outbox: NC_OUTBOX_LEGACY,
4284
+ artifacts: NC_ARTIFACTS_LEGACY
4285
+ };
4286
+ /** The legacy table of an NC durable kind by name, or `undefined` for any other kind. */
4287
+ function ncDurableLegacyTableOf(kind) {
4288
+ for (const [name, table] of Object.entries(NC_DURABLE_LEGACY_TABLES)) if (name === kind) return table;
4289
+ }
4290
+ Object.values(NC_DURABLE_LEGACY_TABLES);
4291
+ //#endregion
4074
4292
  //#region src/durable/legacy-collection-kind.ts
4075
4293
  /** Bounds of a half-open range on the legacy filter (the store has no `>=`). */
4076
4294
  var RANGE_MIN = -864e13;
@@ -4216,8 +4434,8 @@ var LegacyCollectionKind = class {
4216
4434
  /** The handle an NC owner reads and writes through: records when present, else the legacy table. */
4217
4435
  function queriedKind(definition, deps) {
4218
4436
  if (deps.records !== void 0) return deps.records.kind(definition);
4219
- const collection = definition.declaration.adoptsFrom;
4220
- if (collection === void 0) throw new Error(`kind "${definition.name}" names no legacy table (adoptsFrom)`);
4437
+ const collection = ncDurableLegacyTableOf(definition.name);
4438
+ if (collection === void 0) throw new Error(`kind "${definition.name}" has no legacy table to stand in for it`);
4221
4439
  return new LegacyCollectionKind({
4222
4440
  definition,
4223
4441
  collection,
@@ -4262,203 +4480,6 @@ async function collectRecordPages(kind, q, max) {
4262
4480
  }
4263
4481
  }
4264
4482
  //#endregion
4265
- //#region src/durable/durable-record-body.ts
4266
- /**
4267
- * Zod body for phase-3 durable ledger record kinds. The engine validates on
4268
- * write; each owner's `fromRecord` / `toValue` remain authoritative on read.
4269
- */
4270
- var DurableLedgerBodySchema = record(string(), unknown());
4271
- //#endregion
4272
- //#region src/notification-center/nc-durable-record-kinds.ts
4273
- /**
4274
- * Record kinds for NC live-state ledgers (records plan phase 3, steps 25–36).
4275
- *
4276
- * Hosted in the pipeline-analytics addon (`pipeline-analytics` owner stamp).
4277
- * Declared by the hub; every node reads and writes them through `ctx.records`.
4278
- * The legacy settings-store handles (`durableLegacyKind`, `legacyCollectionKind`)
4279
- * serve only a centre built WITHOUT `records` — the specs.
4280
- */
4281
- /** Legacy collection names — keep in sync with owner constants. */
4282
- var NC_LIVENESS_LEGACY = "notification-center:liveness";
4283
- var NC_DEVICE_ALERTS_LEGACY = "notification-center:device-alerts";
4284
- var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
4285
- var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
4286
- var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
4287
- var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
4288
- var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
4289
- var NC_META_LEGACY = "notification-center:meta";
4290
- var NC_SNOOZES_LEGACY = "notification-center:snoozes";
4291
- var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
4292
- var NC_TRACK_GROUPS_LEGACY = "notification-center:track-groups";
4293
- var NC_OUTBOX_LEGACY = "notification-center:outbox";
4294
- var NC_ARTIFACTS_LEGACY = "notification-center:artifacts";
4295
- var ledger = (adoptsFrom, writeTrigger, database = "settings", retention = { none: "mirror load-all at boot; rows drop only via owner prune or device-removal hook" }) => ({
4296
- schema: DurableLedgerBodySchema,
4297
- class: "ledger",
4298
- database,
4299
- writeTrigger,
4300
- device: "optional",
4301
- at: "none",
4302
- retention,
4303
- adoptsFrom
4304
- });
4305
- /**
4306
- * The shape the four QUERIED kinds share with the mirrors. Each spells its own
4307
- * file, trigger, indexes and retention as literals (the records guards read them).
4308
- * `device` and `at` stay what the first (empty) declaration said — the engine
4309
- * refuses to change either — so every read is served by a DATA-field index, and
4310
- * the camera rides the record envelope (`deviceId`) for the D865 removal.
4311
- */
4312
- var QUERIED = {
4313
- schema: DurableLedgerBodySchema,
4314
- device: "optional",
4315
- at: "none"
4316
- };
4317
- var ncDurableRecordKinds = defineRecordKinds({
4318
- liveness: ledger(NC_LIVENESS_LEGACY, "a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
4319
- "device-alerts": ledger(NC_DEVICE_ALERTS_LEGACY, "a sustained device alert episode the centre confirmed or cleared"),
4320
- "system-state": ledger(NC_SYSTEM_STATE_LEGACY, "a system-state subject flip the state summary accepted"),
4321
- "alert-episodes": ledger(NC_ALERT_EPISODES_LEGACY, "an alert episode the centre notified (critical or error); its resolution deletes the row"),
4322
- occupancy: ledger(NC_OCCUPANCY_LEGACY, "a confirmed occupancy level write-through after the watcher decided an edge"),
4323
- "device-prunes": ledger(NC_DEVICE_PRUNES_LEGACY, "one row recording what a deleted-device prune removed"),
4324
- "state-summary-stamps": ledger(NC_STATE_SUMMARY_STAMPS_LEGACY, "digest delivery stamps per rule kind"),
4325
- meta: ledger(NC_META_LEGACY, "outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
4326
- snoozes: ledger(NC_SNOOZES_LEGACY, "operator opens or closes a snooze window (write-through)"),
4327
- "snooze-suppressed": {
4328
- ...QUERIED,
4329
- class: "ledger",
4330
- database: "events",
4331
- writeTrigger: "aggregate count per (window, target, rule, camera) for suppressed notifications",
4332
- indexes: [{
4333
- name: "snooze",
4334
- fields: ["snoozeId"]
4335
- }],
4336
- retention: {
4337
- owner: "notification-center",
4338
- reason: "deleted with its snooze window, 6 h after that window closes"
4339
- },
4340
- adoptsFrom: NC_SNOOZE_SUPPRESSED_LEGACY
4341
- },
4342
- "track-groups": {
4343
- ...QUERIED,
4344
- class: "ledger",
4345
- database: "events",
4346
- writeTrigger: "one row per grouped delivery window opened at admission time",
4347
- indexes: [
4348
- {
4349
- name: "open",
4350
- fields: ["sealed"]
4351
- },
4352
- {
4353
- name: "opened",
4354
- fields: ["openedAt"]
4355
- },
4356
- {
4357
- name: "device_opened",
4358
- fields: ["deviceId", "openedAt"]
4359
- },
4360
- {
4361
- name: "rule_opened",
4362
- fields: ["ruleId", "openedAt"]
4363
- },
4364
- {
4365
- name: "rule_device_opened",
4366
- fields: [
4367
- "ruleId",
4368
- "deviceId",
4369
- "openedAt"
4370
- ]
4371
- }
4372
- ],
4373
- retention: {
4374
- owner: "notification-center",
4375
- reason: "ring: the newest NC_GROUP_MAX_ROWS_PER_DEVICE rows per camera, trimmed on write"
4376
- },
4377
- adoptsFrom: NC_TRACK_GROUPS_LEGACY
4378
- },
4379
- outbox: {
4380
- ...QUERIED,
4381
- class: "ledger",
4382
- database: "events",
4383
- writeTrigger: "one row per (rule, dedupRef, target) enqueued for delivery",
4384
- indexes: [
4385
- {
4386
- name: "created",
4387
- fields: ["createdAt"]
4388
- },
4389
- {
4390
- name: "rule_created",
4391
- fields: ["ruleId", "createdAt"]
4392
- },
4393
- {
4394
- name: "status_created",
4395
- fields: ["status", "createdAt"]
4396
- },
4397
- {
4398
- name: "device_created",
4399
- fields: ["deviceId", "createdAt"]
4400
- },
4401
- {
4402
- name: "rule_status_created",
4403
- fields: [
4404
- "ruleId",
4405
- "status",
4406
- "createdAt"
4407
- ]
4408
- },
4409
- {
4410
- name: "rule_device_created",
4411
- fields: [
4412
- "ruleId",
4413
- "deviceId",
4414
- "createdAt"
4415
- ]
4416
- },
4417
- {
4418
- name: "status_device_created",
4419
- fields: [
4420
- "status",
4421
- "deviceId",
4422
- "createdAt"
4423
- ]
4424
- },
4425
- {
4426
- name: "rule_status_device_created",
4427
- fields: [
4428
- "ruleId",
4429
- "status",
4430
- "deviceId",
4431
- "createdAt"
4432
- ]
4433
- }
4434
- ],
4435
- retention: {
4436
- owner: "notification-center",
4437
- reason: "terminal (sent/dead) rows older than 7 days pruned at boot; a pending row retries to dead first"
4438
- },
4439
- maxDataBytes: RECORD_MAX_DATA_BYTES,
4440
- adoptsFrom: NC_OUTBOX_LEGACY
4441
- },
4442
- artifacts: {
4443
- ...QUERIED,
4444
- class: "ledger",
4445
- database: "events",
4446
- writeTrigger: "retained notification artefact metadata indexed for lookup and expiry",
4447
- indexes: [{
4448
- name: "captured",
4449
- fields: ["capturedAt"]
4450
- }, {
4451
- name: "expires",
4452
- fields: ["expiresAt"]
4453
- }],
4454
- retention: {
4455
- owner: "notification-center",
4456
- reason: "falls with its bytes: expired rows on every node, orphans against the shelf on the producer node"
4457
- },
4458
- adoptsFrom: NC_ARTIFACTS_LEGACY
4459
- }
4460
- });
4461
- //#endregion
4462
4483
  //#region src/notification-center/artifact-index.ts
4463
4484
  /**
4464
4485
  * @durable class=ledger owner=notification-center database=events
@@ -9433,38 +9454,12 @@ var FaceRescoreJobStatusSchema = object({
9433
9454
  * Record kinds owned by the pipeline-analytics addon (records plan phase 2,
9434
9455
  * steps 7-11).
9435
9456
  *
9436
- * Each kind adopts the legacy `pipeline-analytics:*` collection of the same
9437
- * name: the boot guard keeps the addon down until `scripts/hub/records-adopt.ts`
9438
- * has copied it. Declared only on the hub (agents have no `ctx.records`). The
9439
- * addon declares no `execution.group`, so the hub owner stamp is the addon id
9457
+ * Each kind replaced the legacy `pipeline-analytics:*` collection of the same
9458
+ * name; the records migration that copied them is closed (D900) and the tables
9459
+ * are gone (`retired-legacy-tables.ts`). The addon declares no
9460
+ * `execution.group`, so the hub owner stamp is the addon id
9440
9461
  * (`pipeline-analytics`).
9441
9462
  */
9442
- /** Legacy names for phase-3 analytics ledgers (keep in sync with owner constants). */
9443
- var PA_PHANTOM_CELLS = "pipeline-analytics:phantom-cells";
9444
- var PA_PACKAGE_BACKGROUND = "pipeline-analytics:package-background";
9445
- var PA_STATIONARY_EVIDENCE = "pipeline-analytics:stationary-evidence";
9446
- var PA_STATIONARY_OBJECTS = "pipeline-analytics:stationary-objects";
9447
- var PA_SCENE_LATCH = "pipeline-analytics:scene-latch";
9448
- var PA_MOTION_EVENTS = "pipeline-analytics:motion-events";
9449
- var PA_AUDIO_EVENTS = "pipeline-analytics:audio-events";
9450
- var PA_SENSOR_EVENTS = "pipeline-analytics:sensor-events";
9451
- var PA_FACES = "pipeline-analytics:faces";
9452
- var PA_PLATES = "pipeline-analytics:plates";
9453
- var PA_TRACK_ALIASES = "pipeline-analytics:track-aliases";
9454
- var PA_TRACK_PURITY = "pipeline-analytics:track-purity";
9455
- var PA_TRACK_RELATIONS = "pipeline-analytics:track-relations";
9456
- var PA_TRACKING_TAPES = "pipeline-analytics:tracking-tapes";
9457
- var PA_BIRTH_DECISION_LEDGER = "pipeline-analytics:birth-decision-ledger";
9458
- var PA_DEBUG_NOTE_ARCHIVE = "pipeline-analytics:debug-note-archive";
9459
- var PA_OPS_LOG = "pipeline-analytics:ops-log";
9460
- var PA_RETRAIN_FRAMES = "pipeline-analytics:retrain-frames";
9461
- var PA_RETRAIN_ANNOTATIONS = "pipeline-analytics:retrain-annotations";
9462
- var PA_RETRAIN_COLLECTIONS = "pipeline-analytics:retrain-collections";
9463
- var PA_RETRAIN_COLLECTION_FRAMES = "pipeline-analytics:retrain-collection-frames";
9464
- var PA_IDENTITIES = "pipeline-analytics:identities";
9465
- var PA_IDENTITY_SAMPLES = "pipeline-analytics:identity-samples";
9466
- var PA_VEHICLES = "pipeline-analytics:vehicles";
9467
- var PA_VEHICLE_SAMPLES = "pipeline-analytics:vehicle-samples";
9468
9463
  /**
9469
9464
  * Kinds that must agree through ONE `transact` sit in one file under one family
9470
9465
  * name, and the engine refuses a family split across files (records plan risk 8).
@@ -9491,7 +9486,9 @@ var MigrationMarkerBodySchema = object({
9491
9486
  });
9492
9487
  /** The orphan audit's resume position: one row, the whole map in one value on purpose. */
9493
9488
  var SweepCursorBodySchema = object({
9494
- scopes: record(string(), number()),
9489
+ /** A number is an offset (a settings-store scope); a string is the records
9490
+ * engine's opaque keyset cursor (a scope walked through its kind). */
9491
+ scopes: record(string(), union([number(), string()])),
9495
9492
  /** Absent on a row written before the walk existed — absent means "from the top". */
9496
9493
  faceVectorCursor: number().optional(),
9497
9494
  /** One cursor per CLIP index, keyed by index name. Absent on an older row. */
@@ -9516,8 +9513,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9516
9513
  fields: ["deviceId"]
9517
9514
  }],
9518
9515
  retention: { none: "operator intent: a scene goes only through deleteScene, never by age" },
9519
- maxDataBytes: RECORD_MAX_DATA_BYTES,
9520
- adoptsFrom: "pipeline-analytics:scenes"
9516
+ maxDataBytes: RECORD_MAX_DATA_BYTES
9521
9517
  },
9522
9518
  migrations: {
9523
9519
  schema: MigrationMarkerBodySchema,
@@ -9526,8 +9522,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9526
9522
  writeTrigger: "one marker row when a boot migration (label-tier-4g, audio-track-class-d189) completes without truncating",
9527
9523
  device: "none",
9528
9524
  at: "none",
9529
- retention: { none: "its PRESENCE stops the migration re-scanning every track, face and event row on the next boot; bounded by the number of migrations ever written (two today)" },
9530
- adoptsFrom: "pipeline-analytics:migrations"
9525
+ retention: { none: "its PRESENCE stops the migration re-scanning every track, face and event row on the next boot; bounded by the number of migrations ever written (two today)" }
9531
9526
  },
9532
9527
  "sweep-cursors": {
9533
9528
  schema: SweepCursorBodySchema,
@@ -9536,8 +9531,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9536
9531
  writeTrigger: "the end of every orphan-audit pass rewrites the one row holding every scope cursor",
9537
9532
  device: "none",
9538
9533
  at: "none",
9539
- retention: { none: "one row (id orphan-audit); losing it costs a re-walk from offset 0, never a wrong delete" },
9540
- adoptsFrom: "pipeline-analytics:sweep-cursors"
9534
+ retention: { none: "one row (id orphan-audit); losing it costs a re-walk from offset 0, never a wrong delete" }
9541
9535
  },
9542
9536
  "face-rescore-state": {
9543
9537
  schema: FaceRescoreStateBodySchema,
@@ -9545,8 +9539,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9545
9539
  writeTrigger: "a whole-archive face rescore (manual or scheduled) completes",
9546
9540
  device: "none",
9547
9541
  at: "none",
9548
- retention: { none: "one row (id last-run); losing it costs one idempotent rescore on the next scheduler tick" },
9549
- adoptsFrom: "pipeline-analytics:face-rescore-state"
9542
+ retention: { none: "one row (id last-run); losing it costs one idempotent rescore on the next scheduler tick" }
9550
9543
  },
9551
9544
  "phantom-cells": {
9552
9545
  schema: DurableLedgerBodySchema,
@@ -9554,8 +9547,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9554
9547
  writeTrigger: "a phantom cell closes and the short-motionless bar records it",
9555
9548
  device: "required",
9556
9549
  at: "none",
9557
- retention: { none: "bounded by per-device cap in application code" },
9558
- adoptsFrom: PA_PHANTOM_CELLS
9550
+ retention: { none: "bounded by per-device cap in application code" }
9559
9551
  },
9560
9552
  "package-background": {
9561
9553
  schema: DurableLedgerBodySchema,
@@ -9563,8 +9555,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9563
9555
  writeTrigger: "package background state accepted for a device",
9564
9556
  device: "required",
9565
9557
  at: "none",
9566
- retention: { none: "one row per device key the owner tracks" },
9567
- adoptsFrom: PA_PACKAGE_BACKGROUND
9558
+ retention: { none: "one row per device key the owner tracks" }
9568
9559
  },
9569
9560
  "stationary-evidence": {
9570
9561
  schema: DurableLedgerBodySchema,
@@ -9572,8 +9563,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9572
9563
  writeTrigger: "stationary evidence row written on the registry sweep",
9573
9564
  device: "required",
9574
9565
  at: "none",
9575
- retention: { none: "owner sweep drops idle evidence; maxRowsPerDevice in kind metadata" },
9576
- adoptsFrom: PA_STATIONARY_EVIDENCE
9566
+ retention: { none: "owner sweep drops idle evidence; maxRowsPerDevice in kind metadata" }
9577
9567
  },
9578
9568
  "stationary-objects": {
9579
9569
  schema: DurableLedgerBodySchema,
@@ -9581,8 +9571,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9581
9571
  writeTrigger: "parked object registry upsert or flush on the sweep interval",
9582
9572
  device: "required",
9583
9573
  at: "none",
9584
- retention: { none: "rows drop when the object moves or TTL expires" },
9585
- adoptsFrom: PA_STATIONARY_OBJECTS
9574
+ retention: { none: "rows drop when the object moves or TTL expires" }
9586
9575
  },
9587
9576
  "scene-latch": {
9588
9577
  schema: DurableLedgerBodySchema,
@@ -9590,8 +9579,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9590
9579
  writeTrigger: "scene monitor latch state write-through after a verdict",
9591
9580
  device: "required",
9592
9581
  at: "none",
9593
- retention: { none: "one row per (deviceId, monitorId) scene latch" },
9594
- adoptsFrom: PA_SCENE_LATCH
9582
+ retention: { none: "one row per (deviceId, monitorId) scene latch" }
9595
9583
  },
9596
9584
  "motion-events": {
9597
9585
  schema: MotionEventBodySchema,
@@ -9608,8 +9596,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9608
9596
  retention: {
9609
9597
  owner: "device-window",
9610
9598
  reason: "terminal age-keyed root swept with the device retention window (D61/D62)"
9611
- },
9612
- adoptsFrom: PA_MOTION_EVENTS
9599
+ }
9613
9600
  },
9614
9601
  "audio-events": {
9615
9602
  schema: AudioEventBodySchema,
@@ -9626,8 +9613,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9626
9613
  retention: {
9627
9614
  owner: "device-window",
9628
9615
  reason: "terminal age-keyed root on the device window, same as motion-events"
9629
- },
9630
- adoptsFrom: PA_AUDIO_EVENTS
9616
+ }
9631
9617
  },
9632
9618
  "sensor-events": {
9633
9619
  schema: SensorEventBodySchema,
@@ -9647,8 +9633,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9647
9633
  retention: {
9648
9634
  owner: "device-window",
9649
9635
  reason: "terminal age-keyed root on the camera retention window (D61)"
9650
- },
9651
- adoptsFrom: PA_SENSOR_EVENTS
9636
+ }
9652
9637
  },
9653
9638
  faces: {
9654
9639
  schema: FaceLedgerBodySchema,
@@ -9675,8 +9660,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9675
9660
  fields: ["trackId"]
9676
9661
  }
9677
9662
  ],
9678
- retention: { none: "track-owned (D61): rows leave with their track; enrolled faces exempt from capacity sweeps" },
9679
- adoptsFrom: PA_FACES
9663
+ retention: { none: "track-owned (D61): rows leave with their track; enrolled faces exempt from capacity sweeps" }
9680
9664
  },
9681
9665
  identities: {
9682
9666
  schema: IdentityConfigBodySchema,
@@ -9690,8 +9674,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9690
9674
  name: "name",
9691
9675
  fields: ["name"]
9692
9676
  }],
9693
- retention: { none: "operator delete only — losing the gallery un-enrols every person" },
9694
- adoptsFrom: PA_IDENTITIES
9677
+ retention: { none: "operator delete only — losing the gallery un-enrols every person" }
9695
9678
  },
9696
9679
  "identity-samples": {
9697
9680
  schema: IdentitySampleLedgerBodySchema,
@@ -9705,7 +9688,6 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9705
9688
  fields: ["identityId", "addedAt"]
9706
9689
  }],
9707
9690
  retention: { none: "enrollment exempt — rows leave only on removeSample or deleteIdentity" },
9708
- adoptsFrom: PA_IDENTITY_SAMPLES,
9709
9691
  transactFamily: IDENTITY_GALLERY
9710
9692
  },
9711
9693
  vehicles: {
@@ -9720,8 +9702,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9720
9702
  name: "name",
9721
9703
  fields: ["name"]
9722
9704
  }],
9723
- retention: { none: "operator delete only — losing the gallery un-enrols every vehicle" },
9724
- adoptsFrom: PA_VEHICLES
9705
+ retention: { none: "operator delete only — losing the gallery un-enrols every vehicle" }
9725
9706
  },
9726
9707
  "vehicle-samples": {
9727
9708
  schema: VehicleSampleLedgerBodySchema,
@@ -9735,7 +9716,6 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9735
9716
  fields: ["vehicleId", "addedAt"]
9736
9717
  }],
9737
9718
  retention: { none: "enrollment exempt — rows leave only on removeSample or deleteVehicle" },
9738
- adoptsFrom: PA_VEHICLE_SAMPLES,
9739
9719
  transactFamily: VEHICLE_GALLERY
9740
9720
  },
9741
9721
  plates: {
@@ -9759,8 +9739,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9759
9739
  fields: ["recognizedVehicleId", "timestamp"]
9760
9740
  }
9761
9741
  ],
9762
- retention: { none: "track-owned (D61): rows leave with their track; corrected and enrolled reads exempt" },
9763
- adoptsFrom: PA_PLATES
9742
+ retention: { none: "track-owned (D61): rows leave with their track; corrected and enrolled reads exempt" }
9764
9743
  },
9765
9744
  "track-aliases": {
9766
9745
  schema: PaEventsLedgerBodySchema,
@@ -9776,8 +9755,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9776
9755
  name: "device",
9777
9756
  fields: ["deviceId"]
9778
9757
  }],
9779
- retention: { none: "track-owned via survivor `trackId`; widens retention cascade (D683)" },
9780
- adoptsFrom: PA_TRACK_ALIASES
9758
+ retention: { none: "track-owned via survivor `trackId`; widens retention cascade (D683)" }
9781
9759
  },
9782
9760
  "track-purity": {
9783
9761
  schema: PaEventsLedgerBodySchema,
@@ -9793,8 +9771,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9793
9771
  name: "updated",
9794
9772
  fields: ["updatedAt"]
9795
9773
  }],
9796
- retention: { none: "row-count ceiling MAX_TRACK_PURITIES in application code (D681)" },
9797
- adoptsFrom: PA_TRACK_PURITY
9774
+ retention: { none: "row-count ceiling MAX_TRACK_PURITIES in application code (D681)" }
9798
9775
  },
9799
9776
  "track-relations": {
9800
9777
  schema: PaEventsLedgerBodySchema,
@@ -9810,8 +9787,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9810
9787
  name: "updated",
9811
9788
  fields: ["updatedAt"]
9812
9789
  }],
9813
- retention: { none: "row-count ceiling MAX_TRACK_RELATIONS in application code (D681)" },
9814
- adoptsFrom: PA_TRACK_RELATIONS
9790
+ retention: { none: "row-count ceiling MAX_TRACK_RELATIONS in application code (D681)" }
9815
9791
  },
9816
9792
  "tracking-tapes": {
9817
9793
  schema: PaEventsLedgerBodySchema,
@@ -9824,8 +9800,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9824
9800
  name: "device_created",
9825
9801
  fields: ["deviceId", "createdAt"]
9826
9802
  }],
9827
- retention: { none: "promotion budget MAX_PROMOTED_TAPES — refused, never evicted by age" },
9828
- adoptsFrom: PA_TRACKING_TAPES
9803
+ retention: { none: "promotion budget MAX_PROMOTED_TAPES — refused, never evicted by age" }
9829
9804
  },
9830
9805
  "birth-decision-ledger": {
9831
9806
  schema: PaEventsLedgerBodySchema,
@@ -9841,8 +9816,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9841
9816
  name: "device_at",
9842
9817
  fields: ["deviceId", "at"]
9843
9818
  }],
9844
- retention: { none: "row-count ceiling MAX_BIRTH_DECISION_RECORDS in application code" },
9845
- adoptsFrom: PA_BIRTH_DECISION_LEDGER
9819
+ retention: { none: "row-count ceiling MAX_BIRTH_DECISION_RECORDS in application code" }
9846
9820
  },
9847
9821
  "debug-note-archive": {
9848
9822
  schema: PaEventsLedgerBodySchema,
@@ -9858,8 +9832,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9858
9832
  name: "device_archived",
9859
9833
  fields: ["deviceId", "archivedAt"]
9860
9834
  }],
9861
- retention: { none: "row-count ceiling MAX_ARCHIVED_DEBUG_NOTES in application code" },
9862
- adoptsFrom: PA_DEBUG_NOTE_ARCHIVE
9835
+ retention: { none: "row-count ceiling MAX_ARCHIVED_DEBUG_NOTES in application code" }
9863
9836
  },
9864
9837
  "ops-log": {
9865
9838
  schema: PaEventsLedgerBodySchema,
@@ -9878,8 +9851,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9878
9851
  retention: {
9879
9852
  ttlMs: 720 * 60 * 60 * 1e3,
9880
9853
  from: "at"
9881
- },
9882
- adoptsFrom: PA_OPS_LOG
9854
+ }
9883
9855
  },
9884
9856
  "retrain-frames": {
9885
9857
  schema: PaEventsLedgerBodySchema,
@@ -9896,7 +9868,6 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9896
9868
  fields: ["deviceId", "complete"]
9897
9869
  }],
9898
9870
  retention: { none: "operator curation only; blobs on artifacts tenant (D781)" },
9899
- adoptsFrom: PA_RETRAIN_FRAMES,
9900
9871
  transactFamily: RETRAIN
9901
9872
  },
9902
9873
  "retrain-annotations": {
@@ -9921,7 +9892,6 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9921
9892
  }
9922
9893
  ],
9923
9894
  retention: { none: "leaves with frame deselect / operator delete" },
9924
- adoptsFrom: PA_RETRAIN_ANNOTATIONS,
9925
9895
  transactFamily: RETRAIN
9926
9896
  },
9927
9897
  "retrain-collections": {
@@ -9940,7 +9910,6 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9940
9910
  fields: ["createdAt"]
9941
9911
  }],
9942
9912
  retention: { none: "deleted with operator deleteCollection" },
9943
- adoptsFrom: PA_RETRAIN_COLLECTIONS,
9944
9913
  transactFamily: RETRAIN
9945
9914
  },
9946
9915
  "retrain-collection-frames": {
@@ -9958,14 +9927,13 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9958
9927
  fields: ["frameId"]
9959
9928
  }],
9960
9929
  retention: { none: "membership only; frame pixels live in retrain-frames" },
9961
- adoptsFrom: PA_RETRAIN_COLLECTION_FRAMES,
9962
9930
  transactFamily: RETRAIN
9963
9931
  }
9964
9932
  });
9965
9933
  //#endregion
9966
9934
  //#region src/pipeline-analytics/scene/scene-config-ledger.ts
9967
9935
  /** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
9968
- var PAGE_SIZE$1 = 1e3;
9936
+ var PAGE_SIZE$2 = 1e3;
9969
9937
  /**
9970
9938
  * A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
9971
9939
  * half-parsed scene would report a verdict about a region it cannot describe.
@@ -10003,7 +9971,7 @@ var SceneConfigLedger = class {
10003
9971
  let seen = 0;
10004
9972
  let cursor;
10005
9973
  for (;;) {
10006
- const limit = Math.min(PAGE_SIZE$1, this.loadLimit - seen);
9974
+ const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
10007
9975
  if (limit <= 0) break;
10008
9976
  const page = await this.kind.query({
10009
9977
  limit,
@@ -12050,6 +12018,28 @@ var EventLedgerCollection = class EventLedgerCollection {
12050
12018
  if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
12051
12019
  return out;
12052
12020
  }
12021
+ /**
12022
+ * One page of the whole kind in `id` order, resumed from the records
12023
+ * engine's OPAQUE keyset cursor (`null` = from the top). The answer's
12024
+ * `nextCursor` is `null` at the end. A keyset cursor names the last row
12025
+ * read, so deleting rows behind it never shifts what comes next — the
12026
+ * reason a bulk walk that reclaims as it goes needs this, not an offset.
12027
+ *
12028
+ * Records only: without `ctx.records` the caller pages the settings store
12029
+ * itself, and asking here throws rather than inventing a cursor.
12030
+ */
12031
+ async pageById(cursor, limit) {
12032
+ if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
12033
+ const page = await this.kind.query({
12034
+ order: "id",
12035
+ limit,
12036
+ ...cursor === null ? {} : { cursor }
12037
+ });
12038
+ return {
12039
+ rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
12040
+ nextCursor: page.nextCursor
12041
+ };
12042
+ }
12053
12043
  async getManyByIds(ids) {
12054
12044
  if (ids.length === 0) return [];
12055
12045
  if (this.kind === null) return (await this.store.query.query({
@@ -13176,11 +13166,35 @@ var FaceStore = class {
13176
13166
  * carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
13177
13167
  * that is what they are stamped with — a KNOWN historical fact, not a
13178
13168
  * fallback guess. A row that names some other model keeps its own.
13169
+ *
13170
+ * On a records hub the rows are paged through the faces LEDGER: the legacy
13171
+ * table is dropped there, and the adoption copied each row's body whole —
13172
+ * an `embedding` the pass had not drained yet included — so a records-era
13173
+ * row can still need converting (the same shape as `IdentityStore`).
13179
13174
  */
13180
13175
  conversionSpec() {
13176
+ const ledger = this.ledger;
13177
+ const recordsPaging = ledger.usesRecords ? {
13178
+ pageRows: (limit, offset) => ledger.query({
13179
+ orderBy: {
13180
+ field: "id",
13181
+ direction: "asc"
13182
+ },
13183
+ limit,
13184
+ offset
13185
+ }, [
13186
+ "deviceId",
13187
+ "timestamp",
13188
+ "trackId",
13189
+ "embeddingModelId",
13190
+ "embedding"
13191
+ ]),
13192
+ drainEmbedding: (id) => ledger.update(id, { embedding: [] })
13193
+ } : {};
13181
13194
  return {
13182
13195
  collection: FACES_COLLECTION,
13183
13196
  vectors: this.vectors,
13197
+ ...recordsPaging,
13184
13198
  write: (id, embedding, data) => {
13185
13199
  const deviceId = data["deviceId"];
13186
13200
  const timestamp = data["timestamp"];
@@ -38396,22 +38410,8 @@ async function migrateLegacyRuleActions(actions, createSequence) {
38396
38410
  * The notification-center module is hosted inside the pipeline-analytics addon.
38397
38411
  * That addon declares no `execution.group`, so the UDS owner stamp is the addon
38398
38412
  * id: `pipeline-analytics`. Declared hub-only (agents have no `ctx.records`).
38399
- * One deploy covers all 7 kinds; adoption is kind-by-kind.
38400
- */
38401
- /** Legacy collection for the text-catalog document. */
38402
- var NC_TEXTS_COLLECTION$1 = "notification-center:texts";
38403
- /** Legacy collection for shared action sequences. */
38404
- var NC_SEQUENCES_COLLECTION$1 = "notification-center:sequences";
38405
- /** Legacy collection for per-rule AI sections. */
38406
- var NC_RULE_AI_COLLECTION$1 = "notification-center:rule-ai";
38407
- /** Legacy collection for calendar-events rules. */
38408
- var NC_CALENDAR_EVENTS_RULES_COLLECTION$1 = "notification-center:calendar-events-rules";
38409
- /** Legacy collection for multi-camera digest rules. */
38410
- var NC_SUMMARY_RULES_COLLECTION$1 = "notification-center:summary-rules";
38411
- /** Legacy collection for timelapse rules. */
38412
- var NC_TIMELAPSE_RULES_COLLECTION$1 = "notification-center:timelapse-rules";
38413
- /** Legacy collection for notification rules. */
38414
- var NC_RULES_COLLECTION$1 = "notification-center:rules";
38413
+ * One deploy covers all 7 kinds.
38414
+ */
38415
38415
  /**
38416
38416
  * The hub language and operator-authored per-key overrides.
38417
38417
  * One row, id `texts`. Losing it degrades to the shipped English catalog.
@@ -38453,8 +38453,7 @@ var ncRecordKinds = defineRecordKinds({
38453
38453
  writeTrigger: "an operator changes the hub language (setLanguage) or replaces the per-key override set (setOverrides)",
38454
38454
  device: "none",
38455
38455
  at: "none",
38456
- retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" },
38457
- adoptsFrom: NC_TEXTS_COLLECTION$1
38456
+ retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" }
38458
38457
  },
38459
38458
  /**
38460
38459
  * Shared action sequences — named, ordered runs of steps with their own throttle.
@@ -38466,8 +38465,7 @@ var ncRecordKinds = defineRecordKinds({
38466
38465
  writeTrigger: "an admin creates/edits/deletes a shared sequence via notificationRules.createSequence / updateSequence / deleteSequence, or the one-shot boot migration that materialises legacy inline sequences",
38467
38466
  device: "none",
38468
38467
  at: "none",
38469
- retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" },
38470
- adoptsFrom: NC_SEQUENCES_COLLECTION$1
38468
+ retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" }
38471
38469
  },
38472
38470
  /**
38473
38471
  * Per-rule AI section: whether and how a rule narrates itself.
@@ -38479,8 +38477,7 @@ var ncRecordKinds = defineRecordKinds({
38479
38477
  writeTrigger: "an operator switches the AI section of a notification rule on or off, or edits its prompt / word cap / language",
38480
38478
  device: "none",
38481
38479
  at: "none",
38482
- retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" },
38483
- adoptsFrom: NC_RULE_AI_COLLECTION$1
38480
+ retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" }
38484
38481
  },
38485
38482
  /**
38486
38483
  * Calendar-events rules. One row per rule, id = rule UUID.
@@ -38495,8 +38492,7 @@ var ncRecordKinds = defineRecordKinds({
38495
38492
  name: "enabled",
38496
38493
  fields: ["enabled"]
38497
38494
  }],
38498
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38499
- adoptsFrom: NC_CALENDAR_EVENTS_RULES_COLLECTION$1
38495
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38500
38496
  },
38501
38497
  /**
38502
38498
  * Multi-camera digest rules. One row per rule, id = rule UUID.
@@ -38511,8 +38507,7 @@ var ncRecordKinds = defineRecordKinds({
38511
38507
  name: "enabled",
38512
38508
  fields: ["enabled"]
38513
38509
  }],
38514
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38515
- adoptsFrom: NC_SUMMARY_RULES_COLLECTION$1
38510
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38516
38511
  },
38517
38512
  /**
38518
38513
  * Timelapse rules. One row per rule, id = rule UUID.
@@ -38528,8 +38523,7 @@ var ncRecordKinds = defineRecordKinds({
38528
38523
  name: "enabled",
38529
38524
  fields: ["enabled"]
38530
38525
  }],
38531
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38532
- adoptsFrom: NC_TIMELAPSE_RULES_COLLECTION$1
38526
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38533
38527
  },
38534
38528
  /**
38535
38529
  * Notification rules — the main rule set. One row per rule, id = rule UUID.
@@ -38545,8 +38539,7 @@ var ncRecordKinds = defineRecordKinds({
38545
38539
  name: "delivery",
38546
38540
  fields: ["delivery"]
38547
38541
  }],
38548
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38549
- adoptsFrom: NC_RULES_COLLECTION$1
38542
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38550
38543
  }
38551
38544
  });
38552
38545
  //#endregion
@@ -67432,7 +67425,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
67432
67425
  });
67433
67426
  return "old-removal-retried";
67434
67427
  }
67435
- function tally$1(samples, outcomes, dryRun) {
67428
+ function tally$2(samples, outcomes, dryRun) {
67436
67429
  const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
67437
67430
  const refusals = [];
67438
67431
  for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
@@ -67504,7 +67497,7 @@ async function reenrolPhotoSamples(deps, options) {
67504
67497
  outcomes.push(outcomeOf(sample, verdict));
67505
67498
  await deps.pause(pacingMs);
67506
67499
  }
67507
- const result = tally$1(samples, outcomes, dryRun);
67500
+ const result = tally$2(samples, outcomes, dryRun);
67508
67501
  if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
67509
67502
  const { outcomes: _perSample, ...summary } = result;
67510
67503
  deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
@@ -78919,24 +78912,11 @@ function createDiscardJob(deps) {
78919
78912
  });
78920
78913
  await sleep(0);
78921
78914
  }
78922
- for (;;) {
78915
+ const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
78916
+ for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
78923
78917
  if (cancelRequested) return owners;
78924
- const rows = await deps.store.query.query({
78925
- collection: RETRAIN_FRAMES_COLLECTION,
78926
- columns: ["id"],
78927
- filter: {
78928
- where: { locationId },
78929
- limit: MEDIA_PAGE
78930
- }
78931
- });
78932
- if (rows.length === 0) break;
78933
- for (const row of rows) {
78934
- await deps.store.delete.mutate({
78935
- collection: RETRAIN_FRAMES_COLLECTION,
78936
- key: row.id
78937
- });
78938
- deleted += 1;
78939
- }
78918
+ const page = retrainIds.slice(i, i + MEDIA_PAGE);
78919
+ deleted += await deps.retrainFrameLedger.deleteByIds(page);
78940
78920
  report({
78941
78921
  dbRowsDeleted: deleted,
78942
78922
  detail: `deleted ${deleted} media/retrain rows`
@@ -78988,6 +78968,382 @@ function createDiscardJob(deps) {
78988
78968
  };
78989
78969
  }
78990
78970
  //#endregion
78971
+ //#region src/pipeline-analytics/retention/media-forget-actions.ts
78972
+ /**
78973
+ * The operator door for the NARROW media-row forget (`media-row-forget.ts`).
78974
+ *
78975
+ * It forgets `pipeline-analytics:media` rows stamped with ONE location whose
78976
+ * file is provably gone, and nothing else: no event, track, object-event,
78977
+ * face, plate or identity row is read for deletion, ever. Contrast
78978
+ * `storage.discardLocation`, which wipes the disk AND cascades owners.
78979
+ *
78980
+ * `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
78981
+ * also requires `expectAbsent` — the `absent` count a dry run just reported —
78982
+ * so the forget only ever deletes a population a human has already read.
78983
+ */
78984
+ /** Hard ceiling on rows one call examines — the call is synchronous. */
78985
+ var MEDIA_FORGET_MAX_ROWS = 5e4;
78986
+ var MediaForgetInputSchema = object({
78987
+ locationId: string().min(1),
78988
+ /** Restrict to these cameras. Omitted = every row stamped with the location. */
78989
+ deviceIds: array(number().int()).min(1).optional(),
78990
+ dryRun: boolean().default(true),
78991
+ /**
78992
+ * Required when `dryRun` is false: the `totals.absent` of the dry run. A
78993
+ * different count at apply time refuses the whole call before any delete.
78994
+ */
78995
+ expectAbsent: number().int().nonnegative().optional(),
78996
+ /** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
78997
+ maxRows: number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
78998
+ /** Detailed rows returned per verdict. Default 20. */
78999
+ sampleSize: number().int().min(0).max(500).optional()
79000
+ });
79001
+ var VerdictCountsSchema = object({
79002
+ rows: number(),
79003
+ /** File answered ENOENT — the only verdict that may be forgotten. */
79004
+ absent: number(),
79005
+ /** File is there: relocation is incomplete. Never forgotten. */
79006
+ present: number(),
79007
+ /** The stat failed with any errno other than ENOENT. Never forgotten. */
79008
+ error: number(),
79009
+ /** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
79010
+ kept: number(),
79011
+ /** Rows actually deleted (0 on a dry run). */
79012
+ deleted: number()
79013
+ });
79014
+ var MediaForgetSampleSchema = object({
79015
+ mediaId: number(),
79016
+ deviceId: number(),
79017
+ kind: string(),
79018
+ ownerKind: string(),
79019
+ ownerId: string(),
79020
+ path: string(),
79021
+ /** The errno (error) or the reason (kept); absent otherwise. */
79022
+ reason: string().optional()
79023
+ });
79024
+ var mediaForgetActions = defineCustomActions({ "storage.forgetMissingMedia": customAction(MediaForgetInputSchema, object({
79025
+ locationId: string(),
79026
+ dryRun: boolean(),
79027
+ root: string(),
79028
+ /** Rows of this location that were not examined because `maxRows` was hit. */
79029
+ truncated: boolean(),
79030
+ totals: VerdictCountsSchema,
79031
+ byDevice: array(VerdictCountsSchema.extend({ deviceId: number() })),
79032
+ byKind: array(VerdictCountsSchema.extend({ kind: string() })),
79033
+ /** Every id the call deletes (dry run: would delete). The exact list. */
79034
+ absentIds: array(number()),
79035
+ samples: object({
79036
+ absent: array(MediaForgetSampleSchema),
79037
+ present: array(MediaForgetSampleSchema),
79038
+ error: array(MediaForgetSampleSchema),
79039
+ kept: array(MediaForgetSampleSchema)
79040
+ }),
79041
+ /** Absent at classification, no longer ENOENT at delete time: kept. */
79042
+ changedBeforeDelete: number()
79043
+ }), {
79044
+ kind: "mutation",
79045
+ auth: "admin"
79046
+ }) });
79047
+ //#endregion
79048
+ //#region src/pipeline-analytics/retention/media-row-forget.ts
79049
+ /**
79050
+ * The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
79051
+ * with ONE storage location whose file is provably gone — and nothing else.
79052
+ *
79053
+ * Why it exists: a location whose bytes were lost keeps rows naming it, and a
79054
+ * cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
79055
+ * once cascaded ~132 k events away from a read it did not double-check. Here:
79056
+ *
79057
+ * - **Only media rows.** No event, track, object-event, face, plate or
79058
+ * identity row is deleted. A parent keeps existing with that media kind
79059
+ * missing — which is exactly what it already serves (`getBytesById` answers
79060
+ * `null` for a missing row as it does for a missing blob).
79061
+ * - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
79062
+ * location's own root on THIS node. Any other errno keeps the row and is
79063
+ * reported; a present file keeps the row and is reported (relocation is
79064
+ * incomplete). A row with no path, a path that escapes the root, or a
79065
+ * retention-exempt owner (the enrolled gallery) is kept.
79066
+ * - **Only rows stamped with the id.** `locationId IS NULL` rows mean
79067
+ * "wherever eventMedia points now" and are never selected.
79068
+ * - **Only a location nothing writes to.** An active (writable) location is
79069
+ * refused: new rows could land while the walk runs.
79070
+ * - **Dry run first, then the same count.** Applying requires
79071
+ * `expectAbsent`; the whole population is classified before any delete,
79072
+ * and a different absent count refuses the call with nothing written.
79073
+ * Each id is re-`stat`ed right before its batch is deleted, and the delete
79074
+ * predicate repeats `locationId`, so a row repointed in between survives.
79075
+ *
79076
+ * Occupancy needs no separate bookkeeping: `storage-occupancy` sums
79077
+ * `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
79078
+ */
79079
+ var PAGE_SIZE$1 = 200;
79080
+ var DELETE_BATCH = 200;
79081
+ var DEFAULT_SAMPLE_SIZE = 20;
79082
+ function errnoOf(err) {
79083
+ if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
79084
+ return err instanceof Error ? err.message : String(err);
79085
+ }
79086
+ function emptyCounts() {
79087
+ return {
79088
+ rows: 0,
79089
+ absent: 0,
79090
+ present: 0,
79091
+ error: 0,
79092
+ kept: 0,
79093
+ deleted: 0
79094
+ };
79095
+ }
79096
+ function tally$1(counts, verdict) {
79097
+ const rows = counts.rows + 1;
79098
+ switch (verdict) {
79099
+ case "absent": return {
79100
+ ...counts,
79101
+ rows,
79102
+ absent: counts.absent + 1
79103
+ };
79104
+ case "present": return {
79105
+ ...counts,
79106
+ rows,
79107
+ present: counts.present + 1
79108
+ };
79109
+ case "error": return {
79110
+ ...counts,
79111
+ rows,
79112
+ error: counts.error + 1
79113
+ };
79114
+ case "kept": return {
79115
+ ...counts,
79116
+ rows,
79117
+ kept: counts.kept + 1
79118
+ };
79119
+ }
79120
+ }
79121
+ /** The file's absolute path under `root`, or `null` when it would escape it. */
79122
+ function resolveUnder(root, relativePath) {
79123
+ const base = path.resolve(root);
79124
+ const full = path.resolve(base, relativePath);
79125
+ return full.startsWith(base + path.sep) ? full : null;
79126
+ }
79127
+ function toSample(id, data) {
79128
+ return {
79129
+ mediaId: Number(id),
79130
+ deviceId: Number(data["deviceId"] ?? 0),
79131
+ kind: String(data["kind"] ?? ""),
79132
+ ownerKind: String(data["ownerKind"] ?? ""),
79133
+ ownerId: String(data["ownerId"] ?? ""),
79134
+ path: String(data["path"] ?? "")
79135
+ };
79136
+ }
79137
+ async function probe(stat, absolutePath) {
79138
+ try {
79139
+ await stat(absolutePath);
79140
+ return { verdict: "present" };
79141
+ } catch (err) {
79142
+ const code = errnoOf(err);
79143
+ return code === "ENOENT" ? { verdict: "absent" } : {
79144
+ verdict: "error",
79145
+ reason: code
79146
+ };
79147
+ }
79148
+ }
79149
+ async function classify(stat, root, id, data) {
79150
+ const sample = toSample(id, data);
79151
+ const ownerKind = sample.ownerKind;
79152
+ if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
79153
+ key: id,
79154
+ sample: {
79155
+ ...sample,
79156
+ reason: "retention-exempt owner"
79157
+ },
79158
+ verdict: "kept",
79159
+ absolutePath: null
79160
+ };
79161
+ if (sample.path.length === 0) return {
79162
+ key: id,
79163
+ sample: {
79164
+ ...sample,
79165
+ reason: "no path"
79166
+ },
79167
+ verdict: "kept",
79168
+ absolutePath: null
79169
+ };
79170
+ const absolutePath = resolveUnder(root, sample.path);
79171
+ if (absolutePath === null) return {
79172
+ key: id,
79173
+ sample: {
79174
+ ...sample,
79175
+ reason: "path escapes the root"
79176
+ },
79177
+ verdict: "kept",
79178
+ absolutePath
79179
+ };
79180
+ const result = await probe(stat, absolutePath);
79181
+ return {
79182
+ key: id,
79183
+ sample: result.reason === void 0 ? sample : {
79184
+ ...sample,
79185
+ reason: result.reason
79186
+ },
79187
+ verdict: result.verdict,
79188
+ absolutePath
79189
+ };
79190
+ }
79191
+ function resolveTarget(locations, locationId, localNodeId) {
79192
+ const location = locations.find((row) => row.id === locationId);
79193
+ if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
79194
+ if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
79195
+ if (mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
79196
+ const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
79197
+ if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
79198
+ return {
79199
+ location,
79200
+ root: locationBasePath(location)
79201
+ };
79202
+ }
79203
+ /**
79204
+ * Classify every row stamped with `input.locationId` (bounded by `maxRows`),
79205
+ * and — only when `dryRun` is false and the absent count equals
79206
+ * `expectAbsent` — delete the absent ones, per camera, in batches.
79207
+ */
79208
+ async function forgetMissingMediaRows(deps, rawInput) {
79209
+ const input = MediaForgetInputSchema.parse(rawInput);
79210
+ if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
79211
+ const stat = deps.stat ?? ((p) => promises.stat(p));
79212
+ const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
79213
+ const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
79214
+ const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
79215
+ const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
79216
+ const classified = [];
79217
+ let cursor = 0;
79218
+ let truncated = false;
79219
+ for (;;) {
79220
+ const rows = await deps.store.query.query({
79221
+ collection: MEDIA_COLLECTION,
79222
+ columns: [
79223
+ "id",
79224
+ "deviceId",
79225
+ "ownerKind",
79226
+ "ownerId",
79227
+ "kind",
79228
+ "path"
79229
+ ],
79230
+ filter: {
79231
+ where: { locationId: input.locationId },
79232
+ ...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
79233
+ whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
79234
+ orderBy: {
79235
+ field: "id",
79236
+ direction: "asc"
79237
+ },
79238
+ limit: PAGE_SIZE$1
79239
+ }
79240
+ });
79241
+ if (rows.length === 0) break;
79242
+ for (const row of rows) {
79243
+ if (classified.length >= maxRows) {
79244
+ truncated = true;
79245
+ break;
79246
+ }
79247
+ classified.push(await classify(stat, root, row.id, row.data));
79248
+ cursor = Math.max(cursor, Number(row.id));
79249
+ }
79250
+ if (truncated || rows.length < PAGE_SIZE$1) break;
79251
+ await pace();
79252
+ }
79253
+ const absentRows = classified.filter((c) => c.verdict === "absent");
79254
+ const deletedIds = /* @__PURE__ */ new Set();
79255
+ let changedBeforeDelete = 0;
79256
+ if (!input.dryRun) {
79257
+ if (absentRows.length !== input.expectAbsent) throw new Error(`forgetMissingMedia refused: ${String(absentRows.length)} rows are absent now, expectAbsent was ${String(input.expectAbsent)} — run the dry run again; nothing was deleted`);
79258
+ const byDevice = /* @__PURE__ */ new Map();
79259
+ for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
79260
+ for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
79261
+ const batch = [];
79262
+ for (const row of rows.slice(at, at + DELETE_BATCH)) if ((row.absolutePath === null ? null : await probe(stat, row.absolutePath))?.verdict === "absent") batch.push(row);
79263
+ else changedBeforeDelete += 1;
79264
+ if (batch.length === 0) continue;
79265
+ const { deleted } = await deps.store.deleteWhere.mutate({
79266
+ collection: MEDIA_COLLECTION,
79267
+ filter: {
79268
+ where: {
79269
+ locationId: input.locationId,
79270
+ deviceId
79271
+ },
79272
+ whereIn: { id: batch.map((row) => row.key) }
79273
+ }
79274
+ });
79275
+ for (const row of batch) deletedIds.add(row.sample.mediaId);
79276
+ deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
79277
+ tags: { deviceId },
79278
+ meta: {
79279
+ locationId: input.locationId,
79280
+ batch: batch.length,
79281
+ deleted,
79282
+ firstId: batch[0]?.sample.mediaId,
79283
+ lastId: batch[batch.length - 1]?.sample.mediaId
79284
+ }
79285
+ });
79286
+ if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
79287
+ tags: { deviceId },
79288
+ meta: {
79289
+ locationId: input.locationId,
79290
+ batch: batch.length,
79291
+ deleted
79292
+ }
79293
+ });
79294
+ await pace();
79295
+ }
79296
+ }
79297
+ let totals = emptyCounts();
79298
+ const perDevice = /* @__PURE__ */ new Map();
79299
+ const perKind = /* @__PURE__ */ new Map();
79300
+ for (const row of classified) {
79301
+ const isDeleted = deletedIds.has(row.sample.mediaId);
79302
+ const bump = (c) => {
79303
+ const next = tally$1(c, row.verdict);
79304
+ return isDeleted ? {
79305
+ ...next,
79306
+ deleted: next.deleted + 1
79307
+ } : next;
79308
+ };
79309
+ totals = bump(totals);
79310
+ perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
79311
+ perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
79312
+ }
79313
+ const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
79314
+ for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
79315
+ tags: { deviceId },
79316
+ meta: {
79317
+ locationId: input.locationId,
79318
+ dryRun: input.dryRun,
79319
+ ...counts
79320
+ }
79321
+ });
79322
+ return {
79323
+ locationId: input.locationId,
79324
+ dryRun: input.dryRun,
79325
+ root,
79326
+ truncated,
79327
+ totals,
79328
+ byDevice: [...perDevice].map(([deviceId, c]) => ({
79329
+ deviceId,
79330
+ ...c
79331
+ })),
79332
+ byKind: [...perKind].map(([kind, c]) => ({
79333
+ kind,
79334
+ ...c
79335
+ })),
79336
+ absentIds: absentRows.map((r) => r.sample.mediaId),
79337
+ samples: {
79338
+ absent: samplesOf("absent"),
79339
+ present: samplesOf("present"),
79340
+ error: samplesOf("error"),
79341
+ kept: samplesOf("kept")
79342
+ },
79343
+ changedBeforeDelete
79344
+ };
79345
+ }
79346
+ //#endregion
78991
79347
  //#region src/pipeline-analytics/retention/media-reclaim.ts
78992
79348
  var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
78993
79349
  var DEFAULT_MEDIA_RECLAIM_LIMITS = {
@@ -93265,17 +93621,8 @@ function rowIsGallery(data) {
93265
93621
  * D295 calls "a guard nobody believes".
93266
93622
  */
93267
93623
  function collectionsFor(mode) {
93268
- if (mode === "gallery") return [{
93269
- name: MEDIA_COLLECTION,
93270
- order: "timestamp"
93271
- }];
93272
- return [{
93273
- name: MEDIA_COLLECTION,
93274
- order: "timestamp"
93275
- }, {
93276
- name: RETRAIN_FRAMES_COLLECTION,
93277
- order: "copiedAt"
93278
- }];
93624
+ if (mode === "gallery") return [{ lane: "media" }];
93625
+ return [{ lane: "media" }, { lane: "retrainFrames" }];
93279
93626
  }
93280
93627
  /**
93281
93628
  * Does this pass act on this row at all?
@@ -93360,7 +93707,7 @@ var MediaRelocateEngine = class {
93360
93707
  async countUnstamped() {
93361
93708
  try {
93362
93709
  const media = await this.countUnstampedIn(MEDIA_COLLECTION);
93363
- const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
93710
+ const retrainFrames = await this.countUnstampedRetrainFrames();
93364
93711
  return {
93365
93712
  media,
93366
93713
  retrainFrames,
@@ -93412,6 +93759,28 @@ var MediaRelocateEngine = class {
93412
93759
  };
93413
93760
  }
93414
93761
  }
93762
+ /**
93763
+ * The retrain lane's unstamped population. The ledger's records `where` is
93764
+ * equality-only (a `null` predicate is dropped, which would count the whole
93765
+ * dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
93766
+ * dataset is operator-selected and small.
93767
+ */
93768
+ async countUnstampedRetrainFrames() {
93769
+ const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
93770
+ return {
93771
+ present: rows > 0,
93772
+ rows
93773
+ };
93774
+ }
93775
+ /** Every row of one lane, oldest-first. */
93776
+ rowsOf(collection) {
93777
+ return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
93778
+ }
93779
+ /** The retrain-frame ledger, read whole and yielded oldest copy first. */
93780
+ async *retrainFrameRows() {
93781
+ const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
93782
+ for (const row of ordered) yield row;
93783
+ }
93415
93784
  start(input) {
93416
93785
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
93417
93786
  this.cancelCensuses("a relocate pass started");
@@ -93515,22 +93884,22 @@ var MediaRelocateEngine = class {
93515
93884
  let sealable = 0;
93516
93885
  for (const collection of collectionsFor(mode)) {
93517
93886
  if (stop()) return null;
93518
- sealable += await this.deps.store.count.query({
93519
- collection: collection.name,
93887
+ sealable += collection.lane === "media" ? await this.deps.store.count.query({
93888
+ collection: MEDIA_COLLECTION,
93520
93889
  filter: { where: { [LOCATION_ID_FIELD]: null } }
93521
- });
93890
+ }) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
93522
93891
  }
93523
93892
  return { rows: sealable };
93524
93893
  }
93525
93894
  let rows = 0;
93526
93895
  for (const collection of collectionsFor(mode)) {
93527
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93528
- for await (const row of this.pageRows(collection.name, collection.order)) {
93896
+ const isRetrain = collection.lane === "retrainFrames";
93897
+ for await (const row of this.rowsOf(collection)) {
93529
93898
  if (stop()) {
93530
93899
  this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
93531
93900
  mode,
93532
93901
  toLocationId,
93533
- collection: collection.name
93902
+ collection: collection.lane
93534
93903
  } });
93535
93904
  return null;
93536
93905
  }
@@ -93538,7 +93907,7 @@ var MediaRelocateEngine = class {
93538
93907
  this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
93539
93908
  mode,
93540
93909
  toLocationId,
93541
- collection: collection.name,
93910
+ collection: collection.lane,
93542
93911
  rowsVisited: budget.rowsVisited,
93543
93912
  rowBudget: MEDIA_CENSUS_ROW_BUDGET,
93544
93913
  deadlineMs: MEDIA_CENSUS_DEADLINE_MS
@@ -93574,15 +93943,15 @@ var MediaRelocateEngine = class {
93574
93943
  if (job.mode === "seal") {
93575
93944
  for (const collection of collectionsFor(job.mode)) {
93576
93945
  if (job.cancelRequested) break;
93577
- await this.sealCollection(collection.name, job);
93946
+ await this.sealCollection(collection, job);
93578
93947
  }
93579
93948
  job.state = job.cancelRequested ? "cancelled" : "done";
93580
93949
  return;
93581
93950
  }
93582
93951
  for (const collection of collectionsFor(job.mode)) {
93583
93952
  if (job.cancelRequested) break;
93584
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93585
- for await (const row of this.pageRows(collection.name, collection.order)) {
93953
+ const isRetrain = collection.lane === "retrainFrames";
93954
+ for await (const row of this.rowsOf(collection)) {
93586
93955
  if (job.cancelRequested) break;
93587
93956
  const data = row.data;
93588
93957
  if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
@@ -93594,7 +93963,7 @@ var MediaRelocateEngine = class {
93594
93963
  }
93595
93964
  try {
93596
93965
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
93597
- await this.stamp(collection.name, row.id, data, job.toLocationId);
93966
+ await this.stamp(collection, row.id, data, job.toLocationId);
93598
93967
  continue;
93599
93968
  }
93600
93969
  const bytes = await this.deps.storage.read({
@@ -93610,7 +93979,7 @@ var MediaRelocateEngine = class {
93610
93979
  location: job.toLocationId,
93611
93980
  relativePath
93612
93981
  })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
93613
- await this.stamp(collection.name, row.id, data, job.toLocationId);
93982
+ await this.stamp(collection, row.id, data, job.toLocationId);
93614
93983
  await this.deps.storage.delete({
93615
93984
  location: from,
93616
93985
  relativePath
@@ -93621,7 +93990,7 @@ var MediaRelocateEngine = class {
93621
93990
  } catch (err) {
93622
93991
  failedRows++;
93623
93992
  this.deps.logger.debug("media relocate row failed", { meta: {
93624
- collection: collection.name,
93993
+ collection: collection.lane,
93625
93994
  key: row.id,
93626
93995
  error: String(err)
93627
93996
  } });
@@ -93694,7 +94063,17 @@ var MediaRelocateEngine = class {
93694
94063
  * reports a completed seal having stamped nothing. Zero unstamped rows is the
93695
94064
  * single value the migration gate reads as "safe to cut over".
93696
94065
  */
93697
- async sealCollection(collection, job) {
94066
+ async sealCollection(lane, job) {
94067
+ if (lane.lane === "retrainFrames") {
94068
+ for (const row of await this.deps.retrainFrameLedger.query({})) {
94069
+ if (job.cancelRequested) return;
94070
+ if (!rowIsUnstamped(row.data)) continue;
94071
+ await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
94072
+ job.filesMoved += 1;
94073
+ }
94074
+ return;
94075
+ }
94076
+ const collection = MEDIA_COLLECTION;
93698
94077
  for (;;) {
93699
94078
  if (job.cancelRequested) return;
93700
94079
  const batch = await this.deps.store.query.query({
@@ -93715,9 +94094,13 @@ var MediaRelocateEngine = class {
93715
94094
  job.filesMoved += updated;
93716
94095
  }
93717
94096
  }
93718
- async stamp(collection, key, data, locationId) {
94097
+ async stamp(lane, key, data, locationId) {
94098
+ if (lane.lane === "retrainFrames") {
94099
+ await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
94100
+ return;
94101
+ }
93719
94102
  await this.deps.store.set.mutate({
93720
- collection,
94103
+ collection: MEDIA_COLLECTION,
93721
94104
  key,
93722
94105
  value: {
93723
94106
  ...data,
@@ -102164,6 +102547,18 @@ function decodeCursorMap(raw) {
102164
102547
  function nonNegativeInt(value) {
102165
102548
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
102166
102549
  }
102550
+ function decodeScopeCursors(raw) {
102551
+ const out = {};
102552
+ if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
102553
+ if (typeof cursor === "string" && cursor.length > 0) {
102554
+ out[key] = cursor;
102555
+ continue;
102556
+ }
102557
+ const parsed = nonNegativeInt(cursor);
102558
+ if (parsed !== null) out[key] = parsed;
102559
+ }
102560
+ return out;
102561
+ }
102167
102562
  /** Read a stored row back into a cursor map, ignoring anything unreadable. */
102168
102563
  function decodeOrphanAuditCursors(value) {
102169
102564
  const empty = {
@@ -102175,7 +102570,7 @@ function decodeOrphanAuditCursors(value) {
102175
102570
  if (value === null || typeof value !== "object") return empty;
102176
102571
  const row = { ...value };
102177
102572
  return {
102178
- scopes: decodeCursorMap(row["scopes"]),
102573
+ scopes: decodeScopeCursors(row["scopes"]),
102179
102574
  faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
102180
102575
  clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
102181
102576
  updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
@@ -103748,7 +104143,9 @@ async function auditScope(scope, deps, limits, cursor) {
103748
104143
  };
103749
104144
  const deviceIds = /* @__PURE__ */ new Set();
103750
104145
  const graceFloor = deps.now() - limits.graceMs;
103751
- let offset = Math.max(0, cursor);
104146
+ let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
104147
+ let keysetCursor = typeof cursor === "string" ? cursor : null;
104148
+ let keyed = keysetCursor !== null;
103752
104149
  let stoppedBy = "exhausted";
103753
104150
  let collectionSize = null;
103754
104151
  try {
@@ -103765,9 +104162,16 @@ async function auditScope(scope, deps, limits, cursor) {
103765
104162
  stoppedBy = "row-ceiling";
103766
104163
  break;
103767
104164
  }
103768
- const rows = await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
104165
+ if (deps.stop?.() === true) {
104166
+ stoppedBy = "stopped";
104167
+ break;
104168
+ }
104169
+ const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
104170
+ keyed = keyedPage !== null;
104171
+ const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
103769
104172
  if (rows.length === 0) {
103770
104173
  offset = 0;
104174
+ keysetCursor = null;
103771
104175
  break;
103772
104176
  }
103773
104177
  const refs = rows.map((row) => scope.resolve(row));
@@ -103854,9 +104258,17 @@ async function auditScope(scope, deps, limits, cursor) {
103854
104258
  if (deps.breathe) await deps.breathe();
103855
104259
  if (ceilingHit) {
103856
104260
  stoppedBy = "reclaim-ceiling";
103857
- offset += rows.length - reclaimedInPage;
104261
+ if (!keyed) offset += rows.length - reclaimedInPage;
103858
104262
  break;
103859
104263
  }
104264
+ if (keyedPage !== null) {
104265
+ if (keyedPage.nextCursor === null) {
104266
+ keysetCursor = null;
104267
+ break;
104268
+ }
104269
+ keysetCursor = keyedPage.nextCursor;
104270
+ continue;
104271
+ }
103860
104272
  const advanced = rows.length - reclaimedInPage;
103861
104273
  if (advanced === 0 && rows.length < limits.pageSize) {
103862
104274
  offset = 0;
@@ -103872,7 +104284,7 @@ async function auditScope(scope, deps, limits, cursor) {
103872
104284
  collectionSize,
103873
104285
  coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
103874
104286
  stoppedBy,
103875
- nextCursor: offset
104287
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103876
104288
  } });
103877
104289
  const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
103878
104290
  if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
@@ -103899,7 +104311,7 @@ async function auditScope(scope, deps, limits, cursor) {
103899
104311
  unresolvable: tally.unresolvable,
103900
104312
  deviceIds: [...deviceIds],
103901
104313
  stoppedBy,
103902
- nextCursor: offset
104314
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103903
104315
  };
103904
104316
  }
103905
104317
  /**
@@ -103991,7 +104403,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
103991
104403
  //#endregion
103992
104404
  //#region src/pipeline-analytics/retention/orphan-reclaimers.ts
103993
104405
  var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
103994
- var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION, "pipeline-analytics:plates"]);
104406
+ var PLATES_COLLECTION = "pipeline-analytics:plates";
104407
+ var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
103995
104408
  /**
103996
104409
  * The `deleteRow` port: ONE row by its primary key `id` (what every
103997
104410
  * raw-delete collection above declares), answering whether a row went.
@@ -104065,6 +104478,13 @@ function buildOrphanReclaimer(ports) {
104065
104478
  rows: await ports.deleteTrackAlias(key) ? 1 : 0,
104066
104479
  bytes: null
104067
104480
  };
104481
+ if (collection === PLATES_COLLECTION) {
104482
+ const assert = trackIdAssertOf(data);
104483
+ return {
104484
+ rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
104485
+ bytes: null
104486
+ };
104487
+ }
104068
104488
  if (RAW_DELETE_COLLECTIONS.has(collection)) {
104069
104489
  const assert = trackIdAssertOf(data);
104070
104490
  if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
@@ -104086,6 +104506,87 @@ function buildOrphanReclaimer(ports) {
104086
104506
  };
104087
104507
  }
104088
104508
  //#endregion
104509
+ //#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
104510
+ /** Audited collection → its records-kind ledger. Empty without records. */
104511
+ function orphanAuditLedgers(wiring) {
104512
+ if (wiring.records === void 0) return /* @__PURE__ */ new Map();
104513
+ return new Map([
104514
+ [FACES_COLLECTION, wireFaceLedger(wiring)],
104515
+ [PLATES_COLLECTION$1, wirePlateLedger(wiring)],
104516
+ [TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
104517
+ ]);
104518
+ }
104519
+ /** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
104520
+ function keysetPager(ledgers) {
104521
+ return async (collection, cursor, limit) => {
104522
+ const ledger = ledgers.get(collection);
104523
+ if (ledger === void 0) return null;
104524
+ const page = await ledger.pageById(cursor, limit);
104525
+ return {
104526
+ rows: page.rows.map((row) => ({
104527
+ key: row.id,
104528
+ data: row.data
104529
+ })),
104530
+ nextCursor: page.nextCursor
104531
+ };
104532
+ };
104533
+ }
104534
+ /** The kind's count for a ledger-backed collection, `undefined` for any other. */
104535
+ async function ledgerSize(ledgers, collection) {
104536
+ const ledger = ledgers.get(collection);
104537
+ return ledger === void 0 ? void 0 : ledger.count();
104538
+ }
104539
+ /**
104540
+ * The plate reclaim through the plate ledger — records on a hub, the legacy
104541
+ * collection only without records. With `assert`, a row whose `trackId` is not
104542
+ * the dead track's is KEPT (logged), never deleted.
104543
+ */
104544
+ function plateDeleter(plates, logger) {
104545
+ return async (plateId, assert) => {
104546
+ const row = await plates.get(plateId);
104547
+ if (row === null) return false;
104548
+ if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
104549
+ const deviceId = row.data["deviceId"];
104550
+ logger.warn("orphan audit: plate row now names another track — kept", {
104551
+ ...typeof deviceId === "number" ? { tags: { deviceId } } : {},
104552
+ meta: {
104553
+ plateId,
104554
+ expectedTrackId: assert.trackId
104555
+ }
104556
+ });
104557
+ return false;
104558
+ }
104559
+ await plates.deleteKey(plateId);
104560
+ return true;
104561
+ };
104562
+ }
104563
+ function auditCollectionPorts(store, ledgers) {
104564
+ return {
104565
+ pageKeyset: keysetPager(ledgers),
104566
+ pageByOffset: async (collection, offset, limit) => {
104567
+ return (await store.query.query({
104568
+ collection,
104569
+ filter: {
104570
+ orderBy: {
104571
+ field: "id",
104572
+ direction: "asc"
104573
+ },
104574
+ limit,
104575
+ offset
104576
+ }
104577
+ })).map((row) => ({
104578
+ key: row.id,
104579
+ data: row.data
104580
+ }));
104581
+ },
104582
+ size: async (collection) => {
104583
+ const counted = await ledgerSize(ledgers, collection);
104584
+ if (counted !== void 0) return counted;
104585
+ return store.count.query({ collection });
104586
+ }
104587
+ };
104588
+ }
104589
+ //#endregion
104089
104590
  //#region src/pipeline-analytics/retention/retired-device-settings.ts
104090
104591
  /**
104091
104592
  * Every retired knob, with the sentence that explains it. The list is the
@@ -114843,6 +115344,7 @@ var customActions = {
114843
115344
  ...orphanAuditActions,
114844
115345
  ...mediaReclaimActions,
114845
115346
  ...locationDiscardActions,
115347
+ ...mediaForgetActions,
114846
115348
  ...photoEnrollActions,
114847
115349
  ...faceRescoreActions,
114848
115350
  ...stillnessCalibrationActions
@@ -116633,6 +117135,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116633
117135
  orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
116634
117136
  /** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
116635
117137
  discardJob = null;
117138
+ /** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
117139
+ mediaForgetDeps = null;
116636
117140
  /**
116637
117141
  * The CLIP indexes' walks (D649), one resume cursor per index, kept in their
116638
117142
  * OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
@@ -116841,10 +117345,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116841
117345
  const isHub = ownNodeId === "hub";
116842
117346
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
116843
117347
  if (isHub) this.reconcileTapePinsInBackground();
117348
+ this.mediaForgetDeps = {
117349
+ listLocations: async () => api.storage.listLocations.query({}),
117350
+ localNodeId: ownNodeId,
117351
+ store: api.settingsStore,
117352
+ logger: logger.child("MediaForget")
117353
+ };
116844
117354
  this.discardJob = createDiscardJob({
116845
117355
  listLocations: async () => api.storage.listLocations.query({}),
116846
117356
  localNodeId: ownNodeId,
116847
117357
  store: api.settingsStore,
117358
+ retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
116848
117359
  deleteTrackEvents: async (trackId) => {
116849
117360
  await this.eventStore?.deleteByTrack(trackId);
116850
117361
  },
@@ -116865,6 +117376,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116865
117376
  ...orphanAuditActions,
116866
117377
  ...mediaReclaimActions,
116867
117378
  ...locationDiscardActions,
117379
+ ...mediaForgetActions,
116868
117380
  ...photoEnrollActions,
116869
117381
  ...faceRescoreActions,
116870
117382
  ...stillnessCalibrationActions
@@ -117460,6 +117972,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
117460
117972
  });
117461
117973
  this.mediaRelocate = new MediaRelocateEngine({
117462
117974
  store: api.settingsStore,
117975
+ retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
117463
117976
  storage,
117464
117977
  logger: logger.child("MediaRelocate"),
117465
117978
  now: () => Date.now(),
@@ -125202,36 +125715,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
125202
125715
  const trackStore = this.trackStore;
125203
125716
  const eventStore = this.eventStore;
125204
125717
  const mediaStore = this.mediaStore;
125205
- if (!trackStore || !eventStore || !mediaStore) {
125718
+ const settingsStore = this.settingsStore;
125719
+ if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
125206
125720
  if (!alreadyHeld) this.orphanAuditRunning = false;
125207
125721
  return null;
125208
125722
  }
125209
125723
  try {
125210
125724
  const limits = await this.orphanAuditLimits(overrides);
125725
+ const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
125726
+ const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
125727
+ const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
125211
125728
  const deps = {
125729
+ pageKeyset: collectionPorts.pageKeyset,
125730
+ stop: () => this.shuttingDown,
125212
125731
  page: async (collection, shard, offset, limit) => {
125213
125732
  if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
125214
125733
  key: String(row.mediaId),
125215
125734
  data: row.data
125216
125735
  }));
125217
- return (await this.settingsStore?.query.query({
125218
- collection,
125219
- filter: {
125220
- orderBy: {
125221
- field: "id",
125222
- direction: "asc"
125223
- },
125224
- limit,
125225
- offset
125226
- }
125227
- }) ?? []).map((row) => ({
125228
- key: row.id,
125229
- data: row.data
125230
- }));
125736
+ return collectionPorts.pageByOffset(collection, offset, limit);
125231
125737
  },
125232
125738
  size: async (collection, shard) => {
125233
125739
  if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
125234
- return await this.settingsStore?.count.query({ collection }) ?? null;
125740
+ return collectionPorts.size(collection);
125235
125741
  },
125236
125742
  liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
125237
125743
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
@@ -125254,6 +125760,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
125254
125760
  if (aliasStore === null) throw new Error("track alias store not bound");
125255
125761
  return await aliasStore.deleteAlias(aliasId);
125256
125762
  },
125763
+ deletePlate: plateDeleter(plateLedger, this.ctx.logger),
125257
125764
  deleteRow: settingsRowDeleterOf(() => this.settingsStore),
125258
125765
  logger: this.ctx.logger
125259
125766
  });
@@ -126675,7 +127182,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
126675
127182
  jobId: null
126676
127183
  },
126677
127184
  "storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
126678
- "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
127185
+ "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
127186
+ "storage.forgetMissingMedia": async (input) => {
127187
+ const deps = this.mediaForgetDeps;
127188
+ if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
127189
+ return forgetMissingMediaRows(deps, input);
127190
+ }
126679
127191
  };
126680
127192
  }
126681
127193
  /**