@company-semantics/contracts 9.1.0 → 9.2.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 (141) hide show
  1. package/package.json +4 -1
  2. package/src/__tests__/resource-keys.test.ts +30 -23
  3. package/src/admin/authz-simulate.ts +4 -4
  4. package/src/admin/direct-grants.ts +2 -2
  5. package/src/api/generated-spec-hash.ts +2 -2
  6. package/src/api/generated.ts +97 -0
  7. package/src/api/http/routes/ai-chat.ts +3 -3
  8. package/src/api/http/utils/resource-response.ts +5 -2
  9. package/src/api/index.ts +4 -4
  10. package/src/api/primitives.ts +6 -2
  11. package/src/auth/README.md +1 -0
  12. package/src/auth/index.ts +12 -5
  13. package/src/autotune.ts +5 -1
  14. package/src/billing/index.ts +1 -1
  15. package/src/billing/types.ts +1 -1
  16. package/src/chat/README.md +3 -0
  17. package/src/chat/__tests__/runtime-profile.test.ts +68 -48
  18. package/src/chat/index.ts +10 -4
  19. package/src/chat/runtime-profile.ts +25 -10
  20. package/src/chat/schemas.ts +49 -41
  21. package/src/chat/types.ts +48 -42
  22. package/src/ci-envelope/README.md +2 -0
  23. package/src/ci-envelope/__tests__/transitions.test.ts +56 -56
  24. package/src/ci-envelope/index.ts +2 -2
  25. package/src/ci-envelope/types.ts +20 -20
  26. package/src/ci-results/index.ts +2 -2
  27. package/src/ci-results/repo-ci-result.ts +15 -12
  28. package/src/compatibility.ts +6 -6
  29. package/src/content/index.ts +10 -4
  30. package/src/content/schemas.ts +42 -24
  31. package/src/dispatch/index.ts +18 -15
  32. package/src/email/__tests__/registry.test.ts +81 -77
  33. package/src/email/index.ts +3 -3
  34. package/src/email/registry.ts +25 -25
  35. package/src/email/types.ts +43 -43
  36. package/src/errors/index.ts +8 -8
  37. package/src/execution/__tests__/events.test.ts +42 -42
  38. package/src/execution/__tests__/lifecycle.test.ts +192 -190
  39. package/src/execution/__tests__/registry.test.ts +114 -114
  40. package/src/execution/audit-export.ts +4 -4
  41. package/src/execution/errors.ts +7 -7
  42. package/src/execution/event-metadata.ts +4 -4
  43. package/src/execution/events.ts +23 -21
  44. package/src/execution/expiry.ts +5 -5
  45. package/src/execution/hash-chain.ts +2 -2
  46. package/src/execution/index.ts +19 -28
  47. package/src/execution/kinds.ts +7 -7
  48. package/src/execution/lifecycle.ts +33 -33
  49. package/src/execution/registry.ts +63 -63
  50. package/src/execution/schemas.ts +31 -23
  51. package/src/execution/status.ts +45 -26
  52. package/src/execution/summary.ts +16 -17
  53. package/src/execution/timeline-ui.ts +9 -9
  54. package/src/execution/types.ts +31 -25
  55. package/src/generated/openapi-routes.ts +1 -0
  56. package/src/guards/config.ts +22 -18
  57. package/src/guards/index.ts +4 -4
  58. package/src/guards/types.ts +32 -24
  59. package/src/identity/__tests__/avatar.test.ts +68 -59
  60. package/src/identity/avatar.ts +8 -8
  61. package/src/identity/display-name.ts +3 -3
  62. package/src/identity/index.ts +8 -8
  63. package/src/identity/people-org-chart.ts +8 -4
  64. package/src/identity/schemas.ts +28 -18
  65. package/src/identity/types.ts +5 -5
  66. package/src/impersonation/index.ts +5 -5
  67. package/src/impersonation/schemas.ts +15 -9
  68. package/src/impersonation-events.ts +21 -21
  69. package/src/impersonation.ts +25 -24
  70. package/src/index.ts +118 -90
  71. package/src/interfaces/mcp/tools/help.ts +19 -19
  72. package/src/internal-admin.ts +6 -6
  73. package/src/mcp/README.md +2 -0
  74. package/src/mcp/__tests__/capability-graph.test.ts +290 -290
  75. package/src/mcp/capability-graph.ts +42 -40
  76. package/src/mcp/failure-context.ts +1 -3
  77. package/src/mcp/index.ts +57 -57
  78. package/src/mcp/resources.ts +9 -9
  79. package/src/meetings/index.ts +2 -2
  80. package/src/meetings/schemas.ts +51 -34
  81. package/src/message-parts/README.md +2 -0
  82. package/src/message-parts/__tests__/builder.test.ts +142 -142
  83. package/src/message-parts/__tests__/confirmation.test.ts +100 -86
  84. package/src/message-parts/__tests__/preview.test.ts +63 -63
  85. package/src/message-parts/__tests__/wire.test.ts +130 -124
  86. package/src/message-parts/builder.ts +23 -23
  87. package/src/message-parts/confirmation.ts +17 -14
  88. package/src/message-parts/execution.ts +7 -7
  89. package/src/message-parts/index.ts +10 -10
  90. package/src/message-parts/lifecycle.ts +25 -25
  91. package/src/message-parts/preview.ts +30 -30
  92. package/src/message-parts/types.ts +27 -27
  93. package/src/message-parts/wire.ts +24 -24
  94. package/src/mutations.ts +2 -2
  95. package/src/observability.ts +23 -11
  96. package/src/org/__tests__/org-units.test.ts +131 -96
  97. package/src/org/__tests__/tree-ordering.test.ts +57 -37
  98. package/src/org/__tests__/view-scopes.test.ts +40 -40
  99. package/src/org/domain.ts +9 -9
  100. package/src/org/index.ts +24 -21
  101. package/src/org/org-units.ts +34 -20
  102. package/src/org/schemas.ts +201 -127
  103. package/src/org/sharing.ts +17 -13
  104. package/src/org/tree-ordering.ts +3 -1
  105. package/src/org/types.ts +54 -47
  106. package/src/org/view-scopes.ts +9 -9
  107. package/src/permissions/access-levels.ts +7 -2
  108. package/src/permissions/access-source.ts +6 -6
  109. package/src/permissions/index.ts +5 -5
  110. package/src/permissions/orgchart-roles.ts +7 -7
  111. package/src/permissions/permission-introspection.ts +7 -5
  112. package/src/permissions/share-api.ts +19 -9
  113. package/src/pressure.ts +4 -4
  114. package/src/queryIntent.ts +21 -21
  115. package/src/ralph/__tests__/prd-groups.test.ts +159 -159
  116. package/src/ralph/__tests__/prd.test.ts +30 -30
  117. package/src/ralph/index.ts +3 -8
  118. package/src/ralph/prd.ts +33 -33
  119. package/src/ralph/progress.ts +1 -1
  120. package/src/rate-limit/README.md +4 -4
  121. package/src/rate-limit/index.ts +3 -3
  122. package/src/requests.ts +36 -8
  123. package/src/resource-keys.ts +207 -124
  124. package/src/resource-registry.ts +5 -5
  125. package/src/route-builder.ts +3 -3
  126. package/src/safe-mode.ts +2 -2
  127. package/src/security/index.ts +4 -4
  128. package/src/security/org-secrets.ts +13 -9
  129. package/src/security/secret.ts +3 -3
  130. package/src/sse.ts +3 -1
  131. package/src/system/README.md +3 -0
  132. package/src/system/capabilities.ts +22 -23
  133. package/src/system/diagram.ts +45 -45
  134. package/src/system/index.ts +14 -14
  135. package/src/tiers.ts +1 -1
  136. package/src/timeouts.ts +1 -1
  137. package/src/tracing.ts +30 -30
  138. package/src/types/analytics.ts +2 -2
  139. package/src/usage/README.md +3 -0
  140. package/src/usage/execution-types.ts +69 -69
  141. package/src/usage/types.ts +7 -3
