@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
@@ -20,7 +20,7 @@
20
20
  * descriptions live in `.meta({ description })` — structured metadata, not
21
21
  * stringified JSON blobs on the schema.
22
22
  */
23
- import { z } from 'zod';
23
+ import { z } from "zod";
24
24
 
25
25
  // =============================================================================
26
26
  // Recording identity
@@ -33,8 +33,10 @@ import { z } from 'zod';
33
33
  */
34
34
  export const RecordingIdSchema = z
35
35
  .string()
36
- .regex(/^[0-9A-HJKMNP-TV-Z]{26}$/, 'ULID required')
37
- .meta({ description: 'ULID; unified trace key for the recording (INV-MTG-7).' });
36
+ .regex(/^[0-9A-HJKMNP-TV-Z]{26}$/, "ULID required")
37
+ .meta({
38
+ description: "ULID; unified trace key for the recording (INV-MTG-7).",
39
+ });
38
40
  export type RecordingId = z.infer<typeof RecordingIdSchema>;
39
41
 
40
42
  // =============================================================================
@@ -42,8 +44,9 @@ export type RecordingId = z.infer<typeof RecordingIdSchema>;
42
44
  // =============================================================================
43
45
 
44
46
  /** Which capture stream a chunk came from. Two channels: mic (channel 0) and system (channel 1). */
45
- export const RecordingSourceSchema = z.enum(['mic', 'system']).meta({
46
- description: 'Capture stream identity. Mic = channel 0, system audio = channel 1.',
47
+ export const RecordingSourceSchema = z.enum(["mic", "system"]).meta({
48
+ description:
49
+ "Capture stream identity. Mic = channel 0, system audio = channel 1.",
47
50
  });
48
51
  export type RecordingSource = z.infer<typeof RecordingSourceSchema>;
49
52
 
@@ -54,8 +57,10 @@ export type RecordingSource = z.infer<typeof RecordingSourceSchema>;
54
57
  * which the BrowserAudioDetector heuristic identifies (PRD-00655).
55
58
  */
56
59
  export const DetectedMeetingAppSchema = z
57
- .enum(['zoom', 'teams', 'meet-browser', 'slack-huddle', 'manual'])
58
- .meta({ description: 'Auto-detected meeting app that owns the system audio stream.' });
60
+ .enum(["zoom", "teams", "meet-browser", "slack-huddle", "manual"])
61
+ .meta({
62
+ description: "Auto-detected meeting app that owns the system audio stream.",
63
+ });
59
64
  export type DetectedMeetingApp = z.infer<typeof DetectedMeetingAppSchema>;
60
65
 
61
66
  /**
@@ -64,8 +69,11 @@ export type DetectedMeetingApp = z.infer<typeof DetectedMeetingAppSchema>;
64
69
  * the mac side stays provider-opaque).
65
70
  */
66
71
  export const TranscriptionProviderSchema = z
67
- .enum(['deepgram', 'assemblyai', 'whisper-local'])
68
- .meta({ description: 'Server-side selection of transcription provider; never crosses to mac.' });
72
+ .enum(["deepgram", "assemblyai", "whisper-local"])
73
+ .meta({
74
+ description:
75
+ "Server-side selection of transcription provider; never crosses to mac.",
76
+ });
69
77
  export type TranscriptionProvider = z.infer<typeof TranscriptionProviderSchema>;
70
78
 
71
79
  /**
@@ -76,20 +84,20 @@ export type TranscriptionProvider = z.infer<typeof TranscriptionProviderSchema>;
76
84
  */
77
85
  export const RecordingStatusSchema = z
78
86
  .enum([
79
- 'draft',
80
- 'recording',
81
- 'processing',
82
- 'finalized',
83
- 'failed',
84
- 'partial-transcript-network',
85
- 'cancelled',
87
+ "draft",
88
+ "recording",
89
+ "processing",
90
+ "finalized",
91
+ "failed",
92
+ "partial-transcript-network",
93
+ "cancelled",
86
94
  ])
87
- .meta({ description: 'Coarse lifecycle state for a recording entity.' });
95
+ .meta({ description: "Coarse lifecycle state for a recording entity." });
88
96
  export type RecordingStatus = z.infer<typeof RecordingStatusSchema>;
89
97
 
90
98
  /** Quality signal emitted by the capture runtime. Used to surface degradation in the UI. */
91
- export const RecordingQualitySchema = z.enum(['clean', 'degraded']).meta({
92
- description: 'Capture-runtime quality signal. `degraded` triggers a UI hint.',
99
+ export const RecordingQualitySchema = z.enum(["clean", "degraded"]).meta({
100
+ description: "Capture-runtime quality signal. `degraded` triggers a UI hint.",
93
101
  });
94
102
  export type RecordingQuality = z.infer<typeof RecordingQualitySchema>;
95
103
 
@@ -102,10 +110,10 @@ export type RecordingQuality = z.infer<typeof RecordingQualitySchema>;
102
110
  * (`feedback_retrieval_default_off_for_noisy_sources`).
103
111
  */
104
112
  export const MeetingVisibilitySchema = z
105
- .enum(['meeting_only', 'shared', 'org', 'finalized_private'])
113
+ .enum(["meeting_only", "shared", "org", "finalized_private"])
106
114
  .meta({
107
115
  description:
108
- 'Visibility band for the finalized meeting projection. Default `meeting_only`.',
116
+ "Visibility band for the finalized meeting projection. Default `meeting_only`.",
109
117
  });
110
118
  export type MeetingVisibility = z.infer<typeof MeetingVisibilitySchema>;
111
119
 
@@ -128,7 +136,9 @@ export const SourceSegmentSchema = z
128
136
  sampleRateHz: z.literal(16000),
129
137
  channels: z.literal(1),
130
138
  })
131
- .meta({ description: 'Per-source continuous capture segment within a recording.' });
139
+ .meta({
140
+ description: "Per-source continuous capture segment within a recording.",
141
+ });
132
142
  export type SourceSegment = z.infer<typeof SourceSegmentSchema>;
133
143
 
134
144
  // =============================================================================
@@ -153,7 +163,10 @@ export const TranscriptChunkSchema = z
153
163
  speakerLabel: z.string().nullable().optional(),
154
164
  confidence: z.number().min(0).max(1).optional(),
155
165
  })
