@@ -250,6 +250,125 @@ const KIND_TO_LABEL = {
250250 "provider-compatibility" : "provider-compatibility" ,
251251} ;
252252
253+ /**
254+ * Orthogonal product-area labels (additive beside kind/process labels).
255+ * Colors/descriptions are used when the workflow ensures labels exist.
256+ */
257+ const AREA_LABELS = {
258+ provider : {
259+ color : "1D76DB" ,
260+ description : "Provider adapters, OpenAI-compat presets, upstream API quirks" ,
261+ } ,
262+ "account-pool" : {
263+ color : "5319E7" ,
264+ description : "OAuth, credentials, Codex pool, quota, failover, plans" ,
265+ } ,
266+ catalog : {
267+ color : "006B75" ,
268+ description : "Model catalog, slugs, visibility, routed entries" ,
269+ } ,
270+ gui : {
271+ color : "D93F0B" ,
272+ description : "Dashboard, tray, settings UI" ,
273+ } ,
274+ cli : {
275+ color : "FBCA04" ,
276+ description : "CLI, config inject, packaging flags" ,
277+ } ,
278+ proxy : {
279+ color : "0E8A16" ,
280+ description : "HTTP proxy, routing, reverse-proxy / management auth" ,
281+ } ,
282+ platform : {
283+ color : "BFDADC" ,
284+ description : "OS/service/tray/ACL (Windows-heavy, not Windows-only)" ,
285+ } ,
286+ streaming : {
287+ color : "C5DEF5" ,
288+ description : "SSE, WebSocket, terminal stream frames" ,
289+ } ,
290+ tools : {
291+ color : "F9D0C4" ,
292+ description : "tool_calls, MCP, web-search / sidecar tools" ,
293+ } ,
294+ install : {
295+ color : "EDEDED" ,
296+ description : "Installation or packaging" ,
297+ } ,
298+ service : {
299+ color : "EDEDED" ,
300+ description : "Service lifecycle (WinSW/launchd/scheduler)" ,
301+ } ,
302+ } ;
303+
304+ /** Canonical Area dropdown text → area label(s). Keys are lowercased. */
305+ const AREA_FIELD_TO_LABELS = {
306+ cli : [ "cli" ] ,
307+ "proxy and routing" : [ "proxy" ] ,
308+ dashboard : [ "gui" ] ,
309+ "provider adapter" : [ "provider" ] ,
310+ "provider adapters" : [ "provider" ] ,
311+ "authentication and account pool" : [ "account-pool" ] ,
312+ "catalog / models" : [ "catalog" ] ,
313+ streaming : [ "streaming" ] ,
314+ "tools / mcp / web search" : [ "tools" ] ,
315+ "installation or packaging" : [ "install" ] ,
316+ "service lifecycle" : [ "service" ] ,
317+ "service lifecycle (config injection)" : [ "service" ] ,
318+ "platform (windows / macos / linux)" : [ "platform" ] ,
319+ documentation : [ "documentation" ] ,
320+ // No dedicated label; heuristics still run in detectAreaLabels.
321+ "multiple areas" : [ ] ,
322+ other : [ ] ,
323+ } ;
324+
325+ const AREA_HEURISTICS = [
326+ {
327+ label : "account-pool" ,
328+ re : / \b ( o a u t h | r e a u t h | n e e d s r e a u t h | q u o t a | a c c o u n t p o o l | c o d e x .? a u t h | a u t o [ - ] ? s w i t c h | f a i l o v e r | r e f r e s h t o k e n | p l a n _ t y p e | c h a t g p t [ - ] a c c o u n t | r e s e t c r e d i t ) \b / i,
329+ } ,
330+ {
331+ label : "catalog" ,
332+ re : / \b ( c a t a l o g | m o d e l l i s t | m o d e l v i s i b i l i t y | v i r t u a l m o d e l | r o u t e d ( c a t a l o g | e n t r i e s | s l u g ) | m o d e l s l u g ) \b / i,
333+ } ,
334+ {
335+ label : "gui" ,
336+ re : / \b ( d a s h b o a r d | \b g u i \b | t r a y | s i d e b a r | s e t t i n g s t a b ) \b / i,
337+ } ,
338+ {
339+ label : "cli" ,
340+ re : / \b ( o c x \b | c o n f i g \. t o m l | c o n f i g i n j e c t ) \b / i,
341+ } ,
342+ {
343+ label : "proxy" ,
344+ re : / \b ( r e v e r s e [ - ] p r o x y | m a n a g e m e n t a p i | a d m i n [ - ] t o k e n | \/ a p i \/ \* | b i n d ( s ) ? t h e ( o l d ) ? p o r t | p r o x y r u n n i n g ) \b / i,
345+ } ,
346+ {
347+ label : "platform" ,
348+ re : / \b ( w i n d o w s | m a c o s | m a c o s | w i n 3 2 | d a r w i n | w s l | w i n s w | l a u n c h d | s y s t e m d | i c a c l s | \b a c l \b ) \b / i,
349+ } ,
350+ {
351+ label : "streaming" ,
352+ re : / \b ( s s e | w e b s o c k e t | \b w s \b | s t r e a m ( i n g ) ? \b .{ 0 , 40 } \b ( t r u n c a t | t e r m i n a l ) | t e r m i n a l ( s s e ) ? f r a m e | w i t h o u t a t e r m i n a l ) \b / i,
353+ } ,
354+ {
355+ label : "tools" ,
356+ re : / \b ( t o o l _ c a l l s ? | t o o l [ - ] c a l l s ? | \b m c p \b | w e b [ - ] s e a r c h | t o o l [ - ] r e c a l l ) \b / i,
357+ } ,
358+ {
359+ label : "install" ,
360+ re : / \b ( n p m ( g l o b a l ) ? i n s t a l l | p a c k a g i n g | r e l e a s e a s s e t | n p x o c x ) \b / i,
361+ } ,
362+ {
363+ label : "service" ,
364+ re : / \b ( o c x s e r v i c e | w i n s w | s c h e d u l e r b a c k e n d | l a u n c h d s e r v i c e ) \b / i,
365+ } ,
366+ {
367+ label : "provider" ,
368+ re : / \b ( p r o v i d e r a d a p t e r | o p e n a i [ - ] c o m p a t i b l e | p r o v i d e r [ - ] c o m p a t | a d a p t e r q u i r k | u p s t r e a m a p i | b u i l t [ - ] i n p r o v i d e r | p r o v i d e r p r e s e t ) \b / i,
369+ } ,
370+ ] ;
371+
253372/**
254373 * Map a detected issue kind to its triage label. Returns null when unknown.
255374 */
@@ -258,6 +377,70 @@ function labelForKind(kind) {
258377 return KIND_TO_LABEL [ kind ] || null ;
259378}
260379
380+ /**
381+ * Map a template Area dropdown value to orthogonal area label names.
382+ * Returns [] for Other / Multiple areas / unknown / empty.
383+ *
384+ * @param {unknown } areaText
385+ * @returns {string[] }
386+ */
387+ function mapAreaFieldToLabels ( areaText ) {
388+ if ( typeof areaText !== "string" ) return [ ] ;
389+ const key = areaText . replace ( / \s + / g, " " ) . trim ( ) . toLowerCase ( ) ;
390+ if ( ! key ) return [ ] ;
391+ return AREA_FIELD_TO_LABELS [ key ] ? [ ...AREA_FIELD_TO_LABELS [ key ] ] : [ ] ;
392+ }
393+
394+ /**
395+ * Conservative title/body heuristics for orthogonal area labels.
396+ *
397+ * @param {string } title
398+ * @param {string } body
399+ * @returns {string[] }
400+ */
401+ function heuristicAreaLabels ( title , body ) {
402+ const text = `${ title || "" } \n${ body || "" } ` ;
403+ const out = [ ] ;
404+ for ( const { label, re } of AREA_HEURISTICS ) {
405+ if ( re . test ( text ) ) out . push ( label ) ;
406+ }
407+ return out ;
408+ }
409+
410+ /**
411+ * Detect additive product-area labels from Area field, form defaults, and
412+ * title/body heuristics. Never invents per-provider labels.
413+ *
414+ * @param {{ title?: string, body?: string, labels?: string[] } } issue
415+ * @returns {string[] }
416+ */
417+ function detectAreaLabels ( issue ) {
418+ const title = typeof issue ?. title === "string" ? issue . title : "" ;
419+ const body = typeof issue ?. body === "string" ? issue . body : "" ;
420+ const labels = Array . isArray ( issue ?. labels ) ? issue . labels : [ ] ;
421+
422+ const areaSection = extractSection ( body , "Area" ) ;
423+ const fromArea = mapAreaFieldToLabels ( areaSection ) ;
424+ const fromHeur = heuristicAreaLabels ( title , body ) ;
425+ const fromForm = [ ] ;
426+ if ( labels . includes ( "provider-compatibility" ) ) fromForm . push ( "provider" ) ;
427+ // Provider-compat form uses this heading instead of Area.
428+ if ( extractSection ( body , "Provider or upstream service" ) !== null ) {
429+ fromForm . push ( "provider" ) ;
430+ }
431+
432+ const seen = new Set ( ) ;
433+ const out = [ ] ;
434+ for ( const label of [ ...fromArea , ...fromForm , ...fromHeur ] ) {
435+ if ( ! label || seen . has ( label ) ) continue ;
436+ // `documentation` is a kind label and also an Area mapping target.
437+ if ( label !== "documentation" && ! AREA_LABELS [ label ] ) continue ;
438+ seen . add ( label ) ;
439+ out . push ( label ) ;
440+ }
441+ return out ;
442+ }
443+
261444function countHeadings ( body , headings ) {
262445 let n = 0 ;
263446 for ( const h of headings ) {
@@ -952,6 +1135,11 @@ module.exports = {
9521135 hasConcreteDetail,
9531136 labelForKind,
9541137 KIND_TO_LABEL ,
1138+ AREA_LABELS ,
1139+ AREA_FIELD_TO_LABELS ,
1140+ mapAreaFieldToLabels,
1141+ heuristicAreaLabels,
1142+ detectAreaLabels,
9551143 hasSubstantialStructuredContent,
9561144 rejectsWorkflowDispatchPullRequest,
9571145 rejectsWorkflowDispatchNonDefaultBranch,
0 commit comments