@@ -13,7 +13,7 @@
13
13
  * @see ADR-CONT-029 for design rationale
14
14
  */
15
15
 
16
- import type { ISO8601Timestamp } from './types'
16
+ import type { ISO8601Timestamp } from "./types";
17
17
 
18
18
  // =============================================================================
19
19
  // Timeline Status
@@ -23,7 +23,7 @@ import type { ISO8601Timestamp } from './types'
23
23
  * Status for timeline display.
24
24
  * Maps from ExecutionStatus for UI-specific presentation.
25
25
  */
26
- export type TimelineStatus = 'success' | 'failed' | 'pending'
26
+ export type TimelineStatus = "success" | "failed" | "pending";
27
27
 
28
28
  // =============================================================================
29
29
  // Timeline Icon
@@ -33,7 +33,7 @@ export type TimelineStatus = 'success' | 'failed' | 'pending'
33
33
  * Icon for timeline items.
34
34
  * Must match IconName but constrained to timeline-relevant icons.
35
35
  */
36
- export type TimelineIcon = 'plug' | 'unlink'
36
+ export type TimelineIcon = "plug" | "unlink";
37
37
 
38
38
  // =============================================================================
39
39
  // Timeline UI Event
@@ -52,26 +52,26 @@ export type TimelineIcon = 'plug' | 'unlink'
52
52
  */
