@company-semantics/contracts 35.0.0 → 36.0.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 (36) hide show
  1. package/package.json +7 -2
  2. package/src/notifications/README.md +21 -19
  3. package/src/notifications/__tests__/README.md +42 -47
  4. package/src/notifications/__tests__/__snapshots__/README.md +15 -8
  5. package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +31 -0
  6. package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +2580 -215
  7. package/src/notifications/__tests__/context.test.ts +5 -5
  8. package/src/notifications/__tests__/definition.test.ts +2 -2
  9. package/src/notifications/__tests__/fixtures.ts +226 -0
  10. package/src/notifications/__tests__/kinds.test.ts +2 -3
  11. package/src/notifications/__tests__/registry.test.ts +27 -27
  12. package/src/notifications/__tests__/render-snapshot.test.ts +10 -6
  13. package/src/notifications/content.ts +13 -1
  14. package/src/notifications/index.ts +15 -9
  15. package/src/notifications/kinds/README.md +2 -2
  16. package/src/notifications/render.ts +6 -6
  17. package/src/notifications/renderers/email/README.md +132 -17
  18. package/src/notifications/renderers/email/__tests__/README.md +19 -16
  19. package/src/notifications/renderers/email/__tests__/amp.test.ts +217 -0
  20. package/src/notifications/renderers/email/__tests__/colors.test.ts +227 -0
  21. package/src/notifications/renderers/email/__tests__/render.test.ts +130 -36
  22. package/src/notifications/renderers/email/__tests__/styles.test.ts +111 -0
  23. package/src/notifications/renderers/email/chat.ts +44 -32
  24. package/src/notifications/renderers/email/colors.ts +342 -0
  25. package/src/notifications/renderers/email/constants.ts +27 -3
  26. package/src/notifications/renderers/email/cta.ts +60 -10
  27. package/src/notifications/renderers/email/index.ts +23 -8
  28. package/src/notifications/renderers/email/render.ts +31 -15
  29. package/src/notifications/renderers/email/shells.ts +115 -11
  30. package/src/notifications/renderers/email/styles.ts +361 -0
  31. package/src/notifications/renderers/slack/README.md +66 -25
  32. package/src/notifications/renderers/slack/__tests__/index.test.ts +249 -41
  33. package/src/notifications/renderers/slack/index.ts +192 -107
  34. package/src/notifications/text.ts +2 -2
  35. package/src/notifications/__tests__/output-parity.golden.ts +0 -363
  36. package/src/notifications/__tests__/output-parity.test.ts +0 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "35.0.0",
3
+ "version": "36.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -104,6 +104,7 @@
104
104
  "prepublishOnly": "echo 'ERROR: Publishing is CI-only via tag push. Use pnpm release instead.' && exit 1",
105
105
  "test": "vitest run",
106
106
  "test:run": "vitest run",
107
+ "color": "tsx scripts/color.ts",
107
108
  "generate:spec-hash": "tsx scripts/generate-spec-hash.ts",
108
109
  "generate:spec-hash:check": "tsx scripts/generate-spec-hash.ts --check",
109
110
  "generate:api": "pnpm generate:api-types && pnpm generate:spec-hash && pnpm generate:openapi-routes",
@@ -115,17 +116,21 @@
115
116
  "repo-map": "tsx ../company-semantics-ci/scripts/generate-repo-map.ts --roots src --name company-semantics-contracts --write",
116
117
  "repo-map:check": "tsx ../company-semantics-ci/scripts/generate-repo-map.ts --roots src --name company-semantics-contracts --check",
117
118
  "readme-api": "tsx ../company-semantics-ci/scripts/generate-readme-api.ts --roots src --write --jsdoc",
118
- "readme-api:check": "tsx ../company-semantics-ci/scripts/generate-readme-api.ts --roots src --check --jsdoc"
119
+ "readme-api:check": "tsx ../company-semantics-ci/scripts/generate-readme-api.ts --roots src --check --jsdoc",
120
+ "validate:amp": "tsx scripts/validate-amp.ts"
119
121
  },
120
122
  "packageManager": "pnpm@10.25.0",
121
123
  "engines": {
122
124
  "node": "22.x"
123
125
  },
124
126
  "dependencies": {
127
+ "@slack/types": "^3.0.0",
125
128
  "zod": "^4.4.3"
126
129
  },
