"use strict"; const { deploymentIdentity, deploymentEvidenceHash, deploymentAuthorityKey } = require("./deployment-identity.cjs"); const BACKUP = /(?:^|[\\/._-])(backup|bak|archive|snapshot|old|previous)(?:[\\/._-]|$)/i; const RELEASE = /(?:^|[\\/])(releases?|versions?)(?:[\\/]|$)/i; const STAGING = /(?:^|[\\/._-])(staging|stage|test|qa|preview)(?:[\\/._-]|$)/i; const TEMPORARY = /(?:^|[\\/._-])(candidate|rollback|ephemeral)(?:[\\/._-]|$)|^GITEA-ACTIONS-TASK-/i; const SYSTEM = /^(?:traefik|nginx-proxy-manager|watchtower|portainer|dockerman|unraid-|cloudflared|redis|postgres|mariadb|mysql)(?:$|[-_.])/i; function baseClassification(workload) { const location = `${workload.compose?.workingDir || ""} ${(workload.compose?.configFiles || []).join(" ")}`; const sourceRepository = String(workload.metadata?.sourceRepository || "").trim(); const hasGitProvenance = /^(?:git@|ssh:\/\/|https?:\/\/)/i.test(sourceRepository); const decision = workload.reviewDecision; if (["manual-exclude", "exclude-scan-root", "ignore"].includes(decision?.action)) return { type: "manually-excluded", reason: decision.reason || "Persisted manual exclusion", decisionAction: decision.action }; if (["mark-historical", "archive-link"].includes(decision?.action)) return { type: "historical-compose", reason: decision.reason || "Reviewed as historical", decisionAction: decision.action }; if (decision?.action === "monitor-only") return { type: "monitor-only", reason: decision.reason || "Reviewed for monitoring only", decisionAction: decision.action }; if (workload.metadata?.staleLink) return { type: "stale-link", reason: "The linked deployment profile has no matching server workload" }; if (BACKUP.test(location)) return { type: "backup", reason: "Path matches backup/archive evidence" }; if (RELEASE.test(location)) return { type: "release-folder", reason: "Path is below a release/version directory" }; if (STAGING.test(location)) return { type: "staging", reason: "Path or project identifies a staging/test workload" }; if (TEMPORARY.test(`${workload.displayName || ""} ${location}`)) return { type: "temporary-runtime", reason: "Runtime identity marks a candidate, rollback or CI workload" }; if (SYSTEM.test(workload.displayName || "") && !workload.metadata?.sourceRepository) return { type: "system-container", reason: "Known infrastructure identity without repository provenance" }; if (workload.link && workload.runtime?.running) return { type: "active-application", reason: "Linked deployment with running container evidence" }; if (workload.link && !workload.runtime?.running) return { type: "stopped-application", reason: "Linked deployment without a running container" }; if (!workload.containers?.length && workload.compose?.configFiles?.length) return { type: "historical-compose", reason: "Compose definition exists without container runtime" }; if (workload.status === "ambiguous") return { type: "ambiguous", reason: "Multiple candidates have equivalent evidence" }; if (!workload.candidates?.length) return { type: "external-container", reason: hasGitProvenance ? "Repository provenance does not match an accessible configured Gitea repository" : "Runtime has no Git repository provenance and remains monitoring-only" }; if (!workload.runtime?.running && workload.candidates?.length) return { type: "stopped-application", reason: "Stopped runtime has repository evidence" }; return { type: workload.runtime?.running ? "active-application" : "ambiguous", reason: workload.runtime?.running ? "Running application evidence" : "Insufficient authoritative evidence" }; } function classifyInventory(workloads, profiles = [], decisions = []) { const profileById = new Map(profiles.map((profile) => [profile.id, profile])); const decisionByWorkload = new Map(decisions.map((decision) => [decision.workloadId, decision])); const authorities = new Map(); const result = workloads.map((source) => { const workload = structuredClone(source); const profile = profileById.get(workload.link?.profileId) || null; const identity = deploymentIdentity({ workload, profile }); const evidence = { candidates: (workload.candidates || []).map((item) => ({ repository: item.repositoryFullName, score: item.score, exact: item.exact === true })), running: workload.runtime?.running === true, health: workload.runtime?.health || null, configFiles: workload.compose?.configFiles || [] }; const hash = deploymentEvidenceHash(identity, evidence); const stored = decisionByWorkload.get(workload.workloadId); workload.reviewDecision = stored?.evidenceHash === hash ? stored : null; workload.reviewDecisionStale = Boolean(stored && stored.evidenceHash !== hash); workload.identity = identity; if (workload.reviewDecision?.action === "manual-link" && workload.reviewDecision.repositoryFullName) { workload.identity.repository = String(workload.reviewDecision.repositoryFullName).toLowerCase(); } workload.evidenceHash = hash; workload.classification = baseClassification(workload); if (workload.link && ["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) { workload.shadowedLink = workload.link; workload.link = null; } const key = deploymentAuthorityKey(identity); if (identity.repository && (workload.link || workload.candidates?.length) && !["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) { const group = authorities.get(key) || []; group.push(workload); authorities.set(key, group); } return workload; }); for (const group of authorities.values()) { if (group.length < 2) { group[0].authoritative = true; continue; } const ranked = [...group].sort((a, b) => Number(b.reviewDecision?.action === "select-authoritative") - Number(a.reviewDecision?.action === "select-authoritative") || Number(b.runtime?.running) - Number(a.runtime?.running) || Number(Boolean(b.link)) - Number(Boolean(a.link)) || Number(Boolean(b.metadata?.liveRevision)) - Number(Boolean(a.metadata?.liveRevision))); ranked[0].authoritative = true; for (const duplicate of ranked.slice(1)) { duplicate.authoritative = false; duplicate.classification = { type: "duplicate", reason: `Conflicts with authoritative workload ${ranked[0].workloadId}`, authoritativeWorkloadId: ranked[0].workloadId }; duplicate.status = "duplicate"; duplicate.shadowedLink = duplicate.link; duplicate.link = null; } } return result; } module.exports = { classifyInventory, classifyWorkload: baseClassification, inventoryPathPatterns: { BACKUP, RELEASE, STAGING, TEMPORARY, SYSTEM } };