rhachet-roles-ehmpathy 1.8.0 → 1.9.0

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.
Files changed (40) hide show
  1. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].v3.md +87 -0
  2. package/dist/logic/roles/mechanic/.briefs/architecture/directional-dependencies.md +49 -40
  3. package/dist/logic/roles/mechanic/.briefs/criteria.practices/require.knowledge.externalized.md +17 -0
  4. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/bad-practice/forbid.positional-args.md +43 -0
  5. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.contract.inputs.nameargs/best-practice/require.namedargs.md +6 -0
  6. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/.readme.md +0 -0
  7. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.declarative/best-practice/declastruct.[demo].md +485 -0
  8. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.nullable.md +13 -0
  9. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.has.attributes.undefined.md +15 -0
  10. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/bad-practices/blocker.refs.immuatble.md +9 -0
  11. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.objects/best-practice/ref.package.domain-objects.[readme].md +585 -0
  12. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.domain.operations/best-practice/require.sync.names.md +14 -0
  13. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/bad-practices/forbid.hide_errors.md +13 -0
  14. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.[demo].shell.md +17 -0
  15. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.errors.failfast/best-practice/require.fail_fast.md +28 -0
  16. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/directional-dependencies.md +82 -0
  17. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/best-practice/dot-test-and-dot-temp.md +20 -0
  18. package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.typescript.utils/best-practice/ref.package.as-command.[tips].md +7 -0
  19. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.acceptance/best-practice/blackbox.md +5 -0
  20. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.run.[lesson].md +18 -0
  21. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.use.[lesson].md +20 -0
  22. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].md +3 -0
  23. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_integ.md +8 -0
  24. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.[lesson].on_scope.for_units.md +9 -0
  25. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/howto.write.bdd.[lesson].md +280 -0
  26. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/prefer.datadriven.md +41 -0
  27. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/ref.test-fns.[readme].md +185 -0
  28. package/dist/logic/roles/mechanic/.briefs/patterns/code.test.howto/best-practice/whento.snapshots.[lesson].md +23 -0
  29. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/.readme.md +1 -0
  30. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=practices.terms=forbid_prefer_desire_require.md +13 -0
  31. package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/domain=software.terms=prodcode_vs_testcode.md +7 -0
  32. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/.readme.md +3 -0
  33. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.chill-nature.md +0 -0
  34. package/dist/logic/roles/mechanic/.briefs/patterns/lang.tones/prefer.lowercase.md +0 -0
  35. package/dist/logic/roles/mechanic/getMechanicRole.js +1 -1
  36. package/dist/logic/roles/mechanic/getMechanicRole.js.map +1 -1
  37. package/package.json +2 -2
  38. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].[idea].md +0 -35
  39. package/dist/logic/roles/architect/.briefs/criteria.given_when_then.[seed].md +0 -20
  40. /package/dist/logic/roles/architect/.briefs/{criteria.practices → practices}/prefer.env_access.prep_over_dev.md +0 -0