156
- .meta({ description: 'A single transcript window (interim or final) from the provider.' });
166
+ .meta({
167
+ description:
168
+ "A single transcript window (interim or final) from the provider.",
169
+ });
157
170
  export type TranscriptChunk = z.infer<typeof TranscriptChunkSchema>;
158
171
 
159
172
  // =============================================================================
@@ -161,26 +174,26 @@ export type TranscriptChunk = z.infer<typeof TranscriptChunkSchema>;
161
174
  // =============================================================================
162
175
 
163
176
  const SessionOpenedEventSchema = z.object({
164
- kind: z.literal('session_opened'),
177
+ kind: z.literal("session_opened"),
165
178
  recordingId: RecordingIdSchema,
166
179
  atMs: z.number().int().nonnegative(),
167
180
  });
168
181
 
169
182
  const ChunkBatchEventSchema = z.object({
170
- kind: z.literal('chunk_batch'),
183
+ kind: z.literal("chunk_batch"),
171
184
  recordingId: RecordingIdSchema,
172
185
  chunks: z.array(TranscriptChunkSchema).min(1),
173
186
  });
174
187
 
175
188
  const QualityDegradedEventSchema = z.object({
176
- kind: z.literal('quality_degraded'),
189
+ kind: z.literal("quality_degraded"),
177
190
  recordingId: RecordingIdSchema,
178
191
  reason: z.string().min(1),
179
192
  atMs: z.number().int().nonnegative(),
180
193
  });
181
194
 
