Skip to content

Commit f182f08

Browse files
authored
Merge pull request #830 from Wibias/feat/issue-area-labels
feat(issues): add orthogonal area labels for triage
2 parents 6d54e5f + cc0ac77 commit f182f08

8 files changed

Lines changed: 682 additions & 10 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ body:
3535
- Dashboard
3636
- Provider adapter
3737
- Authentication and account pool
38+
- Catalog / models
39+
- Streaming
40+
- Tools / MCP / web search
3841
- Installation or packaging
3942
- Service lifecycle
43+
- Platform (Windows / macOS / Linux)
4044
- Documentation
4145
- Other
4246
validations:

.github/ISSUE_TEMPLATE/feature_request.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ body:
1919
- Dashboard
2020
- Provider adapters
2121
- Authentication and account pool
22+
- Catalog / models
23+
- Streaming
24+
- Tools / MCP / web search
2225
- Installation or packaging
2326
- Service lifecycle
27+
- Platform (Windows / macOS / Linux)
2428
- Documentation
2529
- Multiple areas
2630
- Other

.github/ISSUE_TEMPLATE/provider_compatibility.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Provider or API compatibility
22
description: Report an incompatible provider, endpoint, request shape, response shape, or client integration.
33
labels:
44
- provider-compatibility
5+
- provider
56
body:
67
- type: markdown
78
attributes:

.github/scripts/issue-quality.cjs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,184 @@ 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+
// Do not map to kind label `documentation` — that collides with labelBasedKind
320+
// when a feature/bug form picks Area: Documentation. Docs form already seeds
321+
// the kind label; Area selection alone does not add an area tag.
322+
documentation: [],
323+
// No dedicated label; heuristics still run in detectAreaLabels.
324+
"multiple areas": [],
325+
other: [],
326+
};
327+
328+
/** Body headings used for area heuristics (excludes Environment / OS metadata). */
329+
const AREA_HEURISTIC_BODY_HEADINGS = [
330+
"Summary",
331+
"Reproduction",
332+
"What are you trying to accomplish?",
333+
"What prevents this today?",
334+
"What should OpenCodex do?",
335+
"Example usage or interface",
336+
"Current behaviour",
337+
"Expected behaviour",
338+
"Minimal redacted request or reproduction",
339+
"What is wrong or missing?",
340+
"Documentation problem type",
341+
"Documentation location",
342+
];
343+
344+
/**
345+
* Heuristic rules. `scope: "title"` avoids false hits from template Environment /
346+
* OS fields in the body; `scope: "full"` is for distinctive technical tokens.
347+
*/
348+
const AREA_HEURISTICS = [
349+
{
350+
label: "account-pool",
351+
scope: "full",
352+
re: /\b(oauth|reauth|needsreauth|account pool|codex.?auth|auto[- ]?switch|account failover|refresh token|plan_type|chatgpt[- ]account|reset credit)\b/i,
353+
},
354+
{
355+
label: "account-pool",
356+
scope: "title",
357+
re: /\b(quota|failover|pool account|account switch)\b/i,
358+
},
359+
{
360+
label: "catalog",
361+
scope: "full",
362+
re: /\b(model catalog|opencodex-catalog|model list|model visibility|virtual model|routed (catalog|entries|slug)|model slug)\b/i,
363+
},
364+
{
365+
label: "catalog",
366+
scope: "title",
367+
re: /\bcatalog\b/i,
368+
},
369+
{
370+
label: "gui",
371+
scope: "title",
372+
re: /\b(dashboard|\bgui\b|tray|sidebar|settings (page|tab|ui))\b/i,
373+
},
374+
{
375+
label: "cli",
376+
scope: "title",
377+
re: /\b(ocx\b|config\.toml|config inject)\b/i,
378+
},
379+
{
380+
label: "proxy",
381+
scope: "full",
382+
re: /\b(reverse[- ]proxy|management api|admin[- ]token|\/api\/\*|bind(s)? the (old )?port)\b/i,
383+
},
384+
{
385+
label: "proxy",
386+
scope: "title",
387+
re: /\b(reverse[- ]proxy|management api|admin[- ]token)\b/i,
388+
},
389+
{
390+
label: "platform",
391+
scope: "full",
392+
re: /\b(winsw|launchd|schtasks|icacls|windows-latest|tray host|scheduler backend)\b/i,
393+
},
394+
{
395+
label: "platform",
396+
scope: "title",
397+
re: /\b(\[windows\]|\[macos\]|windows|macos|darwin|win32|wsl)\b/i,
398+
},
399+
{
400+
label: "streaming",
401+
scope: "full",
402+
re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\btruncat\w*|stream(ing)?\b.{0,40}\bterminal\b|terminal (sse )?frame|without a terminal)\b/i,
403+
},
404+
{
405+
label: "tools",
406+
scope: "full",
407+
re: /\b(tool_calls?|tool[- ]calls?|\bmcp\b|web[- ]search|tool[- ]recall)\b/i,
408+
},
409+
{
410+
label: "install",
411+
scope: "full",
412+
re: /\b(npm (global )?install|packaging|release asset|npx ocx)\b/i,
413+
},
414+
{
415+
label: "service",
416+
scope: "full",
417+
re: /\b(ocx service|winsw|scheduler backend|launchd service)\b/i,
418+
},
419+
{
420+
label: "provider",
421+
scope: "full",
422+
re: /\b(provider adapter|openai[- ]compatible|provider[- ]compat|adapter quirk|built[- ]in provider|provider preset)\b/i,
423+
},
424+
{
425+
label: "provider",
426+
scope: "title",
427+
re: /\b(\[provider\]|provider compat|openai[- ]compatible)\b/i,
428+
},
429+
];
430+
253431
/**
254432
* Map a detected issue kind to its triage label. Returns null when unknown.
255433
*/
@@ -258,6 +436,99 @@ function labelForKind(kind) {
258436
return KIND_TO_LABEL[kind] || null;
259437
}
260438

