Documentation
¶
Overview ¶
Package textproc turns repository metadata into the bag-of-words representation the embedding and naming stages both work from.
Index ¶
Constants ¶
const ( WeightTopic = 3.0 // A topic's words are worth less than the topic itself: the label is the // deliberate part, its vocabulary is shared with everything else. WeightTopicWord = 1.2 // Weights for a topic recognized in text rather than applied by a person, // graded by how reliably each source names the project. // // Measured against repositories that do carry topics, a phrase found in // the repository name is really one of its topics 65% of the time, in the // description 43%, and in the opening of the README 33% — falling to 21% // by a hundred words in. Precision tracks how deliberately the text was // written: a name is chosen once and carefully, a description is a // sentence, a README is prose that mentions neighbouring technologies // without being about them. None of these approaches a real topic, so none // is weighted like one. WeightTopicFromName = 3.0 WeightTopicFromText = 2.0 WeightTopicFromReadme = 1.0 // ReadmeInferWords is how far into a README a topic phrase is still // trusted. Precision halves between the opening and the hundred-word mark. ReadmeInferWords = 20 WeightName = 2.0 // The detected primary language is deliberately not counted as evidence. // Every repository in a language shares one identical term, which makes it // an overwhelmingly strong grouper that then absorbs whatever else is // nearby — it was fusing Go with Wardley mapping, and C with Zsh. Measured // on a real account, dropping it raised topic agreement by a tenth and // halved the number of categories named after two unrelated things. A // language the author tagged as a topic still counts, at topic weight, // because that is a choice rather than a detection. WeightLanguage = 0 WeightText = 1.0 // An owner is a publisher, not a subject. Weighting it like the repository // name produces a "Google" category holding a fuzzer, a Bluetooth stack // and a YAML formatter. It still earns a small weight because narrow // owners — kubernetes, rust-lang, bufbuild — really do say what a // repository is about. WeightOwner = 0.5 // A README is a hundred terms against a description's nine, so per term it // has to count for much less if the two are to carry comparable weight. WeightReadme = 0.15 )
Field weights. Topics dominate, then the name (owner/repo often encodes the ecosystem), then language, then free text.
const LangPrefix = "lang:"
LangPrefix namespaces the term derived from a repository's Language field.
Without it the language signal is quietly lost for exactly the languages that matter most: "go" and "js" have to be stopwords because they are common English words and abbreviations in prose, and "JavaScript" camel-splits into "java" and "script", which pulls JavaScript repositories towards Java ones. A namespaced token cannot collide with prose, so it never needs filtering.
const TopicPrefix = "topic:"
TopicPrefix namespaces a repository topic kept whole.
Thirty per cent of topic assignments are multi-word, and splitting them destroys what made them worth the most. "machine-learning" is a label chosen from a curated set; "machin" and "learn" are two common words that collide with any description mentioning learning. The label is kept as one term so that two repositories carrying it match exactly, and its words are kept too, at lower weight, so a repository describing machine learning in prose can still match approximately.
Single-word topics are left alone: for those the decomposed form already is the whole label, and isolating it would only stop it matching prose.
Variables ¶
var Pretty = map[string]string{ "kubernetes": "Kubernetes", "machinelearning": "Machine Learning", "artificialintelligence": "AI", "deeplearning": "Deep Learning", "generativeai": "Generative AI", "llm": "LLM", "commandline": "CLI", "cicd": "CI/CD", "javascript": "JavaScript", "typescript": "TypeScript", "opentelemetry": "OpenTelemetry", "graphql": "GraphQL", "postgresql": "PostgreSQL", "postgres": "Postgres", "nosql": "NoSQL", "grpc": "gRPC", "api": "API", "apis": "APIs", "sdk": "SDK", "http": "HTTP", "dns": "DNS", "tls": "TLS", "aws": "AWS", "gcp": "GCP", "css": "CSS", "html": "HTML", "json": "JSON", "yaml": "YAML", "wasm": "WebAssembly", "ebpf": "eBPF", "gpu": "GPU", "os": "OS", "rss": "RSS", "vpn": "VPN", "ssh": "SSH", "ui": "UI", "ux": "UX", "vim": "Vim", "neovim": "Neovim", "nixos": "NixOS", "macos": "macOS", "ios": "iOS", "imap": "IMAP", "smtp": "SMTP", "jmap": "JMAP", "pop3": "POP3", "mta": "MTA", "csv": "CSV", "xml": "XML", "ntp": "NTP", "smb": "SMB", "nfs": "NFS", "esp32": "ESP32", "esp8266": "ESP8266", "rfc": "RFC", "pdf": "PDF", "svg": "SVG", "oidc": "OIDC", "saml": "SAML", "ldap": "LDAP", "mqtt": "MQTT", "cve": "CVE", "sbom": "SBOM", "tui": "TUI", "ide": "IDE", "vm": "VM", "cpu": "CPU", "github": "GitHub", "gitlab": "GitLab", "gitops": "GitOps", "devsecops": "DevSecOps", "nodejs": "Node.js", "dotnet": ".NET", "cpp": "C++", "csharp": "C#", "fsharp": "F#", LangPrefix + "go": "Go", LangPrefix + "cpp": "C++", LangPrefix + "csharp": "C#", LangPrefix + "fsharp": "F#", LangPrefix + "c": "C", LangPrefix + "objectivec": "Objective-C", LangPrefix + "javascript": "JavaScript", LangPrefix + "typescript": "TypeScript", LangPrefix + "jupyternotebook": "Jupyter Notebook", LangPrefix + "vimscript": "Vim Script", LangPrefix + "emacslisp": "Emacs Lisp", LangPrefix + "powershell": "PowerShell", LangPrefix + "shell": "Shell", LangPrefix + "php": "PHP", LangPrefix + "sql": "SQL", LangPrefix + "html": "HTML", LangPrefix + "css": "CSS", LangPrefix + "scss": "SCSS", LangPrefix + "ocaml": "OCaml", LangPrefix + "matlab": "MATLAB", LangPrefix + "tex": "TeX", LangPrefix + "hcl": "HCL", LangPrefix + "rmarkdown": "R Markdown", LangPrefix + "r": "R", }
Pretty renders a canonical term back into something a human wants to read on a list. Only entries that title-casing gets wrong need to appear here.
Functions ¶
func CanonicalTopic ¶
CanonicalTopic normalizes a topic into the single term representing it.
Types ¶
type Doc ¶
type Doc struct {
Terms map[string]float64
// Bigrams holds adjacent term pairs, kept apart from Terms so they inform
// naming without inflating the vectors. A category is far better described
// by "self hosted" or "home assistant" than by either half of it.
Bigrams map[string]float64
// Surface maps each stem back to a form it was actually written as. Stems
// group well but read terribly — "analytics" and "analytic" both stem to
// "analyt", which no one wants to see on a list — so the vector space uses
// the stem and the label uses the word.
Surface map[string]string
// FromTopic records how much of a term's weight came from the repository's
// topics rather than from prose. Topics are labels somebody applied on
// purpose; a description is marketing copy. When naming a category the
// difference is worth a lot, so the provenance is carried through.
FromTopic map[string]float64
// Cased maps a lower-cased word to the capitalization the corpus writes it
// with. GitHub lower-cases every topic, so this can only come from
// descriptions and repository names — but there it is unambiguous:
// "eBPF" outnumbers "ebpf" by four to one, and "gRPC" and "PostgreSQL"
// are spellings no capitalization rule would ever produce.
Cased map[string]string
}
Doc is one tokenized repository. Terms are weighted: a term that came from the repo's curated topic list counts for more than one that came from prose, because topics are hand-applied labels while descriptions are marketing copy.
type TopicIndex ¶
TopicIndex maps the stemmed word pair a multi-word topic reduces to onto the topic itself, so that the same phrase written in prose can be recognized.
It is built from the topics the corpus already uses, which makes it a closed vocabulary: a description can be matched against labels other repositories have chosen, but no new label can be invented. Only multi-word topics are indexed — "security", "cli" and "api" occur in prose constantly and matching those would label everything.
func NewTopicIndex ¶
func NewTopicIndex(topicLists [][]string, minCount int) TopicIndex
NewTopicIndex builds the index from every repository's topics, keeping those used at least minCount times.
type Weights ¶
type Weights struct {
// Topic is the weight of a multi-word topic kept whole, and of a
// single-word topic, which amounts to the same thing.
Topic float64
// TopicWord is the weight of each word a multi-word topic decomposes into.
TopicWord float64
// TopicFromName, TopicFromText and TopicFromReadme weight a topic
// recognized in each of those, rather than applied to the repository.
TopicFromName float64
TopicFromText float64
TopicFromReadme float64
Name float64
Language float64
Text float64
Owner float64
Readme float64
}
Weights control how much each part of a repository record counts. They matter more than they look: a README contributes a hundred or so terms and a description contributes nine, so an even-handed weighting is not even-handed at all — it lets prose drown the curated topics that carry the most signal per term.
func DefaultWeights ¶
func DefaultWeights() Weights
DefaultWeights are the ones the CLI uses unless told otherwise.