@effect.healthcare/messenger 0.3.1 → 0.4.1

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,350 +1,350 @@
1
- # Validation
2
-
3
- This document defines an implementation policy for **component validation**, then illustrates how specifications are implemented by applying this policy.
4
-
5
- This document is intentionally **validation** only, but part of a set of implementation policies which together defines the policies which should be followed to transform specifications into an implementation which is predictable, maintainable, and production-ready at scale.
6
-
7
- ---
8
-
9
- # How to Read This Policy
10
-
11
- Rules in this policy fall into three strengths. Read each rule with its strength in mind:
12
-
13
- - **Principles** are non-negotiable. They derive from framework correctness or TDD discipline and hold for every component. A conflict with a principle is a defect.
14
- - **Templates** are invariant skeletons with explicit variation points. The skeleton applies to every component; specific steps activate or deactivate based on the component's declared classification (e.g., attribute-backed vs non-attribute, readonly vs writable, payload-bearing vs payload-free). Variation is part of the template, not a deviation from it.
15
- - **Conventions** are canonical defaults that keep implementations readable across components. A second component may deviate when its shape genuinely warrants it; the deviation should be explained at review time, not forbidden by the policy.
16
-
17
- When applying this policy to a new component, classify each rule before reconciling conflicts. A conflict with a principle is a defect; a conflict with a template means a variation point needs to be made explicit; a conflict with a convention is a documented deviation.
18
-
19
- ---
20
-
21
- # 1) Validation Must Be Implemented in a Dedicated File
22
-
23
- All validation logic for a component must live in a single dedicated file: `<component>.validation.ts`.
24
-
25
- Policy:
26
-
27
- - validation logic must not be inlined in `<component>.ts`
28
- - validation logic must not be defined in `<component>.meta.ts`
29
- - `<component>.validation.ts` is the authoritative location for all validation logic in the component
30
-
31
- This separation gives each file a single clear responsibility:
32
-
33
- - `<component>.meta.ts` declares value domains
34
- - `<component>.validation.ts` enforces value domains
35
- - `<component>.ts` invokes validation inside the mutation path
36
-
37
- # Template Variation Points
38
-
39
- Validators vary by the shape of the value domain they enforce:
40
-
41
- - **String-enum value domains** — use the `Object.values(Domain).includes(value as Domain)` membership check (Section 6). The validator accepts `string` and returns the narrowed enum type. This is the most common case and is illustrated by the examples in this document.
42
- - **Structured-object value domains** — validate each declared field according to its declared type and constraints (see the data policy for the `Metadata` example). The validator accepts the structured input and returns the narrowed structured type.
43
- - **Scalar value domains** (numbers, booleans, UUIDs, timestamps) — use the shape check appropriate to the type. For booleans, the membership check is structurally consistent with enums but serves primarily as type narrowing rather than runtime guard.
44
-
45
- The dedicated-file rule (Section 1), the `Validate`-class structure (Sections 2–4), the metadata-sourced domain rule (Section 4), the throw-on-invalid rule (Section 7), the canonical error format (Section 8), and the mutation-path placement (Section 10) are invariant across all value-domain shapes.
46
-
47
- # 2) All Validators Belong to a Single Exported Class Named `Validate`
48
-
49
- All validation methods for a component must be defined as members of a single exported class.
50
-
51
- Policy:
52
-
53
- - the class name must always be `Validate`
54
- - the class must be exported from `<component>.validation.ts`
55
- - all validators for the component must belong to this single class
56
- - the class must not contain non-validation logic
57
-
58
- Example:
59
-
60
- ```typescript
61
- export class Validate {
62
- public static visibility = (value: string) => { ... };
63
- public static status = (value: string) => { ... };
64
- }
65
- ```
66
-
67
- # 3) Validators Must Be Public Static Arrow Function Class Properties
68
-
69
- Each validator must be defined as a `public static` arrow function class property on the `Validate` class.
70
-
71
- Policy:
72
-
73
- - validators must be `public` — they are invoked from `<component>.ts`
74
- - validators must be `static` — they are called on the class directly, without instantiation
75
- - validators must be defined as arrow function class properties — consistent with the implementation pattern used across operations and handlers
76
- - validators must not be prototype methods
77
-
78
- Example:
79
-
80
- ```typescript
81
- export class Validate {
82
- public static visibility = (value: string) => {
83
- ...
84
- };
85
- }
86
- ```
87
-
88
- # 4) Validators Must Source Their Domain from Metadata
89
-
90
- Validators enforce the value domains declared in `<component>.meta.ts`. They must not define allowed values independently.
91
-
92
- Policy:
93
-
94
- - validators must import the relevant value domain objects from `<component>.meta.ts`
95
- - the set of valid values is derived from the imported metadata, not from hardcoded literals inside `<component>.validation.ts`
96
- - `<component>.meta.ts` is the single source of truth for allowed values
97
- - if the allowed values change in metadata, validation must reflect that change automatically
98
-
99
- Example:
100
-
101
- ```typescript
102
- import { Visibility, Status } from "./pin.meta.js";
103
-
104
- export class Validate {
105
- public static visibility = (value: string) => {
106
- const valid = Object.values(Visibility).includes(value as Visibility);
107
- ...
108
- };
109
- }
110
- ```
111
-
112
- # 5) Validator Signature Must Accept the Raw Input Type and Return the Narrowed Type
113
-
114
- The input type of a validator must reflect the raw, unnarrowed value that arrives at the validation boundary.
115
-
116
- The return type must be the narrowed TypeScript type defined by the corresponding value domain in metadata.
117
-
118
- Policy:
119
-
120
- - for string-based enum value domains, the input type is `string`
121
- - the return type is the TypeScript domain type (e.g., `Visibility`, `Status`)
122
- - on valid input, the validator returns the value cast to the narrowed type
123
- - on invalid input, the validator throws — it must not return a fallback or a default value
124
-
125
- Example:
126
-
127
- ```typescript
128
- public static visibility = (value: string) => {
129
- const valid = Object.values(Visibility).includes(value as Visibility);
130
- if (!valid) throw new Error(`Invalid visibility value: ${value}`);
131
- return value as Visibility;
132
- };
133
- ```
134
-
135
- The validator bridges from the unnarrowed boundary type to the narrowed domain type — or throws. There is no third path.
136
-
137
- # 6) Validation Logic Must Use the Enum Membership Pattern
138
-
139
- For string-based enum value domains, validation must test membership using `Object.values`.
140
-
141
- Policy:
142
-
143
- - use `Object.values(Domain).includes(value as Domain)` to test whether the input is a member of the declared value domain
144
- - this pattern derives valid values directly from the metadata object — it does not maintain a separate list
145
- - if the input is not a member, the validator must reject it
146
- - alternative checking strategies may be used only for non-string or non-enum domains
147
-
148
- Example:
149
-
150
- ```typescript
151
- const valid = Object.values(Visibility).includes(value as Visibility);
152
- if (!valid) {
153
- throw new Error(`Invalid visibility value: ${value}`);
154
- }
155
- return value as Visibility;
156
- ```
157
-
158
- # 7) Invalid Input Must Throw a Descriptive Error
159
-
160
- When a value fails validation, the validator must throw.
161
-
162
- Policy:
163
-
164
- - the validator must throw a `new Error(...)` on invalid input
165
- - the validator must not return a fallback value on invalid input
166
- - the validator must not silently ignore invalid input
167
- - the thrown error must carry a message that identifies what was invalid and what was received
168
-
169
- # 8) Error Messages Must Follow a Canonical Format
170
-
171
- The error message thrown by a validator must follow this format:
172
-
173
- ```
174
- Invalid {name} value: {value}
175
- ```
176
-
177
- Where:
178
-
179
- - `{name}` is the camelCase canonical name of the validated property as declared in the specification
180
- - `{value}` is the actual received value, interpolated at runtime using a template literal
181
-
182
- Policy:
183
-
184
- - every validator in the component must use this format consistently
185
- - the property name in the message must match the canonical name from the specification
186
- - the received value must always be included in the message
187
-
188
- Example:
189
-
190
- ```typescript
191
- throw new Error(`Invalid visibility value: ${value}`);
192
- throw new Error(`Invalid status value: ${value}`);
193
- ```
194
-
195
- # 9) Validation Scope Covers Publicly Writable Constructs
196
-
197
- The `Validate` class must define validators for every publicly writable state or data property that accepts external input and has a finite value domain.
198
-
199
- Policy:
200
-
201
- - every publicly writable state property with a finite value domain must have a corresponding validator in `Validate`
202
- - every publicly writable data property with a finite value domain must have a corresponding validator in `Validate`
203
- - readonly state that is only mutated internally does not require a validator in `Validate`
204
- - internally generated values (e.g., UUIDs, timestamps) do not require validation in `Validate`
205
- - the count of validators in `Validate` must equal the count of publicly writable constructs that require boundary enforcement
206
-
207
- # 10) Validators Are Invoked Inside the Authoritative Mutation Path
208
-
209
- Validators are called by `<component>.ts` inside the public setter of each validated construct. They are not called at the gesture or operation layer.
210
-
211
- Policy:
212
-
213
- - validation must be invoked inside the setter — the authoritative mutation path
214
- - validation is the second step of the mutation order, after normalization and before the guard: **Normalize → Validate → Guard → Mutate**
215
- - the return value of the validator — the narrowed typed value — must replace the raw input for the remainder of the mutation path
216
- - if validation throws, the mutation path must not proceed past that point
217
- - gesture handlers and operations must not invoke validators directly — they route through the setter which owns validation
218
-
219
- The mutation order is defined authoritatively in the state policy. The validation policy governs the implementation of the validator itself, not the order it is called in.
220
-
221
- Example showing the validator's position in the mutation path:
222
-
223
- ```typescript
224
- public set visibility(value: Visibility) {
225
- // Normalize
226
- value = value ?? Visibility.VISIBLE;
227
-
228
- // Validate — narrowed return value replaces the raw input
229
- value = Validate.visibility(value);
230
-
231
- // Guard
232
- if (this._visibility === value) return;
233
-
234
- // Mutate
235
- this._visibility = value;
236
- ...
237
- }
238
- ```
239
-
240
- ---
241
-
242
- # Policy Applied to Specification
243
-
244
- The Pin component declares two publicly writable state properties — `visibility` and `status` — both with string-based enum value domains declared in `pin.meta.ts`. Both require boundary enforcement at the public mutation path.
245
-
246
- `pin.meta.ts` — value domain declarations:
247
-
248
- ```typescript
249
- export const Visibility = {
250
- VISIBLE: "visible",
251
- HIDDEN: "hidden",
252
- } as const;
253
-
254
- export type Visibility = (typeof Visibility)[keyof typeof Visibility];
255
-
256
- export const Status = {
257
- PINNED: "pinned",
258
- UNPINNED: "unpinned",
259
- } as const;
260
-
261
- export type Status = (typeof Status)[keyof typeof Status];
262
- ```
263
-
264
- `pin.validation.ts` — the full implementation:
265
-
266
- ```typescript
267
- import { Visibility, Status } from "./pin.meta.js";
268
-
269
- export class Validate {
270
- public static visibility = (value: string) => {
271
- const valid = Object.values(Visibility).includes(value as Visibility);
272
- if (!valid) {
273
- throw new Error(`Invalid visibility value: ${value}`);
274
- }
275
- return value as Visibility;
276
- };
277
-
278
- public static status = (value: string) => {
279
- const valid = Object.values(Status).includes(value as Status);
280
- if (!valid) {
281
- throw new Error(`Invalid status value: ${value}`);
282
- }
283
- return value as Status;
284
- };
285
- }
286
- ```
287
-
288
- How the policy is applied:
289
-
290
- - all validation lives in `pin.validation.ts` — not in `pin.ts` or `pin.meta.ts`
291
- - a single exported `Validate` class contains both validators
292
- - both validators are `public static` arrow function class properties
293
- - both import their value domains from `pin.meta.ts` — no allowed values are hardcoded in `pin.validation.ts`
294
- - both accept `string` as the raw input type and return the narrowed metadata type on success
295
- - both use `Object.values(Domain).includes(value as Domain)` for membership checking
296
- - both throw `new Error(...)` on invalid input — no fallback is returned
297
- - both use the canonical error format: `` `Invalid {name} value: ${value}` ``
298
- - `Validate` has exactly two methods because there are exactly two publicly writable state properties; readonly or internally-generated values are not covered
299
-
300
- Invocation in `pin.ts`:
301
-
302
- ```typescript
303
- // visibility setter — optional attribute-backed, writable
304
- public set visibility(value: Visibility) {
305
- value = value ?? Visibility.VISIBLE; // Normalize
306
- value = Validate.visibility(value); // Validate
307
- if (this._visibility === value) return; // Guard
308
- this._visibility = value; // Mutate
309
- ...
310
- }
311
-
312
- // status setter — compulsory attribute-backed, writable
313
- public set status(value: Status) {
314
- value = Validate.status(value); // Validate (no normalization step)
315
- if (this._status === value) return; // Guard
316
- this._status = value; // Mutate
317
- ...
318
- }
319
- ```
320
-
321
- - `Validate.visibility` is called after normalization because the optional setter must first convert `null` (passed by `attributeChangedCallback` on attribute removal) to the default before validation can safely proceed
322
- - `Validate.status` is called as the first step because the compulsory setter has no normalization step — `null` is not a valid status value and must be rejected by validation
323
- - in both cases the narrowed return value replaces the raw input before the guard check
324
-
325
- ---
326
-
327
- # Policy Checklist
328
-
329
- A compliant component must:
330
-
331
- 1. Implement all validation logic in `<component>.validation.ts`.
332
- 2. Not inline validation logic in `<component>.ts` or `<component>.meta.ts`.
333
- 3. Export a single class named `Validate` from `<component>.validation.ts`.
334
- 4. Define all validators as members of the `Validate` class with no non-validation logic in the class.
335
- 5. Define each validator as a `public static` arrow function class property.
336
- 6. Import value domain objects from `<component>.meta.ts` — do not hardcode allowed values in validators.
337
- 7. Use `Object.values(Domain).includes(value as Domain)` for string-based enum membership checking.
338
- 8. Accept the raw unnarrowed input type (typically `string` for string-based enum domains).
339
- 9. Return the narrowed TypeScript domain type on valid input.
340
- 10. Throw `new Error(...)` on invalid input — do not return a fallback or a default value.
341
- 11. Use the canonical error message format: `` `Invalid {name} value: ${value}` ``.
342
- 12. Use the camelCase canonical property name in the error message.
343
- 13. Include the actual received value in the error message.
344
- 14. Define a validator for every publicly writable state or data property with a finite value domain.
345
- 15. Do not define validators in `Validate` for readonly state that is only internally mutated.
346
- 16. Do not define validators in `Validate` for internally generated values.
347
- 17. Invoke validators inside the public setter — the authoritative mutation path — not in gesture handlers or operations.
348
- 18. Call the validator as the second step of the mutation path: after normalization, before the guard.
349
- 19. Replace the raw input with the narrowed return value of the validator for the remainder of the mutation path.
350
- 20. Ensure that if validation throws, mutation does not proceed.
1
+ # Validation
2
+
3
+ This document defines an implementation policy for **component validation**, then illustrates how specifications are implemented by applying this policy.
4
+
5
+ This document is intentionally **validation** only, but part of a set of implementation policies which together defines the policies which should be followed to transform specifications into an implementation which is predictable, maintainable, and production-ready at scale.
6
+
7
+ ---
8
+
9
+ # How to Read This Policy
10
+
11
+ Rules in this policy fall into three strengths. Read each rule with its strength in mind:
12
+
13
+ - **Principles** are non-negotiable. They derive from framework correctness or TDD discipline and hold for every component. A conflict with a principle is a defect.
14
+ - **Templates** are invariant skeletons with explicit variation points. The skeleton applies to every component; specific steps activate or deactivate based on the component's declared classification (e.g., attribute-backed vs non-attribute, readonly vs writable, payload-bearing vs payload-free). Variation is part of the template, not a deviation from it.
15
+ - **Conventions** are canonical defaults that keep implementations readable across components. A second component may deviate when its shape genuinely warrants it; the deviation should be explained at review time, not forbidden by the policy.
16
+
17
+ When applying this policy to a new component, classify each rule before reconciling conflicts. A conflict with a principle is a defect; a conflict with a template means a variation point needs to be made explicit; a conflict with a convention is a documented deviation.
18
+
19
+ ---
20
+
21
+ # 1) Validation Must Be Implemented in a Dedicated File
22
+
23
+ All validation logic for a component must live in a single dedicated file: `<component>.validation.ts`.
24
+
25
+ Policy:
26
+
27
+ - validation logic must not be inlined in `<component>.ts`
28
+ - validation logic must not be defined in `<component>.meta.ts`
29
+ - `<component>.validation.ts` is the authoritative location for all validation logic in the component
30
+
31
+ This separation gives each file a single clear responsibility:
32
+
33
+ - `<component>.meta.ts` declares value domains
34
+ - `<component>.validation.ts` enforces value domains
35
+ - `<component>.ts` invokes validation inside the mutation path
36
+
37
+ # Template Variation Points
38
+
39
+ Validators vary by the shape of the value domain they enforce:
40
+
41
+ - **String-enum value domains** — use the `Object.values(Domain).includes(value as Domain)` membership check (Section 6). The validator accepts `string` and returns the narrowed enum type. This is the most common case and is illustrated by the examples in this document.
42
+ - **Structured-object value domains** — validate each declared field according to its declared type and constraints (see the data policy for the `Metadata` example). The validator accepts the structured input and returns the narrowed structured type.
43
+ - **Scalar value domains** (numbers, booleans, UUIDs, timestamps) — use the shape check appropriate to the type. For booleans, the membership check is structurally consistent with enums but serves primarily as type narrowing rather than runtime guard.
44
+
45
+ The dedicated-file rule (Section 1), the `Validate`-class structure (Sections 2–4), the metadata-sourced domain rule (Section 4), the throw-on-invalid rule (Section 7), the canonical error format (Section 8), and the mutation-path placement (Section 10) are invariant across all value-domain shapes.
46
+
47
+ # 2) All Validators Belong to a Single Exported Class Named `Validate`
48
+
49
+ All validation methods for a component must be defined as members of a single exported class.
50
+
51
+ Policy:
52
+
53
+ - the class name must always be `Validate`
54
+ - the class must be exported from `<component>.validation.ts`
55
+ - all validators for the component must belong to this single class
56
+ - the class must not contain non-validation logic
57
+
58
+ Example:
59
+
60
+ ```typescript
61
+ export class Validate {
62
+ public static visibility = (value: string) => { ... };
63
+ public static status = (value: string) => { ... };
64
+ }
65
+ ```
66
+
67
+ # 3) Validators Must Be Public Static Arrow Function Class Properties
68
+
69
+ Each validator must be defined as a `public static` arrow function class property on the `Validate` class.
70
+
71
+ Policy:
72
+
73
+ - validators must be `public` — they are invoked from `<component>.ts`
74
+ - validators must be `static` — they are called on the class directly, without instantiation
75
+ - validators must be defined as arrow function class properties — consistent with the implementation pattern used across operations and handlers
76
+ - validators must not be prototype methods
77
+
78
+ Example:
79
+
80
+ ```typescript
81
+ export class Validate {
82
+ public static visibility = (value: string) => {
83
+ ...
84
+ };
85
+ }
86
+ ```
87
+
88
+ # 4) Validators Must Source Their Domain from Metadata
89
+
90
+ Validators enforce the value domains declared in `<component>.meta.ts`. They must not define allowed values independently.
91
+
92
+ Policy:
93
+
94
+ - validators must import the relevant value domain objects from `<component>.meta.ts`
95
+ - the set of valid values is derived from the imported metadata, not from hardcoded literals inside `<component>.validation.ts`
96
+ - `<component>.meta.ts` is the single source of truth for allowed values
97
+ - if the allowed values change in metadata, validation must reflect that change automatically
98
+
99
+ Example:
100
+
101
+ ```typescript
102
+ import { Visibility, Status } from "./pin.meta.js";
103
+
104
+ export class Validate {
105
+ public static visibility = (value: string) => {
106
+ const valid = Object.values(Visibility).includes(value as Visibility);
107
+ ...
108
+ };
109
+ }
110
+ ```
111
+
112
+ # 5) Validator Signature Must Accept the Raw Input Type and Return the Narrowed Type
113
+
114
+ The input type of a validator must reflect the raw, unnarrowed value that arrives at the validation boundary.
115
+
116
+ The return type must be the narrowed TypeScript type defined by the corresponding value domain in metadata.
117
+
118
+ Policy:
119
+
120
+ - for string-based enum value domains, the input type is `string`
121
+ - the return type is the TypeScript domain type (e.g., `Visibility`, `Status`)
122
+ - on valid input, the validator returns the value cast to the narrowed type
123
+ - on invalid input, the validator throws — it must not return a fallback or a default value
124
+
125
+ Example:
126
+
127
+ ```typescript
128
+ public static visibility = (value: string) => {
129
+ const valid = Object.values(Visibility).includes(value as Visibility);
130
+ if (!valid) throw new Error(`Invalid visibility value: ${value}`);
131
+ return value as Visibility;
132
+ };
133
+ ```
134
+
135
+ The validator bridges from the unnarrowed boundary type to the narrowed domain type — or throws. There is no third path.
136
+
137
+ # 6) Validation Logic Must Use the Enum Membership Pattern
138
+
139
+ For string-based enum value domains, validation must test membership using `Object.values`.
140
+
141
+ Policy:
142
+
143
+ - use `Object.values(Domain).includes(value as Domain)` to test whether the input is a member of the declared value domain
144
+ - this pattern derives valid values directly from the metadata object — it does not maintain a separate list
145
+ - if the input is not a member, the validator must reject it
146
+ - alternative checking strategies may be used only for non-string or non-enum domains
147
+
148
+ Example:
149
+
150
+ ```typescript
151
+ const valid = Object.values(Visibility).includes(value as Visibility);
152
+ if (!valid) {
153
+ throw new Error(`Invalid visibility value: ${value}`);
154
+ }
155
+ return value as Visibility;
156
+ ```
157
+
158
+ # 7) Invalid Input Must Throw a Descriptive Error
159
+
160
+ When a value fails validation, the validator must throw.
161
+
162
+ Policy:
163
+
164
+ - the validator must throw a `new Error(...)` on invalid input
165
+ - the validator must not return a fallback value on invalid input
166
+ - the validator must not silently ignore invalid input
167
+ - the thrown error must carry a message that identifies what was invalid and what was received
168
+
169
+ # 8) Error Messages Must Follow a Canonical Format
170
+
171
+ The error message thrown by a validator must follow this format:
172
+
173
+ ```
174
+ Invalid {name} value: {value}
175
+ ```
176
+
177
+ Where:
178
+
179
+ - `{name}` is the camelCase canonical name of the validated property as declared in the specification
180
+ - `{value}` is the actual received value, interpolated at runtime using a template literal
181
+
182
+ Policy:
183
+
184
+ - every validator in the component must use this format consistently
185
+ - the property name in the message must match the canonical name from the specification
186
+ - the received value must always be included in the message
187
+
188
+ Example:
189
+
190
+ ```typescript
191
+ throw new Error(`Invalid visibility value: ${value}`);
192
+ throw new Error(`Invalid status value: ${value}`);
193
+ ```
194
+
195
+ # 9) Validation Scope Covers Publicly Writable Constructs
196
+
197
+ The `Validate` class must define validators for every publicly writable state or data property that accepts external input and has a finite value domain.
198
+
199
+ Policy:
200
+
201
+ - every publicly writable state property with a finite value domain must have a corresponding validator in `Validate`
202
+ - every publicly writable data property with a finite value domain must have a corresponding validator in `Validate`
203
+ - readonly state that is only mutated internally does not require a validator in `Validate`
204
+ - internally generated values (e.g., UUIDs, timestamps) do not require validation in `Validate`
205
+ - the count of validators in `Validate` must equal the count of publicly writable constructs that require boundary enforcement
206
+
207
+ # 10) Validators Are Invoked Inside the Authoritative Mutation Path
208
+
209
+ Validators are called by `<component>.ts` inside the public setter of each validated construct. They are not called at the gesture or operation layer.
210
+
211
+ Policy:
212
+
213
+ - validation must be invoked inside the setter — the authoritative mutation path
214
+ - validation is the second step of the mutation order, after normalization and before the guard: **Normalize → Validate → Guard → Mutate**
215
+ - the return value of the validator — the narrowed typed value — must replace the raw input for the remainder of the mutation path
216
+ - if validation throws, the mutation path must not proceed past that point
217
+ - gesture handlers and operations must not invoke validators directly — they route through the setter which owns validation
218
+
219
+ The mutation order is defined authoritatively in the state policy. The validation policy governs the implementation of the validator itself, not the order it is called in.
220
+
221
+ Example showing the validator's position in the mutation path:
222
+
223
+ ```typescript
224
+ public set visibility(value: Visibility) {
225
+ // Normalize
226
+ value = value ?? Visibility.VISIBLE;
227
+
228
+ // Validate — narrowed return value replaces the raw input
229
+ value = Validate.visibility(value);
230
+
231
+ // Guard
232
+ if (this._visibility === value) return;
233
+
234
+ // Mutate
235
+ this._visibility = value;
236
+ ...
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ # Policy Applied to Specification
243
+
244
+ The Pin component declares two publicly writable state properties — `visibility` and `status` — both with string-based enum value domains declared in `pin.meta.ts`. Both require boundary enforcement at the public mutation path.
245
+
246
+ `pin.meta.ts` — value domain declarations:
247
+
248
+ ```typescript
249
+ export const Visibility = {
250
+ VISIBLE: "visible",
251
+ HIDDEN: "hidden",
252
+ } as const;
253
+
254
+ export type Visibility = (typeof Visibility)[keyof typeof Visibility];
255
+
256
+ export const Status = {
257
+ PINNED: "pinned",
258
+ UNPINNED: "unpinned",
259
+ } as const;
260
+
261
+ export type Status = (typeof Status)[keyof typeof Status];
262
+ ```
263
+
264
+ `pin.validation.ts` — the full implementation:
265
+
266
+ ```typescript
267
+ import { Visibility, Status } from "./pin.meta.js";
268
+
269
+ export class Validate {
270
+ public static visibility = (value: string) => {
271
+ const valid = Object.values(Visibility).includes(value as Visibility);
272
+ if (!valid) {
273
+ throw new Error(`Invalid visibility value: ${value}`);
274
+ }
275
+ return value as Visibility;
276
+ };
277
+
278
+ public static status = (value: string) => {
279
+ const valid = Object.values(Status).includes(value as Status);
280
+ if (!valid) {
281
+ throw new Error(`Invalid status value: ${value}`);
282
+ }
283
+ return value as Status;
284
+ };
285
+ }
286
+ ```
287
+
288
+ How the policy is applied:
289
+
290
+ - all validation lives in `pin.validation.ts` — not in `pin.ts` or `pin.meta.ts`
291
+ - a single exported `Validate` class contains both validators
292
+ - both validators are `public static` arrow function class properties
293
+ - both import their value domains from `pin.meta.ts` — no allowed values are hardcoded in `pin.validation.ts`
294
+ - both accept `string` as the raw input type and return the narrowed metadata type on success
295
+ - both use `Object.values(Domain).includes(value as Domain)` for membership checking
296
+ - both throw `new Error(...)` on invalid input — no fallback is returned
297
+ - both use the canonical error format: `` `Invalid {name} value: ${value}` ``
298
+ - `Validate` has exactly two methods because there are exactly two publicly writable state properties; readonly or internally-generated values are not covered
299
+
300
+ Invocation in `pin.ts`:
301
+
302
+ ```typescript
303
+ // visibility setter — optional attribute-backed, writable
304
+ public set visibility(value: Visibility) {
305
+ value = value ?? Visibility.VISIBLE; // Normalize
306
+ value = Validate.visibility(value); // Validate
307
+ if (this._visibility === value) return; // Guard
308
+ this._visibility = value; // Mutate
309
+ ...
310
+ }
311
+
312
+ // status setter — compulsory attribute-backed, writable
313
+ public set status(value: Status) {
314
+ value = Validate.status(value); // Validate (no normalization step)
315
+ if (this._status === value) return; // Guard
316
+ this._status = value; // Mutate
317
+ ...
318
+ }
319
+ ```
320
+
321
+ - `Validate.visibility` is called after normalization because the optional setter must first convert `null` (passed by `attributeChangedCallback` on attribute removal) to the default before validation can safely proceed
322
+ - `Validate.status` is called as the first step because the compulsory setter has no normalization step — `null` is not a valid status value and must be rejected by validation
323
+ - in both cases the narrowed return value replaces the raw input before the guard check
324
+
325
+ ---
326
+
327
+ # Policy Checklist
328
+
329
+ A compliant component must:
330
+
331
+ 1. Implement all validation logic in `<component>.validation.ts`.
332
+ 2. Not inline validation logic in `<component>.ts` or `<component>.meta.ts`.
333
+ 3. Export a single class named `Validate` from `<component>.validation.ts`.
334
+ 4. Define all validators as members of the `Validate` class with no non-validation logic in the class.
335
+ 5. Define each validator as a `public static` arrow function class property.
336
+ 6. Import value domain objects from `<component>.meta.ts` — do not hardcode allowed values in validators.
337
+ 7. Use `Object.values(Domain).includes(value as Domain)` for string-based enum membership checking.
338
+ 8. Accept the raw unnarrowed input type (typically `string` for string-based enum domains).
339
+ 9. Return the narrowed TypeScript domain type on valid input.
340
+ 10. Throw `new Error(...)` on invalid input — do not return a fallback or a default value.
341
+ 11. Use the canonical error message format: `` `Invalid {name} value: ${value}` ``.
342
+ 12. Use the camelCase canonical property name in the error message.
343
+ 13. Include the actual received value in the error message.
344
+ 14. Define a validator for every publicly writable state or data property with a finite value domain.
345
+ 15. Do not define validators in `Validate` for readonly state that is only internally mutated.
346
+ 16. Do not define validators in `Validate` for internally generated values.
347
+ 17. Invoke validators inside the public setter — the authoritative mutation path — not in gesture handlers or operations.
348
+ 18. Call the validator as the second step of the mutation path: after normalization, before the guard.
349
+ 19. Replace the raw input with the narrowed return value of the validator for the remainder of the mutation path.
350
+ 20. Ensure that if validation throws, mutation does not proceed.