@camstack/types 1.2.88 → 1.2.89

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.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * rule-kinds — WHICH KIND OF RULE THIS IS, WHICH FIELDS IT ACTUALLY HAS, AND
3
+ * WHICH LIST SECTION IT LIVES IN. Pure, UI-framework-free, RN-safe.
4
+ *
5
+ * WHY IT LIVES HERE
6
+ * ─────────────────
7
+ * Two editors author the same rules — the admin UI (React DOM, shipped with
8
+ * `camstack deploy`) and the viewer (React Native / Hermes, shipped by EAS OTA)
9
+ * — and the layer they were NOT sharing is this one: not the schema (already
10
+ * shared) and not the widgets (they cannot be), but the model in between.
11
+ * The divergence it caused is on the record: the viewer filtered conditions on
12
+ * the served `appliesTo` alone, so a viewer-authored occupancy rule was still
13
+ * offered `sensorKinds`, `eventTypeTokens` and `source` — the exact "in
14
+ * occupancy alcuni campi non hanno senso" failure the admin UI fixed on
15
+ * 2026-08-14 and the viewer never got. Beside it, the viewer's rule list had no
16
+ * sections and no seeded creates, so the two features that have no delivery of
17
+ * their own (sound, occupancy) were unauthorable from the phone.
18
+ *
19
+ * The CONTENT is what makes a second copy expensive: a dozen exclusions, each
20
+ * traced to a subject field the engine reads and finds absent, each carrying
21
+ * the sentence shown to the operator. Two tables disagreeing about which fields
22
+ * an occupancy rule has is the same failure as two knobs writing a store nobody
23
+ * read.
24
+ *
25
+ * THE MISSING DIMENSION
26
+ * ─────────────────────
27
+ * The editor had exactly ONE dimension of applicability, the rule's `delivery`,
28
+ * which the served catalog answers through `appliesTo`. Two features have no
29
+ * delivery of their own:
30
+ *
31
+ * - an OCCUPANCY rule is a `device-event` rule carrying `conditions.occupancy`
32
+ * - a SOUND rule is an `immediate` rule carrying `conditions.audio`
33
+ *
34
+ * and the engine discriminates them by that CONDITION (`rule-engine.ts`: an
35
+ * occupancy edge fires only for a rule declaring `occupancy`, a confirmed audio
36
+ * window only for one declaring `audio`). So an occupancy rule was offered the
37
+ * whole `device-event` surface — sensor kinds, raw event tokens, a detection
38
+ * source — every one of which the engine FAILS CLOSED on an occupancy subject,
39
+ * because the subject carries no `sensorKind`, no `eventType` and always the
40
+ * `pipeline` source.
41
+ *
42
+ * This module adds that dimension: `delivery` × `kind`. Visibility is the
43
+ * COMPOSITION of the two — `appliesTo` still answers the trigger question (it is
44
+ * the server's answer and stays authoritative), and the kind subtracts what the
45
+ * sub-type cannot use. Every subtraction is declared WITH ITS REASON and the
46
+ * reason is shown to the operator rather than kept in a comment.
47
+ *
48
+ * LABELS ARE ENGLISH FALLBACKS, NOT TRANSLATIONS. The stable thing here is the
49
+ * ID (`kind`, section `key`, editor-section `key`); the admin UI renders these
50
+ * strings verbatim, the viewer maps the ids onto its `v2.nc.rules.*` i18n keys
51
+ * and falls back to them.
52
+ *
53
+ * The admin UI keeps a pinned local copy for now — `@camstack/types` reaches it
54
+ * as a Module-Federation singleton provided by the HOST, so a newly-added export
55
+ * reads `undefined` there until a server closure carrying it is installed. See
56
+ * `docs/design/2026-08-19-viewer-notification-rule-parity.md` §3 and Phase 3.
57
+ */
58
+ import { type NcAudioCondition, type NcConditions, type NcDelivery, type NcOccupancyCondition } from '../capabilities/notification-rules.cap.js';
59
+ /**
60
+ * The kind of rule an operator is authoring.
61
+ *
62
+ * Coarser than `delivery` in one direction and finer in another, deliberately:
63
+ * `immediate` / `track-end` / `package-event` are ONE kind here (their
64
+ * differences are already the catalog's `appliesTo` job), while `device-event`
65
+ * and `immediate` each split in two on their discriminating condition.
66
+ */
67
+ export type NcRuleKind = 'detection' | 'sensor' | 'occupancy' | 'sound' | 'system';
68
+ export interface NcRuleKindSpec {
69
+ readonly kind: NcRuleKind;
70
+ /** What the conditions section is CALLED for this kind (English fallback). */
71
+ readonly label: string;
72
+ /**
73
+ * The condition that MAKES a rule this kind. Always visible, and never
74
+ * excluded — burying it would hide the whole feature (which is how an
75
+ * operator with a working audio engine reported there was no way to create an
76
+ * audio rule).
77
+ */
78
+ readonly discriminator?: keyof NcConditions;
79
+ /**
80
+ * Conditions the catalog offers for this kind's DELIVERY that a rule of this
81
+ * kind cannot use, each with the engine reason — shown to the operator when a
82
+ * save is about to drop one.
83
+ */
84
+ readonly excluded: Readonly<Record<string, string>>;
85
+ /**
86
+ * Can a notification of this kind carry a picture AT ALL?
87
+ *
88
+ * `false` is not a UI preference, it is the delivery path: the attachment
89
+ * ladder resolves media by OWNER, and only a subject that freezes one has any
90
+ * (`triggerCanCarryStill` in the addon's `test-event.ts` is the same list).
91
+ *
92
+ * Only `system` is left. An infrastructure event is about no camera at all —
93
+ * there is nothing to photograph, so every media control on it is a knob that
94
+ * does nothing. A SOUND rule was here until 2026-08-15 and was right to be:
95
+ * its subject is a sampling window that owns no frame. It now photographs the
96
+ * camera at the match instead (`still-shelf.ts`), which is a picture nothing
97
+ * else could give it.
98
+ */
99
+ readonly carriesMedia: boolean;
100
+ /**
101
+ * Does anything about this kind's subject describe a REGION of the frame?
102
+ *
103
+ * `false` removes the two controls that cut one — the zone crop and the image
104
+ * frame — because neither has an input. A sound was not boxed and carries no
105
+ * zone condition to crop to, so `frame: 'cropped'` resolves nothing and
106
+ * `zoneCrop` has no ids to use. Same argument as `excluded`, applied to the
107
+ * media policy instead of the conditions: a knob that decides nothing is
108
+ * worse than a missing one, because the operator sets it and believes they
109
+ * have asked for something.
110
+ */
111
+ readonly carriesSubjectCrop: boolean;
112
+ /** One line under the section title — what this kind of rule IS. */
113
+ readonly blurb?: string;
114
+ /** One line above the media controls, when the picture needs explaining. */
115
+ readonly mediaBlurb?: string;
116
+ }
117
+ export declare const NC_RULE_KIND_SPECS: readonly NcRuleKindSpec[];
118
+ /** The spec for a kind. Total by construction — every member has an entry. */
119
+ export declare function ruleKindSpec(kind: NcRuleKind): NcRuleKindSpec;
120
+ /** The delivery a system rule rides — named once, compared everywhere. */
121
+ export declare const NC_SYSTEM_DELIVERY = "system-event";
122
+ /**
123
+ * Is this a SYSTEM rule — one about the installation rather than about anything
124
+ * a camera saw? Takes a plain `string` so a rule carrying a delivery this build
125
+ * has never heard of still gets an answer instead of a type error.
126
+ */
127
+ export declare function isSystemDelivery(delivery: string): boolean;
128
+ /**
129
+ * The conditions of a rule, as much of them as classification needs.
130
+ *
131
+ * DELIBERATELY STRUCTURAL, and this is load-bearing. The viewer keeps its OWN
132
+ * Zod mirror of the rule schema on purpose (older/newer hub tolerance —
133
+ * `camstack/src/data/notification-center.ts`), so a parameter typed as THIS
134
+ * package's `NcConditions` would be unusable from the surface this module was
135
+ * hoisted for without a cast, and this repo does not take casts. Two optional
136
+ * `unknown`s is the whole question the classifier asks.
137
+ */
138
+ export interface NcRuleConditionsSubject {
139
+ readonly occupancy?: unknown;
140
+ readonly audio?: unknown;
141
+ }
142
+ /** Every authored condition, keyed by descriptor id — same reasoning as above. */
143
+ export type NcAuthoredConditions = Readonly<Record<string, unknown>>;
144
+ /** What both the kind and the section ask a rule about. */
145
+ export interface NcRuleSectionSubject {
146
+ readonly delivery: string;
147
+ readonly conditions: NcRuleConditionsSubject;
148
+ }
149
+ /**
150
+ * Which kind of rule is this?
151
+ *
152
+ * The CONDITION wins over the delivery wherever the engine says it does, and
153
+ * the order below is the engine's own: `evaluateRule` checks the system branch
154
+ * first, then the occupancy and audio gates. Takes a plain `delivery` string so
155
+ * a trigger this build does not know still classifies (as `detection`, which is
156
+ * the read-only path's own fallback).
157
+ */
158
+ export declare function ruleKindOf(rule: NcRuleSectionSubject): NcRuleKind;
159
+ /**
160
+ * The shape {@link conditionVisibleForKind} reads off a catalog descriptor.
161
+ *
162
+ * `appliesTo` is `readonly string[]`, not `readonly NcDelivery[]`: the catalog
163
+ * arrives at RUNTIME from a server that may be newer than this build, and each
164
+ * editor parses it through its own mirror. Narrowing the parameter to this
165
+ * package's union would reject the viewer's descriptor for a nominal reason
166
+ * while the value is identical.
167
+ */
168
+ export interface NcKindVisibilitySubject {
169
+ readonly id: string;
170
+ readonly appliesTo: readonly string[];
171
+ }
172
+ /**
173
+ * Is this condition visible — and authorable — for a rule of this kind?
174
+ *
175
+ * The COMPOSITION, and the only function that decides it: `appliesTo` is the
176
+ * server's answer about the trigger, the kind's `excluded` map is this build's
177
+ * answer about the sub-type, and the discriminator overrides both (a sound rule
178
+ * must be able to edit its sound even though `audio` is excluded for every
179
+ * OTHER kind that rides `immediate`).
180
+ */
181
+ export declare function conditionVisibleForKind(descriptor: NcKindVisibilitySubject, delivery: string, kind: NcRuleKind): boolean;
182
+ /** Why a condition is not offered for this kind, or `null` when it IS. */
183
+ export declare function conditionExclusionReason(descriptorId: string, kind: NcRuleKind): string | null;
184
+ /**
185
+ * Conditions the rule CARRIES that this kind cannot use — the save will drop
186
+ * them, and the operator is told which and why BEFORE pressing Save.
187
+ *
188
+ * Dropping is the right action and not a compromise: every entry here is a
189
+ * condition the engine fails closed on for this subject, so the rule is
190
+ * currently enabled, in the list, and permanently silent. Keeping it would keep
191
+ * it silent; dropping it is what makes the rule fire again. Saying nothing
192
+ * about the drop would be the third option, and it is the one this repo has
193
+ * paid for before.
194
+ */
195
+ export interface NcDroppedCondition {
196
+ readonly conditionId: string;
197
+ readonly reason: string;
198
+ }
199
+ export declare function droppedConditionsForKind(conditions: NcAuthoredConditions, kind: NcRuleKind): readonly NcDroppedCondition[];
200
+ /**
201
+ * The sections a rule LIST is split into.
202
+ *
203
+ * Operator request, 2026-08-13: "vorrei che le system rules fossero in una
204
+ * sezione a parte, più facile da utilizzare, affianco alle detection rules". A
205
+ * system rule and a detection rule share a store and nothing else — different
206
+ * triggers, different conditions, no media, a different question ("is my
207
+ * installation healthy?" vs "what did the camera see?").
208
+ *
209
+ * `all` is kept and is the DEFAULT: a section that hides rules is how an
210
+ * operator concludes a rule was deleted. Which sections a given SURFACE offers
211
+ * for authoring is that surface's decision — the viewer omits `system` from its
212
+ * create flow and still renders system rules under `all`.
213
+ */
214
+ export type NcRuleSectionKey = 'all' | 'detection' | 'audio' | 'occupancy' | 'system';
215
+ export interface NcRuleSection {
216
+ readonly key: NcRuleSectionKey;
217
+ /** English fallback; the viewer maps `key` onto an i18n key. */
218
+ readonly label: string;
219
+ /** Shown under the section strip — what this section is for. */
220
+ readonly blurb: string;
221
+ /** The create button's words in this section — it authors a DIFFERENT rule. */
222
+ readonly newRuleLabel: string;
223
+ /** What the section says when it holds nothing yet. */
224
+ readonly emptyHint: string;
225
+ }
226
+ export declare const NC_RULE_SECTIONS: readonly NcRuleSection[];
227
+ /** The section for a key, or `undefined` — the lookup behind every label. */
228
+ export declare function ruleSection(key: NcRuleSectionKey): NcRuleSection;
229
+ /** Narrow a raw section string back to a known key (no cast). */
230
+ export declare function parseRuleSection(value: string): NcRuleSectionKey | undefined;
231
+ /** True for a rule whose sound condition makes it a SOUND rule. */
232
+ export declare function isAudioRule(subject: NcRuleSectionSubject): boolean;
233
+ /** True for a rule whose occupancy condition makes it an OCCUPANCY rule. */
234
+ export declare function isOccupancyRule(subject: NcRuleSectionSubject): boolean;
235
+ /**
236
+ * Does a rule belong in this section?
237
+ *
238
+ * `all` always matches — a section that hides a rule is how an operator
239
+ * concludes it was deleted. The other four PARTITION the list: sound and
240
+ * occupancy are claimed by their condition, system by its trigger, and
241
+ * everything left over — including a trigger this build has never heard of —
242
+ * counts as a detection rule so it stays visible somewhere.
243
+ *
244
+ * Keyed on the CONDITION for two of them, because the trigger alone cannot
245
+ * answer: a sound rule is `immediate` and an occupancy rule is `device-event`,
246
+ * and in each case the condition is the discriminator the engine itself uses.
247
+ * A section keyed on delivery would put both back in the pile they are
248
+ * invisible in.
249
+ */
250
+ export declare function ruleMatchesSection(subject: NcRuleSectionSubject, section: NcRuleSectionKey): boolean;
251
+ /** The ONE real section a rule lives in — `all` holds everything and is not it. */
252
+ export type NcRuleOwningSectionKey = Exclude<NcRuleSectionKey, 'all'>;
253
+ /**
254
+ * Which section OWNS this rule — the counter behind a section strip.
255
+ *
256
+ * Derived from {@link ruleMatchesSection} rather than restated, so the strip's
257
+ * counts and the strip's contents can never disagree.
258
+ */
259
+ export declare function ruleSectionOf(subject: NcRuleSectionSubject): NcRuleOwningSectionKey;
260
+ /**
261
+ * The occupancy schema's own defaults, in one place.
262
+ *
263
+ * Both the WIDGET (what an untouched sub-field shows) and the CREATION PRESET
264
+ * (what "New occupancy rule" seeds) have to author exactly these numbers, and
265
+ * two copies of a default is how a preset starts writing a rule that does not
266
+ * match what the editor then displays. Mirrors `NcOccupancyConditionSchema`'s
267
+ * `.default(...)` clauses, and the spec asserts the mirror.
268
+ */
269
+ export declare const NC_OCCUPANCY_DEFAULTS: NcOccupancyCondition;
270
+ /**
271
+ * The seeded sound condition.
272
+ *
273
+ * Carries a level floor rather than the bare defaults: with neither a level nor
274
+ * a label the engine returns false for every window (`matchesAudio`), so a
275
+ * preset without one would hand the operator a rule that can never fire. The
276
+ * offered level is the same one the widget writes, so switching to SOUNDS
277
+ * (label mode — the one that fires on the first labelled frame, D157) is one
278
+ * tap on the mode picker.
279
+ *
280
+ * The seed cannot be a label rule instead: a labels-only condition with no
281
+ * labels chosen yet is fail-closed too, and it would hand the operator the same
282
+ * dead rule from the other side.
283
+ */
284
+ export declare const NC_AUDIO_SEED: NcAudioCondition;
285
+ /**
286
+ * What a "New rule" started from a section opens on.
287
+ *
288
+ * A trigger alone was enough while every section WAS a trigger. It is not for
289
+ * sound and occupancy: the condition is what makes the rule that kind of rule,
290
+ * so the preset seeds it — otherwise the operator lands on a form identical to
291
+ * a detection rule's and has to know that the feature lives behind an expander
292
+ * on a trigger they must pick first. That is exactly the gap reported on
293
+ * 2026-08-13 ("non vedo un modo per creare le nuove rules audio e occupancy")
294
+ * for two features whose engines had already shipped.
295
+ */
296
+ export interface NcRuleSeed extends NcRuleSectionSubject {
297
+ readonly delivery: NcDelivery;
298
+ /** Pre-authored conditions — the ones that DEFINE the kind, nothing else. */
299
+ readonly conditions: NcConditions;
300
+ }
301
+ export declare function ruleSeedForSection(section: NcRuleSectionKey): NcRuleSeed;
302
+ /** The trigger a "New rule" in this section should open on. */
303
+ export declare function defaultDeliveryForSection(section: NcRuleSectionKey): NcDelivery;
304
+ /**
305
+ * The rule editor's sections, in the order an operator meets them.
306
+ *
307
+ * Operator request, 2026-08-04 (admin) and 2026-08-18 (viewer): the editor was
308
+ * ONE scrolling form holding every setting a rule has, so authoring a simple
309
+ * rule meant scrolling past everything a simple rule does not need. Ordered by
310
+ * how often a setting is touched — not by how the schema is shaped.
311
+ *
312
+ * The admin renders them as dialog TABS and the viewer as one scroll of
313
+ * headings (a phone scrolls; dialog tabs are a desktop idiom), and the viewer
314
+ * renders a SUBSET — but the ORDER is shared, because two surfaces that put the
315
+ * same five things in two different orders are two features to learn.
316
+ *
317
+ * The `fields` claim map stays per-surface: each editor's form state is its own
318
+ * type, and each side's spec asserts every field of ITS form is claimed exactly
319
+ * once. What is shared is which sections exist, in what order, and which ones a
320
+ * kind shows at all.
321
+ */
322
+ export type NcRuleEditorSectionKey = 'rule' | 'devices' | 'conditions' | 'notification' | 'actions' | 'advanced';
323
+ export declare const NC_RULE_EDITOR_SECTION_ORDER: readonly NcRuleEditorSectionKey[];
324
+ export interface NcRuleEditorSection {
325
+ readonly key: NcRuleEditorSectionKey;
326
+ /** English fallback; the viewer maps `key` onto an i18n key. */
327
+ readonly label: string;
328
+ }
329
+ /**
330
+ * The sections a rule of THIS kind actually shows.
331
+ *
332
+ * Two rules, both of them about a control that would decide nothing:
333
+ *
334
+ * - a `system-event` rule has no device scope — `conditions.devices` is not
335
+ * evaluated for it and the input builder refuses to write it — so the
336
+ * Cameras section is DROPPED rather than shown and ignored;
337
+ * - a sound or occupancy rule is NAMED on its conditions section. Neither has
338
+ * a trigger of its own, so that section is the only place either feature
339
+ * exists, and a heading that never mentions sound is exactly why an operator
340
+ * with a working audio engine reported there was no way to create an audio
341
+ * rule.
342
+ *
343
+ * The NOTIFICATION section always survives: priority and the template still
344
+ * matter even for a kind that carries no picture, and dropping the section to
345
+ * hide the media controls would take the template with it. Whether the media
346
+ * half renders is `carriesMedia` / `carriesSubjectCrop`, asked separately.
347
+ */
348
+ export declare function ruleEditorSectionsForKind(kind: NcRuleKind): readonly NcRuleEditorSection[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.88",
3
+ "version": "1.2.89",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",