95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""Which resume area a harvested document feeds.
|
|
|
|
The payload `area` is what lets the query path filter *before* it searches, so a
|
|
harvested file has to be classified once, deterministically, and never promoted
|
|
to make a page read better.
|
|
|
|
The classification is keyword evidence, in this order: the path, the repository
|
|
name, the description and topics, then the languages. The result is constrained
|
|
to the platform guide's `fits` list: a platform cannot feed an area it does not
|
|
declare. When nothing matches, the first `fits` entry wins, which is why every
|
|
guide lists its strongest area first.
|
|
|
|
This is a heuristic and it is labelled as one. It is not a claim about Sam's
|
|
career; the curated `content/` tree carries the claims.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from .payloads import AREAS
|
|
|
|
# Order matters: the first area with a hit wins, so the more specific evidence
|
|
# (an ESP32 repository is edge, not platform) is checked before the general.
|
|
_RULES = (
|
|
("edge", (
|
|
"esp32", "esp8266", "arduino", "lora", "lorawan", "mqtt", "sensor",
|
|
"zigbee", "z-wave", "relay", "gpio", "pir", "modbus", "iot",
|
|
"firmware", "breadboard", "raspberry pi", "rpi", "camera", "cam",
|
|
"temperature", "watering", "fish feeder", "driveway", "light",
|
|
"snapcast", "mopidy", "audio", "voice bridge",
|
|
)),
|
|
("ai", (
|
|
"ai", "llm", "agent", "agentic", "rag", "embedding", "vector",
|
|
"model", "ml", "prompt", "graphify", "omniroute", "paperclip",
|
|
"langgraph", "prefect", "whisper", "piper", "chat", "memory",
|
|
"knowledge graph", "vision", "triage",
|
|
)),
|
|
("platform", (
|
|
"nixos", "nix", "docker", "compose", "kubernetes", "k8s", "deploy",
|
|
"deployment", "backup", "borg", "kopia", "restic", "network", "dns",
|
|
"email", "caddy", "proxmox", "postgres", "terraform", "ansible",
|
|
"linux", "systemd", "homelab", "server", "infrastructure", "git",
|
|
"gitea", "monitor", "logging", "security", "firewall", "vpn",
|
|
"tailscale", "storage", "garage", "s3",
|
|
)),
|
|
("products", (
|
|
"product", "app", "web", "dashboard", "react", "laravel", "flutter",
|
|
"kotlin", "calendar", "photo", "kontra", "store", "resume", "site",
|
|
"gps", "tracker", "api", "frontend", "ui", "ux",
|
|
)),
|
|
("history", (
|
|
"history", "agency", "client", "archive", "old", "legacy", "2019",
|
|
"2018", "2020", "2021", "career", "resume copy",
|
|
)),
|
|
("about", ("about", "profile", "bio", "personal", "cv")),
|
|
)
|
|
|
|
_WORD_SPLIT = re.compile(r"[^0-9a-z]+")
|
|
|
|
|
|
def area_for(evidence: str, fits, fallback: str = None) -> str:
|
|
"""Classify one document from its path, name, description and topics."""
|
|
haystack = " " + _WORD_SPLIT.sub(" ", (evidence or "").lower()) + " "
|
|
allowed = [area for area in (fits or AREAS) if area in AREAS]
|
|
if not allowed:
|
|
allowed = list(AREAS)
|
|
for area, needles in _RULES:
|
|
if area not in allowed:
|
|
continue
|
|
for needle in needles:
|
|
if f" {needle} " in haystack or f" {needle}s " in haystack:
|
|
return area
|
|
if fallback in allowed:
|
|
return fallback
|
|
return allowed[0]
|
|
|
|
|
|
def area_for_repo(repo: dict, fits) -> str:
|
|
topics = " ".join(repo.get("topics") or [])
|
|
evidence = " ".join(
|
|
str(part)
|
|
for part in (
|
|
repo.get("full_name", ""),
|
|
repo.get("name", ""),
|
|
repo.get("description", ""),
|
|
topics,
|
|
)
|
|
)
|
|
return area_for(evidence, fits, fallback=(fits or [None])[0])
|
|
|
|
|
|
def area_for_note(path: str, fits) -> str:
|
|
return area_for(path, fits, fallback=(fits or [None])[0])
|