name: PR Labeler

on:
    pull_request_target:
        types: [opened, reopened, synchronize, edited, labeled, unlabeled]
    workflow_dispatch:
        inputs:
            mode:
                description: "Run mode for managed-label governance"
                required: true
                default: "audit"
                type: choice
                options:
                    - audit
                    - repair

concurrency:
    group: pr-labeler-${{ github.event.pull_request.number || github.run_id }}
    cancel-in-progress: true

permissions:
    contents: read
    pull-requests: write
    issues: write

jobs:
    label:
        runs-on: blacksmith-2vcpu-ubuntu-2404
        timeout-minutes: 10
        steps:
            - name: Apply path labels
              if: github.event_name == 'pull_request_target'
              uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
              continue-on-error: true
              with:
                  repo-token: ${{ secrets.GITHUB_TOKEN }}
                  sync-labels: true

            - name: Apply size/risk/module labels
              uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
              continue-on-error: true
              with:
                  script: |
                      const pr = context.payload.pull_request;
                      const owner = context.repo.owner;
                      const repo = context.repo.repo;
                      const action = context.payload.action;
                      const changedLabel = context.payload.label?.name;

                      const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
                      const computedRiskLabels = ["risk: low", "risk: medium", "risk: high"];
                      const manualRiskOverrideLabel = "risk: manual";
                      const managedEnforcedLabels = new Set([
                        ...sizeLabels,
                        manualRiskOverrideLabel,
                        ...computedRiskLabels,
                      ]);
                      if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) {
                        core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`);
                        return;
                      }

                      const contributorTierRules = [
                        { label: "distinguished contributor", minMergedPRs: 50 },
                        { label: "principal contributor", minMergedPRs: 20 },
                        { label: "experienced contributor", minMergedPRs: 10 },
                        { label: "trusted contributor", minMergedPRs: 5 },
                      ];
                      const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
                      const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/auto-response.yml

                      const managedPathLabels = [
                        "docs",
                        "dependencies",
                        "ci",
                        "core",
                        "agent",
                        "channel",
                        "config",
                        "cron",
                        "daemon",
                        "doctor",
                        "gateway",
                        "health",
                        "heartbeat",
                        "integration",
                        "memory",
                        "observability",
                        "onboard",
                        "provider",
                        "runtime",
                        "security",
                        "service",
                        "skillforge",
                        "skills",
                        "tool",
                        "tunnel",
                        "tests",
                        "scripts",
                        "dev",
                      ];
                      const managedPathLabelSet = new Set(managedPathLabels);

                      const moduleNamespaceRules = [
                        { root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/channels/", prefix: "channel", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/config/", prefix: "config", coreEntries: new Set(["mod.rs", "schema.rs"]) },
                        { root: "src/cron/", prefix: "cron", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/daemon/", prefix: "daemon", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/doctor/", prefix: "doctor", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/gateway/", prefix: "gateway", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/health/", prefix: "health", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/heartbeat/", prefix: "heartbeat", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/integrations/", prefix: "integration", coreEntries: new Set(["mod.rs", "registry.rs"]) },
                        { root: "src/memory/", prefix: "memory", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/observability/", prefix: "observability", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/onboard/", prefix: "onboard", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/providers/", prefix: "provider", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/runtime/", prefix: "runtime", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/security/", prefix: "security", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/service/", prefix: "service", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/skillforge/", prefix: "skillforge", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/skills/", prefix: "skills", coreEntries: new Set(["mod.rs"]) },
                        { root: "src/tools/", prefix: "tool", coreEntries: new Set(["mod.rs", "traits.rs"]) },
                        { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) },
                      ];
                      const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))];
                      const orderedOtherLabelStyles = [
                        { label: "health", color: "8EC9B8" },
                        { label: "tool", color: "7FC4B6" },
                        { label: "agent", color: "86C4A2" },
                        { label: "memory", color: "8FCB99" },
                        { label: "channel", color: "7EB6F2" },
                        { label: "service", color: "95C7B6" },
                        { label: "integration", color: "8DC9AE" },
                        { label: "tunnel", color: "9FC8B3" },
                        { label: "config", color: "AABCD0" },
                        { label: "observability", color: "84C9D0" },
                        { label: "docs", color: "8FBBE0" },
                        { label: "dev", color: "B9C1CC" },
                        { label: "tests", color: "9DC8C7" },
                        { label: "skills", color: "BFC89B" },
                        { label: "skillforge", color: "C9C39B" },
                        { label: "provider", color: "958DF0" },
                        { label: "runtime", color: "A3ADD8" },
                        { label: "heartbeat", color: "C0C88D" },
                        { label: "daemon", color: "C8C498" },
                        { label: "doctor", color: "C1CF9D" },
                        { label: "onboard", color: "D2BF86" },
                        { label: "cron", color: "D2B490" },
                        { label: "ci", color: "AEB4CE" },
                        { label: "dependencies", color: "9FB1DE" },
                        { label: "gateway", color: "B5A8E5" },
                        { label: "security", color: "E58D85" },
                        { label: "core", color: "C8A99B" },
                        { label: "scripts", color: "C9B49F" },
                      ];
                      const otherLabelDisplayOrder = orderedOtherLabelStyles.map((entry) => entry.label);
                      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 = [
                        "distinguished contributor",
                        "principal contributor",
                        "experienced contributor",
                        "trusted contributor",
                      ];
                      const modulePrefixPriorityIndex = new Map(
                        modulePrefixPriority.map((prefix, index) => [prefix, index])
                      );
                      const pathLabelPriorityIndex = new Map(
                        pathLabelPriority.map((label, index) => [label, index])
                      );
                      const riskPriorityIndex = new Map(
                        riskDisplayOrder.map((label, index) => [label, index])
                      );
                      const sizePriorityIndex = new Map(
                        sizeDisplayOrder.map((label, index) => [label, index])
                      );
                      const contributorPriorityIndex = new Map(
                        contributorDisplayOrder.map((label, index) => [label, index])
                      );

                      const otherLabelColors = Object.fromEntries(
                        orderedOtherLabelStyles.map((entry) => [entry.label, entry.color])
                      );
                      const staticLabelColors = {
                        "size: XS": "E7CDD3",
                        "size: S": "E1BEC7",
                        "size: M": "DBB0BB",
                        "size: L": "D4A2AF",
                        "size: XL": "CE94A4",
                        "risk: low": "97D3A6",
                        "risk: medium": "E4C47B",
                        "risk: high": "E98E88",
                        "risk: manual": "B7A4E0",
                        ...otherLabelColors,
                      };
                      const staticLabelDescriptions = {
                        "size: XS": "Auto size: <=80 non-doc changed lines.",
                        "size: S": "Auto size: 81-250 non-doc changed lines.",
                        "size: M": "Auto size: 251-500 non-doc changed lines.",
                        "size: L": "Auto size: 501-1000 non-doc changed lines.",
                        "size: XL": "Auto size: >1000 non-doc changed lines.",
                        "risk: low": "Auto risk: docs/chore-only paths.",
                        "risk: medium": "Auto risk: src/** or dependency/config changes.",
                        "risk: high": "Auto risk: security/runtime/gateway/tools/workflows.",
                        "risk: manual": "Maintainer override: keep selected risk label.",
                        docs: "Auto scope: docs/markdown/template files changed.",
                        dependencies: "Auto scope: dependency manifest/lock/policy changed.",
                        ci: "Auto scope: CI/workflow/hook files changed.",
                        core: "Auto scope: root src/*.rs files changed.",
                        agent: "Auto scope: src/agent/** changed.",
                        channel: "Auto scope: src/channels/** changed.",
                        config: "Auto scope: src/config/** changed.",
                        cron: "Auto scope: src/cron/** changed.",
                        daemon: "Auto scope: src/daemon/** changed.",
                        doctor: "Auto scope: src/doctor/** changed.",
                        gateway: "Auto scope: src/gateway/** changed.",
                        health: "Auto scope: src/health/** changed.",
                        heartbeat: "Auto scope: src/heartbeat/** changed.",
                        integration: "Auto scope: src/integrations/** changed.",
                        memory: "Auto scope: src/memory/** changed.",
                        observability: "Auto scope: src/observability/** changed.",
                        onboard: "Auto scope: src/onboard/** changed.",
                        provider: "Auto scope: src/providers/** changed.",
                        runtime: "Auto scope: src/runtime/** changed.",
                        security: "Auto scope: src/security/** changed.",
                        service: "Auto scope: src/service/** changed.",
                        skillforge: "Auto scope: src/skillforge/** changed.",
                        skills: "Auto scope: src/skills/** changed.",
                        tool: "Auto scope: src/tools/** changed.",
                        tunnel: "Auto scope: src/tunnel/** changed.",
                        tests: "Auto scope: tests/** changed.",
                        scripts: "Auto scope: scripts/** changed.",
                        dev: "Auto scope: dev/** changed.",
                      };
                      for (const label of contributorTierLabels) {
                        staticLabelColors[label] = contributorTierColor;
                        const rule = contributorTierRules.find((entry) => entry.label === label);
                        if (rule) {
                          staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
                        }
                      }

                      const modulePrefixColors = Object.fromEntries(
                        modulePrefixPriority.map((prefix) => [
                          `${prefix}:`,
                          otherLabelColors[prefix] || "BFDADC",
                        ])
                      );

                      const providerKeywordHints = [
                        "deepseek",
                        "moonshot",
                        "kimi",
                        "qwen",
                        "mistral",
                        "doubao",
                        "baichuan",
                        "yi",
                        "siliconflow",
                        "vertex",
                        "azure",
                        "perplexity",
                        "venice",
                        "vercel",
                        "cloudflare",
                        "synthetic",
                        "opencode",
                        "zai",
                        "glm",
                        "minimax",
                        "bedrock",
                        "qianfan",
                        "groq",
                        "together",
                        "fireworks",
                        "cohere",
                        "openai",
                        "openrouter",
                        "anthropic",
                        "gemini",
                        "ollama",
                      ];

                      const channelKeywordHints = [
                        "telegram",
                        "discord",
                        "slack",
                        "whatsapp",
                        "matrix",
                        "irc",
                        "imessage",
                        "email",
                        "cli",
                      ];

                      function isDocsLike(path) {
                        return (
                          path.startsWith("docs/") ||
                          path.endsWith(".md") ||
                          path.endsWith(".mdx") ||
                          path === "LICENSE" ||
                          path === ".markdownlint-cli2.yaml" ||
                          path === ".github/pull_request_template.md" ||
                          path.startsWith(".github/ISSUE_TEMPLATE/")
                        );
                      }

                      function normalizeLabelSegment(segment) {
                        return (segment || "")
                          .toLowerCase()
                          .replace(/\.rs$/g, "")
                          .replace(/[^a-z0-9_-]+/g, "-")
                          .replace(/^[-_]+|[-_]+$/g, "")
                          .slice(0, 40);
                      }

                      function containsKeyword(text, keyword) {
                        const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
                        const pattern = new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i");
                        return pattern.test(text);
                      }

                      function parseModuleLabel(label) {
                        const separatorIndex = label.indexOf(":");
                        if (separatorIndex <= 0 || separatorIndex >= label.length - 1) return null;
                        return {
                          prefix: label.slice(0, separatorIndex),
                          segment: label.slice(separatorIndex + 1),
                        };
                      }

                      function sortByPriority(labels, priorityIndex) {
                        return [...new Set(labels)].sort((left, right) => {
                          const leftPriority = priorityIndex.has(left) ? priorityIndex.get(left) : Number.MAX_SAFE_INTEGER;
                          const rightPriority = priorityIndex.has(right)
                            ? priorityIndex.get(right)
                            : Number.MAX_SAFE_INTEGER;
                          if (leftPriority !== rightPriority) return leftPriority - rightPriority;
                          return left.localeCompare(right);
                        });
                      }

                      function sortModuleLabels(labels) {
                        return [...new Set(labels)].sort((left, right) => {
                          const leftParsed = parseModuleLabel(left);
                          const rightParsed = parseModuleLabel(right);
                          if (!leftParsed || !rightParsed) return left.localeCompare(right);

                          const leftPrefixPriority = modulePrefixPriorityIndex.has(leftParsed.prefix)
                            ? modulePrefixPriorityIndex.get(leftParsed.prefix)
                            : Number.MAX_SAFE_INTEGER;
                          const rightPrefixPriority = modulePrefixPriorityIndex.has(rightParsed.prefix)
                            ? modulePrefixPriorityIndex.get(rightParsed.prefix)
                            : Number.MAX_SAFE_INTEGER;

                          if (leftPrefixPriority !== rightPrefixPriority) {
                            return leftPrefixPriority - rightPrefixPriority;
                          }
                          if (leftParsed.prefix !== rightParsed.prefix) {
                            return leftParsed.prefix.localeCompare(rightParsed.prefix);
                          }

                          const leftIsCore = leftParsed.segment === "core";
                          const rightIsCore = rightParsed.segment === "core";
                          if (leftIsCore !== rightIsCore) return leftIsCore ? 1 : -1;

                          return leftParsed.segment.localeCompare(rightParsed.segment);
                        });
                      }

                      function refineModuleLabels(rawLabels) {
                        const refined = new Set(rawLabels);
                        const segmentsByPrefix = new Map();

                        for (const label of rawLabels) {
                          const parsed = parseModuleLabel(label);
                          if (!parsed) continue;
                          if (!segmentsByPrefix.has(parsed.prefix)) {
                            segmentsByPrefix.set(parsed.prefix, new Set());
                          }
                          segmentsByPrefix.get(parsed.prefix).add(parsed.segment);
                        }

                        for (const [prefix, segments] of segmentsByPrefix) {
                          const hasSpecificSegment = [...segments].some((segment) => segment !== "core");
                          if (hasSpecificSegment) {
                            refined.delete(`${prefix}:core`);
                          }
                        }

                        return refined;
                      }

                      function compactModuleLabels(labels) {
                        const groupedSegments = new Map();
                        const compactedModuleLabels = new Set();
                        const forcePathPrefixes = new Set();

                        for (const label of labels) {
                          const parsed = parseModuleLabel(label);
                          if (!parsed) {
                            compactedModuleLabels.add(label);
                            continue;
                          }
                          if (!groupedSegments.has(parsed.prefix)) {
                            groupedSegments.set(parsed.prefix, new Set());
                          }
                          groupedSegments.get(parsed.prefix).add(parsed.segment);
                        }

                        for (const [prefix, segments] of groupedSegments) {
                          const uniqueSegments = [...new Set([...segments].filter(Boolean))];
                          if (uniqueSegments.length === 0) continue;

                          if (uniqueSegments.length === 1) {
                            compactedModuleLabels.add(`${prefix}:${uniqueSegments[0]}`);
                          } else {
                            forcePathPrefixes.add(prefix);
                          }
                        }

                        return {
                          moduleLabels: compactedModuleLabels,
                          forcePathPrefixes,
                        };
                      }

                      function colorForLabel(label) {
                        if (staticLabelColors[label]) return staticLabelColors[label];
                        const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix));
                        if (matchedPrefix) return modulePrefixColors[matchedPrefix];
                        return "BFDADC";
                      }

                      function descriptionForLabel(label) {
                        if (staticLabelDescriptions[label]) return staticLabelDescriptions[label];

                        const parsed = parseModuleLabel(label);
                        if (parsed) {
                          if (parsed.segment === "core") {
                            return `Auto module: ${parsed.prefix} core files changed.`;
                          }
                          return `Auto module: ${parsed.prefix}/${parsed.segment} changed.`;
                        }

                        return "Auto-managed label.";
                      }

                      async function ensureLabel(name, existing = null) {
                        const expectedColor = colorForLabel(name);
                        const expectedDescription = descriptionForLabel(name);
                        try {
                          const current = existing || (await github.rest.issues.getLabel({ owner, repo, name })).data;
                          const currentColor = (current.color || "").toUpperCase();
                          const currentDescription = (current.description || "").trim();
                          if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
                            await github.rest.issues.updateLabel({
                              owner,
                              repo,
                              name,
                              new_name: name,
                              color: expectedColor,
                              description: expectedDescription,
                            });
                          }
                        } catch (error) {
                          if (error.status !== 404) throw error;
                          await github.rest.issues.createLabel({
                            owner,
                            repo,
                            name,
                            color: expectedColor,
                            description: expectedDescription,
                          });
                        }
                      }

                      function isManagedLabel(label) {
                        if (label === manualRiskOverrideLabel) return true;
                        if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return true;
                        if (managedPathLabelSet.has(label)) return true;
                        if (contributorTierLabels.includes(label)) return true;
                        if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return true;
                        return false;
                      }

                      async function ensureManagedRepoLabelsMetadata() {
                        const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
                          owner,
                          repo,
                          per_page: 100,
                        });

                        for (const existingLabel of repoLabels) {
                          const labelName = existingLabel.name || "";
                          if (!isManagedLabel(labelName)) continue;
                          await ensureLabel(labelName, existingLabel);
                        }
                      }

                      function selectContributorTier(mergedCount) {
                        const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
                        return matchedTier ? matchedTier.label : null;
                      }

                      if (context.eventName === "workflow_dispatch") {
                        const mode = (context.payload.inputs?.mode || "audit").toLowerCase();
                        const shouldRepair = mode === "repair";
                        const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
                          owner,
                          repo,
                          per_page: 100,
                        });

                        let managedScanned = 0;
                        const drifts = [];

                        for (const existingLabel of repoLabels) {
                          const labelName = existingLabel.name || "";
                          if (!isManagedLabel(labelName)) continue;
                          managedScanned += 1;

                          const expectedColor = colorForLabel(labelName);
                          const expectedDescription = descriptionForLabel(labelName);
                          const currentColor = (existingLabel.color || "").toUpperCase();
                          const currentDescription = (existingLabel.description || "").trim();
                          if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
                            drifts.push({
                              name: labelName,
                              currentColor,
                              expectedColor,
                              currentDescription,
                              expectedDescription,
                            });
                            if (shouldRepair) {
                              await ensureLabel(labelName, existingLabel);
                            }
                          }
                        }

                        core.summary
                          .addHeading("Managed Label Governance", 2)
                          .addRaw(`Mode: ${shouldRepair ? "repair" : "audit"}`)
                          .addEOL()
                          .addRaw(`Managed labels scanned: ${managedScanned}`)
                          .addEOL()
                          .addRaw(`Drifts found: ${drifts.length}`)
                          .addEOL();

                        if (drifts.length > 0) {
                          const sample = drifts.slice(0, 30).map((entry) => [
                            entry.name,
                            `${entry.currentColor} -> ${entry.expectedColor}`,
                            `${entry.currentDescription || "(blank)"} -> ${entry.expectedDescription}`,
                          ]);
                          core.summary.addTable([
                            [{ data: "Label", header: true }, { data: "Color", header: true }, { data: "Description", header: true }],
                            ...sample,
                          ]);
                          if (drifts.length > sample.length) {
                            core.summary
                              .addRaw(`Additional drifts not shown: ${drifts.length - sample.length}`)
                              .addEOL();
                          }
                        }

                        await core.summary.write();

                        if (!shouldRepair && drifts.length > 0) {
                          core.info(`Managed-label metadata drifts detected: ${drifts.length}. Re-run with mode=repair to auto-fix.`);
                        } else if (shouldRepair) {
                          core.info(`Managed-label metadata repair applied to ${drifts.length} labels.`);
                        } else {
                          core.info("No managed-label metadata drift detected.");
                        }

                        return;
                      }

                      const files = await github.paginate(github.rest.pulls.listFiles, {
                        owner,
                        repo,
                        pull_number: pr.number,
                        per_page: 100,
                      });

                      const detectedModuleLabels = new Set();
                      for (const file of files) {
                        const path = (file.filename || "").toLowerCase();
                        for (const rule of moduleNamespaceRules) {
                          if (!path.startsWith(rule.root)) continue;

                          const relative = path.slice(rule.root.length);
                          if (!relative) continue;

                          const first = relative.split("/")[0];
                          const firstStem = first.endsWith(".rs") ? first.slice(0, -3) : first;
                          let segment = firstStem;

                          if (rule.coreEntries.has(first) || rule.coreEntries.has(firstStem)) {
                            segment = "core";
                          }

                          segment = normalizeLabelSegment(segment);
                          if (!segment) continue;

                          detectedModuleLabels.add(`${rule.prefix}:${segment}`);
                        }
                      }

                      const providerRelevantFiles = files.filter((file) => {
                        const path = file.filename || "";
                        return (
                          path.startsWith("src/providers/") ||
                          path.startsWith("src/integrations/") ||
                          path.startsWith("src/onboard/") ||
                          path.startsWith("src/config/")
                        );
                      });

                      if (providerRelevantFiles.length > 0) {
                        const searchableText = [
                          pr.title || "",
                          pr.body || "",
                          ...providerRelevantFiles.map((file) => file.filename || ""),
                          ...providerRelevantFiles.map((file) => file.patch || ""),
                        ]
                          .join("\n")
                          .toLowerCase();

                        for (const keyword of providerKeywordHints) {
                          if (containsKeyword(searchableText, keyword)) {
                            detectedModuleLabels.add(`provider:${keyword}`);
                          }
                        }
                      }

                      const channelRelevantFiles = files.filter((file) => {
                        const path = file.filename || "";
                        return (
                          path.startsWith("src/channels/") ||
                          path.startsWith("src/onboard/") ||
                          path.startsWith("src/config/")
                        );
                      });

                      if (channelRelevantFiles.length > 0) {
                        const searchableText = [
                          pr.title || "",
                          pr.body || "",
                          ...channelRelevantFiles.map((file) => file.filename || ""),
                          ...channelRelevantFiles.map((file) => file.patch || ""),
                        ]
                          .join("\n")
                          .toLowerCase();

                        for (const keyword of channelKeywordHints) {
                          if (containsKeyword(searchableText, keyword)) {
                            detectedModuleLabels.add(`channel:${keyword}`);
                          }
                        }
                      }

                      const refinedModuleLabels = refineModuleLabels(detectedModuleLabels);
                      const compactedModuleState = compactModuleLabels(refinedModuleLabels);
                      const selectedModuleLabels = compactedModuleState.moduleLabels;
                      const forcePathPrefixes = compactedModuleState.forcePathPrefixes;
                      const modulePrefixesWithLabels = new Set(
                        [...selectedModuleLabels]
                          .map((label) => parseModuleLabel(label)?.prefix)
                          .filter(Boolean)
                      );

                      const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
                        owner,
                        repo,
                        issue_number: pr.number,
                      });
                      const currentLabelNames = currentLabels.map((label) => label.name);
                      const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label));
                      const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]);

                      const dedupedPathLabels = [...candidatePathLabels].filter((label) => {
                        if (label === "core") return true;
                        if (forcePathPrefixes.has(label)) return true;
                        return !modulePrefixesWithLabels.has(label);
                      });

                      const excludedLockfiles = new Set(["Cargo.lock"]);
                      const changedLines = files.reduce((total, file) => {
                        const path = file.filename || "";
                        if (isDocsLike(path) || excludedLockfiles.has(path)) {
                          return total;
                        }
                        return total + (file.additions || 0) + (file.deletions || 0);
                      }, 0);

                      let sizeLabel = "size: XL";
                      if (changedLines <= 80) sizeLabel = "size: XS";
                      else if (changedLines <= 250) sizeLabel = "size: S";
                      else if (changedLines <= 500) sizeLabel = "size: M";
                      else if (changedLines <= 1000) sizeLabel = "size: L";

                      const hasHighRiskPath = files.some((file) => {
                        const path = file.filename || "";
                        return (
                          path.startsWith("src/security/") ||
                          path.startsWith("src/runtime/") ||
                          path.startsWith("src/gateway/") ||
                          path.startsWith("src/tools/") ||
                          path.startsWith(".github/workflows/")
                        );
                      });

                      const hasMediumRiskPath = files.some((file) => {
                        const path = file.filename || "";
                        return (
                          path.startsWith("src/") ||
                          path === "Cargo.toml" ||
                          path === "Cargo.lock" ||
                          path === "deny.toml" ||
                          path.startsWith(".githooks/")
                        );
                      });

                      let riskLabel = "risk: low";
                      if (hasHighRiskPath) {
                        riskLabel = "risk: high";
                      } else if (hasMediumRiskPath) {
                        riskLabel = "risk: medium";
                      }

                      await ensureManagedRepoLabelsMetadata();

                      const labelsToEnsure = new Set([
                        ...sizeLabels,
                        ...computedRiskLabels,
                        manualRiskOverrideLabel,
                        ...managedPathLabels,
                        ...contributorTierLabels,
                        ...selectedModuleLabels,
                      ]);

                      for (const label of labelsToEnsure) {
                        await ensureLabel(label);
                      }

                      let contributorTierLabel = null;
                      const authorLogin = pr.user?.login;
                      if (authorLogin && pr.user?.type !== "Bot") {
                        try {
                          const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({
                            q: `repo:${owner}/${repo} is:pr is:merged author:${authorLogin}`,
                            per_page: 1,
                          });
                          const mergedCount = mergedSearch.total_count || 0;
                          contributorTierLabel = selectContributorTier(mergedCount);
                        } catch (error) {
                          core.warning(`failed to compute contributor tier label: ${error.message}`);
                        }
                      }

                      const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel);
                      const keepNonManagedLabels = currentLabelNames.filter((label) => {
                        if (label === manualRiskOverrideLabel) return true;
                        if (contributorTierLabels.includes(label)) return false;
                        if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false;
                        if (managedPathLabelSet.has(label)) return false;
                        if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false;
                        return true;
                      });

                      const manualRiskSelection =
                        currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel;

                      const moduleLabelList = sortModuleLabels([...selectedModuleLabels]);
                      const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : [];
                      const selectedRiskLabels = hasManualRiskOverride
                        ? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex)
                        : sortByPriority([riskLabel], riskPriorityIndex);
                      const selectedSizeLabels = sortByPriority([sizeLabel], sizePriorityIndex);
                      const sortedContributorLabels = sortByPriority(contributorLabelList, contributorPriorityIndex);
                      const sortedPathLabels = sortByPriority(dedupedPathLabels, pathLabelPriorityIndex);
                      const sortedKeepNonManagedLabels = [...new Set(keepNonManagedLabels)].sort((left, right) =>
                        left.localeCompare(right)
                      );

                      const nextLabels = [
                        ...new Set([
                          ...selectedRiskLabels,
                          ...selectedSizeLabels,
                          ...sortedContributorLabels,
                          ...moduleLabelList,
                          ...sortedPathLabels,
                          ...sortedKeepNonManagedLabels,
                        ]),
                      ];

                      await github.rest.issues.setLabels({
                        owner,
                        repo,
                        issue_number: pr.number,
                        labels: nextLabels,
                      });