@@ -0,0 +1,485 @@
1
+ # .brief.demo = declastruct pattern, implemented for the stripe sdk
2
+
3
+ Use the declastruct pattern whenever we need to construct and control remote resources. Each remote resource is considered a structure that we declaratively control, via idempotent get+set semantics.
4
+
5
+
6
+ ### 1. first, declare the entities we wish to construct
7
+
8
+ declare them as explicit domain-objects
9
+
10
+ most importantly, we must understand
11
+ - the unique, natural key upon which we can drive idempotency
12
+ - the primary, artificial key upon which we can reference the resource with in foreign keys with other entities
13
+
14
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/domain/objects/DeclaredStripeCustomer.ts
15
+
16
+ ```ts
17
+ import { DomainEntity, DomainLiteral } from 'domain-objects';
18
+
19
+ /**
20
+ * .what = a declarative structure which represents a Stripe Customer
21
+ */
22
+ export interface DeclaredStripeCustomer {
23
+ /**
24
+ * the public stripe customer id of this customer
25
+ */
26
+ id?: string;
27
+
28
+ /**
29
+ * the email address of the customer
30
+ *
31
+ * note
32
+ * - stripe does not enforce this to be a unique key
33
+ * - however, to create a pit-of-success, this is used as the unique key with declastruct, since it is the only non-id field that we can search on
34
+ */
35
+ email: string;
36
+
37
+ /**
38
+ * then name of the customer, if set
39
+ */
40
+ name: null | string;
41
+
42
+ /**
43
+ * a description of the customer, if set
44
+ */
45
+ description: null | string;
46
+
47
+ /**
48
+ * then phone of the customer, if set
49
+ */
50
+ phone: null | string;
51
+
52
+ /**
53
+ * metadata that the customer was tagged with
54
+ */
55
+ metadata: null | Record<string, string>;
56
+ }
57
+
58
+ export class DeclaredStripeCustomer
59
+ extends DomainEntity<DeclaredStripeCustomer>
60
+ implements DeclaredStripeCustomer
61
+ {
62
+ public static primary = ['id'] as const;
63
+ public static unique = ['email'] as const;
64
+ public static nested = {
65
+ metadata: DomainLiteral,
66
+ };
67
+ }
68
+ ```
69
+
70
+
71
+ ### 2. next, declare how to translate from the sdk's shape to our declared shape
72
+
73
+ declare how to translate from the sdk's shape to our declared shape
74
+
75
+ why? because its rare that the simplest way to represent a domain-entity is the way that the api has represented it, due to backwards compat && practice differences
76
+
77
+ our objective is
78
+ - make things as simple and intuitive to understand
79
+ - provide a pit of success
80
+
81
+ therefore, we always cast into our own representation, to give ourselves the flexibility to speak more clearly about entities
82
+
83
+ additionally, this enables us to cast them into `domain-object` instances, which give us explicit declarations of the distinct objects and domain-driven features like references
84
+
85
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/cast/castToDeclaredStripeCustomer.ts
86
+ ```ts
87
+ import { UnexpectedCodePathError } from 'helpful-errors';
88
+ import Stripe from 'stripe';
89
+ import { HasMetadata, omit } from 'type-fns';
90
+
91
+ import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
92
+
93
+ export const castToDeclaredStripeCustomer = (
94
+ input: Stripe.Customer,
95
+ ): HasMetadata<DeclaredStripeCustomer> => {
96
+ return new DeclaredStripeCustomer({
97
+ id: input.id,
98
+ email:
99
+ input.email ??
100
+ UnexpectedCodePathError.throw(
101
+ 'no email found for customer. not a valid declared stripe customer',
102
+ { input },
103
+ ),
104
+ description: input.description ?? null,
105
+ name: input.name ?? null,
106
+ phone: input.phone ?? null,
107
+ metadata: (() => {
108
+ const obj = input.metadata ? omit(input.metadata, ['exid']) : {};
109
+ if (Object.keys(obj).length === 0) return null;
110
+ return obj;
111
+ })(),
112
+ }) as HasMetadata<DeclaredStripeCustomer>;
113
+ };
114
+
115
+ ```
116
+
117
+
118
+ ### 3. next, declare how to get the resource from the remote repository
119
+
120
+ support get.by.unique, get.by.primary, and get.by.ref
121
+
122
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/getCustomer.ts
123
+ ```ts
124
+ import { Ref, RefByPrimary, RefByUnique, isUniqueKeyRef } from 'domain-objects';
125
+ import { BadRequestError, UnexpectedCodePathError } from 'helpful-errors';
126
+ import { HasMetadata, PickOne } from 'type-fns';
127
+ import { VisualogicContext } from 'visualogic';
128
+
129
+ import { StripeApiContext } from '../../domain/constants';
130
+ import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
131
+ import { castToDeclaredStripeCustomer } from '../cast/castToDeclaredStripeCustomer';
132
+
133
+ /**
134
+ * .what = gets a customer from stripe
135
+ */
136
+ export const getCustomer = async (
137
+ input: {
138
+ by: PickOne<{
139
+ primary: RefByPrimary<typeof DeclaredStripeCustomer>;
140
+ unique: RefByUnique<typeof DeclaredStripeCustomer>;
141
+ ref: Ref<typeof DeclaredStripeCustomer>;
142
+ }>;
143
+ },
144
+ context: StripeApiContext & VisualogicContext,
145
+ ): Promise<HasMetadata<DeclaredStripeCustomer> | null> => {
146
+ // handle by ref
147
+ if (input.by.ref)
148
+ return isUniqueKeyRef({ of: DeclaredStripeCustomer })(input.by.ref)
149
+ ? getCustomer({ by: { unique: input.by.ref } }, context)
150
+ : getCustomer({ by: { primary: input.by.ref } }, context);
151
+
152
+ // handle get by id
153
+ if (input.by.primary) {
154
+ try {
155
+ const customer = await context.stripe.customers.retrieve(
156
+ input.by.primary.id,
157
+ );
158
+ if (customer.deleted) return null;
159
+ return castToDeclaredStripeCustomer(customer);
160
+ } catch (error) {
161
+ if (!(error instanceof Error)) throw error;
162
+ if (error.message.includes('No such customer')) return null; // handle "null" responses without an error
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ // handle get by email
168
+ if (input.by.unique) {
169
+ const {
170
+ data: [customer, ...otherCustomers],
171
+ } = await context.stripe.customers.list({
172
+ email: input.by.unique.email,
173
+ });
174
+ if (otherCustomers.length)
175
+ throw new BadRequestError('more than one customer for this email', {
176
+ input,
177
+ customers: [customer, ...otherCustomers],
178
+ });
179
+ if (!customer) return null;
180
+ return castToDeclaredStripeCustomer(customer);
181
+ }
182
+
183
+ // otherwise, unexpected input
184
+ throw new UnexpectedCodePathError('invalid input', { input });
185
+ };
186
+ ```
187
+
188
+ and dont forget the tests!
189
+
190
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/getCustomer.integration.test.ts
191
+ ```ts
192
+ import { given, then, when, useBeforeAll } from 'test-fns';
193
+ import { HasMetadata } from 'type-fns';
194
+ import { getUuid } from 'uuid-fns';
195
+
196
+ import { getStripeCredentials } from '../../__test_assets__/getStripeCredentials';
197
+ import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
198
+ import { getStripe } from '../auth/getStripe';
199
+ import { getCustomer } from './getCustomer';
200
+ import { setCustomer } from './setCustomer';
201
+
202
+ describe('getCustomer', () => {
203
+ given('a by.primary', () => {
204
+ when('the customer does not exist', () => {
205
+ const stripeCustomerId = getUuid();
206
+
207
+ then('we should get null', async () => {
208
+ const customer = await getCustomer(
209
+ { by: { primary: { id: stripeCustomerId } } },
210
+ { stripe: await getStripe(getStripeCredentials()), log: console },
211
+ );
212
+ expect(customer).toEqual(null);
213
+ });
214
+ });
215
+
216
+ when('the customer does exist', () => {
217
+ const customerFound: HasMetadata<DeclaredStripeCustomer> = useBeforeAll(
218
+ async () =>
219
+ await setCustomer(
220
+ {
221
+ finsert: {
222
+ email: 'svc-protools@ahbode.dev',
223
+ name: 'svc-protools.test',
224
+ description: 'test',
225
+ metadata: null,
226
+ phone: null,
227
+ },
228
+ },
229
+ { stripe: await getStripe(getStripeCredentials()), log: console },
230
+ ),
231
+ );
232
+
233
+ when('we attempt to get the customer', () => {
234
+ then('we should get null', async () => {
235
+ const customer = await getCustomer(
236
+ { by: { primary: { id: customerFound.id } } },
237
+ { stripe: await getStripe(getStripeCredentials()), log: console },
238
+ );
239
+ expect(customer?.id).toEqual(customerFound.id);
240
+ });
241
+ });
242
+ });
243
+ });
244
+ });
245
+ ```
246
+
247
+ ### 4. last, declare how to set the resource into the remote repository
248
+
249
+ support set via both set.finsert and set.upsert idempotent operations
250
+
251
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/setCustomer.ts
252
+
253
+ ```ts
254
+ import { serialize } from 'domain-objects';
255
+ import { toHashSha256Sync } from 'hash-fns';
256
+ import { UnexpectedCodePathError } from 'helpful-errors';
257
+ import { HasMetadata, PickOne } from 'type-fns';
258
+ import {
259
+ getResourceNameFromFileName,
260
+ VisualogicContext,
261
+ withLogTrail,
262
+ } from 'visualogic';
263
+
264
+ import { StripeApiContext } from '../../domain/constants';
265
+ import { DeclaredStripeCustomer } from '../../domain/objects/DeclaredStripeCustomer';
266
+ import { castToDeclaredStripeCustomer } from '../cast/castToDeclaredStripeCustomer';
267
+ import { getCustomer } from './getCustomer';
268
+
269
+ export const setCustomer = withLogTrail(
270
+ async (
271
+ input: PickOne<{
272
+ finsert: DeclaredStripeCustomer;
273
+ upsert: DeclaredStripeCustomer;
274
+ }>,
275
+ context: StripeApiContext & VisualogicContext,
276
+ ): Promise<HasMetadata<DeclaredStripeCustomer>> => {
277
+ // lookup the customer
278
+ const customerFound = await getCustomer(
279
+ {
280
+ by: {
281
+ unique: {
282
+ email:
283
+ input.finsert?.email ??
284
+ input.upsert?.email ??
285
+ UnexpectedCodePathError.throw('no email in input', { input }),
286
+ },
287
+ },
288
+ },
289
+ context,
290
+ );
291
+
292
+ // sanity check that if the customer exists, their id matches the user's expectations, if any
293
+ const stripeCustomerIdExpected = input.finsert?.id || input.upsert?.id;
294
+ if (
295
+ customerFound &&
296
+ stripeCustomerIdExpected &&
297
+ stripeCustomerIdExpected !== customerFound.id
298
+ )
299
+ throw new UnexpectedCodePathError(
300
+ 'asked to setCustomer with a .primary=id which does not match the .unique=email',
301
+ {
302
+ stripeCustomerIdExpected,
303
+ stripeCustomerIdFound: customerFound.id,
304
+ customerFound,
305
+ },
306
+ );
307
+
308
+ // if the customer was found, then handle that
309
+ if (customerFound) {
310
+ // if asked to finsert, then we can return it now
311
+ if (input.finsert) return customerFound;
312
+
313
+ // if asked to upsert, then we can update it now
314
+ if (input.upsert)
315
+ return castToDeclaredStripeCustomer(
316
+ await context.stripe.customers.update(customerFound.id, {
317
+ name: input.upsert.name ?? undefined,
318
+ description: input.upsert.description ?? undefined,
319
+ phone: input.upsert.phone ?? undefined,
320
+ metadata: input.upsert.metadata ?? undefined,
321
+ }),
322
+ );
323
+ }
324
+
325
+ // otherwise, create the customer
326
+ const customerDesired: DeclaredStripeCustomer =
327
+ input.upsert ?? input.finsert;
328
+ const customerCreated = await context.stripe.customers.create(
329
+ {
330
+ email: customerDesired.email,
331
+ name: customerDesired.name ?? undefined,
332
+ description: customerDesired.description ?? undefined,
333
+ phone: customerDesired.phone ?? undefined,
334
+ metadata: customerDesired.metadata ?? undefined,
335
+ },
336
+ {
337
+ idempotencyKey: toHashSha256Sync(
338
+ [
339
+ // stage, // todo: pull stage from context.environment
340
+ 'v1.0.0',
341
+ serialize({ ...customerDesired }),
342
+ ].join(';'),
343
+ ),
344
+ },
345
+ );
346
+ return castToDeclaredStripeCustomer(customerCreated);
347
+ },
348
+ { name: getResourceNameFromFileName(__filename) },
349
+ );
350
+ ```
351
+
352
+
353
+ and of course, the tests
354
+
355
+ ref: https://github.com/ehmpathy/declastruct-stripe-sdk/blob/1f9e2ecefb46028f75348aed8a5f9e3528eb5c1e/src/logic/customer/setCustomer.integration.test.ts
356
+ ```ts
357
+ import { given, then, useBeforeAll, when } from 'test-fns';
358
+ import { getUuid } from 'uuid-fns';
359
+
360
+ import { getStripeCredentials } from '../../__test_assets__/getStripeCredentials';
361
+ import { getStripe } from '../auth/getStripe';
362
+ import { setCustomer } from './setCustomer';
363
+
364
+ const log = console;
365
+
366
+ describe('setCustomer', () => {
367
+ given('a .finsert', () => {
368
+ when('the customer does not exist yet', () => {
369
+ const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
370
+
371
+ then('we should create it', async () => {
372
+ const customer = await setCustomer(
373
+ {
374
+ finsert: {
375
+ email,
376
+ name: 'svc-protools.test',
377
+ description: 'test',
378
+ metadata: null,
379
+ phone: null,
380
+ },
381
+ },
382
+ { stripe: await getStripe(getStripeCredentials()), log },
383
+ );
384
+ console.log(customer);
385
+ expect(customer.id).toContain('cus_');
386
+ });
387
+ });
388
+
389
+ when('the customer already exists', () => {
390
+ const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
391
+ const customerBefore = useBeforeAll(
392
+ async () =>
393
+ await setCustomer(
394
+ {
395
+ finsert: {
396
+ email,
397
+ name: 'svc-protools.test',
398
+ description: 'test',
399
+ metadata: null,
400
+ phone: null,
401
+ },
402
+ },
403
+ { stripe: await getStripe(getStripeCredentials()), log },
404
+ ),
405
+ );
406
+
407
+ then('it should not update the customers attributes', async () => {
408
+ const customerAfter = await setCustomer(
409
+ {
410
+ finsert: {
411
+ email,
412
+ name: 'new name',
413
+ description: 'test',
414
+ metadata: null,
415
+ phone: null,
416
+ },
417
+ },
418
+ { stripe: await getStripe(getStripeCredentials()), log },
419
+ );
420
+ expect(customerAfter.name).not.toEqual('new name');
421
+ expect(customerAfter.name).toEqual(customerBefore.name);
422
+ });
423
+ });
424
+ });
425
+ given('a .upsert', () => {
426
+ when('the customer does not exist yet', () => {
427
+ const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
428
+
429
+ then('we should create it', async () => {
430
+ const customer = await setCustomer(
431
+ {
432
+ upsert: {
433
+ email,
434
+ name: 'svc-protools.test',
435
+ description: 'test',
436
+ metadata: null,
437
+ phone: null,
438
+ },
439
+ },
440
+ { stripe: await getStripe(getStripeCredentials()), log },
441
+ );
442
+ console.log(customer);
443
+ expect(customer.id).toContain('cus_');
444
+ });
445
+ });
446
+
447
+ when('the customer already exists', () => {
448
+ const email = `svc-protools.test.${getUuid()}@ahbode.dev`; // random uuid => new customer
449
+ const customerBefore = useBeforeAll(
450
+ async () =>
451
+ await setCustomer(
452
+ {
453
+ upsert: {
454
+ email,
455
+ name: 'svc-protools.test',
456
+ description: 'test',
457
+ metadata: null,
458
+ phone: null,
459
+ },
460
+ },
461
+ { stripe: await getStripe(getStripeCredentials()), log },
462
+ ),
463
+ );
464
+
465
+ then('it should update the customers attributes', async () => {
466
+ const customerAfter = await setCustomer(
467
+ {
468
+ upsert: {
469
+ email,
470
+ name: 'new name',
471
+ description: 'test',
472
+ metadata: null,
473
+ phone: null,
474
+ },
475
+ },
476
+ { stripe: await getStripe(getStripeCredentials()), log },
477
+ );
478
+ expect(customerAfter.name).toEqual('new name');
479
+ expect(customerAfter.name).not.toEqual(customerBefore.name);
480
+ });
481
+ });
482
+ });
483
+ });
484
+ ```
485
+
@@ -0,0 +1,13 @@
1
+ we need to be very skeptical of nullable attributes
2
+
3
+ there needs to be a clear domain reason for why an attribute can be null - and it should be clearly articulated
4
+
5
+ ---
6
+
7
+ if there's no clear reason to support it as null, ensure the attribute has a clear type declared
8
+
9
+ rational or its forbidden
10
+
11
+ ===
12
+
13
+ this is a BLOCKER level violation
@@ -0,0 +1,15 @@
1
+ never allow "undefined" attributes for domain objects UNLESS they are database-generated metadata
2
+
3
+
4
+ i.e.,
5
+ - ids, createdAt, updatedAt, etc - these are allowed to be undefined, since they're metadata, and only known on read
6
+
7
+ however, everything else should be explicitly knowable
8
+ - if we know that it is null, then set it as null
9
+ - however, we should be really clear as to WHY it can be null; if there is no good domain reason for null, forbid it
10
+ - otherwise, define the exact type
11
+
12
+
13
+ =====
14
+
15
+ this is a BLOCKER level violation
@@ -0,0 +1,9 @@
1
+ refs must be immutable
2
+
3
+ primary keys must be immutable; otherwise, they are not a stable identifier => not a reference key candidate
4
+
5
+ unique keys must be immutable; otherwise, they are not a stable identifier => not a reference key candidate
6
+
7
+ ===
8
+
9
+ this is a BLOCKER level violation when detected