File size: 8,536 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using Repoverse.Core.Model;

namespace Repoverse.Core.Generation;

/// <summary>
/// Deterministic, rule-based semantic classification. Each rule contributes weighted
/// evidence; the highest total wins and the winning evidence becomes the provenance.
/// Purpose signals (topics, description, file layout) outweigh language (§80).
/// </summary>
public static class Classifier
{
    /// <summary>A rule returns how many independent signals matched; weight applies per signal, capped at 3.</summary>
    private sealed record Rule(SemanticCategory Category, int Weight, string Name, Func<CanonicalRepository, int> Match);

    /// <summary>
    /// Counts keywords present as whole words in name, description and topics. Text and keywords are
    /// both reduced to space-separated alphanumeric tokens, so "Guppy-to-IR" matches "ir" and
    /// "multi-agent" matches "multi agent". A trailing '*' makes a keyword a word prefix.
    /// </summary>
    private static int Mentions(CanonicalRepository r, params string[] words)
    {
        var text = Tokens($"{r.Id.Name} {r.Description} {string.Join(' ', r.Topics)}");
        return words.Count(w => w.EndsWith('*')
            ? text.Contains(Tokens(w[..^1]).TrimEnd(), StringComparison.Ordinal)
            : text.Contains(Tokens(w), StringComparison.Ordinal));
    }

    private static string Tokens(string s) => " " + TokenSplit.Replace(s.ToLowerInvariant(), " ").Trim() + " ";
    private static readonly System.Text.RegularExpressions.Regex TokenSplit = new("[^a-z0-9]+");

    private static int One(bool b) => b ? 1 : 0;

    private static bool HasFile(CanonicalRepository r, Func<string, bool> pred) =>
        r.Tree.Any(e => e.Kind == TreeEntryKind.File && pred(e.Path.ToLowerInvariant()));

    private static bool HasExt(CanonicalRepository r, params string[] exts) =>
        HasFile(r, p => exts.Any(x => p.EndsWith(x, StringComparison.Ordinal)));

    private static bool Lang(CanonicalRepository r, params string[] langs) =>
        r.PrimaryLanguage is { } l && langs.Contains(l, StringComparer.OrdinalIgnoreCase);

