#!/usr/bin/env python3
"""tag_projects.py — deterministic tagging of internship project topics.

Reads the structured D2 forms (``forms/extracted/d2.json``) plus the flattened
``forms/extracted/projects.csv`` and assigns three orthogonal tag axes to every
project topic:

  * ``topics``        — what the project is about (web, mobile, ai, cv, ...)
  * ``lang_tags``     — normed programming languages / markup / query / shell
  * ``framework_tags``— normed frameworks, libraries, runtimes, platforms,
                        databases, tools, protocols, standards

No model calls: every tag comes from a keyword/regex rule or a normalized
stack value, and every assignment records its evidence (see ``project_tags.json``).
The taxonomy, vocabulary classes and rules are specified in ``TAGGING.md``.

Outputs (all under ``forms/extracted/``):

  * ``projects_tagged.csv``  — projects.csv + ``project_key`` + tag columns;
                               joinable 1:1 with projects.csv by ``source_file``+``no``
  * ``project_tags.json``    — one record per unique project, with evidence
  * ``tag_vocabulary.json``  — emitted slug → class/count/source values
  * ``tag_report.json``      — coverage stats, unmapped values, topic counts

Usage:  python3 tag_projects.py [--report]
"""

import argparse
import csv
import json
import re
import sys
from collections import Counter, OrderedDict, defaultdict
from functools import lru_cache
from hashlib import sha1
from pathlib import Path

ROOT = Path(__file__).resolve().parent
EXTRACTED = ROOT / "forms" / "extracted"

# ---------------------------------------------------------------------------
# Stack vocabulary
#
# Each entry: (slug | (slug, ...), class, [regex patterns]).
# Matching is span-masking: patterns are tried longest-first so that e.g.
# "react native" wins over "react" and "sql server" over "sql".  A pattern may
# emit several slugs ("django rest framework" → django + django-rest-framework).
#
# Classes decide which axis a slug lands on:
#   languages axis: language | markup | dsl | query | shell
#   frameworks axis: framework | library | runtime | platform | database |
#                    tool | service | protocol | standard | practice | concept
# ---------------------------------------------------------------------------

LANG_CLASSES = {"language", "markup", "dsl", "query", "shell"}