53
53
  export interface TimelineUIEvent {
54
54
  /** Execution ID for correlation and detail fetching */
55
- executionId: string
55
+ executionId: string;
56
56
 
57
57
  /**
58
58
  * Display label (past tense).
59
59
  * Derived from EXECUTION_KINDS[kind].display.pastTenseLabel
60
60
  */
61
- label: string
61
+ label: string;
62
62
 
63
63
  /**
64
64
  * Icon identifier.
65
65
  * Derived from EXECUTION_KINDS[kind].display.icon
66
66
  */
67
- icon: TimelineIcon
67
+ icon: TimelineIcon;
68
68
 
69
69
  /** Visual status indicator */
70
- status: TimelineStatus
70
+ status: TimelineStatus;
71
71
 
72
72
  /** When this event occurred */
73
- timestamp: ISO8601Timestamp
73
+ timestamp: ISO8601Timestamp;
74
74
 
75
75
  /** Whether this event has expandable details */
76
- expandable: boolean
76
+ expandable: boolean;
77
77
  }
@@ -6,8 +6,8 @@
6
6
  * @see ADR-CONT-029 for design rationale
7
7
  */
8
8
 
9
- import type { ExecutionKind } from './kinds'
10
- import type { ConfirmationRiskLevel } from '../message-parts/confirmation'
9
+ import type { ExecutionKind } from "./kinds";
10
+ import type { ConfirmationRiskLevel } from "../message-parts/confirmation";
11
11
 
12
12
  // =============================================================================
13
13
  // Utility Types
@@ -17,13 +17,13 @@ import type { ConfirmationRiskLevel } from '../message-parts/confirmation'
17
17
  * ISO 8601 timestamp string.
18
18
  * Example: "2026-01-08T14:30:00.000Z"
19
19
  */
20
- export type ISO8601Timestamp = string
20
+ export type ISO8601Timestamp = string;
21
21
 
22
22
  /**
23
23
  * Icon names for execution kinds.
24
24
  * Must be supported by the frontend icon system.
25
25
  */
26
- export type IconName = 'plug' | 'unlink' | 'pencil' | 'send'
26
+ export type IconName = "plug" | "unlink" | "pencil" | "send";
27
27
 
28
28
  // =============================================================================
29
29
  // Execution Domain
@@ -32,7 +32,13 @@ export type IconName = 'plug' | 'unlink' | 'pencil' | 'send'
32
32
  /**
33
33
  * Domain categories for execution kinds.
34
34
  */
35
- export type ExecutionDomain = 'integration' | 'policy' | 'data' | 'system' | 'profile' | 'communication'
35
+ export type ExecutionDomain =
36
+ | "integration"
37
+ | "policy"
38
+ | "data"
39
+ | "system"
40
+ | "profile"
41
+ | "communication";
36
42
 
37
43
  // =============================================================================
38
44
  // Execution Kind Definition
@@ -50,46 +56,46 @@ export type ExecutionDomain = 'integration' | 'policy' | 'data' | 'system' | 'pr
50
56
  */
51
57
  export interface ExecutionKindDefinition {
52
58
  /** The execution kind this definition describes */
53
- kind: ExecutionKind
59
+ kind: ExecutionKind;
54
60
 
55
61
  /** Domain category for grouping and filtering */
56
- domain: ExecutionDomain
62
+ domain: ExecutionDomain;
57
63
 
58
64
  /** Display metadata for UI rendering */
59
65
  display: {
60
66
  /** Label shown on buttons/actions (imperative: "Connect Slack") */
61
- label: string
67
+ label: string;
62
68
  /** Label for completed actions (past tense: "Slack connected") */
63
- pastTenseLabel: string
69
+ pastTenseLabel: string;
64
70
  /** Icon identifier for UI */
65
- icon: IconName
66
- }
71
+ icon: IconName;
72
+ };
67
73
 