182
195
  const SessionStoppedEventSchema = z.object({
183
- kind: z.literal('session_stopped'),
196
+ kind: z.literal("session_stopped"),
184
197
  recordingId: RecordingIdSchema,
185
198
  atMs: z.number().int().nonnegative(),
186
199
  expectedLastSequence: z.number().int().nonnegative(),
@@ -196,7 +209,7 @@ const SessionStoppedEventSchema = z.object({
196
209
  * `quality_degraded` may interleave.
197
210
  */
198
211
  export const RecordingEventSchema = z
199
- .discriminatedUnion('kind', [
212
+ .discriminatedUnion("kind", [
200
213
  SessionOpenedEventSchema,
201
214
  ChunkBatchEventSchema,
202
215
  QualityDegradedEventSchema,
@@ -204,7 +217,7 @@ export const RecordingEventSchema = z
204
217
  ])
205
218
  .meta({
206
219
  description:
207
- 'Event emitted by the capture runtime during a recording session (INV-MTG ordering).',
220
+ "Event emitted by the capture runtime during a recording session (INV-MTG ordering).",
208
221
  });
209
222
  export type RecordingEvent = z.infer<typeof RecordingEventSchema>;
210
223
 
@@ -228,7 +241,7 @@ export const TranscriptionSessionGrantSchema = z
228
241
  audioConfig: z.object({
229
242
  sampleRateHz: z.literal(16000),
230
243
  channels: z.literal(2),
231
- encoding: z.literal('linear16'),
244
+ encoding: z.literal("linear16"),
232
245
  chunkMs: z.literal(200),
233
246
  }),
234
247
  capabilities: z.object({
@@ -239,9 +252,11 @@ export const TranscriptionSessionGrantSchema = z
239
252
  })
240
253
  .meta({
241
254
  description:
242
- 'Provider-agnostic grant the mac shell consumes. Mac never sees provider name (INV-MTG-15).',
255
+ "Provider-agnostic grant the mac shell consumes. Mac never sees provider name (INV-MTG-15).",
243
256
  });
244
- export type TranscriptionSessionGrant = z.infer<typeof TranscriptionSessionGrantSchema>;
257
+ export type TranscriptionSessionGrant = z.infer<
258
+ typeof TranscriptionSessionGrantSchema
259
+ >;
245
260
 
246
261
  // =============================================================================
247
262
  // Meeting metadata projection (finalized read model)
@@ -271,6 +286,8 @@ export const MeetingMetadataProjectionSchema = z
271
286
  transcriptAvailable: z.boolean(),
272
287
  })
273
288
  .meta({
274
- description: 'Finalized read projection for a recording (excludes drafts).',
289
+ description: "Finalized read projection for a recording (excludes drafts).",
275
290
  });
276
- export type MeetingMetadataProjection = z.infer<typeof MeetingMetadataProjectionSchema>;
291
+ export type MeetingMetadataProjection = z.infer<
292
+ typeof MeetingMetadataProjectionSchema
293
+ >;
@@ -18,6 +18,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
18
18
  ## Public API
19
19
 
20
20
  **Types:**
21
+
21
22
  - `TextPart`, `SurfacePart`, `AssistantMessagePart` — core part unions
22
23
  - `ToolListPart`, `StatusPanelPart`, `ChartPart`, `TablePart` — surface part variants
23
24
  - `PreviewData`, `PreviewArtifact`, `PreviewPart` — action preview surfaces
@@ -27,6 +28,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
27
28
  - `IntegrationKey`, `IntegrationProvider` — integration identifiers
28
29
 
29
30
  **Runtime values:**
31
+
30
32
  - `CONFIRMATION_LABELS` — exhaustive map of ExecutionKind to human-readable labels
31
33
  - `getConfirmationLabel(kind, fallback)` — label lookup with fallback
32
34
  - `WireSurfaceBuilder` — factory for creating wire-format data parts
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  createPartBuilder,
4
4
  addText,
@@ -6,176 +6,176 @@ import {
6
6
  addPart,
7
7
  buildParts,
8
8
  getDroppedCount,
9
- } from '../builder.js'
10
- import type { SurfacePart, TextPart } from '../types.js'
9
+ } from "../builder.js";
10
+ import type { SurfacePart, TextPart } from "../types.js";
11
11
 
12
12
  const mockSurface: SurfacePart = {
13
- type: 'tool-list',
13
+ type: "tool-list",
14
14
  tools: [],
15
- } as unknown as SurfacePart
15
+ } as unknown as SurfacePart;
16
16
 
17
17
  const anotherSurface: SurfacePart = {
18
- type: 'status-panel',
19
- title: 'Status',
18
+ type: "status-panel",
19
+ title: "Status",
20
20
  entries: [],
21
- }
21
+ };
22
22
 
23
23
  // =============================================================================
24
24
  // Builder Creation
25
25
  // =============================================================================
26
26
 
27
- describe('createPartBuilder', () => {
28
- it('returns initial state with defaults', () => {
29
- const state = createPartBuilder()
30
- expect(state.parts).toEqual([])
31
- expect(state.hasSurface).toBe(false)
32
- expect(state.devMode).toBe(false)
33
- expect(state.droppedCount).toBe(0)
34
- })
35
-
36
- it('returns state with devMode=true when passed true', () => {
37
- const state = createPartBuilder(true)
38
- expect(state.devMode).toBe(true)
39
- expect(state.parts).toEqual([])
40
- expect(state.hasSurface).toBe(false)
41
- expect(state.droppedCount).toBe(0)
42
- })
43
- })
27
+ describe("createPartBuilder", () => {
28
+ it("returns initial state with defaults", () => {
29
+ const state = createPartBuilder();
30
+ expect(state.parts).toEqual([]);
31
+ expect(state.hasSurface).toBe(false);
32
+ expect(state.devMode).toBe(false);
33
+ expect(state.droppedCount).toBe(0);
34
+ });
35
+
36
+ it("returns state with devMode=true when passed true", () => {
37
+ const state = createPartBuilder(true);
38
+ expect(state.devMode).toBe(true);
39
+ expect(state.parts).toEqual([]);
40
+ expect(state.hasSurface).toBe(false);
41
+ expect(state.droppedCount).toBe(0);
42
+ });
43
+ });
44
44
 
45
45
  // =============================================================================
46
46
  // NarrativeState (before any surface)
47
47
  // =============================================================================
48
48
 
49
- describe('NarrativeState', () => {
50
- it('addText is accepted and appends text part', () => {
51
- const state = createPartBuilder()
52
- const result = addText(state, 'hello')
53
- expect(result.accepted).toBe(true)
54
- expect(result.state.parts).toEqual([{ type: 'text', text: 'hello' }])
55
- })
56
-
57
- it('multiple addText calls accumulate parts in order', () => {
58
- let state = createPartBuilder()
59
- state = addText(state, 'first').state
60
- state = addText(state, 'second').state
61
- state = addText(state, 'third').state
49
+ describe("NarrativeState", () => {
50
+ it("addText is accepted and appends text part", () => {
51
+ const state = createPartBuilder();
52
+ const result = addText(state, "hello");
53
+ expect(result.accepted).toBe(true);
54
+ expect(result.state.parts).toEqual([{ type: "text", text: "hello" }]);
55
+ });
56
+
57
+ it("multiple addText calls accumulate parts in order", () => {
58
+ let state = createPartBuilder();
59
+ state = addText(state, "first").state;
60
+ state = addText(state, "second").state;
61
+ state = addText(state, "third").state;
62
62
  expect(state.parts).toEqual([
63
- { type: 'text', text: 'first' },
64
- { type: 'text', text: 'second' },
65
- { type: 'text', text: 'third' },
66
- ])
67
- })
68
-
69
- it('addSurface is accepted and transitions hasSurface to true', () => {
70
- const state = createPartBuilder()
71
- const result = addSurface(state, mockSurface)
72
- expect(result.accepted).toBe(true)
73
- expect(result.state.hasSurface).toBe(true)
74
- })
75
- })
63
+ { type: "text", text: "first" },
64
+ { type: "text", text: "second" },
65
+ { type: "text", text: "third" },
66
+ ]);
67
+ });
68
+
69
+ it("addSurface is accepted and transitions hasSurface to true", () => {
70
+ const state = createPartBuilder();
71
+ const result = addSurface(state, mockSurface);
72
+ expect(result.accepted).toBe(true);
73
+ expect(result.state.hasSurface).toBe(true);
74
+ });
75
+ });
76
76
 
