diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 1e97fa5..a05b3f6 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -418,6 +418,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 +600,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 +616,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 +674,7 @@ jobs: manualRiskOverrideLabel, ...managedPathLabels, ...contributorTierLabels, - ...refinedModuleLabels, + ...selectedModuleLabels, ]); for (const label of labelsToEnsure) { @@ -663,7 +710,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 00711b3..f455ee1 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -31,6 +31,7 @@ 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: risk + size labels are auto-corrected on manual PR label edits (`labeled`/`unlabeled` events); apply `risk: manual` when maintainers intentionally override automated risk selection diff --git a/docs/pr-workflow.md b/docs/pr-workflow.md index 753d44d..c894652 100644 --- a/docs/pr-workflow.md +++ b/docs/pr-workflow.md @@ -50,6 +50,7 @@ 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). - `Auto Response` posts first-time guidance and handles label-driven routing for low-signal items.