    private static readonly Rule[] Rules =
    [
        new(SemanticCategory.FormalVerification, 6, "proof-sources", r => One(HasExt(r, ".lean", ".v", ".agda", ".idr", ".thy", ".tla"))),
        new(SemanticCategory.FormalVerification, 4, "mentions-proof", r => Mentions(r, "proof*", "verified", "formal", "theorem*", "lean4", "coq")),
        new(SemanticCategory.Compiler, 5, "mentions-compiler", r => Mentions(r, "compiler", "codegen", "llvm", "parser", "interpreter", "wasm", "ir", "lowering")),
        new(SemanticCategory.Compiler, 3, "grammar-files", r => One(HasExt(r, ".g4", ".y", ".lalrpop", ".ll"))),
        new(SemanticCategory.OperatingSystem, 6, "mentions-os", r => Mentions(r, "kernel", "microkernel", "operating system", "bootloader", "hypervisor")),
        new(SemanticCategory.OperatingSystem, 3, "linker-script", r => One(HasExt(r, ".ld", ".lds"))),
        new(SemanticCategory.CryptographicCore, 6, "mentions-crypto", r => Mentions(r, "crypto", "cipher", "signature", "ed25519", "zk", "zero-knowledge", "encryption")),
        new(SemanticCategory.Financial, 6, "mentions-finance", r => Mentions(r, "finance", "trading", "ledger", "payment", "bank", "exchange", "portfolio")),
        new(SemanticCategory.Quantum, 6, "mentions-quantum", r => Mentions(r, "quantum", "qubit", "qiskit", "qir", "circuit")),
        new(SemanticCategory.Hardware, 6, "hdl-sources", r => One(HasExt(r, ".v", ".sv", ".vhd", ".vhdl", ".kicad_pcb", ".sch"))),
        new(SemanticCategory.Hardware, 4, "mentions-hardware", r => Mentions(r, "fpga", "firmware", "embedded", "pcb", "risc-v", "microcontroller")),
        new(SemanticCategory.Training, 6, "mentions-training", r => Mentions(r, "dataset", "corpus", "training", "fine-tune", "finetune")),
        new(SemanticCategory.AgentSystem, 5, "mentions-agent", r => Mentions(r, "agent*", "multi-agent", "autonomous", "llm", "mcp")),
        new(SemanticCategory.Simulation, 5, "mentions-simulation", r => Mentions(r, "simulat*", "voxel", "physics", "game")),
        new(SemanticCategory.Database, 6, "mentions-database", r => Mentions(r, "database", "storage engine", "sql", "kv store")),
        new(SemanticCategory.Networking, 5, "mentions-network", r => Mentions(r, "network", "protocol", "http", "rpc", "proxy", "p2p")),
        new(SemanticCategory.WebInterface, 4, "mentions-web", r => Mentions(r, "frontend", "website", "dashboard", "ui", "react", "web app")),
        new(SemanticCategory.WebInterface, 3, "web-language", r => One(Lang(r, "TypeScript", "JavaScript", "HTML", "CSS", "Vue", "Svelte"))),
        new(SemanticCategory.Documentation, 4, "docs-only", r => One(r.Tree.Count > 0 && r.Tree.Where(e => e.Kind == TreeEntryKind.File)
            .All(e => e.Path.EndsWith(".md", StringComparison.OrdinalIgnoreCase) || e.Path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) || e.Path.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)))),
        new(SemanticCategory.SystemsInfrastructure, 3, "systems-language", r => One(Lang(r, "Rust", "C", "C++", "Zig", "Go", "Assembly"))),
        new(SemanticCategory.SystemsInfrastructure, 4, "mentions-infra", r => Mentions(r, "infrastructure", "deploy", "orchestrat*", "ci/cd", "terraform", "runtime")),
    ];

    public static Derived<SemanticCategory> Classify(CanonicalRepository repo, Overrides? overrides = null)
    {
        if (overrides?.Category(repo.Id) is { } forced)
            return new(forced, Provenance.Override(overrides.Source));

        var scores = new Dictionary<SemanticCategory, (int Score, List<string> Evidence)>();
        foreach (var rule in Rules)
        {
            var hits = Math.Min(3, rule.Match(repo));
            if (hits == 0) continue;
            if (!scores.TryGetValue(rule.Category, out var s)) s = (0, []);
            s.Evidence.Add(hits > 1 ? $"{rule.Name}x{hits}" : rule.Name);
            scores[rule.Category] = (s.Score + rule.Weight * hits, s.Evidence);
        }

        if (scores.Count == 0)
            return new(SemanticCategory.Experimental, new("classifier:fallback", "no rule matched"));

        // Ties break on enum order so the result never depends on dictionary iteration.
        var best = scores.OrderByDescending(kv => kv.Value.Score).ThenBy(kv => (int)kv.Key).First();
        return new(best.Key, new("classifier:rules", $"{string.Join('+', best.Value.Evidence)} (score {best.Value.Score})"));
    }

    public static District DistrictFor(SemanticCategory c, bool archived) => archived ? District.Archive : c switch
    {
        SemanticCategory.OperatingSystem or SemanticCategory.Compiler => District.Kernel,
        SemanticCategory.SystemsInfrastructure or SemanticCategory.Networking => District.Infrastructure,
        SemanticCategory.CryptographicCore => District.Security,
        SemanticCategory.FormalVerification => District.FormalMethods,
        SemanticCategory.Financial => District.Financial,
        SemanticCategory.Quantum => District.QuantumResearch,
        SemanticCategory.Hardware => District.HardwareIndustrial,
        SemanticCategory.Training => District.Training,
        SemanticCategory.AgentSystem => District.Agent,
        SemanticCategory.Simulation => District.Ai,
        SemanticCategory.Database => District.Data,
        SemanticCategory.WebInterface => District.WebInterface,
        SemanticCategory.Documentation => District.Archive,
        _ => District.Experimental,
    };

    public static BuildingArchetype ArchetypeFor(SemanticCategory c) => c switch
    {
        SemanticCategory.Compiler => BuildingArchetype.Foundry,
        SemanticCategory.OperatingSystem or SemanticCategory.SystemsInfrastructure => BuildingArchetype.ControlTower,
        SemanticCategory.CryptographicCore => BuildingArchetype.Vault,
        SemanticCategory.FormalVerification => BuildingArchetype.ProofChamber,
        SemanticCategory.Financial => BuildingArchetype.Exchange,
        SemanticCategory.Quantum or SemanticCategory.Hardware or SemanticCategory.Simulation => BuildingArchetype.Laboratory,
        SemanticCategory.Training or SemanticCategory.Documentation => BuildingArchetype.Library,
        SemanticCategory.WebInterface => BuildingArchetype.Studio,
        SemanticCategory.Database => BuildingArchetype.Warehouse,
        SemanticCategory.Networking => BuildingArchetype.Relay,
        SemanticCategory.AgentSystem => BuildingArchetype.Commons,
        _ => BuildingArchetype.Workshop,
    };
}