77
77
  // =============================================================================
78
78
  // SurfaceState — THE CRITICAL INVARIANT
79
79
  // =============================================================================
80
80
 
81
- describe('SurfaceState (text after surface)', () => {
82
- it('silently drops text in prod mode (devMode=false)', () => {
83
- let state = createPartBuilder(false)
84
- state = addSurface(state, mockSurface).state
85
- const partsBefore = state.parts.length
86
-
87
- const result = addText(state, 'dropped')
88
- expect(result.accepted).toBe(false)
89
- expect(result.state.droppedCount).toBe(1)
90
- expect(result.state.parts.length).toBe(partsBefore)
91
- })
92
-
93
- it('throws invariant violation in dev mode (devMode=true)', () => {
94
- let state = createPartBuilder(true)
95
- state = addSurface(state, mockSurface).state
96
-
97
- expect(() => addText(state, 'illegal')).toThrow('invariant violation')
98
- })
99
-
100
- it('addSurface after addSurface is always accepted', () => {
101
- let state = createPartBuilder()
102
- state = addSurface(state, mockSurface).state
103
- const result = addSurface(state, anotherSurface)
104
- expect(result.accepted).toBe(true)
105
- expect(result.state.parts.length).toBe(2)
106
- })
107
-
108
- it('droppedCount accumulates across multiple drops in prod mode', () => {
109
- let state = createPartBuilder(false)
110
- state = addSurface(state, mockSurface).state
111
-
112
- state = addText(state, 'drop1').state
113
- state = addText(state, 'drop2').state
114
- state = addText(state, 'drop3').state
115
-
116
- expect(state.droppedCount).toBe(3)
117
- })
118
- })
81
+ describe("SurfaceState (text after surface)", () => {
82
+ it("silently drops text in prod mode (devMode=false)", () => {
83
+ let state = createPartBuilder(false);
84
+ state = addSurface(state, mockSurface).state;
85
+ const partsBefore = state.parts.length;
86
+
87
+ const result = addText(state, "dropped");
88
+ expect(result.accepted).toBe(false);
89
+ expect(result.state.droppedCount).toBe(1);
90
+ expect(result.state.parts.length).toBe(partsBefore);
91
+ });
92
+
93
+ it("throws invariant violation in dev mode (devMode=true)", () => {
94
+ let state = createPartBuilder(true);
95
+ state = addSurface(state, mockSurface).state;
96
+
97
+ expect(() => addText(state, "illegal")).toThrow("invariant violation");
98
+ });
99
+
100
+ it("addSurface after addSurface is always accepted", () => {
101
+ let state = createPartBuilder();
102
+ state = addSurface(state, mockSurface).state;
103
+ const result = addSurface(state, anotherSurface);
104
+ expect(result.accepted).toBe(true);
105
+ expect(result.state.parts.length).toBe(2);
106
+ });
107
+
108
+ it("droppedCount accumulates across multiple drops in prod mode", () => {
109
+ let state = createPartBuilder(false);
110
+ state = addSurface(state, mockSurface).state;
111
+
112
+ state = addText(state, "drop1").state;
113
+ state = addText(state, "drop2").state;
114
+ state = addText(state, "drop3").state;
115
+
116
+ expect(state.droppedCount).toBe(3);
117
+ });
118
+ });
119
119
 
