@@ -70,44 +70,48 @@ export function parseSpeakeasyNotes(text: string): ParsedChange[] {
7070 return changes ;
7171}
7272
73- /**
74- * Simplify a deep field path for human-readable summary.
75- * e.g. "request.chatRequest.messages[].citations[].sourceFile.metadata.status.enum(partiallyProcessed)"
76- * -> "partiallyProcessed"
77- *
78- * But for simple paths: "request.document.nativeAppUrl" -> "nativeAppUrl"
79- */
8073function simplifyFieldPath ( field : string ) : string {
81- // Extract enum value if present: .enum(value) -> "value enum"
8274 const enumMatch = field . match ( / \. e n u m \( ( \w + ) \) $ / i) ;
8375 if ( enumMatch ) {
8476 return enumMatch [ 1 ] ;
8577 }
8678
87- // Get the last meaningful segment
8879 const segments = field . split ( '.' ) ;
8980 const last = segments [ segments . length - 1 ]
9081 . replace ( / \[ \] / g, '' )
9182 . replace ( / M a p < \w + > / g, '' ) ;
9283 return last ;
9384}
9485
95- /**
96- * Extract a short method name from the full SDK path.
97- * e.g. "Glean.Client.Chat.Create()" -> "chat create"
98- * e.g. "glean.client.chat.create()" -> "chat create"
99- * e.g. "Glean.Governance.Createfindingsexport()" -> "governance createfindingsexport"
100- */
10186function shortMethodName ( method : string ) : string {
102- const parts = method . replace ( / \( \) $ / , '' ) . split ( '.' ) ;
103- // Skip "Glean"/"glean" and "Client"/" client" prefixes
104- const meaningful = parts . filter ( ( p ) => ! / ^ ( g l e a n | c l i e n t ) $ / i . test ( p ) ) ;
105- return meaningful . join ( ' ') . toLowerCase ( ) ;
87+ // Strip the leading "Glean." namespace but preserve everything else,
88+ // so we keep meaningful paths like " client.insights.retrieve()" or
89+ // "governance.createfindingsexport()".
90+ return method . replace ( / ^ [ G g ] l e a n \. / , ' ') . toLowerCase ( ) ;
10691}
10792
93+ function joinList ( items : string [ ] ) : string {
94+ if ( items . length === 0 ) return '' ;
95+ if ( items . length === 1 ) return items [ 0 ] ;
96+ if ( items . length === 2 ) return `${ items [ 0 ] } and ${ items [ 1 ] } ` ;
97+ return `${ items . slice ( 0 , - 1 ) . join ( ', ' ) } , and ${ items [ items . length - 1 ] } ` ;
98+ }
99+
100+ const VERBS : Array < {
101+ action : ParsedChange [ 'action' ] ;
102+ verb : string ;
103+ preposition : string ;
104+ } > = [
105+ { action : 'added' , verb : 'Added' , preposition : 'to' } ,
106+ { action : 'removed' , verb : 'Removed' , preposition : 'from' } ,
107+ { action : 'changed' , verb : 'Changed' , preposition : 'on' } ,
108+ { action : 'deprecated' , verb : 'Deprecated' , preposition : 'on' } ,
109+ ] ;
110+
108111/**
109- * Format parsed Speakeasy changes into a human-readable summary
110- * matching the repo's established entry format.
112+ * Format parsed Speakeasy changes into prose: one sentence per action verb,
113+ * with backticked field and method names. Deterministic — only emits
114+ * identifiers that appear in the parsed changes.
111115 */
112116export function formatSpeakeasySummary (
113117 changes : ParsedChange [ ] ,
@@ -117,47 +121,26 @@ export function formatSpeakeasySummary(
117121 return 'Maintenance updates and improvements.' ;
118122 }
119123
120- const maxBullets = opts . maxBullets ?? 3 ;
121124 const maxChars = opts . maxChars ?? 300 ;
122-
123- // Group changes by action type
124- const byAction = new Map < string , ParsedChange [ ] > ( ) ;
125- for ( const c of changes ) {
126- const existing = byAction . get ( c . action ) || [ ] ;
127- existing . push ( c ) ;
128- byAction . set ( c . action , existing ) ;
125+ const itemsPerVerb = Math . max ( 1 , opts . maxBullets ?? 4 ) ;
126+
127+ const sentences : string [ ] = [ ] ;
128+ for ( const { action, verb, preposition } of VERBS ) {
129+ const items = changes . filter ( ( c ) => c . action === action ) ;
130+ if ( items . length === 0 ) continue ;
131+
132+ const phrases = items . slice ( 0 , itemsPerVerb ) . map ( ( c ) => {
133+ const field = simplifyFieldPath ( c . field ) ;
134+ const method = shortMethodName ( c . method ) ;
135+ return `\`${ field } \` ${ preposition } \`${ method } \`` ;
136+ } ) ;
137+
138+ const overflow = items . length - itemsPerVerb ;
139+ const tail = overflow > 0 ? ` and ${ overflow } more` : '' ;
140+ sentences . push ( `${ verb } ${ joinList ( phrases ) } ${ tail } .` ) ;
129141 }
130142
131- // Group changes by simplified method for the intro
132- const methodNames = new Set ( changes . map ( ( c ) => shortMethodName ( c . method ) ) ) ;
133- const methodList = [ ...methodNames ] . slice ( 0 , 4 ) ;
134- const methodSuffix =
135- methodNames . size > 4 ? ` and ${ methodNames . size - 4 } more` : '' ;
136-
137- // Build intro sentence
138- const actionCounts : string [ ] = [ ] ;
139- for ( const [ action , items ] of byAction ) {
140- actionCounts . push (
141- `${ items . length } field${ items . length > 1 ? 's' : '' } ${ action } ` ,
142- ) ;
143- }
144-
145- const intro = `${ actionCounts . join ( ', ' ) } across ${ methodList . join ( ', ' ) } ${ methodSuffix } endpoints.` ;
146-
147- // Build detail bullets (up to maxBullets) using ` - ` separator format
148- const details : string [ ] = [ ] ;
149- for ( const c of changes . slice ( 0 , maxBullets ) ) {
150- const fieldName = simplifyFieldPath ( c . field ) ;
151- const method = shortMethodName ( c . method ) ;
152- details . push ( `${ c . action } ${ fieldName } in ${ method } ` ) ;
153- }
154-
155- let result = details . length > 0 ? intro + ' - ' + details . join ( ' - ' ) : intro ;
156-
157- // Capitalize first letter
158- result = result . charAt ( 0 ) . toUpperCase ( ) + result . slice ( 1 ) ;
159-
160- // Truncate if needed
143+ let result = sentences . join ( ' ' ) ;
161144
162145 if ( result . length > maxChars ) {
163146 result =
0 commit comments