68
74
  /** Governance constraints */
69
75
  governance: {
70
76
  /** Who can see this execution in lists/timelines */
71
- visibility: 'admin' | 'user'
77
+ visibility: "admin" | "user";
72
78
  /** Whether admin role is required to initiate */
73
- requiresAdmin: boolean
79
+ requiresAdmin: boolean;
74
80
  /** If reversible, which kind reverses this action */
75
- reversibleBy?: ExecutionKind
76
- }
81
+ reversibleBy?: ExecutionKind;
82
+ };
77
83
 
78
84
  /** UI behavior hints */
79
85
  ui: {
80
86
  /** Show in admin settings panel */
81
- showInAdmin: boolean
87
+ showInAdmin: boolean;
82
88
  /** Show in user-facing timeline */
83
- showInTimeline: boolean
89
+ showInTimeline: boolean;
84
90
  /** Require confirmation dialog before execution */
85
- confirmBeforeRun?: boolean
86
- }
91
+ confirmBeforeRun?: boolean;
92
+ };
87
93
 
88
94
  /** Explanation generation metadata */
89
95
  explanation: {
90
96
  /** Template ID for explanation builder */
91
- templateId: string
92
- }
97
+ templateId: string;
98
+ };
93
99
  }
94
100
 
95
101
  // =============================================================================