439+
/**
440+
* Map a template Area dropdown value to orthogonal area label names.
441+
* Returns [] for Other / Multiple areas / unknown / empty.
442+
*
443+
* @param {unknown} areaText
444+
* @returns {string[]}
445+
*/
446+
function mapAreaFieldToLabels(areaText) {
447+
if (typeof areaText !== "string") return [];
448+
const key = areaText.replace(/\s+/g, " ").trim().toLowerCase();
449+
if (!key) return [];
450+
return AREA_FIELD_TO_LABELS[key] ? [...AREA_FIELD_TO_LABELS[key]] : [];
451+
}
452+
453+
/**
454+
* Build heuristic text from title-relevant semantic sections only — never from
455+
* Operating system / Version / Checks metadata that every template includes.
456+
*
457+
* @param {string} body
458+
* @returns {string}
459+
*/
460+
function bodyForAreaHeuristics(body) {
461+
if (typeof body !== "string" || !body.trim()) return "";
462+
const parts = [];
463+
for (const heading of AREA_HEURISTIC_BODY_HEADINGS) {
464+
const section = extractSection(body, heading);
465+
if (section) parts.push(section);
466+
}
467+
return parts.join("\n\n");
468+
}
469+
470+
/**
471+
* Conservative title/body heuristics for orthogonal area labels.
472+
*
473+
* @param {string} title
474+
* @param {string} body semantic body text (already filtered)
475+
* @returns {string[]}
476+
*/
477+
function heuristicAreaLabels(title, body) {
478+
const titleText = title || "";
479+
const fullText = `${titleText}\n${body || ""}`;
480+
const seen = new Set();
481+
const out = [];
482+
for (const { label, re, scope } of AREA_HEURISTICS) {
483+
const text = scope === "title" ? titleText : fullText;
484+
if (!re.test(text) || seen.has(label)) continue;
485+
seen.add(label);
486+
out.push(label);
487+
}
488+
return out;
489+
}
490+
491+
/**
492+
* Detect additive product-area labels from Area field, form defaults, and
493+
* title/body heuristics. Never invents per-provider labels.
494+
*
495+
* @param {{
496+
* title?: string,
497+
* body?: string,
498+
* labels?: string[],
499+
* heuristicBody?: string,
500+
* }} issue
501+
* `body` is the source form (for Area / provider headings).
502+
* `heuristicBody` may include English translation text for heuristics only.
503+
* @returns {string[]}
504+
*/
505+
function detectAreaLabels(issue) {
506+
const title = typeof issue?.title === "string" ? issue.title : "";
507+
const body = typeof issue?.body === "string" ? issue.body : "";
508+
const labels = Array.isArray(issue?.labels) ? issue.labels : [];
509+
const heuristicSource = typeof issue?.heuristicBody === "string" ? issue.heuristicBody : body;
510+
511+
const areaSection = extractSection(body, "Area");
512+
const fromArea = mapAreaFieldToLabels(areaSection);
513+
const fromHeur = heuristicAreaLabels(title, bodyForAreaHeuristics(heuristicSource));
514+
const fromForm = [];
515+
if (labels.includes("provider-compatibility")) fromForm.push("provider");
516+
// Provider-compat form uses this heading instead of Area.
517+
if (extractSection(body, "Provider or upstream service") !== null) {
518+
fromForm.push("provider");
519+
}
520+
521+
const seen = new Set();
522+
const out = [];
523+
for (const label of [...fromArea, ...fromForm, ...fromHeur]) {
524+
if (!label || seen.has(label)) continue;
525+
if (!AREA_LABELS[label]) continue;
526+
seen.add(label);
527+
out.push(label);
528+
}
529+
return out;
530+
}
531+
261532
function countHeadings(body, headings) {
262533
let n = 0;
263534
for (const h of headings) {
@@ -952,6 +1223,12 @@ module.exports = {
9521223
hasConcreteDetail,
9531224
labelForKind,
9541225
KIND_TO_LABEL,
1226+
AREA_LABELS,
1227+
AREA_FIELD_TO_LABELS,
1228+
mapAreaFieldToLabels,
1229+
bodyForAreaHeuristics,
1230+
heuristicAreaLabels,
1231+
detectAreaLabels,
9551232
hasSubstantialStructuredContent,
9561233
rejectsWorkflowDispatchPullRequest,
9571234
rejectsWorkflowDispatchNonDefaultBranch,

0 commit comments

Comments
 (0)