STACK_VOCAB = [
    # -- languages / markup / dsl / query / shell --------------------------
    ("python", "language", [r"python"]),
    ("javascript", "language", [r"javascript", r"java script", r"\bjs\b"]),
    ("typescript", "language", [r"typescript"]),
    ("java", "language", [r"\bjava\b"]),
    ("csharp", "language", [r"c#", r"c\s?sharp"]),
    ("cpp", "language", [r"c\+\+", r"\bcpp\b", r"vc\+\+", r"\bvc\b"]),
    ("c", "language", [r"\bc\b(?![#+])"]),
    ("go", "language", [r"golang", r"\bgo\b"]),
    ("kotlin", "language", [r"kotlin"]),
    ("dart", "language", [r"dart"]),
    ("swift", "language", [r"\bswift\b"]),
    ("ruby", "language", [r"\bruby\b"]),
    ("rust", "language", [r"\brust\b"]),
    ("scala", "language", [r"\bscala\b"]),
    ("matlab", "language", [r"matlab"]),
    ("verilog", "language", [r"\bverilog\b"]),
    ("systemverilog", "language", [r"systemverilog", r"system verilog"]),
    ("vhdl", "language", [r"\bvhdl\b"]),
    ("vba", "language", [r"\bvba\b", r"visual basic"]),
    ("assembly", "language", [r"\bassembly\b", r"\basm\b"]),
    ("capl", "language", [r"\bcapl\b"]),
    ("cython", "language", [r"cython"]),
    ("perl", "language", [r"\bperl\b"]),
    ("lua", "language", [r"luau", r"\blua\b"]),
    ("groovy", "language", [r"groovy"]),
    ("solidity", "language", [r"solidity"]),
    ("objective-c", "language", [r"objective-?c", r"\bobjc\b"]),
    ("tcl", "language", [r"\btcl\b", r"tool command language"]),
    ("qml", "language", [r"\bqml\b"]),
    ("sagemath", "language", [r"sagemath"]),
    ("powershell", "shell", [r"powershell", r"power shell"]),
    ("bash", "shell", [r"bash", r"shell script", r"shell scripting", r"\bshell\b", r"\bzsh\b"]),
    ("cshell", "shell", [r"c\s?shell", r"cshell"]),
    ("awk", "shell", [r"\bawk\b"]),
    ("skill-lang", "language", [r"\bskill\b"]),
    ("power-fx", "language", [r"power fx"]),
    ("sql", "query", [r"\bsql\b", r"pl/?sql", r"t-?sql"]),
    ("html", "markup", [r"html5?", r"\bhtml\b"]),
    ("css", "markup", [r"css\d?", r"\bcss\b"]),
    ("markdown", "markup", [r"markdown"]),
    ("json", "dsl", [r"json"]),
    ("yaml", "dsl", [r"yaml", r"\byml\b"]),
    ("xml", "dsl", [r"\bxml\b"]),
    ("php", "language", [r"\bphp\b"]),
    # -- frontend frameworks / libraries ----------------------------------
    ("react", "framework", [r"react\.?js", r"react js", r"\breact\b"]),
    ("react-native", "framework", [r"react[ -]native"]),
    ("nextjs", "framework", [r"next\.?js", r"\bnextjs\b"]),
    ("vue", "framework", [r"vue\.?js", r"\bvuejs\b", r"\bvue\b"]),
    ("angular", "framework", [r"angular"]),
    ("svelte", "framework", [r"svelte"]),
    ("nestjs", "framework", [r"nest\.?js", r"\bnestjs\b"]),
    ("express", "framework", [r"express\.?js", r"\bexpress\b"]),
    ("fastify", "framework", [r"fastify"]),
    ("hono", "framework", [r"\bhono\b"]),
    ("gatsby", "framework", [r"gatsby"]),
    ("vite", "tool", [r"\bvite"]),
    ("webpack", "tool", [r"webpack"]),
    ("esbuild", "tool", [r"esbuild"]),
    ("tsup", "tool", [r"\btsup\b"]),
    ("jquery", "library", [r"jquery"]),
    ("ajax", "concept", [r"ajax"]),
    ("bootstrap", "library", [r"bootstrap"]),
    ("tailwind", "framework", [r"tailwind\w*"]),
    ("mui", "library", [r"\bmui\b", r"material ui"]),
    ("shadcn", "library", [r"shadcn"]),
    ("sass", "tool", [r"\bsass\b", r"\bscss\b"]),
    ("redux", "library", [r"redux"]),
    ("zustand", "library", [r"zustand"]),
    ("pinia", "library", [r"pinia"]),
    ("react-router", "library", [r"react[- ]router"]),
    ("react-query", "library", [r"react[- ]query"]),
    ("react-hook-form", "library", [r"react[- ]hook[- ]form"]),
    ("tanstack", "library", [r"tanstack"]),
    ("zod", "library", [r"\bzod\b"]),
    ("class-validator", "library", [r"class-validator"]),
    ("recharts", "library", [r"recharts"]),
    ("chartjs", "library", [r"chart\.?js"]),
    ("echarts", "library", [r"echarts"]),
    # -- backend frameworks / runtimes ------------------------------------
    ("nodejs", "runtime", [r"node\.?js", r"node js", r"\bnode\b"]),
    ("node-red", "tool", [r"node-?red"]),
    ("deno", "runtime", [r"\bdeno\b"]),
    ("fastapi", "framework", [r"fast ?api"]),
    ("flask", "framework", [r"\bflask\b"]),
    ("django", "framework", [r"\bdjango\b"]),
    ("django-rest-framework", "framework", [r"django rest framework", r"\bdrf\b"]),
    ("laravel", "framework", [r"laravel"]),
    ("symfony", "framework", [r"symfony"]),
    ("rails", "framework", [r"ruby on rails", r"\brails\b"]),
    ("spring", "framework", [r"spring\.?boot", r"spring framework", r"spring web mvc",
                             r"spring security", r"spring data", r"spring core",
                             r"spring scheduler", r"\bspring\b"]),
    ("dotnet", "platform", [r"dotnet", r"\.net", r"asp\.?\s?net", r"\.net framework",
                            r"\.net core", r"\.net\s?\d+", r"\.net8"]),
    ("quarkus", "framework", [r"quarkus"]),
    ("hibernate", "library", [r"hibernate"]),
    ("jpa", "standard", [r"\bjpa\b"]),
    ("jsp", "framework", [r"\bjsp\b"]),
    ("servlet", "framework", [r"servlet"]),
    ("swing", "library", [r"java swing", r"java2d", r"\bswing\b"]),
    ("mfc", "framework", [r"\bmfc\b"]),
    ("winforms", "framework", [r"winform\\w*"]),
    ("wpf", "framework", [r"\bwpf\b"]),
    ("blazor", "framework", [r"blazor"]),
    ("prisma", "library", [r"prisma"]),
    ("sequelize", "library", [r"sequelize"]),
    ("typeorm", "library", [r"typeorm"]),
    ("gorm", "library", [r"\bgorm\b"]),
    ("sqlalchemy", "library", [r"sqlalchemy"]),
    ("entity-framework", "library", [r"entity framework"]),
    ("odoo", "platform", [r"odoo"]),
    ("suitecrm", "platform", [r"suitecrm"]),
    ("dotnetnuke", "platform", [r"dotnetnuke", r"\bdnn\b"]),
    ("mcp", "protocol", [r"\bmcp\b"]),
    ("orm", "concept", [r"\borm\b"]),
    # -- microservices / api ----------------------------------------------
    ("rest-api", "protocol", [r"restful? ?api", r"\brest api\b", r"\brest\b"]),
    ("graphql", "protocol", [r"graphql"]),
    ("grpc", "protocol", [r"grpc"]),
    ("websocket", "protocol", [r"websocket\w*", r"\bsse\b"]),
    ("socketio", "library", [r"socket\.?io"]),
    ("openapi", "standard", [r"openapi"]),
    ("swagger", "tool", [r"swagger"]),
    ("oapi-codegen", "tool", [r"oapi-codegen"]),
    ("jwt", "standard", [r"\bjwt\b"]),
    ("oauth", "standard", [r"oauth\d?"]),
    ("keycloak", "tool", [r"keycloak"]),
    ("oauth2-proxy", "tool", [r"oauth2-proxy"]),
    ("postman", "tool", [r"postman"]),
    ("bruno", "tool", [r"\bbruno\b"]),
    ("microservices", "concept", [r"micro-?services?"]),
    ("mvc", "practice", [r"\bmvc\b"]),
    ("mvvm", "practice", [r"mvvm"]),
    ("clean-architecture", "practice", [r"clean architecture"]),
    ("ddd", "practice", [r"domain-driven", r"\bddd\b"]),
    ("design-pattern", "practice", [r"design pattern"]),
    ("repository-pattern", "practice", [r"repository pattern"]),
    # -- mobile / desktop --------------------------------------------------
    ("flutter", "framework", [r"flutter"]),
    ("expo", "tool", [r"\bexpo\b"]),
    ("android", "platform", [r"android", r"\baosp\b"]),
    ("ios", "platform", [r"\bios\b", r"iphone", r"ipad"]),
    ("swiftui", "framework", [r"swiftui"]),
    ("jetpack", "library", [r"jetpack"]),
    ("qt", "framework", [r"qt framework", r"qt quick", r"\bqt\b"]),
    ("pyside", "library", [r"pyside\d?"]),
    ("pyqt", "library", [r"pyqt\d?", r"pyqtgraph"]),
    ("tkinter", "library", [r"tkinter"]),
    ("electron", "framework", [r"electron"]),
    ("xamarin", "framework", [r"xamarin"]),
    # -- game --------------------------------------------------------------
    ("unity", "framework", [r"unity\w*"]),
    ("unreal", "framework", [r"unreal"]),
    ("roblox", "platform", [r"roblox"]),
    ("cocos", "framework", [r"cocos"]),
    ("game-engine", "concept", [r"game engine"]),
    # -- ai / ml -----------------------------------------------------------
    ("pytorch", "library", [r"pytorch", r"\btorch\b"]),
    ("tensorflow", "library", [r"tensorflow", r"\btflite\b"]),
    ("keras", "library", [r"keras"]),
    ("sklearn", "library", [r"scikit-?learn", r"sklearn"]),
    ("xgboost", "library", [r"xgboost"]),
    ("lightgbm", "library", [r"lightgbm"]),
    ("transformers", "library", [r"transformers"]),
    ("huggingface", "platform", [r"hugging ?face", r"huggingface"]),
    ("accelerate", "library", [r"accelerate"]),
    ("peft", "library", [r"peft", r"qlora", r"\blora\b"]),
    ("deepspeed", "library", [r"deepspeed", r"\bfsdp\b"]),
    ("vllm", "tool", [r"vllm"]),
    ("ollama", "tool", [r"ollama"]),
    ("llama-index", "framework", [r"llama ?index"]),
    ("langchain", "framework", [r"lang ?chain"]),
    ("langgraph", "framework", [r"lang ?graph"]),
    ("langfuse", "tool", [r"langfuse"]),
    ("ragas", "library", [r"ragas"]),
    ("dify", "platform", [r"dify"]),
    ("n8n", "platform", [r"n8n"]),
    ("zapier", "platform", [r"zapier"]),
    ("make-integromat", "platform", [r"make \(integromat\)", r"integromat"]),
    ("mlflow", "tool", [r"mlflow"]),
    ("wandb", "tool", [r"weights ?& ?biases", r"\bwandb\b"]),
    ("optuna", "library", [r"optuna"]),
    ("ray", "library", [r"\bray\b"]),
    ("gymnasium", "library", [r"gymnasium"]),
    ("stable-baselines3", "library", [r"stable[- ]baseline"]),
    ("onnx", "tool", [r"onnx"]),
    ("triton", "tool", [r"\btriton\b"]),
    ("kserve", "tool", [r"kserve"]),
    ("faiss", "library", [r"faiss"]),
    ("chromadb", "database", [r"chroma(?:db)?"]),
    ("qdrant", "database", [r"qdrant"]),
    ("pinecone", "database", [r"pinecone"]),
    ("milvus", "database", [r"milvus"]),
    ("pgvector", "database", [r"pgvector"]),
    ("vector-database", "concept", [r"vector (database|db)"]),
    ("sentence-transformers", "library", [r"sentence[- ]transformers"]),
    ("flagembedding", "library", [r"flagembedding"]),
    ("phobert", "library", [r"phobert"]),
    ("whisper", "library", [r"whisper"]),
    ("vall-e", "library", [r"vall-?e"]),
    ("rasa", "framework", [r"\brasa\b"]),
    ("openai", "service", [r"openai", r"\bchatgpt\b", r"\bgpt\b", r"\bgpt-?\d"]),
    ("gemini", "service", [r"gemini"]),
    ("claude", "service", [r"claude"]),
    ("anthropic", "service", [r"anthropic"]),
    ("bedrock", "service", [r"bedrock"]),
    ("vertex-ai", "service", [r"vertex"]),
    ("sagemaker", "service", [r"sagemaker"]),
    ("nano-banana", "service", [r"nano ?banana"]),
    ("prompt-engineering", "practice", [r"prompt engineering", r"ai\\s?prom(?:pt)?", r"prompt"]),
    ("ai", "concept", [r"\bai\b", r"\bai[- ](api|agent|service|framework|orchestration)"]),
    ("llm", "concept", [r"\bllm", r"local llm", r"mô hình llm", r"large language model"]),
    ("machine-learning", "concept", [r"machine learning"]),
    ("deep-learning", "concept", [r"deep learning"]),
    ("computer-vision", "concept", [r"computer vision"]),
    ("machine-vision", "concept", [r"machine vision"]),
    ("nlp", "concept", [r"\bnlp\b"]),
    ("rag", "concept", [r"\brag\b", r"retrieval-augmented"]),
    ("ai-agent", "concept", [r"ai agent", r"agentic"]),
    ("mlops", "practice", [r"mlops"]),
    ("data-science", "practice", [r"data science"]),
    ("ocr", "concept", [r"\bocr\b"]),
    ("yolo", "library", [r"yolo(?:v\\d+)?", r"ultralytics"]),
    ("opencv", "library", [r"opencv"]),
    ("mediapipe", "library", [r"mediapipe"]),
    ("deepface", "library", [r"deepface"]),
    ("insightface", "library", [r"insightface"]),
    ("face-recognition", "library", [r"face recognition"]),
    ("segment-anything", "library", [r"segment anything"]),
    ("tesseract", "tool", [r"tesseract"]),
    ("easyocr", "library", [r"easyocr"]),
    ("paddleocr", "library", [r"paddle ?ocr", r"paddle"]),
    ("docling", "library", [r"docling"]),
    ("doclayout-yolo", "library", [r"doclayout"]),
    ("label-studio", "tool", [r"label ?studio"]),
    ("cvat", "tool", [r"\bcvat\b"]),
    ("simpleitk", "library", [r"simpleitk"]),
    ("nibabel", "library", [r"nibabel"]),
    ("nnunet", "library", [r"nn-?u-?net"]),
    ("stable-diffusion", "library", [r"stable ?diffusion"]),
    ("tinygrad", "library", [r"tinygrad"]),
    # -- data / analytics --------------------------------------------------
    ("pandas", "library", [r"pandas"]),
    ("numpy", "library", [r"numpy"]),
    ("scipy", "library", [r"scipy"]),
    ("matplotlib", "library", [r"matplotlib"]),
    ("seaborn", "library", [r"seaborn"]),
    ("plotly", "library", [r"plotly"]),
    (("plotly", "dash"), "framework", [r"plotly dash"]),
    ("dash", "framework", [r"\bdash\b"]),
    ("streamlit", "framework", [r"streamlit"]),
    ("gradio", "framework", [r"gradio"]),
    ("openpyxl", "library", [r"openpyxl"]),
    ("excel", "tool", [r"excel"]),
    ("power-query", "tool", [r"power query"]),
    ("google-sheets", "service", [r"google sheets"]),
    ("jupyter", "tool", [r"jupyter"]),
    ("databricks", "platform", [r"databricks"]),
    ("delta-lake", "concept", [r"delta lake"]),
    ("data-lakehouse", "concept", [r"lakehouse"]),
    ("hadoop", "platform", [r"hadoop"]),
    ("hbase", "database", [r"hbase"]),
    ("hdfs", "platform", [r"hdfs"]),
    ("spark", "platform", [r"\bspark\b"]),
    ("kafka", "platform", [r"kafka"]),
    ("flink", "platform", [r"flink"]),
    ("airflow", "platform", [r"airflow"]),
    ("nifi", "platform", [r"\bnifi\b"]),
    ("debezium", "tool", [r"debezium"]),
    ("trino", "platform", [r"\btrino\b"]),
    ("solr", "platform", [r"solr"]),
    ("great-expectations", "library", [r"great expectations"]),
    ("dvc", "tool", [r"\bdvc\b"]),
    ("datahub", "tool", [r"datahub"]),
    ("openmetadata", "tool", [r"openmetadata"]),
    ("minio", "platform", [r"minio"]),
    ("ceph", "platform", [r"\bceph\b"]),
    ("looker", "tool", [r"looker"]),
    ("metabase", "tool", [r"metabase"]),
    ("superset", "tool", [r"superset"]),
    ("power-bi", "tool", [r"power ?bi"]),
    ("tableau", "tool", [r"tableau"]),
    ("sensor-tower", "tool", [r"sensortower"]),
    ("ga4", "tool", [r"\bga4\b"]),
    ("rabbitmq", "platform", [r"rabbitmq"]),
    ("message-queue", "concept", [r"message queue"]),
    ("prometheus", "tool", [r"prometheus"]),
    ("grafana", "tool", [r"grafana"]),
    ("loki", "tool", [r"\bloki\b"]),
    ("elk", "platform", [r"\belk\b", r"elasticsearch", r"logstash"]),
    ("kibana", "tool", [r"kibana"]),
    ("dotnet-ef", "tool", [r"ef core"]),
    ("data-pipeline", "concept", [r"data pipeline"]),
    ("etl", "concept", [r"\betl\b", r"\belt\b"]),
    ("bi", "concept", [r"\bbi\b"]),
    ("database", "database", [r"database", r"cơ sở dữ liệu"]),
    ("postgresql", "database", [r"postgres(?:ql)?", r"psql"]),
    ("mysql", "database", [r"mysql", r"mariadb"]),
    ("sql-server", "database", [r"sql server", r"mssql", r"ms sql", r"\bssms\b"]),
    ("oracle-db", "database", [r"oracle"]),
    ("sqlite", "database", [r"sqlite\d?", r"better-sqlite3"]),
    ("mongodb", "database", [r"mongo(?:db)?"]),
    ("redis", "database", [r"redis"]),
    ("dynamodb", "database", [r"dynamodb"]),
    ("clickhouse", "database", [r"clickhouse"]),
    ("duckdb", "database", [r"duckdb"]),
    ("influxdb", "database", [r"influxdb"]),
    ("bigquery", "platform", [r"bigquery"]),
    ("snowflake", "platform", [r"snowflake"]),
    ("supabase", "platform", [r"supabase"]),
    ("firebase", "platform", [r"firebase"]),
    # -- cloud / devops ----------------------------------------------------
    ("aws", "platform", [r"\baws\b", r"amazon web service"]),
    ("aws-s3", "service", [r"amazon s3", r"\bs3\b", r"s3-compatible"]),
    ("aws-lambda", "service", [r"aws lambda", r"edge functions"]),
    ("aws-cdk", "tool", [r"aws cdk", r"\bcdk\b"]),
    ("aws-sdk", "library", [r"aws sdk"]),
    ("cloudwatch", "tool", [r"cloudwatch"]),
    ("azure", "platform", [r"azure"]),
    ("gcp", "platform", [r"\bgcp\b", r"google cloud"]),
    ("openstack", "platform", [r"openstack"]),
    ("openshift", "platform", [r"openshift"]),
    ("docker", "tool", [r"docker"]),
    ("kubernetes", "platform", [r"kubernetes", r"\bk8s\b", r"\bk3s\b"]),
    ("helm", "tool", [r"\bhelm\b"]),
    ("argocd", "tool", [r"argo ?cd"]),
    ("jenkins", "tool", [r"jenkins"]),
    ("terraform", "tool", [r"terraform"]),
    ("ansible", "tool", [r"ansible"]),
    ("pulumi", "tool", [r"pulumi"]),
    ("argocd-config", "tool", [r"kustomize"]),
    ("cloud-native", "concept", [r"cloud[- ]native"]),
    ("devops", "practice", [r"devops"]),
    ("ci-cd", "practice", [r"ci/cd", r"ci-cd", r"continuous integration"]),
    ("linux", "platform", [r"linux", r"ubuntu", r"unix"]),
    ("windows-server", "platform", [r"windows server"]),
    ("active-directory", "platform", [r"active directory"]),
    ("nginx", "tool", [r"nginx"]),
    ("iis", "platform", [r"\biis\b"]),
    ("pam", "tool", [r"\bpam\b"]),
    ("itil", "practice", [r"\bitil\b"]),
    ("sdlc", "practice", [r"\bsdlc\b"]),
    ("agile", "practice", [r"agile"]),
    ("scrum", "practice", [r"scrum"]),
    ("uml", "standard", [r"\buml\b"]),
    ("grc", "practice", [r"\bgrc\b"]),
    ("shadow-it", "concept", [r"shadow it"]),
    # -- security ----------------------------------------------------------
    ("owasp", "standard", [r"owasp"]),
    ("mitre-attack", "standard", [r"mitre", r"att&ck", r"attack framework"]),
    ("cyber-kill-chain", "standard", [r"kill chain"]),
    ("nist", "standard", [r"\bnist\b"]),
    ("iso-27001", "standard", [r"iso 27001"]),
    ("iso-21434", "standard", [r"iso/?sae 21434", r"\b21434\b"]),
    ("gdpr", "standard", [r"gdpr"]),
    ("pci-dss", "standard", [r"pci[- ]dss"]),
    ("burp", "tool", [r"burp ?suite", r"burpsuite"]),
    ("frida", "tool", [r"\bfrida\b"]),
    ("ida", "tool", [r"\bida\b", r"ida pro"]),
    ("jadx", "tool", [r"jadx"]),
    ("apktool", "tool", [r"apktool"]),
    ("apksigner", "tool", [r"apksigner"]),
    ("mobsf", "tool", [r"mobsf"]),
    ("metasploit", "tool", [r"metasploit"]),
    ("nmap", "tool", [r"\bnmap\b"]),
    ("wireshark", "tool", [r"wireshark"]),
    ("kali", "platform", [r"\bkali\b"]),
    ("hackerone", "service", [r"hackerone"]),
    ("bugcrowd", "service", [r"bugcrowd"]),
    ("hackthebox", "platform", [r"hack ?the ?box"]),
    ("portswigger", "platform", [r"portswigger"]),
    ("dvwa", "tool", [r"\bdvwa\b"]),
    ("juice-shop", "tool", [r"juice shop"]),
    ("virustotal", "service", [r"virustotal"]),
    ("abuseipdb", "service", [r"abuseipdb"]),
    ("misp", "platform", [r"\bmisp\b"]),
    ("yara", "tool", [r"\byara\b"]),
    ("sigma-rules", "standard", [r"sigma rules"]),
    ("sysmon", "tool", [r"sysmon"]),
    ("atomic-red-team", "tool", [r"atomic red team"]),
    ("splunk", "platform", [r"splunk"]),
    ("siem", "concept", [r"\bsiem\b"]),
    ("wazuh", "platform", [r"wazuh"]),
    ("dlp", "concept", [r"\bdlp\b"]),
    ("iam", "concept", [r"\biam\b"]),
    ("edr", "concept", [r"\bedr\b", r"av/edr"]),
    ("ransomware", "concept", [r"ransomware"]),
    ("firewall", "concept", [r"firewall", r"tường lửa"]),
    ("waf", "concept", [r"\bwaf\b", r"application firewall"]),
    ("fortify", "tool", [r"fortify"]),
    ("opentext", "platform", [r"opentext"]),
    ("palo-alto", "platform", [r"palo alto"]),
    ("sophos", "platform", [r"sophos"]),
    ("pentest", "practice", [r"pentest", r"penetration test"]),
    ("forensics", "practice", [r"forensic"]),
    ("vulnerability", "concept", [r"vulnerabilit\w*", r"lỗ hổng"]),
    ("exploit", "concept", [r"exploit\w*"]),
    ("rsa", "standard", [r"\brsa\b"]),
    ("pqc", "concept", [r"hậu lượng tử", r"post-?quantum", r"ml-kem", r"ml-dsa"]),
    ("cryptography", "concept", [r"cryptograph\w*", r"mật mã"]),
    ("threat-intelligence", "concept", [r"threat intelligence", r"threat intel"]),
    ("detection-engineering", "practice", [r"detection engineering"]),
    ("bas", "concept", [r"attack simulation", r"\bbas\b"]),
    ("blueteam", "practice", [r"blue ?team"]),
    ("redteam", "practice", [r"red ?team"]),
    # -- networking --------------------------------------------------------
    ("dhcp", "protocol", [r"\bdhcp\b"]),
    ("dns", "protocol", [r"\bdns\b"]),
    ("gns3", "tool", [r"gns3"]),
    ("draytek", "platform", [r"draytek"]),
    ("aruba", "platform", [r"aruba"]),
    ("openwrt", "platform", [r"openwrt"]),
    ("sdn", "concept", [r"\bsdn\b"]),
    ("sdwan", "concept", [r"sdwan", r"sd-wan"]),
    ("openran", "concept", [r"openran", r"open ran"]),
    ("cisco", "platform", [r"cisco"]),
    ("juniper", "platform", [r"junos", r"juniper"]),
    ("wifi", "protocol", [r"wi-?fi", r"wlan"]),
    ("bluetooth", "protocol", [r"bluetooth", r"\bble\b"]),
    ("zigbee", "protocol", [r"zigbee"]),
    ("amx", "platform", [r"\bamx\b"]),
    ("ptz", "protocol", [r"\bptz\b"]),
    # -- embedded / iot ----------------------------------------------------
    ("arduino", "platform", [r"arduino"]),
    ("platformio", "tool", [r"platformio"]),
    ("esp32", "platform", [r"esp32", r"esp8266"]),
    ("esp-idf", "framework", [r"esp-?idf", r"espressif"]),
    ("stm32", "platform", [r"stm32\w*"]),
    ("stm32cube", "tool", [r"stm32cube", r"cubeide", r"cube mx"]),
    ("hal", "library", [r"\bhal\b"]),
    ("freertos", "rtos", [r"freertos", r"free ?rtos"]),
    ("zephyr", "rtos", [r"zephyr"]),
    ("nrf", "platform", [r"\bnrf\w*"]),
    ("rtos", "concept", [r"\brtos\b"]),
    ("keil", "tool", [r"\bkeil", r"mdk arm"]),
    ("yocto", "tool", [r"yocto"]),
    ("petalinux", "tool", [r"petalinux"]),
    ("raspberry-pi", "platform", [r"raspberry ?pi"]),
    ("i2c", "protocol", [r"\bi2c\b"]),
    ("spi", "protocol", [r"\bspi\b"]),
    ("uart", "protocol", [r"\buart\b"]),
    ("gpio", "protocol", [r"\bgpio\b"]),
    ("modbus", "protocol", [r"modbus"]),
    ("mqtt", "protocol", [r"mqtt"]),
    ("home-assistant", "platform", [r"home ?assistant"]),
    ("thingsboard", "platform", [r"thingsboard"]),
    ("iot", "concept", [r"\biot\b", r"iiot", r"aiot"]),
    ("embedded", "concept", [r"embedded", r"nhúng"]),
    ("firmware", "concept", [r"firmware"]),
    ("can-bus", "protocol", [r"can ?bus", r"can stack", r"can-uart"]),
    ("canoe", "tool", [r"canoe"]),
    ("ecu-test", "tool", [r"ecu-?test"]),
    ("ascet", "tool", [r"ascet"]),
    ("autosar", "standard", [r"autosar"]),
    ("sensor-fusion", "concept", [r"sensor fusion"]),
    ("tinyml", "concept", [r"tinyml"]),
    ("servo", "concept", [r"servo"]),
    ("plc", "concept", [r"\bplc\b"]),
    ("scada", "concept", [r"\bscada\b"]),
    ("ifix", "tool", [r"ifix"]),
    ("cnc", "concept", [r"\bcnc\b"]),
    # -- robotics ----------------------------------------------------------
    ("ros", "framework", [r"\bros\s?2?\b", r"robot operating system"]),
    ("gazebo", "tool", [r"gazebo"]),
    ("rviz", "tool", [r"rviz\d*"]),
    ("isaac-sim", "tool", [r"isaac (sim|lab)"]),
    ("px4", "platform", [r"\bpx4\b"]),
    ("mavsdk", "library", [r"mavsdk"]),
    ("qgroundcontrol", "tool", [r"qgroundcontrol"]),
    ("slam", "concept", [r"\bslam\b"]),
    ("rvo2", "library", [r"rvo2"]),
    ("ubtech", "platform", [r"ubtech"]),
    ("inverse-kinematic", "concept", [r"inverse kinematic"]),
    ("colcon", "tool", [r"colcon"]),
    ("navigation2", "framework", [r"nav2", r"navigation2"]),
    ("robotics", "concept", [r"robot(?:ics|ic|s)?"]),
    # -- chip / hardware ---------------------------------------------------
    ("vivado", "tool", [r"vivado"]),
    ("vitis", "tool", [r"\bvitis\b"]),
    ("modelsim", "tool", [r"modelsim"]),
    ("questasim", "tool", [r"questa\w*"]),
    ("verilator", "tool", [r"verilator"]),
    ("icarus-verilog", "tool", [r"icarus"]),
    ("systemc", "library", [r"systemc", r"system\s?c\b"]),
    ("cocotb", "tool", [r"cocotb"]),
    ("uvm", "standard", [r"\buvm\b"]),
    ("gtkwave", "tool", [r"gtkwave"]),
    ("klayout", "tool", [r"klayout"]),
    ("yosys", "tool", [r"yosys"]),
    ("openroad", "tool", [r"openroad"]),
    ("opensta", "tool", [r"opensta"]),
    ("sky130", "standard", [r"sky ?130"]),
    ("chisel", "library", [r"\bchisel\b"]),
    ("riscv", "platform", [r"risc-?v\w*"]),
    ("cadence-innovus", "tool", [r"innovus"]),
    ("cadence-genus", "tool", [r"cadence genus", r"\bgenus\b"]),
    ("synopsys", "tool", [r"synopsys"]),
    ("ic-compiler", "tool", [r"ic compiler", r"icc2"]),
    ("virtuoso", "tool", [r"virtuoso"]),
    ("calibre", "tool", [r"calibre"]),
    ("verdi", "tool", [r"verdi"]),
    ("vcs", "tool", [r"\bvcs\b"]),
    ("spyglass", "tool", [r"spyglass"]),
    ("polyspace", "tool", [r"polyspace"]),
    ("cantata", "tool", [r"cantata"]),
    ("hdlbits", "platform", [r"hdlbits"]),
    ("rtl", "concept", [r"\brtl\b"]),
    ("fpga", "platform", [r"fpga", r"xilinx", r"zcu\d+", r"altera"]),
    ("asic", "concept", [r"\bASIC\b"]),
    ("soc", "concept", [r"\bSoC\b"]),
    ("pdk", "concept", [r"\bpdk\b"]),
    ("eda", "concept", [r"\beda\b"]),
    ("ic-design", "concept", [r"ic design", r"digital ic"]),
    ("ams", "concept", [r"\bams\b"]),
    ("tlm", "concept", [r"\btlm\b", r"transaction-level"]),
    ("ftdtd", "tool", [r"fdtd"]),
    ("kicad", "tool", [r"kicad"]),
    ("altium", "tool", [r"altium"]),
    ("cadence-virtuoso-suite", "tool", [r"cadence"]),
    ("opengl", "library", [r"opengl"]),
    ("oscilloscope", "tool", [r"oscilloscope", r"logic analyzer"]),
    ("cuda", "platform", [r"cuda"]),
    # -- testing -----------------------------------------------------------
    ("playwright", "tool", [r"playwright"]),
    ("selenium", "tool", [r"selenium"]),
    ("cypress", "tool", [r"cypress"]),
    ("appium", "tool", [r"appium"]),
    ("testcafe", "tool", [r"testcafe"]),
    ("codeceptjs", "tool", [r"codecept\w*"]),
    ("robot-framework", "tool", [r"robot framework"]),
    ("pytest", "tool", [r"pytest", r"\bpy test\b"]),
    ("jest", "tool", [r"\bjest\b"]),
    ("vitest", "tool", [r"vitest"]),
    ("google-test", "library", [r"google test", r"googletest", r"gtest"]),
    ("junit", "library", [r"junit"]),
    ("cucumber", "tool", [r"cucumber"]),
    ("browserstack", "service", [r"browserstack"]),
    ("tradefed", "tool", [r"tradefed"]),
    ("nunit", "library", [r"nunit"]),
    ("qa", "practice", [r"\bqa\b", r"\bqc\b", r"quality assurance", r"kiểm thử", r"kiểm định chất lượng"]),
    ("tdd", "practice", [r"\btdd\b", r"test driven development"]),
    ("bdd", "practice", [r"\bbdd\b", r"behavior driven development"]),
    ("exploratory-testing", "practice", [r"exploratory testing"]),
    ("regression-testing", "practice", [r"regression test"]),
    ("smoke-testing", "practice", [r"smoke test"]),
    ("functional-testing", "practice", [r"functional test"]),
    ("v-model", "practice", [r"v-model"]),
    # -- misc tools --------------------------------------------------------
    ("git", "tool", [r"\bgit\b"]),
    ("github", "platform", [r"github"]),
    ("gitlab", "platform", [r"gitlab"]),
    ("github-actions", "tool", [r"github actions"]),
    ("svn", "tool", [r"\bsvn\b", r"subversion"]),
    ("jira", "tool", [r"jira"]),
    ("trello", "tool", [r"trello"]),
    ("notion", "tool", [r"notion"]),
    ("redmine", "tool", [r"redmine"]),
    ("slack", "tool", [r"slack"]),
    ("teams", "tool", [r"microsoft teams", r"ms teams", r"\bteams\b"]),
    ("zoom", "tool", [r"\bzoom\b"]),
    ("figma", "tool", [r"figma"]),
    ("lark", "platform", [r"lark"]),
    ("crm", "concept", [r"\bcrm\b"]),
    ("erp", "concept", [r"\berp\b"]),
    ("archer", "platform", [r"\barcher\b"]),
    ("power-platform", "platform", [r"power platform"]),
    ("power-apps", "platform", [r"power apps?"]),
    ("power-automate", "tool", [r"power automate"]),
    ("appsheet", "platform", [r"appsheet"]),
    ("sharepoint", "platform", [r"sharepoint"]),
    ("dataverse", "platform", [r"dataverse"]),
    ("vscode", "tool", [r"vs ?code", r"vscode"]),
    ("visual-studio", "tool", [r"visual studio"]),
    ("eclipse", "tool", [r"\beclipse\b"]),
    ("adb", "tool", [r"\badb\b"]),
    ("strace", "tool", [r"\bstrace\b"]),
    ("ebpf", "concept", [r"ebpf"]),
    ("pydantic", "library", [r"pydantic"]),
    ("httpx", "library", [r"httpx"]),
    ("requests", "library", [r"\brequests\b"]),
    ("beautifulsoup", "library", [r"beautiful ?soup"]),
    ("pillow", "library", [r"\bpillow\b", r"\bpil\b"]),
    ("pyyaml", "library", [r"pyyaml"]),
    ("psutil", "library", [r"psutil"]),
    ("pyserial", "library", [r"pyserial"]),
    ("winreg", "library", [r"winreg"]),
    ("sphinx", "tool", [r"\bsphinx\b"]),
    ("nodemailer", "library", [r"nodemailer"]),
    ("bullmq", "library", [r"bullmq"]),
    ("xlsx", "library", [r"epplus"]),
    ("vscode", "tool", [r"vs ?code", r"vscode"]),
    # -- gis ---------------------------------------------------------------
    ("arcgis", "platform", [r"arcgis", r"esri"]),
    ("leaflet", "library", [r"leaflet"]),
    ("postgis", "database", [r"postgis"]),
    ("geopandas", "library", [r"geopandas"]),
    ("openstreetmap", "platform", [r"openstreetmap", r"\bosm\b", r"osrm"]),
    ("geocoding", "concept", [r"geo-?cod\w*"]),
    ("gis", "concept", [r"\bgis\b", r"geospatial", r"địa không gian"]),
    # -- blockchain --------------------------------------------------------
    ("blockchain", "concept", [r"blockchain", r"chuỗi khối"]),
    ("smart-contract", "concept", [r"smart contract"]),
    ("web3", "concept", [r"web3"]),
    ("truffle", "tool", [r"\btruffle\b"]),
    ("testnet", "platform", [r"testnet"]),
    ("nft", "concept", [r"\bnft\b"]),
    ("defi", "concept", [r"\bdefi\b"]),
]

