| | | 1 | | package repometa |
| | | 2 | | |
| | | 3 | | import "slices" |
| | | 4 | | |
| | | 5 | | // Manifest is the result of scanning a repository. Consumers iterate |
| | | 6 | | // [Manifest.Components]; the ordering is deterministic (by root path, |
| | | 7 | | // then by kind) so serialized output is diff-friendly. |
| | | 8 | | type Manifest struct { |
| | | 9 | | // Root is the absolute path that was scanned. All [Component.Root] |
| | | 10 | | // and [Evidence.Path] values in this manifest are relative to Root. |
| | | 11 | | Root string |
| | | 12 | | |
| | | 13 | | // Components lists every buildable unit detected under Root. May be |
| | | 14 | | // empty if the scan encountered no known ecosystem manifests. |
| | | 15 | | Components []Component |
| | | 16 | | |
| | | 17 | | // Stats reports counters from the walk. Non-zero cap-hit fields |
| | | 18 | | // indicate the scan was truncated and may be incomplete. |
| | | 19 | | Stats ScanStats |
| | | 20 | | } |
| | | 21 | | |
| | | 22 | | // Kind identifies the type of a [Component]. Values are open-ended |
| | | 23 | | // strings so new detectors can be added without breaking consumers on |
| | | 24 | | // enum drift; unknown values are safe to log or ignore. |
| | | 25 | | type Kind string |
| | | 26 | | |
| | | 27 | | // Kind values for every ecosystem detector shipped in this package. See |
| | | 28 | | // the README's "Supported detectors" table for the mapping to ecosystem. |
| | | 29 | | const ( |
| | | 30 | | KindGoModule Kind = "go-module" |
| | | 31 | | KindRustCrate Kind = "rust-crate" |
| | | 32 | | KindRustWorkspace Kind = "rust-workspace" |
| | | 33 | | KindPythonPackage Kind = "python-package" |
| | | 34 | | KindNodePackage Kind = "node-package" |
| | | 35 | | KindDotNetProject Kind = "dotnet-project" |
| | | 36 | | KindDotNetSolution Kind = "dotnet-solution" |
| | | 37 | | KindCppProject Kind = "cpp-project" |
| | | 38 | | KindJavaProject Kind = "java-project" |
| | | 39 | | KindCMakeProject Kind = "cmake-project" |
| | | 40 | | KindMakeProject Kind = "make-project" |
| | | 41 | | KindCSource Kind = "c-source-tree" |
| | | 42 | | KindAsmSource Kind = "asm-source-tree" |
| | | 43 | | ) |
| | | 44 | | |
| | | 45 | | // Component describes a single buildable unit inside the repo, anchored |
| | | 46 | | // at a specific directory. A directory may produce multiple Components |
| | | 47 | | // when more than one ecosystem's manifest is present (e.g. a Rust crate |
| | | 48 | | // with a co-located Node package). |
| | | 49 | | type Component struct { |
| | | 50 | | // Kind identifies the ecosystem and shape of the component. |
| | | 51 | | Kind Kind |
| | | 52 | | |
| | | 53 | | // Root is the path relative to [Manifest.Root]; "." for the repo root. |
| | | 54 | | Root string |
| | | 55 | | |
| | | 56 | | // Evidence lists the files that led to this component being reported. |
| | | 57 | | Evidence []Evidence |
| | | 58 | | |
| | | 59 | | // Confidence is in [0.0, 1.0]; heuristic hits report < 1.0, and |
| | | 60 | | // manifest-driven detections report 1.0. |
| | | 61 | | Confidence float64 |
| | | 62 | | |
| | | 63 | | // Workspaces lists any monorepo layouts anchored at this component. |
| | | 64 | | Workspaces []Workspace |
| | | 65 | | |
| | | 66 | | // Attributes carries ecosystem-specific metadata. Keys are namespaced |
| | | 67 | | // (e.g. "js.framework", "python.pm"). See the README for the current |
| | | 68 | | // set; unknown keys are safe to ignore. |
| | | 69 | | Attributes map[string]string |
| | | 70 | | } |
| | | 71 | | |
| | | 72 | | // Evidence records a single fact that led to a [Component] being |
| | | 73 | | // reported. Path is relative to [Manifest.Root] so evidence remains |
| | | 74 | | // meaningful when the manifest is transported. |
| | | 75 | | type Evidence struct { |
| | | 76 | | // Path is relative to [Manifest.Root]; forward slashes on every |
| | | 77 | | // platform. |
| | | 78 | | Path string |
| | | 79 | | |
| | | 80 | | // Reason is a short human-readable description of why this file |
| | | 81 | | // counts as evidence (e.g. "go.mod present", "workspace = [...]"). |
| | | 82 | | Reason string |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | // Workspace describes a monorepo layout anchored at a [Component]. |
| | | 86 | | // Members may be empty when the workspace kind was detected by file |
| | | 87 | | // presence alone rather than by parsing an explicit member list. |
| | | 88 | | type Workspace struct { |
| | | 89 | | // Kind identifies the monorepo tooling. |
| | | 90 | | Kind WorkspaceKind |
| | | 91 | | |
| | | 92 | | // Members lists the workspace's constituent package paths, relative |
| | | 93 | | // to [Manifest.Root], forward-slash separated. |
| | | 94 | | Members []string |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | // WorkspaceKind identifies the workspace / monorepo tooling in use. Like |
| | | 98 | | // [Kind], values are open-ended strings — unknown values are safe to |
| | | 99 | | // log or ignore. |
| | | 100 | | type WorkspaceKind string |
| | | 101 | | |
| | | 102 | | // WorkspaceKind values for every workspace layout recognized by the |
| | | 103 | | // detectors in this package. |
| | | 104 | | const ( |
| | | 105 | | WorkspaceGo WorkspaceKind = "go-workspace" |
| | | 106 | | WorkspaceCargo WorkspaceKind = "cargo-workspace" |
| | | 107 | | WorkspaceNpmYarn WorkspaceKind = "npm-yarn-workspace" |
| | | 108 | | WorkspacePnpm WorkspaceKind = "pnpm-workspace" |
| | | 109 | | WorkspaceNx WorkspaceKind = "nx" |
| | | 110 | | WorkspaceTurborepo WorkspaceKind = "turborepo" |
| | | 111 | | WorkspaceUv WorkspaceKind = "uv-workspace" |
| | | 112 | | WorkspaceDotNetSolution WorkspaceKind = "dotnet-solution" |
| | | 113 | | WorkspaceMavenMultiModule WorkspaceKind = "maven-multi-module" |
| | | 114 | | WorkspaceGradleMultiProject WorkspaceKind = "gradle-multi-project" |
| | | 115 | | ) |
| | | 116 | | |
| | | 117 | | // Language is a coarse ecosystem label derived from a [Component.Kind]. |
| | | 118 | | // Multiple Kinds may map to the same Language: [KindRustCrate] and |
| | | 119 | | // [KindRustWorkspace] both report [LanguageRust]; [KindCMakeProject], |
| | | 120 | | // [KindMakeProject], and [KindCSource] all report [LanguageC]; |
| | | 121 | | // [KindDotNetProject] and [KindDotNetSolution] both report |
| | | 122 | | // [LanguageDotNet]. This grouping is used by the helpers on [Manifest] |
| | | 123 | | // to distinguish single-language repositories from polyglot ones — |
| | | 124 | | // treat Component.Kind as the source of truth for finer-grained work. |
| | | 125 | | type Language string |
| | | 126 | | |
| | | 127 | | // Language values. LanguageUnknown is used when a Component.Kind is not |
| | | 128 | | // in the mapping table (added by a future detector; safely reported |
| | | 129 | | // rather than dropped). |
| | | 130 | | const ( |
| | | 131 | | LanguageGo Language = "go" |
| | | 132 | | LanguageRust Language = "rust" |
| | | 133 | | LanguagePython Language = "python" |
| | | 134 | | LanguageJavaScript Language = "javascript" |
| | | 135 | | LanguageDotNet Language = "dotnet" |
| | | 136 | | LanguageJava Language = "java" |
| | | 137 | | LanguageC Language = "c" |
| | | 138 | | LanguageAssembly Language = "assembly" |
| | | 139 | | LanguageUnknown Language = "unknown" |
| | | 140 | | ) |
| | | 141 | | |
| | | 142 | | // Language returns the coarse ecosystem label for this Component. See |
| | | 143 | | // [Language] for the mapping and rationale; unrecognized kinds return |
| | | 144 | | // [LanguageUnknown]. |
| | | 145 | | func (c Component) Language() Language { |
| | | 146 | | switch c.Kind { |
| | | 147 | | case KindGoModule: |
| | | 148 | | return LanguageGo |
| | | 149 | | case KindRustCrate, KindRustWorkspace: |
| | | 150 | | return LanguageRust |
| | | 151 | | case KindPythonPackage: |
| | | 152 | | return LanguagePython |
| | | 153 | | case KindNodePackage: |
| | | 154 | | return LanguageJavaScript |
| | | 155 | | case KindDotNetProject, KindDotNetSolution: |
| | | 156 | | return LanguageDotNet |
| | | 157 | | case KindJavaProject: |
| | | 158 | | return LanguageJava |
| | | 159 | | case KindCMakeProject, KindMakeProject, KindCppProject, KindCSource: |
| | | 160 | | return LanguageC |
| | | 161 | | case KindAsmSource: |
| | | 162 | | return LanguageAssembly |
| | | 163 | | } |
| | | 164 | | return LanguageUnknown |
| | | 165 | | } |
| | | 166 | | |
| | | 167 | | // Languages returns the sorted, de-duplicated list of [Language] values |
| | | 168 | | // present across every [Component] in the manifest. A manifest with no |
| | | 169 | | // components returns an empty slice. |
| | 16 | 170 | | func (m *Manifest) Languages() []Language { |
| | 16 | 171 | | seen := make(map[Language]struct{}, len(m.Components)) |
| | 16 | 172 | | for _, c := range m.Components { |
| | 44 | 173 | | seen[c.Language()] = struct{}{} |
| | 44 | 174 | | } |
| | 16 | 175 | | out := make([]Language, 0, len(seen)) |
| | 16 | 176 | | for l := range seen { |
| | 32 | 177 | | out = append(out, l) |
| | 32 | 178 | | } |
| | 16 | 179 | | slices.Sort(out) |
| | 16 | 180 | | return out |
| | | 181 | | } |
| | | 182 | | |
| | | 183 | | // Polyglot reports whether the manifest contains components spanning |
| | | 184 | | // more than one [Language]. A manifest with zero components, or with |
| | | 185 | | // components in a single Language, returns false. [LanguageUnknown] |
| | | 186 | | // counts as its own language, so a repo mixing a known ecosystem with |
| | | 187 | | // one repometa doesn't yet recognize is reported as polyglot. |
| | 8 | 188 | | func (m *Manifest) Polyglot() bool { |
| | 8 | 189 | | return len(m.Languages()) > 1 |
| | 8 | 190 | | } |
| | | 191 | | |
| | | 192 | | // ScanStats reports counters from the walk. Non-zero DepthCapHits or |
| | | 193 | | // DirCapHits indicate the scan was truncated by [WithMaxDepth] or |
| | | 194 | | // [WithMaxDirs] respectively — callers should widen the cap and rescan |
| | | 195 | | // if a complete manifest is required. |
| | | 196 | | type ScanStats struct { |
| | | 197 | | // DirsVisited is the total number of directories entered by the walk. |
| | | 198 | | DirsVisited int |
| | | 199 | | |
| | | 200 | | // FilesSeen is the total number of file entries observed (whether or |
| | | 201 | | // not their contents were read). |
| | | 202 | | FilesSeen int |
| | | 203 | | |
| | | 204 | | // DepthCapHits counts the number of directories the walk refused to |
| | | 205 | | // descend into because the [WithMaxDepth] cap was reached. |
| | | 206 | | DepthCapHits int |
| | | 207 | | |
| | | 208 | | // DirCapHits is 1 if the [WithMaxDirs] cap fired and aborted the walk; |
| | | 209 | | // 0 otherwise. |
| | | 210 | | DirCapHits int |
| | | 211 | | |
| | | 212 | | // SymlinksSkipped counts the number of symlink entries skipped — |
| | | 213 | | // symlinks are never traversed regardless of target. |
| | | 214 | | SymlinksSkipped int |
| | | 215 | | } |