120
120
  // =============================================================================
121
121
  // addPart dispatch
122
122
  // =============================================================================
123
123
 
124
- describe('addPart', () => {
125
- it('dispatches TextPart to addText behavior', () => {
126
- const state = createPartBuilder()
127
- const textPart: TextPart = { type: 'text', text: 'dispatched' }
128
- const result = addPart(state, textPart)
129
- expect(result.accepted).toBe(true)
130
- expect(result.state.parts).toEqual([{ type: 'text', text: 'dispatched' }])
131
- })
132
-
133
- it('dispatches SurfacePart to addSurface behavior', () => {
134
- const state = createPartBuilder()
135
- const result = addPart(state, mockSurface)
136
- expect(result.accepted).toBe(true)
137
- expect(result.state.hasSurface).toBe(true)
138
- })
139
- })
124
+ describe("addPart", () => {
125
+ it("dispatches TextPart to addText behavior", () => {
126
+ const state = createPartBuilder();
127
+ const textPart: TextPart = { type: "text", text: "dispatched" };
128
+ const result = addPart(state, textPart);
129
+ expect(result.accepted).toBe(true);
130
+ expect(result.state.parts).toEqual([{ type: "text", text: "dispatched" }]);
131
+ });
132
+
133
+ it("dispatches SurfacePart to addSurface behavior", () => {
134
+ const state = createPartBuilder();
135
+ const result = addPart(state, mockSurface);
136
+ expect(result.accepted).toBe(true);
137
+ expect(result.state.hasSurface).toBe(true);
138
+ });
139
+ });
140
140
 