# Flatten to (pattern, [slugs], class), longest pattern first so that
# span-masking prefers the most specific match.  Alphanumeric-edged patterns
# get word guards so that e.g. "unity" cannot match inside "community".
def _guarded(p):
    out = p
    if re.match(r"^[A-Za-z0-9]", p):
        out = r"(?<![A-Za-z0-9])" + out
    if re.search(r"[A-Za-z0-9]$", p):
        out = out + r"(?![A-Za-z0-9])"
    return out


_FLAT = []
for _slug, _cls, _pats in STACK_VOCAB:
    _slugs = (_slug,) if isinstance(_slug, str) else tuple(_slug)
    for _p in _pats:
        _FLAT.append((_p, _slugs, _cls))
_FLAT.sort(key=lambda e: -len(e[0]))
_STACK_RE = [(re.compile(_guarded(p), re.IGNORECASE), slugs, cls)
             for p, slugs, cls in _FLAT]


@lru_cache(maxsize=None)
def match_stack(text):
    """Return ordered unique (slug, class, matched_text) found in ``text``."""
    if not text:
        return []
    work = text
    out = []
    seen = set()
    for rx, slugs, cls in _STACK_RE:
        while True:
            m = rx.search(work)
            if not m:
                break
            hit = m.group(0)
            for s in slugs:
                if s not in seen:
                    seen.add(s)
                    out.append((s, cls, hit))
            work = work[:m.start()] + "\x00" * (m.end() - m.start()) + work[m.end():]
    return out