@@ -116,8 +122,8 @@ export interface ExecutionKindDefinition {
116
122
  * from the execution domain barrel only, NOT from root index.ts.
117
123
  */
118
124
  export interface ExecutionIntent {
119
- executionKind: ExecutionKind
120
- requiresConfirmation: boolean
121
- requiresApproval: boolean
122
- risk: ConfirmationRiskLevel
125
+ executionKind: ExecutionKind;
126
+ requiresConfirmation: boolean;
127
+ requiresApproval: boolean;
128
+ risk: ConfirmationRiskLevel;
123
129
  }
@@ -48,6 +48,7 @@ export const openApiRoutes = {
48
48
  '/api/executions/{executionId}/timeline': ['GET'],
49
49
  '/api/executions/{executionId}/undo': ['POST'],
50
50
  '/api/factory/floor': ['GET'],
51
+ '/api/factory/snapshot': ['GET'],
51
52
  '/api/internal-admin/impersonate/end': ['POST'],
52
53
  '/api/internal-admin/impersonate/session': ['GET'],
53
54
  '/api/internal-admin/impersonate/start': ['POST'],
@@ -8,7 +8,7 @@
8
8
  * Each repo provides its own config values; shared guards consume them.
9
9
  */
10
10
 
11
- import type { CheckResult, Soc2ControlArea } from './types';
11
+ import type { CheckResult, Soc2ControlArea } from "./types";
12
12
 
13
13
  // =============================================================================
14
14
  // Size Limits
@@ -101,7 +101,7 @@ export interface GuardConfig {
101
101
  */
102
102
  export type GuardCheck = (
103
103
  repoRoot: string,
104
- config: GuardConfig
104
+ config: GuardConfig,
105
105
  ) => Promise<CheckResult>;
106
106
 
107
107
  /**
@@ -109,7 +109,7 @@ export type GuardCheck = (
109
109
  * Factories create check functions with optional overrides.
110
110
  */
111
111
  export type GuardCheckFactory = (
112
- overrides?: Partial<GuardConfig>
112
+ overrides?: Partial<GuardConfig>,
113
113
  ) => GuardCheck;
114
114
 
115
115
  // =============================================================================
@@ -138,7 +138,11 @@ export interface ContractsFreshnessBaseline {
138
138
  */
139
139
  compatibility: {
140
140
  minSupported: string;
141
- deprecations: readonly { range: string; reason: string; removeIn: string }[];
141
+ deprecations: readonly {
142
+ range: string;
143
+ reason: string;
144
+ removeIn: string;
145
+ }[];
142
146
  };
143
147
 
144
148
  /**
@@ -563,30 +567,30 @@ export interface GuardCheckRegistry {
563
567
  * Default directories to skip when walking file trees.
564
568
  */
565
569
  export const DEFAULT_SKIP_DIRECTORIES = [
566
- 'node_modules',
567
- 'dist',
568
- '.git',
569
- '.next',
570
- 'coverage',
571
- '.tsbuild',
570
+ "node_modules",
571
+ "dist",
572
+ ".git",
573
+ ".next",
574
+ "coverage",
575
+ ".tsbuild",
572
576
  ];
573
577
 
574
578
  /**
575
579
  * Default README sections for domain directories.
576
580
  */
577
581
  export const DEFAULT_DOMAIN_SECTIONS = [
578
- 'Purpose',
579
- 'Invariants',
580
- 'Public API',
581
- 'Dependencies',
582
+ "Purpose",
583
+ "Invariants",
584
+ "Public API",
585
+ "Dependencies",
582
586
  ];
583
587
 
584
588
  /**
585
589
  * Default README sections for infrastructure directories.
586
590
  */
587
591
  export const DEFAULT_INFRA_SECTIONS = [
588
- 'Purpose',
589
- 'How it works',
590
- 'Integration',
591
- 'Maintenance',
592
+ "Purpose",
593
+ "How it works",
594
+ "Integration",
595
+ "Maintenance",
592
596
  ];
@@ -23,7 +23,7 @@ export type {
23
23
  Soc2ControlStatus,
24
24
  Soc2ControlResult,
25
25
  Soc2ComplianceOutput,
26
- } from './types';
26
+ } from "./types";
27
27
 
28
28
  // Constants (these are type-level, not runtime)
29
29
  export {
@@ -34,7 +34,7 @@ export {
34
34
  REQUIRED_SOC2_CONTROLS,
35
35
  BLOCKING_SOC2_CONTROLS,
36
36
  SOC2_SCHEMA_VERSION,
37
- } from './types';
37
+ } from "./types";
38
38
 
39
39
  // Config types
40
40
  export type {
@@ -63,11 +63,11 @@ export type {
63
63
  AlertsConfigGuardConfig,
64
64
  BackupConfigGuardConfig,
65
65
  Soc2Baselines,
66
- } from './config';
66
+ } from "./config";
67
67
 
68
68
  // Config constants (type-level defaults)
69
69
  export {
70
70
  DEFAULT_SKIP_DIRECTORIES,
71
71
  DEFAULT_DOMAIN_SECTIONS,
72
72
  DEFAULT_INFRA_SECTIONS,
73
- } from './config';
73
+ } from "./config";
@@ -58,7 +58,12 @@ export interface GuardTierResult {
58
58
  * - evolution: uncovered patterns, growth tracking (observational)
59
59
  * - meta: guards that protect the guard system itself (coverage correctness)
60
60
  */
61
- export type GuardTier = 'structural' | 'behavioral' | 'invariants' | 'evolution' | 'meta';
61
+ export type GuardTier =
62
+ | "structural"
63
+ | "behavioral"
64
+ | "invariants"
65
+ | "evolution"
66
+ | "meta";
62
67
 
63
68
  /**
64
69
  * Full guard results, organized by tier.
@@ -114,18 +119,18 @@ export interface GuardOutput {
114
119
  * Current schema version.
115
120
  * v2.0.0: Tiered results (breaking change from v1.x flattened structure)
116
121
  */
117
- export const GUARD_SCHEMA_VERSION = '2.0.0';
122
+ export const GUARD_SCHEMA_VERSION = "2.0.0";
118
123
 
119
124
  /**
120
125
  * All guard tiers in order.
121
126
  * Useful for iteration when aggregating results.
122
127
  */
123
128
  export const GUARD_TIERS: readonly GuardTier[] = [
124
- 'structural',
125
- 'behavioral',
126
- 'invariants',
127
- 'evolution',
128
- 'meta',
129
+ "structural",
130
+ "behavioral",
131
+ "invariants",
132
+ "evolution",
133
+ "meta",
129
134
  ] as const;
130
135
 
131
136
  // =============================================================================
@@ -172,7 +177,7 @@ export interface VulnerabilityConfig {
172
177
  * - BR: Backup & Recovery
173
178
  * - AI: Audit Integrity
174
179
  */
175
- export type Soc2ControlArea = 'CM' | 'AC' | 'LM' | 'SD' | 'BR' | 'AI';
180
+ export type Soc2ControlArea = "CM" | "AC" | "LM" | "SD" | "BR" | "AI";
176
181
 
177
182
  /**
178
183
  * Control status semantics:
@@ -186,18 +191,18 @@ export type Soc2ControlArea = 'CM' | 'AC' | 'LM' | 'SD' | 'BR' | 'AI';
186
191
  * - Is never blocking
187
192
  * - Must be explicit (configured in soc2Baselines), not implicit
188
193
  */
189
- export type Soc2ControlStatus = 'PASS' | 'WARN' | 'FAIL' | 'SKIP';
194
+ export type Soc2ControlStatus = "PASS" | "WARN" | "FAIL" | "SKIP";
190
195
 
191
196
  /**
192
197
  * Human-readable names for SOC 2 control areas.
193
198
  */
194
199
  export const SOC2_CONTROL_NAMES: Record<Soc2ControlArea, string> = {
195
- CM: 'Change Management',
196
- AC: 'Access Control',
197
- LM: 'Logging & Monitoring',
198
- SD: 'Secure SDLC',
199
- BR: 'Backup & Recovery',
200
- AI: 'Audit Integrity',
200
+ CM: "Change Management",
201
+ AC: "Access Control",
202
+ LM: "Logging & Monitoring",
203
+ SD: "Secure SDLC",
204
+ BR: "Backup & Recovery",
205
+ AI: "Audit Integrity",
201
206
  } as const;
202
207
 
203
208
  /**
@@ -205,19 +210,22 @@ export const SOC2_CONTROL_NAMES: Record<Soc2ControlArea, string> = {
205
210
  * Aggregator fails if any control is missing from the output.
206
211
  */
207
212
  export const REQUIRED_SOC2_CONTROLS: readonly Soc2ControlArea[] = [
208
- 'CM',
209
- 'AC',
210
- 'LM',
211
- 'SD',
212
- 'BR',
213
- 'AI',
213
+ "CM",
214
+ "AC",
215
+ "LM",
216
+ "SD",
217
+ "BR",
218
+ "AI",
214
219
  ] as const;
215
220
 
216
221
  /**
217
222
  * Controls that block CI when status is FAIL.
218
223
  * Non-blocking controls report FAIL but don't set exit code to 1.
219
224
  */
220
- export const BLOCKING_SOC2_CONTROLS: readonly Soc2ControlArea[] = ['CM', 'AC'] as const;
225
+ export const BLOCKING_SOC2_CONTROLS: readonly Soc2ControlArea[] = [
226
+ "CM",
227
+ "AC",
228
+ ] as const;
221
229
 
222
230
  /**
223
231
  * A single SOC 2 control assertion result.
@@ -264,11 +272,11 @@ export interface Soc2ComplianceOutput {
264
272
  /** All control results (must include all REQUIRED_SOC2_CONTROLS) */
265
273
  controls: Soc2ControlResult[];
266
274
  /** Overall pass/fail based on blocking controls only */
267
- overallStatus: 'PASS' | 'FAIL';
275
+ overallStatus: "PASS" | "FAIL";
268
276
  }
269
277
 
270
278
  /**
271
279
  * Current SOC 2 compliance schema version.
272
280
  * Bump on breaking schema changes.
273
281
  */
274
- export const SOC2_SCHEMA_VERSION = '1.0.0';
282
+ export const SOC2_SCHEMA_VERSION = "1.0.0";
@@ -1,75 +1,84 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { generateInitials, resolveAvatar } from '../avatar.js'
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateInitials, resolveAvatar } from "../avatar.js";
3
3
 
4
- describe('generateInitials', () => {
5
- it('returns single uppercase initial for single-word name', () => {
6
- expect(generateInitials('Madonna')).toBe('M')
7
- })
4
+ describe("generateInitials", () => {
5
+ it("returns single uppercase initial for single-word name", () => {
6
+ expect(generateInitials("Madonna")).toBe("M");
7
+ });
8
8
 
9
- it('returns first+last uppercase initials for two-word name', () => {
10
- expect(generateInitials('Ian Heidt')).toBe('IH')
11
- })
9
+ it("returns first+last uppercase initials for two-word name", () => {
10
+ expect(generateInitials("Ian Heidt")).toBe("IH");
11
+ });
12
12
 
13
- it('returns first+last initials, skipping middle for multi-word name', () => {
14
- expect(generateInitials('Mary Jane Doe')).toBe('MD')
15
- })
13
+ it("returns first+last initials, skipping middle for multi-word name", () => {
14
+ expect(generateInitials("Mary Jane Doe")).toBe("MD");
15
+ });
16
16
 
17
- it('returns empty string for empty input', () => {
18
- expect(generateInitials('')).toBe('')
19
- })
17
+ it("returns empty string for empty input", () => {
18
+ expect(generateInitials("")).toBe("");
19
+ });
20
20
 
21
- it('returns empty string for whitespace-only input', () => {
22
- expect(generateInitials(' ')).toBe('')
23
- })
21
+ it("returns empty string for whitespace-only input", () => {
22
+ expect(generateInitials(" ")).toBe("");
23
+ });
24
24
 
25
- it('handles name with extra whitespace', () => {
26
- expect(generateInitials(' Ian Heidt ')).toBe('IH')
27
- })
25
+ it("handles name with extra whitespace", () => {
26
+ expect(generateInitials(" Ian Heidt ")).toBe("IH");
27
+ });
28
28
 
29
- it('returns single character for single character name', () => {
30
- expect(generateInitials('X')).toBe('X')
31
- })
29
+ it("returns single character for single character name", () => {
30
+ expect(generateInitials("X")).toBe("X");
31
+ });
32
32
 
33
- it('returns uppercase for lowercase input', () => {
34
- expect(generateInitials('ian heidt')).toBe('IH')
35
- })
36
- })
33
+ it("returns uppercase for lowercase input", () => {
34
+ expect(generateInitials("ian heidt")).toBe("IH");
35
+ });
36
+ });
37
37
 
38
- describe('resolveAvatar', () => {
39
- it('initials is always populated when source is photo (critical invariant)', () => {
40
- const result = resolveAvatar({ avatarUrl: 'https://example.com/img.jpg', fullName: 'Ian Heidt' })
41
- expect(result.source).toBe('photo')
42
- expect(result.url).toBe('https://example.com/img.jpg')
38
+ describe("resolveAvatar", () => {
39
+ it("initials is always populated when source is photo (critical invariant)", () => {
40
+ const result = resolveAvatar({
41
+ avatarUrl: "https://example.com/img.jpg",
42
+ fullName: "Ian Heidt",
43
+ });
44
+ expect(result.source).toBe("photo");
45
+ expect(result.url).toBe("https://example.com/img.jpg");
43
46
  // INVARIANT: initials is ALWAYS populated, regardless of source.
44
47
  // This prevents UI regressions when the photo fails to load.
45
- expect(typeof result.initials).toBe('string')
46
- expect(result.initials).toBe('IH')
47
- })
48
+ expect(typeof result.initials).toBe("string");
49
+ expect(result.initials).toBe("IH");
50
+ });
48
51
 
49
- it('returns initials source when no avatarUrl', () => {
50
- const result = resolveAvatar({ fullName: 'Ian Heidt' } as any)
51
- expect(result.source).toBe('initials')
52
- expect(result.initials).toBe('IH')
53
- expect(result.url).toBeUndefined()
54
- })
52
+ it("returns initials source when no avatarUrl", () => {
53
+ const result = resolveAvatar({ fullName: "Ian Heidt" } as any);
54
+ expect(result.source).toBe("initials");
55
+ expect(result.initials).toBe("IH");
56
+ expect(result.url).toBeUndefined();
57
+ });
55
58
 
56
- it('returns photo source with empty initials when fullName is empty', () => {
57
- const result = resolveAvatar({ avatarUrl: 'https://example.com/img.jpg', fullName: '' })
58
- expect(result.source).toBe('photo')
59
- expect(result.url).toBe('https://example.com/img.jpg')
59
+ it("returns photo source with empty initials when fullName is empty", () => {
60
+ const result = resolveAvatar({
61
+ avatarUrl: "https://example.com/img.jpg",
62
+ fullName: "",
63
+ });
64
+ expect(result.source).toBe("photo");
65
+ expect(result.url).toBe("https://example.com/img.jpg");
60
66
  // Empty is valid — just not undefined
61
- expect(result.initials).toBe('')
62
- })
67
+ expect(result.initials).toBe("");
68
+ });
63
69
 
64
- it('returns initials source when avatarUrl is explicitly undefined', () => {
65
- const result = resolveAvatar({ avatarUrl: undefined, fullName: 'Ian Heidt' })
66
- expect(result.source).toBe('initials')
67
- expect(result.initials).toBe('IH')
68
- })
70
+ it("returns initials source when avatarUrl is explicitly undefined", () => {
71
+ const result = resolveAvatar({
72
+ avatarUrl: undefined,
73
+ fullName: "Ian Heidt",
74
+ });
75
+ expect(result.source).toBe("initials");
76
+ expect(result.initials).toBe("IH");
77
+ });
69
78
 
70
- it('returns initials source when avatarUrl is empty string (falsy)', () => {
71
- const result = resolveAvatar({ avatarUrl: '', fullName: 'Ian Heidt' })
72
- expect(result.source).toBe('initials')
73
- expect(result.initials).toBe('IH')
74
- })
75
- })
79
+ it("returns initials source when avatarUrl is empty string (falsy)", () => {
80
+ const result = resolveAvatar({ avatarUrl: "", fullName: "Ian Heidt" });
81
+ expect(result.source).toBe("initials");
82
+ expect(result.initials).toBe("IH");
83
+ });
84
+ });
@@ -7,7 +7,7 @@
7
7
  * @see ADR-BE-063 for design rationale
8
8
  */
9
9
 
10
- import type { UserIdentity } from './types';
10
+ import type { UserIdentity } from "./types";
11
11
 
12
12
  // =============================================================================
13
13
  // Types
@@ -18,7 +18,7 @@ import type { UserIdentity } from './types';
18
18
  * - 'photo': Using a resolved profile photo
19
19
  * - 'initials': Using generated initials (fallback)
20
20
  */
21
- export type AvatarSource = 'photo' | 'initials';
21
+ export type AvatarSource = "photo" | "initials";
22
22
 
23
23
  /**
24
24
  * Resolved avatar for UI rendering.
@@ -61,16 +61,16 @@ export interface ResolvedAvatar {
61
61
  */
62
62
  export function generateInitials(fullName: string): string {
63
63
  const trimmed = fullName.trim();
64
- if (!trimmed) return '';
64
+ if (!trimmed) return "";
65
65
 
66
66
  const words = trimmed.split(/\s+/);
67
- const firstInitial = words[0]?.[0]?.toUpperCase() ?? '';
67
+ const firstInitial = words[0]?.[0]?.toUpperCase() ?? "";
68
68
 
69
69
  if (words.length === 1) {
70
70
  return firstInitial;
71
71
  }
72
72
 
73
- const lastInitial = words[words.length - 1]?.[0]?.toUpperCase() ?? '';
73
+ const lastInitial = words[words.length - 1]?.[0]?.toUpperCase() ?? "";
74
74
  return `${firstInitial}${lastInitial}`;
75
75
  }
76
76
 
@@ -100,20 +100,20 @@ export function generateInitials(fullName: string): string {
100
100
  * // { source: 'initials', initials: 'IH' }
101
101
  */
102
102
  export function resolveAvatar(
103
- identity: Pick<UserIdentity, 'avatarUrl' | 'fullName'>
103
+ identity: Pick<UserIdentity, "avatarUrl" | "fullName">,
104
104
  ): ResolvedAvatar {
105
105
  const initials = generateInitials(identity.fullName);
106
106
 
107
107
  if (identity.avatarUrl) {
108
108
  return {
109
- source: 'photo',
109
+ source: "photo",
110
110
  url: identity.avatarUrl,
111
111
  initials,
112
112
  };
113
113
  }
114
114
 
115
115
  return {
116
- source: 'initials',
116
+ source: "initials",
117
117
  initials,
118
118
  };
119
119
  }
@@ -7,7 +7,7 @@
7
7
  * @see ADR-CONT-032 for design rationale
8
8
  */
9
9
 
10
- import type { UserIdentity } from './types';
10
+ import type { UserIdentity } from "./types";
11
11
 
12
12
  // =============================================================================
13
13
  // Helper Functions
@@ -28,7 +28,7 @@ import type { UserIdentity } from './types';
28
28
  */
29
29
  export function extractFirstWord(fullName: string): string {
30
30
  const trimmed = fullName.trim();
31
- const spaceIndex = trimmed.indexOf(' ');
31
+ const spaceIndex = trimmed.indexOf(" ");
32
32
  return spaceIndex === -1 ? trimmed : trimmed.slice(0, spaceIndex);
33
33
  }
34
34
 
@@ -57,7 +57,7 @@ export function extractFirstWord(fullName: string): string {
57
57
  * resolveDisplayName({ fullName: "Madonna" }) // "Madonna"
58
58
  */
59
59
  export function resolveDisplayName(
60
- identity: Pick<UserIdentity, 'preferredName' | 'fullName'>
60
+ identity: Pick<UserIdentity, "preferredName" | "fullName">,
61
61
  ): string {
62
62
  return identity.preferredName?.trim() || extractFirstWord(identity.fullName);
63
63
  }