@company-semantics/contracts 40.0.0 → 42.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "40.0.0",
3
+ "version": "42.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -129,17 +129,17 @@
129
129
  "zod": "^4.4.3"
130
130
  },
131
131
  "devDependencies": {
132
- "@types/node": "^22.20.1",
132
+ "@types/node": "^26.1.2",
133
133
  "amphtml-validator": "^1.0.38",
134
134
  "culori": "^4.0.2",
135
135
  "husky": "^9.1.7",
136
- "lint-staged": "^17.1.0",
137
- "markdownlint-cli2": "^0.23.1",
136
+ "lint-staged": "^17.2.0",
137
+ "markdownlint-cli2": "^0.23.2",
138
138
  "openapi-typescript": "^7.13.0",
139
139
  "prettier": "^3.9.6",
140
140
  "tsx": "^4.23.1",
141
141
  "typescript": "^5.8.3",
142
- "vite": "^7.3.6",
142
+ "vite": "^8.2.0",
143
143
  "vitest": "^4.1.10",
144
144
  "yaml": "^2.9.0"
145
145
  },
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '402a9b3f7788' as const;
3
- export const SPEC_HASH_FULL = '402a9b3f77881eef9d41ee5ad49aa2b184a5b838ce948cc43ea4226e434f9b81' as const;
2
+ export const SPEC_HASH = 'c0f32d11bf0f' as const;
3
+ export const SPEC_HASH_FULL = 'c0f32d11bf0fdc59ee0a7bf84bee984f2f8109838d2659b6e66709e0198dcd4e' as const;
@@ -4603,6 +4603,11 @@ export interface components {
4603
4603
  /** @enum {string} */
4604
4604
  accessState?: "full" | "redacted_admin" | "locked_requestable";
4605
4605
  canRequestAccess?: boolean;
4606
+ pendingAccessRequest?: {
4607
+ id: string;
4608
+ requestedAccessLevel: ("editor" | "commenter" | "viewer") | null;
4609
+ createdAt: string;
4610
+ } | null;
4606
4611
  /** @enum {string} */
4607
4612
  visibility: "private" | "unit" | "org";
4608
4613
  /** @enum {string} */
@@ -4726,9 +4731,12 @@ export interface components {
4726
4731
  ApproveAccessRequest: {
4727
4732
  /** @enum {string} */
4728
4733
  accessLevel: "editor" | "commenter" | "viewer";
4734
+ notify?: boolean;
4735
+ message?: string;
4729
4736
  };
4730
4737
  DenyAccessRequest: {
4731
4738
  reason?: string;
4739
+ notify?: boolean;
4732
4740
  };
4733
4741
  /** @description Org-wide context-doc discovery hits the caller may view. */
4734
4742
  SearchContextDocsResponse: {
@@ -42,7 +42,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
42
42
  - `ChatRuntimeProfile` _(type)_ — Chat runtime profile — user-selectable orchestration strategy.
43
43
  - `ChatRuntimeProfileInfo` _(type)_ — Runtime profile metadata for UI rendering.
44
44
  - `ChatShareInfo` _(type)_ — Chat share record with snapshot metadata.
45
- - `ChatShareInfoSchema`
45
+ - `ChatShareInfoSchema` — A chat share, as returned by the share APIs.
46
46
  - `ChatSseEvent` _(type)_ — SSE event types for chat updates.
47
47
  - `ChatSseEventSchema`
48
48
  - `ChatSuccessResponse` _(type)_ — Generic mutation success confirmation.
@@ -58,12 +58,28 @@ export const SharedChatMessageSchema = z.object({
58
58
  parts: z.array(z.unknown()).optional(),
59
59
  });
60
60
 
61
+ /**
62
+ * A chat share, as returned by the share APIs.
63
+ *
64
+ * SHOW-ONCE (ADR-BE-460). `token` and `shareUrl` are present ONLY on the
65
+ * response that creates the share. Share tokens are stored as a digest, so the
66
+ * server cannot redisplay one afterwards — listing or updating a share returns
67
+ * the record without them, and re-obtaining a link means revoke-and-reshare.
68
+ *
69
+ * This is the industry norm for capability links, and it is what removes the
70
+ * last reason to keep a recoverable copy of the token: a share list that can
71
+ * still show you the URL is a share list that can leak every live link at once.
72
+ * Consumers must treat both fields as optional and drive their copy-link
73
+ * affordance off creation, not off a fetched share.
74
+ */
61
75
  export const ChatShareInfoSchema = z.object({
62
76
  id: z.string(),
63
77
  chatId: z.string(),
64
- token: z.string(),
78
+ /** Create-time only — never returned by list or update (show-once). */
79
+ token: z.string().optional(),
65
80
  visibility: ChatVisibilitySchema,
66
- shareUrl: z.string(),
81
+ /** Create-time only — never returned by list or update (show-once). */
82
+ shareUrl: z.string().optional(),
67
83
  messageCountAtShare: z.number().int(),
68
84
  titleAtShare: z.string(),
69
85
  createdAt: IsoDateString,
@@ -12,6 +12,7 @@
12
12
  import { z } from "zod";
13
13
  import { CursorPageSchema } from "../api/primitives";
14
14
  import { EffectiveAccessResponseSchema } from "../permissions/share-api";
15
+ import { ViewerPendingAccessRequestSchema } from "../permissions/access-request";
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Company.md Sub-schemas
@@ -112,6 +113,15 @@ export const CompanyMdDocResponseSchema = z.object({
112
113
  .optional(),
113
114
  /** Whether the requesting actor may request access (ADR-BE-348 / ADR-BE-338). */
114
115
  canRequestAccess: z.boolean().optional(),
116
+ /**
117
+ * The requesting actor's own OPEN request on this node, if any
118
+ * (ADR-CONTRACTS-106). `canRequestAccess` answers "may you ask?" and is
119
+ * derived from discoverability + read-state alone, so it stays true after you
120
+ * have asked — this field is what distinguishes "you may ask" from "you
121
+ * already did", and it is the only thing standing between the locked view and
122
+ * an actor re-submitting a request they already filed.
123
+ */
124
+ pendingAccessRequest: ViewerPendingAccessRequestSchema.nullable().optional(),
115
125
  visibility: z.enum(["private", "unit", "org"]),
116
126
  // Governance + discovery projections (ADR-BE-348 / ADR-CTRL-194). Present on
117
127
  // the TS contract since the projection split; now promoted into the published
@@ -11,6 +11,7 @@ exports[`NOTIFICATION_DEFINITIONS > titles every kind 1`] = `
11
11
  "chat.shared · No preview": "A chat has been shared with you",
12
12
  "chat.shared · Short": "A chat has been shared with you",
13
13
  "companyMd.access_request_approved · Editor": "Your access request was approved",
14
+ "companyMd.access_request_approved · Editor with message": "Your access request was approved",
14
15
  "companyMd.access_request_approved · Viewer": "Your access request was approved",
15
16
  "companyMd.access_request_denied · No reason": "Your access request was reviewed",
16
17
  "companyMd.access_request_denied · With reason": "Your access request was reviewed",
@@ -1603,6 +1603,185 @@ This notification was sent via Company Semantics.
1603
1603
  Access is now active.
1604
1604
 
1605
1605
 
1606
+ /* EOM */
1607
+ ⓒ 2026 • Company Semantics
1608
+ https://companysemantics.ai
1609
+ ",
1610
+ }
1611
+ `;
1612
+
1613
+ exports[`email render > companyMd.access_request_approved · Editor with message 1`] = `
1614
+ {
1615
+ "amp": "<!DOCTYPE html>
1616
+ <html amp4email data-css-strict lang="en">
1617
+ <head><meta charset="utf-8">
1618
+ <script async src="https://cdn.ampproject.org/v0.js"></script>
1619
+ <style amp4email-boilerplate>body{visibility:hidden}</style>
1620
+ <style amp-custom>.csr-body { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0; }
1621
+ .csr-frame { max-width: 520px; margin: 0 auto; }
1622
+ .csr-p-normal { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0; }
1623
+ .csr-p-tight { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0; }
1624
+ .csr-p-none { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0; }
1625
+ .csr-eom { color: #bbb; }
1626
+ .csr-signature-link { color: #0047FF; text-decoration: none; }
1627
+ .csr-warning { color: #e7000b; }
1628
+ .csr-hr-normal { border: none; border-top: 1px solid #bbb; margin: 0 0 20px 0; }
1629
+ .csr-hr-tight { border: none; border-top: 1px solid #bbb; margin: 0 0 4px 0; }
1630
+ .csr-hr-none { border: none; border-top: 1px solid #bbb; margin: 0; }
1631
+ .csr-hero-normal { max-width: 100%; margin: 0 0 20px 0; }
1632
+ .csr-hero-tight { max-width: 100%; margin: 0 0 4px 0; }
1633
+ .csr-hero-none { max-width: 100%; margin: 0; }
1634
+ .csr-cta-box-normal { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 20px 0; max-width: 220px; }
1635
+ .csr-cta-box-tight { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 4px 0; max-width: 220px; }
1636
+ .csr-cta-box-none { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px; }
1637
+ .csr-cta-cell-linked { text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
1638
+ .csr-cta-cell-code { padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
1639
+ .csr-cta-label { text-decoration: underline; }
1640
+ .csr-cta-anchor { display: block; padding: 16px 24px; color: #0047FF; text-decoration: none; }
1641
+ .csr-cta-code { color: #0047FF; }
1642
+ .csr-chat-rule-top { border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0; }
1643
+ .csr-chat-rule-bottom { border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0; }
1644
+ .csr-chat-row-24 { margin: 0 0 24px 0; }
1645
+ .csr-chat-row-16 { margin: 0 0 16px 0; }
1646
+ .csr-chat-attribution { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch; }
1647
+ .csr-chat-avatar-left { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; white-space: nowrap; }
1648
+ .csr-chat-avatar-left-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
1649
+ .csr-chat-avatar-right { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap; }
1650
+ .csr-chat-avatar-right-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
1651
+ .csr-chat-channel-left { width: 100%; text-align: left; }
1652
+ .csr-chat-channel-right { width: 100%; text-align: right; }
1653
+ .csr-chat-bubble-wrap { display: inline-block; max-width: 100%; vertical-align: bottom; }
1654
+ .csr-bubble-user { border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
1655
+ .csr-bubble-assistant { border-radius: 8px 8px 8px 0; padding: 10px 14px; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
1656
+ .csr-dots-over-cta { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; text-align: center; margin: 0 0 16px 0; }
1657
+ .csr-dots-cell { width: 100%; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; }
1658
+ .csr-cta-stack { display: inline-block; text-align: left; }
1659
+ .cs-cta-hover:hover { background-color: #d1ffff; }</style></head>
1660
+ <body class="csr-body">
1661
+ <div class="csr-frame">
1662
+ <p class="csr-p-normal">Hi there,</p>
1663
+ <p class="csr-p-normal">Your access request was approved.</p>
1664
+ <hr class="cs-faint csr-chat-rule-top">
1665
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-16">
1666
+ <tr>
1667
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
1668
+ <td class="csr-chat-channel-right"><table cellpadding="0" cellspacing="0" border="0" class="csr-chat-bubble-wrap">
1669
+ <tr><td class="cs-bubble csr-bubble-user">Welcome aboard — start with the onboarding section.</td></tr>
1670
+ </table></td>
1671
+ <td class="csr-chat-avatar-right">(•̀_ರ╮)</td>
1672
+ </tr>
1673
+ <tr>
1674
+ <td></td>
1675
+ <td class="cs-meta csr-chat-attribution">Jordan Lee</td>
1676
+ <td></td>
1677
+ </tr>
1678
+ </table>
1679
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-24">
1680
+ <tr>
1681
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
1682
+ <td class="csr-chat-channel-right"><div class="csr-cta-stack"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover csr-cta-box-none">
1683
+ <tr><td class="csr-cta-cell-linked">
1684
+ <a href="https://app.companysemantics.ai/doc/handbook" class="cs-link csr-cta-anchor">&gt;&gt; <span class="csr-cta-label">OPEN</span> &lt;&lt;</a>
1685
+ </td></tr>
1686
+ </table></div></td>
1687
+ <td class="csr-chat-avatar-right-hidden">(•̀_ರ╮)</td>
1688
+ </tr>
1689
+ </table>
1690
+ <hr class="cs-faint csr-chat-rule-bottom">
1691
+ <p class="csr-p-tight">Document: &quot;Engineering Handbook&quot;</p>
1692
+ <p class="csr-p-tight">Access: can edit</p>
1693
+ <p class="csr-p-normal">Approved by: Jordan Lee</p>
1694
+ <p class="csr-p-none">This notification was sent via Company Semantics.<br>Access is now active.</p>
1695
+ <p class="csr-p-none"><br><br><span class="cs-faint csr-eom">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link csr-signature-link">https://companysemantics.ai</a></p>
1696
+ </div>
1697
+ </body>
1698
+ </html>",
1699
+ "html": "<!DOCTYPE html>
1700
+ <html lang="en">
1701
+ <head><meta charset="UTF-8">
1702
+ <meta name="color-scheme" content="light dark">
1703
+ <meta name="supported-color-schemes" content="light dark">
1704
+ <style>.cs-cta-hover:hover { background-color: #d1ffff !important; }
1705
+ @media (prefers-color-scheme: dark) {
1706
+ body { background-color: #1a1a1a !important; color: #e8e8e8 !important; }
1707
+ .cs-faint { color: #666 !important; border-top-color: #666 !important; }
1708
+ .cs-meta { color: #8a8a8a !important; }
1709
+ .cs-cta { border-color: #00ffff !important; }
1710
+ .cs-cta-hover:hover { background-color: #154848 !important; }
1711
+ .cs-bubble { background: #154848 !important; color: #ffffff !important; }
1712
+ .cs-dots { color: #8a8a8a !important; }
1713
+ .cs-link { color: #00ffff !important; }
1714
+ .cs-destructive { color: #ff6b6b !important; }
1715
+ }</style>
1716
+ <!--[if mso]><style>.cs-cta td { padding: 16px 24px !important; }
1717
+ .cs-cta a { padding: 0 !important; }</style><![endif]--></head>
1718
+ <body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0;">
1719
+ <div style="max-width: 520px; margin: 0 auto;">
1720
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Hi there,</p>
1721
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Your access request was approved.</p>
1722
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0;">
1723
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 16px 0;">
1724
+ <tr>
1725
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
1726
+ <td style="width: 100%; text-align: right;"><table cellpadding="0" cellspacing="0" border="0" style="display: inline-block; max-width: 100%; vertical-align: bottom;">
1727
+ <tr><td class="cs-bubble" style="border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666;">Welcome aboard — start with the onboarding section.</td></tr>
1728
+ </table></td>
1729
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap;">(•̀_ರ╮)</td>
1730
+ </tr>
1731
+ <tr>
1732
+ <td></td>
1733
+ <td class="cs-meta" style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch;">Jordan Lee</td>
1734
+ <td></td>
1735
+ </tr>
1736
+ </table>
1737
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 24px 0;">
1738
+ <tr>
1739
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
1740
+ <td style="width: 100%; text-align: right;"><div style="display: inline-block; text-align: left;"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover" style="display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px;">
1741
+ <tr><td style="text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px;">
1742
+ <a href="https://app.companysemantics.ai/doc/handbook" class="cs-link" style="display: block; padding: 16px 24px; color: #0047FF; text-decoration: none;">&gt;&gt; <span style="text-decoration: underline;">OPEN</span> &lt;&lt;</a>
1743
+ </td></tr>
1744
+ </table></div></td>
1745
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">(•̀_ರ╮)</td>
1746
+ </tr>
1747
+ </table>
1748
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0;">
1749
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Document: &quot;Engineering Handbook&quot;</p>
1750
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Access: can edit</p>
1751
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Approved by: Jordan Lee</p>
1752
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;">This notification was sent via Company Semantics.<br>Access is now active.</p>
1753
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;"><br><br><span class="cs-faint" style="color: #bbb;">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link" style="color: #0047FF; text-decoration: none;">https://companysemantics.ai</a></p>
1754
+ </div>
1755
+ </body>
1756
+ </html>",
1757
+ "subject": "Your access request was approved",
1758
+ "text": "Hi there,
1759
+
1760
+ Your access request was approved.
1761
+
1762
+ ________________________________________________________
1763
+
1764
+ ┌──────────────────────────────────────┐
1765
+ │ Welcome aboard — start with the │
1766
+ │ onboarding section. │ (•̀_ರ╮)
1767
+ └──────────────────────────────────────┘
1768
+ Jordan Lee
1769
+
1770
+ *----------------*
1771
+ | >> OPEN << |
1772
+ *----------------*
1773
+
1774
+ https://app.companysemantics.ai/doc/handbook
1775
+ ________________________________________________________
1776
+
1777
+ Document: "Engineering Handbook"
1778
+ Access: can edit
1779
+ Approved by: Jordan Lee
1780
+
1781
+ This notification was sent via Company Semantics.
1782
+ Access is now active.
1783
+
1784
+
1606
1785
  /* EOM */
1607
1786
  ⓒ 2026 • Company Semantics
1608
1787
  https://companysemantics.ai
@@ -212,6 +212,15 @@ add("companyMd.access_request_approved", "Editor", {
212
212
  accessLevel: "editor",
213
213
  docUrl: `${APP}/doc/handbook`,
214
214
  });
215
+ // The approver's note replaces the "Open to view." prompt rather than stacking
216
+ // on it — the branch this variant exists to pin.
217
+ add("companyMd.access_request_approved", "Editor with message", {
218
+ approverName: "Jordan Lee",
219
+ docTitle: "Engineering Handbook",
220
+ accessLevel: "editor",
221
+ docUrl: `${APP}/doc/handbook`,
222
+ message: "Welcome aboard — start with the onboarding section.",
223
+ });
215
224
  add("companyMd.access_request_approved", "Viewer", {
216
225
  approverName: "Jordan Lee",
217
226
  docTitle: "Engineering Handbook",
@@ -212,6 +212,15 @@ add("companyMd.access_request_approved", "Editor", {
212
212
  accessLevel: "editor",
213
213
  docUrl: `${APP}/doc/handbook`,
214
214
  });
215
+ // The approver's note replaces the "Open to view." prompt rather than stacking
216
+ // on it — the branch this variant exists to pin, across every channel.
217
+ add("companyMd.access_request_approved", "Editor with message", {
218
+ approverName: "Jordan Lee",
219
+ docTitle: "Engineering Handbook",
220
+ accessLevel: "editor",
221
+ docUrl: `${APP}/doc/handbook`,
222
+ message: "Welcome aboard — start with the onboarding section.",
223
+ });
215
224
  add("companyMd.access_request_approved", "Viewer", {
216
225
  approverName: "Jordan Lee",
217
226
  docTitle: "Engineering Handbook",
@@ -1,5 +1,12 @@
1
1
  /**
2
2
  * Company.md access-approved notification (sent to the requester).
3
+ *
4
+ * The chat unit carries EITHER the approver's own words or the bare "Open to
5
+ * view." prompt — never both. An approver who wrote a note has already said what
6
+ * the prompt would say, and stacking the two reads as a form letter with a
7
+ * comment bolted on. Same shape as `share.granted` (ADR-CONTRACTS-106); the call
8
+ * to action stays in place either way, since approval always resolves to a doc
9
+ * the requester can now open.
3
10
  */
4
11
 
5
12
  import type { NotificationDefinition } from "../definition";
@@ -10,7 +17,7 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
10
17
  {
11
18
  kind: "companyMd.access_request_approved",
12
19
  compose: (payload, context) => {
13
- const { approverName, docTitle, accessLevel, docUrl } = payload;
20
+ const { approverName, docTitle, accessLevel, docUrl, message } = payload;
14
21
 
15
22
  return {
16
23
  metadata: {
@@ -25,11 +32,18 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
25
32
  {
26
33
  type: "chatUnit",
27
34
  items: [
28
- {
29
- type: "message",
30
- role: "assistant",
31
- text: "Open to view.",
32
- },
35
+ message
36
+ ? {
37
+ type: "message",
38
+ role: "user",
39
+ text: message,
40
+ from: approverName,
41
+ }
42
+ : {
43
+ type: "message",
44
+ role: "assistant",
45
+ text: "Open to view.",
46
+ },
33
47
  { type: "callToAction", label: "OPEN", href: docUrl },
34
48
  ],
35
49
  },
@@ -151,6 +151,12 @@ export interface NotificationPayloads {
151
151
  accessLevel: "editor" | "commenter" | "viewer";
152
152
  /** Full URL to view the node */
153
153
  docUrl: string;
154
+ /**
155
+ * Optional note from the approver, included in the notification only (not
156
+ * persisted) — the approval counterpart of the deny kind's `reason`
157
+ * (ADR-CONTRACTS-106).
158
+ */
159
+ message?: string;
154
160
  };
155
161
  /** Sent to the requester when an owner denies (ADR-BE-338). */
156
162
  "companyMd.access_request_denied": {
@@ -6,6 +6,8 @@
6
6
  * @see PRD-00173 for design rationale
7
7
  */
8
8
 
9
+ import type { ViewerPendingAccessRequest } from "../permissions/access-request";
10
+
9
11
  /**
10
12
  * Visibility band for a Company.md document — the canonical AUTH
11
13
  * `entity_visibility` vocabulary (ADR-CONT-049, backend ADR-BE-178).
@@ -218,6 +220,13 @@ export interface CompanyMdDocCore extends CompanyMdNodeIdentity {
218
220
  * recipient to an `owners` node may still be allowed to request).
219
221
  */
220
222
  readonly canRequestAccess?: boolean;
223
+ /**
224
+ * The requesting actor's own OPEN request on this node (ADR-CONTRACTS-106).
225
+ * `canRequestAccess` is derived from discoverability + read-state and stays
226
+ * true once you have asked, so this is the field that separates "you may ask"
227
+ * from "you already did". Absent/`null` ⇒ nothing open.
228
+ */
229
+ readonly pendingAccessRequest?: ViewerPendingAccessRequest | null;
221
230
  /**
222
231
  * Whether this doc is served by the real-time collaboration protocol
223
232
  * (ADR-CONT-102). When `true` the client may open `/collab/sync` + the collab
@@ -93,18 +93,89 @@ export const AccessRequestSchema = z.object({
93
93
  });
94
94
  export type AccessRequest = z.infer<typeof AccessRequestSchema>;
95
95
 
96
+ /**
97
+ * The requesting actor's OWN open request on a node, surfaced on the single-doc
98
+ * read so a locked view can state that they already asked instead of offering
99
+ * the ask a second time (ADR-CONTRACTS-106).
100
+ *
101
+ * A deliberately NARROWER projection than the owner inbox's
102
+ * {@link AccessRequestSchema}: a requester is not authorized to learn who else
103
+ * asked or how anything was resolved, and needs only what they asked for and
104
+ * when. Absent/`null` ⇒ no open request, which is what an actor who never asked,
105
+ * and an actor whose request was already resolved, both see.
106
+ *
107
+ * Single-doc read ONLY — deliberately not on the tree node schema, where a
108
+ * per-node pending lookup would be an N+1 across the whole tree.
109
+ */
110
+ export const ViewerPendingAccessRequestSchema = z.object({
111
+ id: z.string(),
112
+ /**
113
+ * The band they asked for. Null for requests filed before the field existed
114
+ * or by a client that named none.
115
+ */
116
+ requestedAccessLevel: GrantableAccessLevelSchema.nullable(),
117
+ createdAt: z.string(),
118
+ });
119
+ export type ViewerPendingAccessRequest = z.infer<
120
+ typeof ViewerPendingAccessRequestSchema
121
+ >;
122
+
123
+ /**
124
+ * Character cap on the approver's note to the requester.
125
+ *
126
+ * Deliberately the 2000 the deny `reason` already used, not the requester's 280:
127
+ * the requester writes a one-line ask into an owner's inbox row, the approver
128
+ * writes a reply that is read on its own in an email body.
129
+ */
130
+ export const ACCESS_DECISION_MESSAGE_MAX_LENGTH = 2000;
131
+
132
+ /**
133
+ * Whether the requester is told about the decision.
134
+ *
135
+ * ABSENT MEANS SEND (ADR-CONTRACTS-106). Approve/deny have always emailed the
136
+ * requester unconditionally, so a client that omits the field — an older app, a
137
+ * script — must keep getting that behavior. Only an explicit `false` suppresses
138
+ * the notification; a default of `false` here would silence every existing
139
+ * caller.
140
+ */
141
+ const NotifySchema = z.boolean().optional();
142
+
96
143
  /**
97
144
  * POST body to approve a request. The owner chooses the granted level (default
98
145
  * `viewer` is applied server-side if the field is omitted by older clients).
99
146
  */
100
147
  export const ApproveAccessRequestSchema = z.object({
101
148
  accessLevel: GrantableAccessLevelSchema,
149
+ /** Whether to email the requester. Absent ⇒ send (see {@link NotifySchema}). */
150
+ notify: NotifySchema,
151
+ /**
152
+ * Optional note from the approver, quoted to the requester in the approval
153
+ * notification only — never persisted on the request or the minted grant.
154
+ * Ignored when `notify` is false: there is no other surface that would show
155
+ * it. Mirrors `share.granted`'s granter message.
156
+ */
157
+ message: z
158
+ .string()
159
+ .max(ACCESS_DECISION_MESSAGE_MAX_LENGTH)
160
+ .regex(PLAIN_TEXT, "Message must be plain text")
161
+ .optional(),
102
162
  });
103
163
  export type ApproveAccessRequest = z.infer<typeof ApproveAccessRequestSchema>;
104
164
 
105
165
  /** POST body to deny a request. */
106
166
  export const DenyAccessRequestSchema = z.object({
107
- /** Optional reason, surfaced to the requester (email only). */
108
- reason: z.string().max(2000).optional(),
167
+ /**
168
+ * Optional reason, surfaced to the requester (email only). This is the deny
169
+ * verb's message carrier — the approver's composer writes here rather than to
170
+ * a second `message` field, because the denied notification already quotes
171
+ * `reason` and a doc's denial has no other content.
172
+ */
173
+ reason: z
174
+ .string()
175
+ .max(ACCESS_DECISION_MESSAGE_MAX_LENGTH)
176
+ .regex(PLAIN_TEXT, "Reason must be plain text")
177
+ .optional(),
178
+ /** Whether to email the requester. Absent ⇒ send (see {@link NotifySchema}). */
179
+ notify: NotifySchema,
109
180
  });
110
181
  export type DenyAccessRequest = z.infer<typeof DenyAccessRequestSchema>;
@@ -64,8 +64,12 @@ export const AUTH_TOKEN_POLICY_REQUIREMENTS = {
64
64
  conformance: {
65
65
  status: "unmet",
66
66
  gap:
67
- "Minted with randomUUID() (122 bits) and persisted verbatim: sessions.token " +
68
- "holds the cookie value itself, and lookup is raw equality against it.",
67
+ "Issuance now conforms: minted by issueCapabilityToken at 256 bits and " +
68
+ "stored as a sha256:v1: digest (ADR-BE-460 deploy B). Still UNMET because " +
69
+ "the dual-read window is open — sessions.token retains the raw value so a " +
70
+ "rollback keeps working, and every lookup carries a plaintext fallback arm. " +
71
+ "Conformance is reached when migration C retires the column and deploy D " +
72
+ "removes the fallbacks.",
69
73
  },
70
74
  },
71
75
  ChatShareToken: {