# ---------------------------------------------------------------------------
# Topic taxonomy
#
# Every topic: {"any": [patterns], "all": [[group ...], ...] (optional),
#               "stack": [stack slugs] (optional)}
# A topic fires when any ``any`` pattern matches, or all ``all`` groups have a
# match, or any ``stack`` slug is present in the normalized stack.
# Topics are aspects, not exclusive classes: a project may carry several.
# ---------------------------------------------------------------------------

TOPICS = OrderedDict([
    ("web", {
        "any": [r"\bweb\b", r"website", r"web ?app", r"trang web", r"landing page",
                r"front[- ]?end", r"back[- ]?end", r"full[- ]?stack",
                r"giao diện (web|người dùng)", r"portal", r"cổng (thông tin|giao tiếp)",
                r"\bcms\b", r"trình duyệt", r"browser", r"web ?server",
                r"\bhtml\b", r"\bcss\b", r"asp\.?net", r"micro-?services",
                r"restful? ?api", r"\bapi\b", r"graphql",
                r"(nền tảng|hệ thống|ứng dụng) (web|trực tuyến)", r"web-based"],
        "stack": ["react", "nextjs", "vue", "angular", "svelte", "nestjs", "express",
                  "fastapi", "flask", "django", "laravel", "rails", "spring", "dotnet",
                  "nodejs", "html", "css", "tailwind", "bootstrap", "jquery", "php",
                  "streamlit", "dash"],
    }),
    ("mobile", {
        "any": [r"\bmobile\b", r"di động", r"điện thoại", r"smartphone",
                r"\bandroid\b", r"\bios\b", r"iphone", r"ipad",
                r"google play", r"ch play", r"app store",
                r"ứng dụng (mobile|di động|điện thoại)"],
        "stack": ["android", "ios", "flutter", "react-native", "expo", "swiftui",
                  "jetpack", "kotlin"],
    }),
    ("game", {
        "any": [r"\bgame\b", r"gaming", r"trò chơi", r"gameplay",
                r"\bunity\b", r"unreal", r"roblox", r"cocos", r"esport", r"e-sport",
                r"live ops", r"game (casual|puzzle|mobile)", r"casual/puzzle"],
        "stack": ["unity", "unreal", "roblox", "cocos", "game-engine"],
    }),
    ("ai", {
        "any": [r"\bai\b", r"trí tuệ nhân tạo", r"artificial intelligence",
                r"machine learning", r"học máy", r"học sâu", r"deep learning",
                r"reinforcement learning", r"học tăng cường",
                r"\bllm\b", r"large language model", r"mô hình ngôn ngữ",
                r"ngôn ngữ lớn", r"chatbot", r"(?<!ro)\bbot\b", r"\brag\b",
                r"retrieval-augmented", r"generative", r"tạo sinh", r"agentic",
                r"ai agent", r"\bagent\b", r"\bgpt\b", r"openai", r"gemini",
                r"claude", r"fine-?tun",
                r"(dự báo|dự đoán|forecast|predict)", r"recommendation", r"gợi ý",
                r"nhận diện", r"nhận dạng", r"phân loại", r"phân cụm", r"clustering",
                r"classification", r"huấn luyện", r"mlops", r"data science",
                r"edge ai", r"aiot", r"mô hình (ai|học|ngôn ngữ|phân loại|dự báo)"],
        "stack": ["pytorch", "tensorflow", "keras", "sklearn", "xgboost", "lightgbm",
                  "transformers", "huggingface", "peft", "deepspeed", "vllm", "ollama",
                  "langchain", "langgraph", "llama-index", "langfuse", "ragas", "dify",
                  "mlflow", "wandb", "optuna", "ray", "gymnasium", "stable-baselines3",
                  "onnx", "triton", "kserve", "faiss", "chromadb", "qdrant", "pinecone",
                  "milvus", "pgvector", "vector-database", "sentence-transformers",
                  "flagembedding", "phobert", "whisper", "vall-e", "rasa", "openai",
                  "gemini", "claude", "anthropic", "bedrock", "vertex-ai", "sagemaker",
                  "prompt-engineering", "machine-learning", "deep-learning", "mlops",
                  "data-science", "ai-agent", "rag", "llm", "ai", "tinygrad",
                  "stable-diffusion", "nnunet"],
    }),
    ("computer-vision", {
        "any": [r"thị giác máy", r"computer vision", r"machine vision", r"xử lý ảnh",
                r"image processing", r"\bocr\b", r"nhận diện", r"nhận dạng",
                r"nhận dạng khuôn mặt", r"face id", r"khuôn mặt", r"biển số",
                r"object detection", r"phát hiện đối tượng", r"phân đoạn",
                r"segmentation", r"hình ảnh", r"ảnh (số|y tế|dce|mri|vệ tinh)",
                r"video", r"camera (ai|ip|giám sát)", r"nhãn giá", r"document ai",
                r"đếm (người|số lượng)", r"trích xuất (thông tin|dữ liệu) từ (hình ảnh|ảnh|tệp|pdf|tài liệu)"],
        "stack": ["opencv", "yolo", "mediapipe", "deepface", "insightface",
                  "face-recognition", "segment-anything", "tesseract", "easyocr",
                  "paddleocr", "docling", "doclayout-yolo", "label-studio", "cvat",
                  "computer-vision", "machine-vision", "ocr", "simpleitk", "nibabel"],
    }),
    ("nlp", {
        "any": [r"\bnlp\b", r"ngôn ngữ tự nhiên", r"natural language",
                r"xử lý ngôn ngữ", r"text mining", r"text-to-", r"text to ",
                r"văn bản", r"speech", r"giọng nói", r"tiếng nói", r"\bvoice",
                r"\btts\b", r"text-to-speech", r"clone voice", r"\basr\b",
                r"phiên dịch", r"dịch thuật", r"dịch (văn bản|ngôn ngữ)",
                r"translation", r"chatbot", r"hỏi[- ]đáp", r"hỏi đáp",
                r"sentiment", r"cảm xúc", r"đa ngôn ngữ", r"multilingual",
                r"voice ?bot", r"call ?bot", r"agent (hội thoại|talking)"],
        "stack": ["phobert", "whisper", "vall-e", "rasa", "sentence-transformers",
                  "flagembedding", "transformers", "nlp"],
    }),
    ("data", {
        "any": [r"data (pipeline|engineer|science|analytics|lake|warehouse|quality|"
                r"validation|migration|mining|catalog|lineage|driven|streams?)",
                r"\betl\b", r"\belt\b", r"\bcdc\b",
                r"trực quan hóa",
                r"kho dữ liệu", r"data lake", r"big ?data",
                r"phân tích (số liệu|hiệu suất|hành vi|thị trường|kinh doanh|log|doanh số|dữ liệu)",
                r"business intelligence", r"\bbi\b", r"power ?bi",
                r"superset|metabase|tableau|looker",
                r"trích xuất (thông tin|dữ liệu)", r"metadata", r"data-driven",
                r"mlops", r"data model"],
        "title_any": [r"dashboard", r"báo cáo", r"report(ing)?", r"thống kê|statistic",
                      r"cơ sở dữ liệu", r"database", r"\bsql\b", r"bảng điều khiển",
                      r"dữ liệu", r"data (mining|cleansing|entry)"],
        "stack": ["pandas", "numpy", "scipy", "matplotlib", "seaborn", "plotly", "dash",
                  "sql", "sql-server", "mysql", "postgresql", "mongodb",
                  "redis", "sqlite", "oracle-db", "dynamodb", "elasticsearch", "elk",
                  "clickhouse", "duckdb", "influxdb", "spark", "kafka", "flink",
                  "airflow", "nifi", "debezium", "trino", "solr", "bigquery",
                  "databricks", "snowflake", "supabase", "datahub", "openmetadata",
                  "great-expectations", "dvc", "power-bi", "tableau", "superset",
                  "metabase", "looker", "grafana", "kibana", "mlflow", "jupyter",
                  "openpyxl", "excel", "power-query", "etl", "data-pipeline",
                  "data-lakehouse", "delta-lake", "hadoop", "hbase", "hdfs", "database",
                  "bi", "ga4", "sensor-tower", "prometheus", "streamlit", "rabbitmq"],
    }),
    ("cloud-devops", {
        "any": [r"cloud", r"đám mây", r"devops", r"ci/cd", r"ci-cd",
                r"continuous (integration|deployment|delivery)",
                r"kubernetes|k8s", r"docker", r"terraform", r"infrastructure as code",
                r"\biac\b", r"hạ tầng", r"infrastructure", r"serverless",
                r"\baws\b|amazon web service", r"azure", r"\bgcp\b|google cloud",
                r"(máy chủ|server)\b", r"triển khai (lên|trên) (cloud|aws|azure|gcp|server|máy chủ)",
                r"sao lưu|backup", r"giám sát hệ thống", r"giám sát (hiệu năng|tài nguyên)",
                r"vận hành hệ thống",
                r"service mesh", r"canary", r"load balanc", r"máy ảo",
                r"high availability", r"observability",
                r"cost optimization", r"lưu trữ đám mây"],
        "stack": ["aws", "azure", "gcp", "docker", "kubernetes", "helm", "argocd",
                  "jenkins", "terraform", "ansible", "pulumi", "openshift", "openstack",
                  "cloud-native", "devops", "ci-cd", "cloudwatch", "prometheus",
                  "grafana", "aws-lambda", "aws-s3", "sagemaker",
                  "windows-server", "nginx", "iis", "aws-cdk", "aws-sdk",
                  "minio", "ceph"],
    }),
    ("security", {
        "any": [r"pentest", r"penetration test(ing)?", r"kiểm thử xâm nhập", r"appsec",
                r"red ?team", r"blue ?team", r"\bsoc\b", r"siem", r"forensic",
                r"mã độc|malware", r"ransomware", r"lỗ hổng", r"vulnerabilit",
                r"exploit", r"attack simulation", r"threat (intel|hunting|detection)",
                r"owasp", r"\bcve\b", r"nist", r"jailbreak",
                r"hacking|tấn công mạng",
                r"compromise", r"detection engineering", r"mitre|att&ck",
                r"zero[- ]trust", r"hậu lượng tử|post-quantum|ml-kem|ml-dsa",
                r"giám sát an ninh", r"quét lỗ hổng", r"phòng thủ", r"khai thác (lỗ hổng|hành vi)"],
        "title_any": [r"bảo mật", r"an ninh mạng", r"an toàn thông tin", r"\bsecurity\b",
                      r"\bcyber\b", r"mã hóa|encrypt", r"mật mã|cryptograph",
                      r"endpoint", r"firewall|tường lửa"],
        "stack": ["owasp", "mitre-attack", "cyber-kill-chain", "nist", "iso-27001",
                  "iso-21434", "gdpr", "pci-dss", "burp", "frida", "ida", "jadx",
                  "apktool", "apksigner", "mobsf", "metasploit", "nmap", "wireshark",
                  "kali", "hackerone", "bugcrowd", "hackthebox", "portswigger", "dvwa",
                  "juice-shop", "virustotal", "abuseipdb", "misp", "yara", "sigma-rules",
                  "sysmon", "atomic-red-team", "splunk", "siem", "wazuh", "dlp", "edr",
                  "ransomware", "waf", "fortify", "opentext", "palo-alto", "sophos",
                  "pentest", "forensics", "vulnerability", "exploit", "rsa", "pqc",
                  "cryptography", "threat-intelligence", "detection-engineering",
                  "firewall"],
    }),
    ("networking", {
        "any": [r"thiết kế giải pháp mạng", r"hệ thống mạng", r"quản trị mạng",
                r"mạng (doanh nghiệp|máy tính|viễn thông|không dây|4g|5g|lte)",
                r"\bnetwork", r"\b5g\b", r"\b4g\b", r"\blte\b", r"\bnr\b",
                r"openran", r"wi-?fi", r"\bmesh\b", r"giao thức|protocol",
                r"router|switching|dns|dhcp|\bvpn\b", r"viễn thông|telecom",
                r"truyền thông|communication (stack|protocol)",
                r"\bble\b|bluetooth", r"\bzigbee\b", r"tcp/ip|ipv6",
                r"\bsdn\b|sd-wan|sdwan", r"api gateway", r"\bptz\b",
                r"modbus|can ?bus|\bopc\b", r"\bioc\b|\bnoc\b", r"\bict\b",
                r"cáp quang|optical", r"định tuyến|routing", r"tường lửa|firewall",
                r"cấu hình thiết bị mạng"],
        "stack": ["dhcp", "dns", "gns3", "draytek", "aruba", "openwrt", "sdn", "sdwan",
                  "openran", "cisco", "juniper", "wifi", "bluetooth", "zigbee",
                  "modbus", "mqtt", "ptz", "amx", "wireshark", "can-bus", "uart"],
    }),
    ("embedded", {
        "any": [r"embedded", r"hệ thống nhúng|lập trình nhúng|nhúng (iot|firmware|embedded|vi điều khiển)",
                r"firmware", r"vi điều khiển", r"vi xử lý",
                r"\bmcu\b", r"stm32", r"esp32", r"arduino", r"\brtos\b", r"freertos",
                r"zephyr", r"bare-?metal", r"bringup", r"\bpcb\b", r"bo mạch|mạch in",
                r"mạch (rs485|điện tử|cảm biến|giao tiếp)", r"\bi2c\b|\bspi\b|\buart\b|\bjtag\b",
                r"logic analyzer", r"điều khiển (servo|động cơ|thiết bị|cánh tay)",
                r"device driver", r"\bbsp\b", r"hmi nhúng", r"qt ?/ ?qml",
                r"máy ghi âm", r"đo lường (điện|công nghiệp)", r"sensor",
                r"cảm biến", r"tín hiệu (số|tương tự)"],
        "stack": ["arduino", "platformio", "esp32", "esp-idf", "stm32", "stm32cube",
                  "hal", "freertos", "zephyr", "nrf", "rtos", "keil", "yocto",
                  "petalinux", "raspberry-pi", "i2c", "spi", "uart", "gpio", "qt",
                  "qml", "embedded", "firmware", "tinyml", "oscilloscope", "pyserial"],
    }),
    ("iot", {
        "any": [r"\biot\b", r"iiot", r"aiot", r"internet vạn vật|vạn vật",
                r"thiết bị thông minh", r"smart (home|city|lock|meter|energy|touch)",
                r"nhà thông minh", r"đồng hồ (điện|nước) thông minh",
                r"điều khiển (từ xa|thiết bị)", r"giám sát (từ xa|thiết bị)",
                r"quản lý thiết bị", r"datalogger", r"edge (device|ai)",
                r"truyền (dữ liệu|tải) (không dây|qua|lên)", r"nền tảng giám sát",
                r"tủ lạnh|tủ mát", r"cảm biến", r"sensor"],
        "stack": ["mqtt", "home-assistant", "thingsboard", "iot", "influxdb",
                  "zigbee", "esp32", "arduino", "node-red", "modbus"],
    }),
    ("chip-design", {
        "any": [r"\brtl\b", r"\bic design", r"thiết kế mạch",
                r"vlsi", r"verilog", r"systemverilog", r"\bvhdl\b",
                r"\bfpga\b", r"semiconductor", r"bán dẫn", r"vi mạch",
                r"physical design", r"thiết kế vật lý", r"floorplan", r"static timing",
                r"risc-?v", r"logic synthesis", r"tổng hợp mạch", r"transaction-level|\btlm\b",
                r"\buvm\b", r"\bip (block|core)\b", r"test pattern",
                r"netlist", r"tape-?out", r"design (rule|kit)", r"\bpdk\b",
                r"mô phỏng (mạch|rtl|logic)", r"kiểm tra thiết kế"],
        "title_any": [r"kiểm chứng|verification", r"\blayout\b", r"\banalog\b",
                      r"\bams\b", r"\bsta\b", r"register (transfer|if)", r"\bchip\b",
                      r"cadence|synopsys"],
        "all": [[r"\bsoc\b"],
                [r"chip|vi mạch|risc-?v|fpga|\brtl\b|asic|thiết kế (mạch|vi mạch|soc|chip)"]],
        "stack": ["verilog", "systemverilog", "vhdl", "vivado", "vitis", "modelsim",
                  "questasim", "verilator", "icarus-verilog", "cocotb", "uvm", "gtkwave",
                  "klayout", "yosys", "openroad", "opensta", "sky130", "chisel", "riscv",
                  "cadence-innovus", "cadence-genus", "synopsys", "ic-compiler",
                  "virtuoso", "calibre", "verdi", "vcs", "spyglass", "hdlbits", "rtl",
                  "fpga", "asic", "soc", "pdk", "eda", "ic-design", "ams", "tlm",
                  "cadence-virtuoso-suite", "ftdtd"],
    }),
    ("robotics", {
        "any": [r"robot", r"drone", r"\buav\b", r"tự hành|tự lái", r"autonomous",
                r"\bros\b|ros ?2", r"\bamr\b", r"vda5050", r"manipulation|gắp",
                r"navigation (robot|autonomous|indoor|stack)|robot navigation|traffic simulation|nav2|định vị (xe|robot|vật|tàu|máy bay|người|hàng|amr)|điều hướng (robot|xe|tự hành|uav|amr|bay)", r"\bslam\b", r"mecanum", r"cánh tay",
                r"tránh vật cản", r"gazebo|isaac sim", r"inh kinematic"],
        "stack": ["ros", "gazebo", "rviz", "isaac-sim", "px4", "mavsdk",
                  "qgroundcontrol", "slam", "rvo2", "ubtech", "inverse-kinematic",
                  "colcon", "navigation2", "robotics"],
    }),
    ("automotive", {
        "any": [r"ô tô|ôtô", r"automotive", r"xe hơi", r"xe tự (hành|lái)", r"\badas\b",
                r"\becu\b|\bvcu\b|\bdcu\b", r"can ?bus", r"canoe", r"autosar",
                r"túi khí|airbag", r"động cơ (xăng|dầu|điện|bldc)",
                r"(trong xe|trên xe|in-vehicle)", r"hệ thống truyền thông trong xe",
                r"\bcapl\b|\bascet\b|\bfbl\b", r"cụm đồng hồ", r"phần mềm ô tô",
                r"hệ thống treo|phanh|khung gầm", r"trạm sạc", r"pin xe điện",
                r"xe buýt|xe tải|đội xe", r"định vị xe"],
        "stack": ["can-bus", "canoe", "ecu-test", "ascet", "autosar", "capl"],
    }),
    ("healthcare", {
        "any": [r"y tế", r"sức khỏe", r"\bhealthcare\b|\bhealthtech\b",
                r"bệnh (nhân|viện|nhiệt đới|phổi|lý|án)",
                r"medical", r"clinical", r"lâm sàng", r"dược", r"\bemr\b|\behr\b|\bhie\b",
                r"hl7|fhir", r"điều trị", r"ung thư", r"aphasia",
                r"phục hồi chức năng", r"bác sĩ|nhân viên y tế", r"hồ sơ (bệnh|sức khỏe)",
                r"thực phẩm (chức năng|dinh dưỡng)", r"dinh dưỡng"],
        "all": [[r"chẩn đoán"],
                [r"bệnh|y tế|sức khỏe|medical|lâm sàng|triệu chứng"]],
        "stack": ["hl7-fhir"],
    }),
    ("education", {
        "title_any": [r"giáo dục", r"education", r"e-learning", r"học tập", r"đào tạo",
                r"bài giảng", r"khóa học|khoá học", r"\blms\b", r"trường (đại học|học)",
                r"\bstem\b", r"đề thi", r"chấm (bài|điểm)", r"trắc nghiệm", r"tutor(?:ing)?\b",
                r"giảng viên|giảng dạy", r"học sinh", r"học viên", r"tài liệu học",
                r"\bcamp\b", r"trại hè", r"đại học bách khoa", r"tuyển sinh"],
        "stack": [],
    }),
    ("ecommerce", {
        "any": [r"e-?commerce", r"thương mại điện tử", r"tmđt", r"bán hàng", r"mua (sắm|hàng)",
                r"đơn hàng", r"\border\b", r"retail", r"bán lẻ", r"\bpos\b", r"\bshop\b",
                r"sàn\b", r"giỏ hàng", r"\bd2c\b", r"\bb2c\b|\bb2b\b", r"esales",
                r"quản lý (kho|bán hàng|đơn)", r"kho hàng|kho vận", r"giao hàng|vận chuyển",
                r"đặt hàng|đặt lịch|đặt chỗ|booking|đặt vé", r"website bán",
                r"quảng cáo|marketing", r"logistics", r"tồn kho", r"nhà cung cấp|npp"],
        "stack": [],
    }),
    ("fintech", {
        "any": [r"ngân hàng|\bbank(?:ing)?\b", r"tài chính|finance|fintech",
                r"thanh toán|payment", r"wallet|ví điện tử|ví ngân hàng",
                r"chứng khoán|\bstock\b|securities", r"đầu tư|\binvest(?:ment|ing|or)?\b",
                r"bảo hiểm|insurance", r"tín dụng|\bcredit\b", r"khoản vay|\bloan\b",
                r"giao dịch (tài chính|chứng khoán|ngân hàng)|trading|đặt lệnh",
                r"ngân sách|thu nhập|chi tiêu",
                r"tín dụng doanh nghiệp", r"thẻ (tín dụng|ngân hàng)"],
        "title_any": [r"kế toán", r"hóa đơn|invoice", r"billing", r"thuế|\btax\b",
                      r"tài sản", r"định giá|định phí", r"chi phí|expense", r"lương|payroll"],
        "stack": [],
    }),
    ("enterprise-systems", {
        "title_any": [r"\berp\b|\bcrm\b|\bhrm\b", r"nhân sự|nhân viên", r"tuyển dụng|recruitment",
                r"timesheet|chấm công|tính lương", r"quản lý (công việc|dự án|task|văn bản|hồ sơ|khách hàng|người dùng|tài khoản|phản hồi|yêu cầu|tiến độ|chất lượng)",
                r"task management", r"workflow", r"quy trình (nội bộ|nghiệp vụ|vận hành|doanh nghiệp)",
                r"nội bộ|nghiệp vụ", r"quản trị doanh nghiệp",
                r"helpdesk|ticket", r"hỗ trợ (khách hàng|nội bộ)", r"call ?center|contact center",
                r"onboarding", r"cấp quyền", r"lịch hẹn", r"lark|suitecrm|odoo",
                r"trải nghiệm khách hàng", r"chăm sóc khách hàng",
                r"website quản trị", r"back ?office|backoffice",
                r"quản lý (mạng|tài sản|tài liệu)", r"phê duyệt",
                r"quản lý (bán|kho|vận tải|nhà hàng|khách sạn|đặt)", r"điều hành"],
        "stack": ["odoo", "suitecrm", "lark", "jira", "notion", "trello",
                  "redmine", "crm", "erp", "power-platform", "power-apps",
                  "power-automate", "appsheet", "sharepoint", "dataverse"],
    }),
    ("testing-qa", {
        "title_any": [r"kiểm thử", r"kiểm định chất lượng", r"quality (assurance|control)",
                r"\bqa\b|\bqc\b", r"test automation|automation test|kiểm thử tự động",
                r"unit test|integration test|e2e|end-to-end test", r"test case",
                r"kịch bản (kiểm thử|test)", r"độ bao phủ|coverage", r"regression",
                r"smoke test", r"tester", r"kiểm tra (chức năng|thiết kế|hệ thống|phần mềm|mạch|bo mạch|tích hợp)",
                r"xác minh|kiểm chứng|verification|validation", r"đối ứng",
                r"đảm bảo chất lượng", r"software testing", r"test framework",
                r"fuzzing", r"đánh giá chất lượng", r"kiểm soát chất lượng",
                r"đo coverage"],
        "stack": ["pytest", "playwright", "selenium", "cypress", "appium", "testcafe",
                  "codeceptjs", "robot-framework", "jest", "vitest", "google-test",
                  "junit", "cucumber", "browserstack", "tradefed", "nunit", "qa",
                  "tdd", "bdd", "exploratory-testing", "regression-testing",
                  "smoke-testing", "functional-testing", "uvm", "cantata",
                  "polyspace", "ecu-test", "postman"],
    }),
    ("blockchain", {
        "any": [r"blockchain", r"chuỗi khối", r"smart contract", r"solidity", r"web3",
                r"\bnft\b", r"\bdefi\b", r"dapp", r"tiền (mã hóa|kỹ thuật số)",
                r"truy xuất nguồn gốc", r"testnet", r"sổ cái (phân tán|điện tử)"],
        "stack": ["blockchain", "smart-contract", "web3", "truffle", "testnet", "nft",
                  "defi", "solidity"],
    }),
    ("ar-vr", {
        "any": [r"\bar\b", r"\bvr\b", r"thực tế ảo", r"thực tế tăng cường",
                r"augmented reality", r"virtual reality", r"metaverse",
                r"digital twin|bản sao số", r"ảo hóa 3d", r"3d (model|skeletal|animation|gis)",
                r"mô phỏng 3d", r"unreal engine", r"bản vẽ (2d|3d)"],
        "stack": [],
    }),
    ("gis", {
        "any": [r"\bgis\b", r"geospatial", r"địa không gian",
                r"địa lý|địa hình", r"arcgis|esri", r"geobox",
                r"không gian (địa lý|dữ liệu)", r"vệ tinh|satellite",
                r"map (layer|sdk|tile|view)|google maps"],
        "all": [[r"bản đồ"],
                [r"\bgis\b|địa lý|địa không gian|địa hình|tọa độ|số hóa|quy hoạch|tài nguyên|bản đồ số"]],
        "stack": ["arcgis", "leaflet", "postgis", "geopandas", "openstreetmap",
                  "geocoding", "gis"],
    }),
    ("industrial", {
        "any": [r"nhà máy|nhà xưởng", r"dây chuyền|băng chuyền", r"\bscada\b",
                r"\bplc\b", r"\bmes\b", r"điều khiển công nghiệp",
                r"tự động hóa (sản xuất|công nghiệp|nhà máy)",
                r"điện lực|trạm biến áp|tủ điện", r"máy bơm", r"cấp nước|xử lý nước",
                r"điện năng|năng lượng|smart (power|energy|grid)", r"cnc|gia công",
                r"máy móc|thiết bị công nghiệp", r"công trường", r"kho bãi",
                r"quản lý sản xuất|sản xuất (thông minh|tự động)|kế hoạch sản xuất|sản lượng",
                r"industrial", r"manufacturing", r"kiểm tra (chất lượng )?sản phẩm|defect",
                r"\boee\b", r"bảo trì (máy|thiết bị|hệ thống)", r"đo lường (điện|công nghiệp)",
                r"trạm 110kv|điện cao thế", r"laser", r"robot công nghiệp", r"tự động điều tiết"],
        "stack": ["plc", "scada", "ifix", "cnc", "servo"],
    }),
])

