@company-semantics/contracts 0.24.0 → 0.25.1

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": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,6 +59,9 @@
59
59
  "test": "vitest run scripts/ci/__tests__"
60
60
  },
61
61
  "packageManager": "pnpm@10.25.0",
62
+ "engines": {
63
+ "node": "22.x"
64
+ },
62
65
  "devDependencies": {
63
66
  "@types/node": "^25.0.3",
64
67
  "husky": "^9.1.7",
@@ -392,6 +392,8 @@ export interface GuardCheckRegistry {
392
392
  structural?: BoundGuardCheck[];
393
393
  behavioral?: BoundGuardCheck[];
394
394
  invariants?: BoundGuardCheck[];
395
+ /** Evolution guards: drift detection (advisory only) */
396
+ evolution?: BoundGuardCheck[];
395
397
  /** Meta-guards: protect the guard system itself (coverage correctness) */
396
398
  meta?: BoundGuardCheck[];
397
399
  };
package/src/index.ts CHANGED
@@ -99,4 +99,34 @@ export type {
99
99
  MCPToolDescriptor,
100
100
  ToolDiscoveryResponse,
101
101
  ToolListMessagePart,
102
+ ToolListDataPart,
102
103
  } from './mcp/index.js'
104
+
105
+ // Message part types and builder functions
106
+ // @see ADR-2026-01-022 for design rationale
107
+ export type {
108
+ TextPart,
109
+ ToolListPart,
110
+ StatusPanelPart,
111
+ StatusPanelEntry,
112
+ ChartPart,
113
+ ChartDataPoint,
114
+ TablePart,
115
+ ConfirmationPart,
116
+ SurfacePart,
117
+ AssistantMessagePart,
118
+ PartBuilderState,
119
+ AddPartResult,
120
+ } from './message-parts/index.js'
121
+
122
+ export {
123
+ isTextPart,
124
+ isSurfacePart,
125
+ createPartBuilder,
126
+ addText,
127
+ addSurface,
128
+ addPart,
129
+ buildParts,
130
+ getDroppedCount,
131
+ WireSurfaceBuilder,
132
+ } from './message-parts/index.js'
package/src/mcp/index.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  * Types for tool discovery flow (read-only, UI-first).
5
5
  * Same descriptor used for discovery and invocation, different fields matter.
6
6
  *
7
+ * TWO SHAPES (same conceptual data, different layers):
8
+ * - ToolDiscoveryResponse: HTTP transport (GET /api/capabilities/tools response)
9
+ * - ToolListMessagePart: Chat protocol (atomic message part in conversation)
10
+ *
7
11
  * INVARIANTS:
8
12
  * - Discovery is descriptive (never executes)
9
13
  * - Invocation is authoritative (runtime is executor)
@@ -83,3 +87,17 @@ export interface ToolListMessagePart {
83
87
  type: 'tool-list'
84
88
  tools: MCPToolDescriptor[]
85
89
  }
90
+
91
+ /**
92
+ * Wire protocol type for tool list data part.
93
+ *
94
+ * Transport-level type (SSE stream part).
95
+ * Uses AI SDK's `data-{name}` convention for custom data parts.
96
+ *
97
+ * The `data-` prefix signals to the AI SDK that this is a custom data part.
98
+ * Frontend normalizes this to `tool-list` for semantic handling.
99
+ */
100
+ export interface ToolListDataPart {
101
+ type: 'data-tool-list'
102
+ data: { tools: MCPToolDescriptor[] }
103
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Part Builder Functions
3
+ *
4
+ * Functional builder for constructing well-ordered message parts.
5
+ * Enforces invariant: narrative (text) parts before surface parts.
6
+ *
7
+ * STATE MACHINE MODEL:
8
+ * PartBuilder implements a two-state machine:
9
+ *
10
+ * NarrativeState → accepts text or surface
11
+ * SurfaceState → accepts surface only (text rejected)
12
+ *
13
+ * Transitions are one-way. Once the first surface is added,
14
+ * the turn is in "surface phase" permanently.
15
+ *
16
+ * This prevents "post-surface commentary" features from sneaking in.
17
+ *
18
+ * @see DECISIONS.md ADR-2026-01-022 for design rationale
19
+ */
20
+
21
+ import type { AssistantMessagePart, TextPart, SurfacePart } from './types.js'
22
+
23
+ // =============================================================================
24
+ // Builder State
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Immutable state for the part builder.
29
+ * Tracks collected parts and whether surfaces have been added.
30
+ */
31
+ export interface PartBuilderState {
32
+ /** Collected parts in order */
33
+ readonly parts: readonly AssistantMessagePart[]
34
+ /** Whether any surface part has been added (true = SurfaceState) */
35
+ readonly hasSurface: boolean
36
+ /** Whether dev mode is enabled (throws on invariant violation) */
37
+ readonly devMode: boolean
38
+ /** Count of dropped text parts (for logging) */
39
+ readonly droppedCount: number
40
+ }
41
+
42
+ /**
43
+ * Result of adding a part to the builder.
44
+ */
45
+ export interface AddPartResult {
46
+ /** Updated builder state */
47
+ state: PartBuilderState
48
+ /** Whether the part was accepted (false = dropped) */
49
+ accepted: boolean
50
+ }
51
+
52
+ // =============================================================================
53
+ // Builder Creation
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Create a new part builder state.
58
+ *
59
+ * @param devMode - If true, throws on invariant violations. If false, silently drops.
60
+ * @returns Initial builder state (NarrativeState)
61
+ *
62
+ * @example
63
+ * const state = createPartBuilder(process.env.NODE_ENV !== 'production');
64
+ */
65
+ export function createPartBuilder(devMode = false): PartBuilderState {
66
+ return {
67
+ parts: [],
68
+ hasSurface: false,
69
+ devMode,
70
+ droppedCount: 0,
71
+ }
72
+ }
73
+
74
+ // =============================================================================
75
+ // Part Addition
76
+ // =============================================================================
77
+
78
+ /**
79
+ * Add a text part to the builder.
80
+ *
81
+ * INVARIANT: Text parts MUST come before surface parts.
82
+ * - Dev mode: throws Error if text added after surface
83
+ * - Prod mode: silently drops, increments droppedCount
84
+ *
85
+ * @param state - Current builder state
86
+ * @param text - Text content to add
87
+ * @returns Result with updated state and acceptance status
88
+ *
89
+ * @example
90
+ * let state = createPartBuilder();
91
+ * const result = addText(state, "Hello, world!");
92
+ * state = result.state;
93
+ */
94
+ export function addText(state: PartBuilderState, text: string): AddPartResult {
95
+ const part: TextPart = { type: 'text', text }
96
+
97
+ if (state.hasSurface) {
98
+ if (state.devMode) {
99
+ throw new Error(
100
+ 'PartBuilder invariant violation: text part added after surface part. ' +
101
+ 'Narrative content must come before structured surfaces. ' +
102
+ `Parts so far: ${state.parts.length}, dropped: ${state.droppedCount}`
103
+ )
104
+ }
105
+ // Prod mode: drop silently
106
+ return {
107
+ state: { ...state, droppedCount: state.droppedCount + 1 },
108
+ accepted: false,
109
+ }
110
+ }
111
+
112
+ return {
113
+ state: { ...state, parts: [...state.parts, part] },
114
+ accepted: true,
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Add a surface part to the builder.
120
+ *
121
+ * Surface parts mark the transition from NarrativeState to SurfaceState.
122
+ * After a surface is added, subsequent text parts are rejected.
123
+ *
124
+ * @param state - Current builder state
125
+ * @param part - Surface part to add
126
+ * @returns Result with updated state (always accepted)
127
+ *
128
+ * @example
129
+ * let state = createPartBuilder();
130
+ * state = addSurface(state, { type: 'tool-list', tools: [...] }).state;
131
+ */
132
+ export function addSurface(
133
+ state: PartBuilderState,
134
+ part: SurfacePart
135
+ ): AddPartResult {
136
+ return {
137
+ state: {
138
+ ...state,
139
+ parts: [...state.parts, part],
140
+ hasSurface: true,
141
+ },
142
+ accepted: true,
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Add any message part to the builder.
148
+ * Dispatches to addText or addSurface based on part type.
149
+ *
150
+ * @param state - Current builder state
151
+ * @param part - Part to add
152
+ * @returns Result with updated state and acceptance status
153
+ */
154
+ export function addPart(
155
+ state: PartBuilderState,
156
+ part: AssistantMessagePart
157
+ ): AddPartResult {
158
+ if (part.type === 'text') {
159
+ return addText(state, part.text)
160
+ }
161
+ return addSurface(state, part)
162
+ }
163
+
164
+ // =============================================================================
165
+ // Builder Finalization
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Extract the final parts array from the builder.
170
+ *
171
+ * @param state - Final builder state
172
+ * @returns Array of message parts in correct order
173
+ */
174
+ export function buildParts(state: PartBuilderState): AssistantMessagePart[] {
175
+ return [...state.parts]
176
+ }
177
+
178
+ /**
179
+ * Get count of dropped parts (useful for logging/metrics).
180
+ *
181
+ * @param state - Builder state
182
+ * @returns Number of parts dropped due to invariant violations
183
+ */
184
+ export function getDroppedCount(state: PartBuilderState): number {
185
+ return state.droppedCount
186
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Message Parts Barrel
3
+ *
4
+ * Re-exports message part vocabulary types and builder functions.
5
+ * Import from '@company-semantics/contracts' (root).
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ TextPart,
11
+ ToolListPart,
12
+ StatusPanelPart,
13
+ StatusPanelEntry,
14
+ ChartPart,
15
+ ChartDataPoint,
16
+ TablePart,
17
+ ConfirmationPart,
18
+ SurfacePart,
19
+ AssistantMessagePart,
20
+ } from './types.js'
21
+
22
+ // Type guards
23
+ export { isTextPart, isSurfacePart } from './types.js'
24
+
25
+ // Builder functions
26
+ export type { PartBuilderState, AddPartResult } from './builder.js'
27
+ export {
28
+ createPartBuilder,
29
+ addText,
30
+ addSurface,
31
+ addPart,
32
+ buildParts,
33
+ getDroppedCount,
34
+ } from './builder.js'
35
+
36
+ // Wire format builder
37
+ export { WireSurfaceBuilder } from './wire.js'
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Message Parts Vocabulary
3
+ *
4
+ * Canonical types for structured assistant message output.
5
+ * Enables rich UI rendering beyond plain text.
6
+ *
7
+ * INVARIANTS:
8
+ * - Narrative (text) parts MUST come before surface parts
9
+ * - SurfacePart is an extensible union (add new kinds as needed)
10
+ * - TextPart is the ONLY part type that may be streamed char-by-char
11
+ * - Once a surface is added, the turn is in "surface phase" permanently
12
+ *
13
+ * @see DECISIONS.md ADR-2026-01-022 for design rationale
14
+ */
15
+
16
+ import type { ToolListMessagePart } from '../mcp/index.js'
17
+
18
+ // =============================================================================
19
+ // Narrative Parts (Streamable)
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Text content part.
24
+ * The only part type that MAY be streamed character-by-character.
25
+ *
26
+ * Design: Aligns with AI SDK's TextPart for interoperability.
27
+ */
28
+ export interface TextPart {
29
+ type: 'text'
30
+ /** The text content (may be partial during streaming) */
31
+ text: string
32
+ }
33
+
34
+ // =============================================================================
35
+ // Surface Parts (Atomic, Non-Streamable)
36
+ // =============================================================================
37
+
38
+ /**
39
+ * Re-export ToolListMessagePart as ToolListPart for consistency.
40
+ */
41
+ export type ToolListPart = ToolListMessagePart
42
+
43
+ /**
44
+ * Status panel surface part.
45
+ * Displays system status, connection states, or progress indicators.
46
+ */
47
+ export interface StatusPanelPart {
48
+ type: 'status-panel'
49
+ /** Panel title */
50
+ title: string
51
+ /** Status entries */
52
+ entries: StatusPanelEntry[]
53
+ }
54
+
55
+ /**
56
+ * Single entry in a status panel.
57
+ */
58
+ export interface StatusPanelEntry {
59
+ /** Entry label */
60
+ label: string
61
+ /** Current status */
62
+ status: 'ok' | 'warning' | 'error' | 'pending'
63
+ /** Optional detail text */
64
+ detail?: string
65
+ }
66
+
67
+ /**
68
+ * Chart surface part.
69
+ * Renders a data visualization.
70
+ */
71
+ export interface ChartPart {
72
+ type: 'chart'
73
+ /** Chart type */
74
+ chartType: 'bar' | 'line' | 'pie' | 'area'
75
+ /** Chart title */
76
+ title?: string
77
+ /** Data points */
78
+ data: ChartDataPoint[]
79
+ }
80
+
81
+ /**
82
+ * Single data point for charts.
83
+ */
84
+ export interface ChartDataPoint {
85
+ label: string
86
+ value: number
87
+ /** Optional color (CSS color string) */
88
+ color?: string
89
+ }
90
+
91
+ /**
92
+ * Table surface part.
93
+ * Renders tabular data.
94
+ */
95
+ export interface TablePart {
96
+ type: 'table'
97
+ /** Table title */
98
+ title?: string
99
+ /** Column headers */
100
+ headers: string[]
101
+ /** Row data (each row is an array of cell values) */
102
+ rows: string[][]
103
+ }
104
+
105
+ /**
106
+ * Confirmation request surface part.
107
+ * Requests user confirmation before proceeding.
108
+ */
109
+ export interface ConfirmationPart {
110
+ type: 'confirmation'
111
+ /** What is being confirmed */
112
+ action: string
113
+ /** Optional details */
114
+ details?: string
115
+ /** Confirmation ID for response tracking */
116
+ confirmationId: string
117
+ }
118
+
119
+ // =============================================================================
120
+ // Part Unions
121
+ // =============================================================================
122
+
123
+ /**
124
+ * Surface parts are rendered atomically (never streamed).
125
+ * Extensible: add new surface types to this union.
126
+ */
127
+ export type SurfacePart =
128
+ | ToolListPart
129
+ | StatusPanelPart
130
+ | ChartPart
131
+ | TablePart
132
+ | ConfirmationPart
133
+
134
+ /**
135
+ * All assistant message part types.
136
+ * Union of narrative (text) and surface parts.
137
+ */
138
+ export type AssistantMessagePart = TextPart | SurfacePart
139
+
140
+ // =============================================================================
141
+ // Type Guards
142
+ // =============================================================================
143
+
144
+ /**
145
+ * Type guard for TextPart.
146
+ */
147
+ export function isTextPart(part: AssistantMessagePart): part is TextPart {
148
+ return part.type === 'text'
149
+ }
150
+
151
+ /**
152
+ * Type guard for SurfacePart.
153
+ */
154
+ export function isSurfacePart(part: AssistantMessagePart): part is SurfacePart {
155
+ return part.type !== 'text'
156
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Wire Surface Builder
3
+ *
4
+ * Factory functions for creating wire-format surface parts.
5
+ * These follow AI SDK's `data-{name}` convention for custom data parts.
6
+ *
7
+ * Use these in backend code when writing to UIMessageStream.
8
+ * Frontend normalizes wire format to semantic types.
9
+ *
10
+ * @see DECISIONS.md ADR-2026-01-022 for design rationale
11
+ */
12
+
13
+ import type { MCPToolDescriptor, ToolListDataPart } from '../mcp/index.js'
14
+
15
+ /**
16
+ * Factory for creating wire-format surface parts.
17
+ *
18
+ * Wire parts use AI SDK's `data-{name}` type convention.
19
+ * These are transport-level types, not semantic types.
20
+ *
21
+ * @example
22
+ * // In ChatService.ts
23
+ * import { WireSurfaceBuilder } from '@company-semantics/contracts';
24
+ * writer.write(WireSurfaceBuilder.toolList(userTools));
25
+ */
26
+ export const WireSurfaceBuilder = {
27
+ /**
28
+ * Build a tool-list data part for streaming.
29
+ * Use after LLM text stream completes.
30
+ *
31
+ * @param tools - Array of tool descriptors to include
32
+ * @returns Wire-format tool list part ready for stream
33
+ */
34
+ toolList(tools: MCPToolDescriptor[]): ToolListDataPart {
35
+ return {
36
+ type: 'data-tool-list',
37
+ data: { tools },
38
+ }
39
+ },
40
+ } as const