diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 1e97fa5822..c66fe5b485 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -50,7 +50,7 @@ jobs: { label: "experienced contributor", minMergedPRs: 10 }, ]; const contributorTierLabels = contributorTierRules.map((rule) => rule.label); - const contributorTierColor = "C5D7A2"; + const contributorTierColor = "58BCFF"; const managedPathLabels = [ "docs", @@ -108,39 +108,39 @@ jobs: { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) }, ]; const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))]; - const modulePrefixPriority = [ - "security", - "runtime", - "gateway", + const otherLabelDisplayOrder = [ + "health", "tool", - "provider", - "channel", - "config", - "memory", "agent", - "integration", - "observability", - "onboard", + "memory", + "channel", "service", + "integration", "tunnel", - "cron", + "config", + "observability", + "docs", + "dev", + "tests", + "skills", + "skillforge", + "provider", + "runtime", + "heartbeat", "daemon", "doctor", - "health", - "heartbeat", - "skillforge", - "skills", - ]; - const pathLabelPriority = [ - ...modulePrefixPriority, - "core", + "onboard", + "cron", "ci", "dependencies", - "tests", + "gateway", + "security", + "core", "scripts", - "dev", - "docs", ]; + const modulePrefixSet = new Set(moduleNamespaceRules.map((rule) => rule.prefix)); + const modulePrefixPriority = otherLabelDisplayOrder.filter((label) => modulePrefixSet.has(label)); + const pathLabelPriority = [...otherLabelDisplayOrder]; const riskDisplayOrder = ["risk: high", "risk: medium", "risk: low", "risk: manual"]; const sizeDisplayOrder = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; const contributorDisplayOrder = [ @@ -164,44 +164,47 @@ jobs: contributorDisplayOrder.map((label, index) => [label, index]) ); + const otherLabelColors = { + health: "74AF9E", + tool: "58A89B", + agent: "63A685", + memory: "6FAF87", + channel: "4D90E4", + service: "81B2A4", + integration: "6FAE98", + tunnel: "83AD97", + config: "8FA3B8", + observability: "63A9B1", + docs: "6E9FC7", + dev: "9EA8B2", + tests: "86B1B0", + skills: "A3B08F", + skillforge: "AEA98A", + provider: "756AD8", + runtime: "8794BF", + heartbeat: "A1AE85", + daemon: "AAA680", + doctor: "A5B88D", + onboard: "B8A66F", + cron: "B89E7E", + ci: "9098B7", + dependencies: "8298C7", + gateway: "9B8DD3", + security: "CD6B63", + core: "AE8F7D", + scripts: "AE9C8A", + }; const staticLabelColors = { - "size: XS": "E9F0F3", - "size: S": "DDE8EE", - "size: M": "CEDBE4", - "size: L": "BDCEDB", - "size: XL": "AEBFCD", - "risk: low": "B8D8B0", - "risk: medium": "E2D391", - "risk: high": "E0A090", - "risk: manual": "B7AFCF", - docs: "B7CAD6", - dependencies: "D8C99A", - ci: "AFA2CF", - core: "4A4F4A", - agent: "9FC4B8", - channel: "AFC4D6", - config: "C3BCD8", - cron: "C7D6A5", - daemon: "7C7F95", - doctor: "A8D6CD", - gateway: "D8A58F", - health: "A7DCCB", - heartbeat: "B7ACE0", - integration: "8CAFC4", - memory: "7F96B2", - observability: "6D7482", - onboard: "E6E0C8", - provider: "8A7896", - runtime: "8E88AF", - security: "D99084", - service: "B3C7D6", - skillforge: "B9B2DA", - skills: "C8C2E0", - tool: "9BCFBF", - tunnel: "8DAEC0", - tests: "DCE9EE", - scripts: "E7DFC6", - dev: "C4D3DE", + "size: XS": "D6B1B7", + "size: S": "CFA0A8", + "size: M": "C78F98", + "size: L": "BF7E89", + "size: XL": "B66E7A", + "risk: low": "7CBB8A", + "risk: medium": "D8B46B", + "risk: high": "D96A6A", + "risk: manual": "8E7CC3", + ...otherLabelColors, }; const staticLabelDescriptions = { "size: XS": "Auto size: <=80 non-doc changed lines.", @@ -250,29 +253,12 @@ jobs: } } - const modulePrefixColors = { - "agent:": "9FC4B8", - "channel:": "AFC4D6", - "config:": "C3BCD8", - "cron:": "C7D6A5", - "daemon:": "7C7F95", - "doctor:": "A8D6CD", - "gateway:": "D8A58F", - "health:": "A7DCCB", - "heartbeat:": "B7ACE0", - "integration:": "8CAFC4", - "memory:": "7F96B2", - "observability:": "6D7482", - "onboard:": "E6E0C8", - "provider:": "8A7896", - "runtime:": "8E88AF", - "security:": "D99084", - "service:": "B3C7D6", - "skillforge:": "B9B2DA", - "skills:": "C8C2E0", - "tool:": "9BCFBF", - "tunnel:": "8DAEC0", - }; + const modulePrefixColors = Object.fromEntries( + modulePrefixPriority.map((prefix) => [ + `${prefix}:`, + otherLabelColors[prefix] || "BFDADC", + ]) + ); const providerKeywordHints = [ "deepseek", @@ -418,6 +404,48 @@ jobs: return refined; } + function compactNoisyModuleLabels(labels) { + const noisyPrefixes = new Set(["tool", "provider", "channel"]); + const groupedSegments = new Map(); + const compacted = new Set(); + const forcePathPrefixes = new Set(); + + for (const label of labels) { + const parsed = parseModuleLabel(label); + if (!parsed) continue; + if (!groupedSegments.has(parsed.prefix)) { + groupedSegments.set(parsed.prefix, new Set()); + } + groupedSegments.get(parsed.prefix).add(parsed.segment); + } + + for (const label of labels) { + const parsed = parseModuleLabel(label); + if (!parsed) continue; + if (!noisyPrefixes.has(parsed.prefix)) { + compacted.add(label); + } + } + + for (const [prefix, segments] of groupedSegments) { + if (!noisyPrefixes.has(prefix)) continue; + + const specificSegments = [...segments].filter((segment) => segment !== "core"); + const uniqueSpecificSegments = [...new Set(specificSegments)]; + + if (uniqueSpecificSegments.length === 1) { + compacted.add(`${prefix}:${uniqueSpecificSegments[0]}`); + } else { + forcePathPrefixes.add(prefix); + } + } + + return { + moduleLabels: compacted, + forcePathPrefixes, + }; + } + function colorForLabel(label) { if (staticLabelColors[label]) return staticLabelColors[label]; const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix)); @@ -558,8 +586,11 @@ jobs: } const refinedModuleLabels = refineModuleLabels(detectedModuleLabels); + const compactedModuleState = compactNoisyModuleLabels(refinedModuleLabels); + const selectedModuleLabels = compactedModuleState.moduleLabels; + const forcePathPrefixes = compactedModuleState.forcePathPrefixes; const modulePrefixesWithLabels = new Set( - [...refinedModuleLabels] + [...selectedModuleLabels] .map((label) => parseModuleLabel(label)?.prefix) .filter(Boolean) ); @@ -571,9 +602,11 @@ jobs: }); const currentLabelNames = currentLabels.map((label) => label.name); const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label)); + const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]); - const dedupedPathLabels = currentPathLabels.filter((label) => { + const dedupedPathLabels = [...candidatePathLabels].filter((label) => { if (label === "core") return true; + if (forcePathPrefixes.has(label)) return true; return !modulePrefixesWithLabels.has(label); }); @@ -627,7 +660,7 @@ jobs: manualRiskOverrideLabel, ...managedPathLabels, ...contributorTierLabels, - ...refinedModuleLabels, + ...selectedModuleLabels, ]); for (const label of labelsToEnsure) { @@ -663,7 +696,7 @@ jobs: const manualRiskSelection = currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel; - const moduleLabelList = sortModuleLabels([...refinedModuleLabels]); + const moduleLabelList = sortModuleLabels([...selectedModuleLabels]); const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : []; const selectedRiskLabels = hasManualRiskOverride ? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex) diff --git a/docs/ci-map.md b/docs/ci-map.md index 00711b3d23..d3880b5a2b 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -31,8 +31,10 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u - Additional behavior: label descriptions are auto-managed as hover tooltips to explain each auto-judgment rule - Additional behavior: provider-related keywords in provider/config/onboard/integration changes are promoted to `provider:*` labels (for example `provider:kimi`, `provider:deepseek`) - Additional behavior: hierarchical de-duplication keeps only the most specific scope labels (for example `tool:composio` suppresses `tool:core` and `tool`) + - Additional behavior: noisy namespaces (`tool`, `provider`, `channel`) are compacted — one specific module keeps `prefix:component`; multiple specifics collapse to just `prefix` - Additional behavior: applies contributor tiers on PRs by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50) - Additional behavior: final label set is priority-sorted (`risk:*` first, then `size:*`, then contributor tier, then module/path labels) + - Additional behavior: managed label colors follow display order to produce a smooth left-to-right gradient when many labels are present - Additional behavior: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection - High-risk heuristic paths: `src/security/**`, `src/runtime/**`, `src/gateway/**`, `src/tools/**`, `.github/workflows/**` - Guardrail: maintainers can apply `risk: manual` to freeze automated risk recalculation diff --git a/docs/pr-workflow.md b/docs/pr-workflow.md index 753d44d650..b2cb4ea0c6 100644 --- a/docs/pr-workflow.md +++ b/docs/pr-workflow.md @@ -50,8 +50,10 @@ Maintain these branch protection rules on `main`: - Contributor opens PR with full `.github/pull_request_template.md`. - `PR Labeler` applies scope/path labels + size labels + risk labels + module labels (for example `channel:telegram`, `provider:kimi`, `tool:shell`) and contributor tiers by merged PR count (`experienced` >=10, `principal` >=20, `distinguished` >=50), while de-duplicating less-specific scope labels when a more specific module label is present. +- For `tool` / `provider` / `channel`, module labels are compacted to reduce noise: one specific module keeps `prefix:component`, but multiple specifics collapse to the base scope label. - Label ordering is priority-first: `risk:*` -> `size:*` -> contributor tier -> module/path labels. - Hovering a label in GitHub shows its auto-managed description (rule/threshold summary). +- Managed label colors are arranged by display order to create a smooth gradient across long label rows. - `Auto Response` posts first-time guidance and handles label-driven routing for low-signal items. ### Step B: Validation diff --git a/src/channels/mod.rs b/src/channels/mod.rs index f0399da01f..218ee23593 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -1783,3 +1783,5 @@ mod tests { assert!(calls.load(Ordering::SeqCst) >= 1); } } + +// color preview seed on main diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 4164fff081..5ad1907291 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -845,3 +845,5 @@ mod tests { assert_eq!(result, input); } } + +// color preview seed on main diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 95660b3937..6933b0792a 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -358,3 +358,5 @@ mod tests { assert!(!names.contains(&"delegate")); } } + +// color preview seed on main