TOPIC_RE = {
    t: {
        "any": [re.compile(p, re.IGNORECASE) for p in spec.get("any", [])],
        "title_any": [re.compile(p, re.IGNORECASE) for p in spec.get("title_any", [])],
        "all": [[re.compile(p, re.IGNORECASE) for p in group]
                for group in spec.get("all", [])],
        "stack": spec.get("stack", []),
    }
    for t, spec in TOPICS.items()
}


def slugify(text, maxlen=60):
    """Lowercase, diacritics-stripped, hyphenated slug for passthrough values."""
    import unicodedata
    norm = unicodedata.normalize("NFD", text)
    norm = "".join(c for c in norm if unicodedata.category(c) != "Mn")
    norm = norm.replace("đ", "d").replace("Đ", "D")
    norm = re.sub(r"[^a-zA-Z0-9]+", "-", norm.lower()).strip("-")
    return norm[:maxlen] or "unknown"


def classify_project(title, tasks, raw_langs, raw_frameworks):
    """Tag one project. Returns (topics, lang_tags, framework_tags, unmapped, evidence)."""
    text = " . ".join(x for x in [title] + list(tasks) if x)
    stack = match_stack(" ; ".join(raw_langs + raw_frameworks))

    stack_by_slug = {s: (cls, hit) for s, cls, hit in stack}
    lang_tags = sorted({s for s, cls, _ in stack if cls in LANG_CLASSES})
    framework_tags = sorted({s for s, cls, _ in stack if cls not in LANG_CLASSES})

    # Unmatched language values are preserved verbatim for review (a value
    # whose tech mapped onto the frameworks axis is not "unmatched"); unmatched
    # framework values pass through as slugs (open-ended vocabulary).
    unmatched_langs = [v for v in raw_langs if not match_stack(v)]
    for v in raw_frameworks:
        if not match_stack(v):
            framework_tags.append(slugify(v))
    framework_tags = sorted(set(framework_tags))

    topics = []
    evidence = []
    for topic, rx in TOPIC_RE.items():
        hits = []
        for p in rx["any"]:
            m = p.search(text)
            if m:
                hits.append(("text", m.group(0)))
        for p in rx["title_any"]:
            m = p.search(title)
            if m:
                hits.append(("title", m.group(0)))
        if not hits and rx["all"]:
            group_hits = []
            ok = True
            for group in rx["all"]:
                gm = next((g.search(text) for g in group if g.search(text)), None)
                if gm is None:
                    ok = False
                    break
                group_hits.append(("text", gm.group(0)))
            if ok:
                hits = group_hits
        if not hits and rx["stack"]:
            for s in rx["stack"]:
                if s in stack_by_slug:
                    hits.append(("stack", s))
        if hits:
            topics.append(topic)
            key = next((h for h in hits if h[0] == "title"),
                       next((h for h in hits if h[0] == "text"), hits[0]))
            evidence.append({"topic": topic, "via": key[0], "match": key[1],
                             "n_matches": len(hits)})

    # de-duplicate unmapped lists, keep order
    unmapped_langs = list(dict.fromkeys(unmatched_langs))
    return topics, lang_tags, framework_tags, unmapped_langs, evidence


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--report", action="store_true",
                    help="print coverage stats only")
    args = ap.parse_args()

    d2 = json.loads((EXTRACTED / "d2.json").read_text(encoding="utf-8"))

    # (file, no) -> project record
    lookup = {}
    for rec in d2:
        for p in (rec["fields"].get("projects") or []):
            no = p.get("no")
            key = (rec["file"], str(no) if no is not None else None)
            lookup[key] = (rec, p)

    rows = list(csv.DictReader((EXTRACTED / "projects.csv").open(encoding="utf-8")))
    out_rows = []
    by_project = {}          # project_key -> aggregate record
    vocab = {"languages": defaultdict(lambda: {"class": None, "count": 0, "sources": set()}),
             "frameworks": defaultdict(lambda: {"class": None, "count": 0, "sources": set()}),
             "topics": defaultdict(lambda: {"count": 0, "examples": []})}
    unmapped_lang_counts = Counter()
    passthrough_counts = Counter()
    missing = 0

    for row in rows:
        src = row["source_file"]
        no = row["no"] or None
        rec_p = lookup.get((src, no))
        if rec_p is None:
            # fall back to title match within the same file
            cands = [(r, p) for (f, n), (r, p) in lookup.items()
                     if f == src and (p.get("title") or "").strip() == (row["title"] or "").strip()]
            if cands:
                rec_p = cands[0]
            else:
                missing += 1
                rec_p = (None, {"title": row["title"], "languages": [], "frameworks": [],
                                "timeline": []})
        rec, p = rec_p
        raw_langs = [x.strip() for x in (p.get("languages") or []) if x and x.strip()]
        raw_fws = [x.strip() for x in (p.get("frameworks") or []) if x and x.strip()]
        tasks = [x.get("task") or "" for x in (p.get("timeline") or [])]
        title = (p.get("title") or row["title"] or "").strip()

        topics, langs, fws, unmapped, evidence = classify_project(
            title, tasks, raw_langs, raw_fws)

        company_id = (rec or {}).get("company_id") or row["company_shortname"]
        norm_title = re.sub(r"\s+", " ", title).lower().rstrip(". ")
        project_key = sha1(f"{company_id}\x1f{norm_title}".encode()).hexdigest()[:12]

        for t in topics:
            vocab["topics"][t]["count"] += 1
            if len(vocab["topics"][t]["examples"]) < 5:
                vocab["topics"][t]["examples"].append(title[:110])
        stack_all = match_stack(" ; ".join(raw_langs + raw_fws))
        cls_by_slug = {s: c for s, c, _ in stack_all}
        for s in langs:
            v = vocab["languages"][s]
            v["class"] = cls_by_slug[s]
            v["count"] += 1
        for s in fws:
            v = vocab["frameworks"][s]
            v["class"] = cls_by_slug.get(s, "unclassified")
            v["count"] += 1
        for v in unmapped:
            unmapped_lang_counts[v] += 1
        for raw in raw_fws:
            if not match_stack(raw):
                passthrough_counts[raw] += 1
        for raw in raw_langs + raw_fws:
            for s, cls, hit in match_stack(raw):
                bucket = "languages" if cls in LANG_CLASSES else "frameworks"
                vocab[bucket][s]["sources"].add(raw)

        out_rows.append({
            **row,
            "project_key": project_key,
            "topics": "; ".join(topics),
            "lang_tags": "; ".join(langs),
            "framework_tags": "; ".join(fws),
            "unmapped_languages": "; ".join(unmapped),
        })

        agg = by_project.setdefault(project_key, {
            "project_key": project_key,
            "company_id": company_id,
            "company_shortname": row["company_shortname"],
            "titles": set(), "nos": set(), "files": set(),
            "topics": set(), "langs": set(), "frameworks": set(),
            "unmapped_languages": set(), "weeks": row.get("weeks"),
            "evidence": {},
        })
        agg["titles"].add(title)
        agg["nos"].add(str(no))
        agg["files"].add(src)
        agg["topics"].update(topics)
        agg["langs"].update(langs)
        agg["frameworks"].update(fws)
        agg["unmapped_languages"].update(unmapped)
        for e in evidence:
            agg["evidence"].setdefault(e["topic"], e)

    # ---- write projects_tagged.csv
    fieldnames = list(rows[0].keys()) + ["project_key", "topics", "lang_tags",
                                         "framework_tags", "unmapped_languages"]
    with (EXTRACTED / "projects_tagged.csv").open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=fieldnames)
        w.writeheader()
        w.writerows(out_rows)

    # ---- write project_tags.json
    proj_out = []
    for k, a in sorted(by_project.items()):
        proj_out.append({
            "project_key": k,
            "company_shortname": a["company_shortname"],
            "company_id": a["company_id"],
            "titles": sorted(a["titles"]),
            "nos": sorted(a["nos"], key=lambda x: (len(x), x)),
            "source_files": sorted(a["files"]),
            "weeks": a["weeks"],
            "topics": sorted(a["topics"]),
            "languages": sorted(a["langs"]),
            "frameworks": sorted(a["frameworks"]),
            "unmapped_languages": sorted(a["unmapped_languages"]),
            "evidence": sorted(a["evidence"].values(), key=lambda e: e["topic"]),
        })
    (EXTRACTED / "project_tags.json").write_text(
        json.dumps(proj_out, ensure_ascii=False, indent=1), encoding="utf-8")

    # ---- write tag_vocabulary.json
    for bucket in ("languages", "frameworks"):
        for s, v in vocab[bucket].items():
            v["sources"] = sorted(v["sources"])[:12]
    vocab_out = {
        "languages": dict(sorted(vocab["languages"].items(),
                                  key=lambda kv: -kv[1]["count"])),
        "frameworks": dict(sorted(vocab["frameworks"].items(),
                                  key=lambda kv: -kv[1]["count"])),
        "topics": dict(sorted(vocab["topics"].items(), key=lambda kv: -kv[1]["count"])),
    }
    (EXTRACTED / "tag_vocabulary.json").write_text(
        json.dumps(vocab_out, ensure_ascii=False, indent=1), encoding="utf-8")

    # ---- write tag_report.json
    topic_counts = {t: sum(1 for r in proj_out if t in r["topics"]) for t in TOPICS}
    no_topic = [r for r in proj_out if not r["topics"]]
    report = {
        "projects_total": len(proj_out),
        "project_rows_total": len(out_rows),
        "rows_without_source_record": missing,
        "topics_coverage": sum(1 for r in proj_out if r["topics"]),
        "projects_without_topics": len(no_topic),
        "topic_counts": dict(sorted(topic_counts.items(), key=lambda kv: -kv[1])),
        "languages_normalized": len(vocab["languages"]),
        "frameworks_normalized": len(vocab["frameworks"]),
        "unmapped_language_values": dict(sorted(unmapped_lang_counts.items(),
                                                key=lambda kv: -kv[1])),
        "frameworks_passthrough_top": dict(sorted(passthrough_counts.items(),
                                                  key=lambda kv: -kv[1])[:80]),
        "projects_without_topics_examples": [r["titles"][0] for r in no_topic[:40]],
    }
    (EXTRACTED / "tag_report.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=1), encoding="utf-8")

    print(f"projects: {len(proj_out)} unique / {len(out_rows)} rows  "
          f"(missing source record: {missing})")
    print(f"topics:   {len(topic_counts)} labels, "
          f"{report['topics_coverage']}/{len(proj_out)} projects tagged, "
          f"{report['projects_without_topics']} without")
    print(f"stack:    {len(vocab['languages'])} languages, "
          f"{len(vocab['frameworks'])} frameworks, "
          f"{len(unmapped_lang_counts)} unmapped language values")
    if args.report:
        for t, c in sorted(topic_counts.items(), key=lambda kv: -kv[1]):
            print(f"  {c:4d}  {t}")


if __name__ == "__main__":
    main()