141
141
  // =============================================================================
142
142
  // Finalization
143
143
  // =============================================================================
144
144
 
145
- describe('buildParts', () => {
146
- it('returns array copy — mutating result does not affect builder', () => {
147
- let state = createPartBuilder()
148
- state = addText(state, 'text1').state
149
- state = addSurface(state, mockSurface).state
150
-
151
- const parts = buildParts(state)
152
- parts.push({ type: 'text', text: 'injected' })
153
-
154
- expect(buildParts(state)).toHaveLength(2)
155
- })
156
-
157
- it('preserves correct part ordering (text before surfaces)', () => {
158
- let state = createPartBuilder()
159
- state = addText(state, 'narrative').state
160
- state = addSurface(state, mockSurface).state
161
-
162
- const parts = buildParts(state)
163
- expect(parts[0].type).toBe('text')
164
- expect(parts[1].type).not.toBe('text')
165
- })
166
- })
167
-
168
- describe('getDroppedCount', () => {
169
- it('returns current droppedCount from state', () => {
170
- let state = createPartBuilder(false)
171
- state = addSurface(state, mockSurface).state
172
- state = addText(state, 'dropped').state
173
-
174
- expect(getDroppedCount(state)).toBe(1)
175
- })
176
-
177
- it('returns 0 when no drops have occurred', () => {
178
- const state = createPartBuilder()
179
- expect(getDroppedCount(state)).toBe(0)
180
- })
181
- })
145
+ describe("buildParts", () => {
146
+ it("returns array copy — mutating result does not affect builder", () => {
147
+ let state = createPartBuilder();
148
+ state = addText(state, "text1").state;
149
+ state = addSurface(state, mockSurface).state;
150
+
151
+ const parts = buildParts(state);
152
+ parts.push({ type: "text", text: "injected" });
153
+
154
+ expect(buildParts(state)).toHaveLength(2);
155
+ });
156
+
157
+ it("preserves correct part ordering (text before surfaces)", () => {
158
+ let state = createPartBuilder();
159
+ state = addText(state, "narrative").state;
160
+ state = addSurface(state, mockSurface).state;
161
+
162
+ const parts = buildParts(state);
163
+ expect(parts[0].type).toBe("text");
164
+ expect(parts[1].type).not.toBe("text");
165
+ });
166
+ });
167
+
168
+ describe("getDroppedCount", () => {
169
+ it("returns current droppedCount from state", () => {
170
+ let state = createPartBuilder(false);
171
+ state = addSurface(state, mockSurface).state;
172
+ state = addText(state, "dropped").state;
173
+
174
+ expect(getDroppedCount(state)).toBe(1);
175
+ });
176
+
177
+ it("returns 0 when no drops have occurred", () => {
178
+ const state = createPartBuilder();
179
+ expect(getDroppedCount(state)).toBe(0);
180
+ });
181
+ });