127
130
  "devDependencies": {
128
131
  "@types/node": "^22.20.1",
132
+ "amphtml-validator": "^1.0.38",
133
+ "culori": "^4.0.2",
129
134
  "husky": "^9.1.7",
130
135
  "lint-staged": "^17.0.8",
131
136
  "markdownlint-cli2": "^0.23.0",
@@ -9,15 +9,17 @@ into `Definition → Payload → compose(payload, context) → Content → Rende
9
9
  This is the package's SOLE notification surface. `@company-semantics/contracts/email`
10
10
  and every `Email*` symbol are gone, with no shim and no alias.
11
11
 
12
- The domain holds the **output-parity baseline** a committed, byte-exact
13
- capture of what the email layer rendered at 32.0.0, plus the test that proves
14
- this layer still renders exactly that. The baseline was deliberately landed
15
- _before_ the abstraction it protects, so that every later step in the migration
16
- had a mechanical answer to "did any real email change?". Now that the old layer
17
- is deleted, the golden is also the only surviving record of those bytes.
18
-
19
- On top of it sits the **semantic model**: the channel-agnostic vocabulary the
20
- migration is heading toward.
12
+ The domain holds the **semantic model**: the channel-agnostic vocabulary an
13
+ element names, and the renderers that decide what it looks like per channel.
14
+
15
+ It used to hold an **output-parity baseline** too a byte-exact capture of what
16
+ the email layer rendered at 32.0.0, landed deliberately _before_ the abstraction
17
+ it protected, so every step of the migration had a mechanical answer to "did any
18
+ real email change?". That baseline did its job: the answer was "none", and it
19
+ shipped in 35.0.0. ADR-CONTRACTS-087 retired it, because a proof that the
20
+ migration changed nothing cannot also serve as permission to change something,
21
+ and email design is not frozen. `__tests__/render-snapshot.test.ts` is the fence
22
+ now; the retired bytes are readable in git at `v35.0.0`.
21
23
 
22
24
  - `content.ts` — `NotificationContent { metadata, sections }`, `Section`, and the
23
25
  flat `NotificationElement` union (greeting, body, keyValueTable, callToAction,
@@ -91,16 +93,16 @@ deliberately (ADR-CONTRACTS-086):
91
93
 
92
94
  ## Invariants
93
95
 
94
- - The golden in `__tests__/output-parity.golden.ts` records what the email layer
95
- rendered at **32.0.0**, and is now the only record of it — there is no longer a
96
- layer to re-capture it from. It is a regression baseline, not a specification:
97
- when it and this render layer disagree, the GOLDEN is right and the renderer
98
- must be fixed, never the golden regenerated to match.
99
- - Every kind in `NOTIFICATION_DEFINITIONS` needs at least one parity fixture,
100
- enforced by the test. The registry is the source of that list — it is total, so
101
- "every kind the entry point accepts" and "every kind with a fixture" are the
102
- same claim.
103
- - Rendering for parity MUST pin the year to `OUTPUT_PARITY_CLOCK`'s — by passing
96
+ - `__tests__/render-snapshot.test.ts` locks what this layer renders. A diff there
97
+ means a real sent email changed: review it, then regenerate. It is a regression
98
+ fence, not a specification but since ADR-CONTRACTS-087 retired the parity
99
+ golden that used to outrank it, reading the diff is the ONLY control left. A
100
+ snapshot refreshed on sight proves nothing.
101
+ - Every kind in `NOTIFICATION_DEFINITIONS` needs at least one fixture in
102
+ `__tests__/fixtures.ts`, enforced by the tests. The registry is the source of
103
+ that list — it is total, so "every kind the entry point accepts" and "every
104
+ kind with a fixture" are the same claim.
105
+ - Rendering MUST pin the year to `FIXTURE_CLOCK`'s — by passing
104
106
  `copyrightYear` in the `RenderContext`, NOT by freezing the clock. `signature`
105
107
  used to read `new Date().getFullYear()` mid-render, which is why the fixtures
106
108
  once needed fake timers; the year is a value now, and no test in this domain
@@ -2,53 +2,48 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- The output-parity proof for the multi-channel notification render layer
6
- (ADR-CONTRACTS-086), and the snapshot suite that documents the markup.
7
-
8
- - `output-parity.golden.ts` — the fixtures (kind + payload) and
9
- `OUTPUT_PARITY_GOLDEN`, the exact `{ subject, text, html }` each one rendered
10
- to through the email layer **as it stood before any of this existed**.
11
- - `output-parity.test.ts` renders every fixture through `renderToChannel`, the
12
- new entry point, and asserts byte equality against that golden.
13
- - `render-snapshot.test.ts` + `__snapshots__/` — the same fixtures as vitest
14
- snapshots, moved here from the old `src/email/render/__tests__/` with the
15
- markup they document.
16
-
17
- The parity test used to pin the old `renderEmail` against itself, which was the
18
- point: the bytes were captured _before_ the layer they describe was touched. That
19
- baseline has now done its job the same fixtures render through
20
- `compose Content Renderer` and produce the same bytes, so the generalisation
21
- is proven to have changed no email. `src/email/` was deleted on that proof.
22
-
23
- Prefer the golden when the two suites disagree. `vitest -u` can regenerate a
24
- snapshot in a keystroke, and a snapshot regenerated under refactor pressure is
25
- worth nothing; the golden is a committed module, so making it pass is a
26
- deliberate, reviewable act.
5
+ The shared fixture corpus for the multi-channel notification render layer
6
+ (ADR-CONTRACTS-086), and the snapshot suite that documents the markup it renders
7
+ to.
8
+
9
+ - `fixtures.ts` every implemented kind × representative payload, plus
10
+ `FIXTURE_CLOCK` and `fixtureKey`. Shared by every suite here and by
11
+ `../renderers/email/__tests__/render.test.ts`, so a kind added to the registry
12
+ is covered everywhere by being added once.
13
+ - `render-snapshot.test.ts` + `__snapshots__/` — those fixtures rendered through
14
+ the email channel, as vitest snapshots. This is the fence: **a diff here means
15
+ a real sent email changed.**
16
+
17
+ ## The retired output-parity golden (ADR-CONTRACTS-087)
18
+
19
+ `fixtures.ts` was `output-parity.golden.ts`, and it used to carry
20
+ `OUTPUT_PARITY_GOLDEN` the exact `{ subject, text, html }` the email layer
21
+ emitted at 32.0.0, before any of this existed asserted byte-for-byte by
22
+ `output-parity.test.ts` at the entry point and by the email renderer's own suite
23
+ at the renderer. Those two tests were the "no output change" contract for the
24
+ generalisation: `src/email/` was deleted on that proof.
25
+
26
+ **The proof passed and shipped in 35.0.0, which discharged it.** It was a claim
27
+ about a migration, not a freeze on email design — and the moment email design is
28
+ deliberately exercised, byte-equality to a deleted layer fails by construction
29
+ and keeps failing.
30
+
31
+ It was **retired rather than regenerated**, and the distinction is the point:
32
+ rewriting those bytes to match a redesign would have made the file assert
33
+ something false about a layer that no longer exists to contradict it. The bytes
34
+ are readable in git at `v35.0.0`; nothing in the tree claims to be them.
27
35
 
28
36
  ## Invariants
29
37
 
30
- - The year MUST be pinned to `OUTPUT_PARITY_CLOCK`'s via the `RenderContext`, not
31
- by freezing the clock. It is baked into every case by `signature`, so an
32
- unpinned render fails on the next New Year's Day for calendar reasons alone.
33
- Fake timers are no longer needed anywhere here, and that is the improvement
34
- this layer was built to deliver do not reintroduce them.
35
- - The captured year is **not** redacted or normalised. It is part of the output
36
- surface this proof exists to protect.
37
- - A failure here means a **real sent email changed**. Review the diff before
38
- regenerating; the golden is not a snapshot to refresh on sight. `src/email/` is
39
- deleted, so these bytes cannot be re-captured — the golden is the only record
40
- of what that layer emitted, and regenerating it destroys the proof rather than
41
- updating it.
42
- - The golden and the fixtures MUST stay in exact correspondence — no missing and
43
- no orphaned entries. The test enforces both directions.
44
- - Every kind in `NOTIFICATION_DEFINITIONS` MUST have a fixture, enforced by both
45
- suites.
46
- - The golden's BYTES are frozen; its TYPES were re-anchored onto this domain when
47
- the `Email*` originals were deleted. `OutputParityFixture.options` went with
48
- them — it was declared as `RenderEmailOptions` and carried the old layer's
49
- `includeRequestMetadata`, which `compose` replaced with "does the payload carry
50
- request details?". It had been left in place as frozen evidence, but its type
51
- no longer exists and no test ever read it; the one fixture that set it supplies
52
- both request fields anyway, so the gate opens regardless. The byte equality is
53
- what proves that — which is exactly why dropping the field was safe and
54
- re-recording a byte would not be.
38
+ - A snapshot diff means **a real sent email changed**. Review it, then
39
+ `vitest -u` never the other way round. Regenerating on sight is the whole
40
+ hazard; the review is the control, and it is now the only one.
41
+ - The year MUST be pinned via `FIXTURE_CLOCK` on the `RenderContext`, never by
42
+ freezing the clock. `signature` bakes it into every case, so an unpinned render
43
+ fails on the next New Year's Day for calendar reasons alone. Fake timers are
44
+ not needed anywhere here that is the impurity ADR-CONTRACTS-086 retired, so
45
+ do not reintroduce them.
46
+ - The pinned year is **not** redacted or normalised. It is part of the output
47
+ surface these suites exist to protect.
48
+ - Every kind in `NOTIFICATION_DEFINITIONS` MUST have a fixture, enforced by
49
+ `./definition.test.ts` and `./registry.test.ts`.
@@ -13,6 +13,11 @@ stayed `{kind} · {name}`, so every snapshot key still resolves against the same
13
13
  file. That is the claim the move is making — the keys matched, so the bytes
14
14
  matched.
15
15
 
16
+ The `security.alert` cases have since moved: the `warning` banner is drawn in the
17
+ product's destructive colour (ADR-CONTRACTS-087). That is the first deliberate
18
+ change to this markup since the move, and the reason the parity golden that used
19
+ to guard it is gone.
20
+
16
21
  This directory exists because vitest resolves snapshots adjacent to the test
17
22
  file, so the `.snap` had to travel with it.
18
23
 
@@ -20,13 +25,15 @@ file, so the `.snap` had to travel with it.
20
25
 
21
26
  - This is **production-parity email markup**. A diff here means a real sent email
22
27
  changed. Review it, then `vitest -u` — never the other way round.
23
- - These snapshots are **not** the migration's proof. That is
24
- `../output-parity.golden.ts`, a committed module `vitest -u` cannot touch. When
25
- the two disagree, the golden is right. Regenerating this file to make a
26
- refactor pass is the exact failure ADR-CONTRACTS-086 exists to prevent, and it
27
- is one keystroke away which is why the golden is not a snapshot.
28
- - The copyright year is pinned to `OUTPUT_PARITY_CLOCK`'s, never the wall clock.
29
- It is baked into every case via `signature`, so an unpinned capture would fail
30
- on the next New Year's Day for calendar reasons alone.
28
+ - These snapshots are now the **only** automated record of that markup. Until
29
+ ADR-CONTRACTS-087 the migration's proof sat beside them in
30
+ `../output-parity.golden.ts`, a committed module `vitest -u` could not touch,
31
+ and it was the tie-breaker when the two disagreed. That proof was discharged in
32
+ 35.0.0 and retired, because it asserted byte-equality with a deleted layer and
33
+ email design is allowed to change. Nothing now catches a snapshot regenerated
34
+ under refactor pressure except reading the diff so read it.
35
+ - The copyright year is pinned to `FIXTURE_CLOCK`'s, never the wall clock. It is
36
+ baked into every case via `signature`, so an unpinned capture would fail on the
37
+ next New Year's Day for calendar reasons alone.
31
38
  - The year is **not** redacted or normalised. It is part of the output surface
32
39
  these snapshots exist to protect.
@@ -0,0 +1,31 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`NOTIFICATION_DEFINITIONS > titles every kind 1`] = `
4
+ {
5
+ "auth.otp · Default": "Your login code for Company Semantics",
6
+ "auth.otp · Short expiry": "Your login code for Company Semantics",
7
+ "auth.otp · With request metadata": "Your login code for Company Semantics",
8
+ "chat.shared · Default": "A chat has been shared with you",
9
+ "chat.shared · Long preview": "A chat has been shared with you",
10
+ "chat.shared · Long title": "A chat has been shared with you",
11
+ "chat.shared · No preview": "A chat has been shared with you",
12
+ "chat.shared · Short": "A chat has been shared with you",
13
+ "companyMd.access_request_approved · Editor": "Your access request was approved",
14
+ "companyMd.access_request_approved · Viewer": "Your access request was approved",
15
+ "companyMd.access_request_denied · No reason": "Your access request was reviewed",
16
+ "companyMd.access_request_denied · With reason": "Your access request was reviewed",
17
+ "companyMd.access_requested · No message": "Someone requested access to a document",
18
+ "companyMd.access_requested · With message": "Someone requested access to a document",
19
+ "org.invite · Admin": "You've been invited to join Acme Corp on Company Semantics",
20
+ "org.invite · Member": "You've been invited to join Acme Corp on Company Semantics",
21
+ "org.ownership_transfer · No note (admin)": "You've been invited to become a workspace owner",
22
+ "org.ownership_transfer · With note + from": "You've been invited to become a workspace owner",
23
+ "org.ownership_transfer_completed · Default": "Workspace ownership has been transferred",
24
+ "org.unit_owner_granted · Delegate with expiry": "You've been added as delegate owner to Platform Engineering in Acme Corp",
25
+ "org.unit_owner_granted · Unit owner + message": "You've been added as owner to Platform Engineering in Acme Corp",
26
+ "security.alert · Excessive OTP requests": "Security alert for your account",
27
+ "security.alert · Unusual login location": "Security alert for your account",
28
+ "share.granted · Document · editor · message": "Something has been shared with you",
29
+ "share.granted · Meeting · viewer · no title": "Something has been shared with you",
30
+ }
31
+ `;