Skip to main content

contract_cli/
kinds.rs

1// Registry of contract kinds: one place for the kind's identity — number
2// prefix, party role labels, description, and the trigger tags used by
3// `contract kinds find` / `--kind-like` matching. Adding a kind here plus a
4// clause pack under clauses/<kind>/ is all it takes to teach the CLI a new
5// contract type.
6
7pub struct KindSpec {
8    pub kind: &'static str,
9    pub prefix: &'static str,
10    /// (our role, their role) as printed on the contract
11    pub roles: (&'static str, &'static str),
12    pub description: &'static str,
13    pub tags: &'static [&'static str],
14}
15
16pub const KINDS: &[KindSpec] = &[
17    KindSpec {
18        kind: "nda",
19        prefix: "NDA",
20        roles: ("Disclosing Party", "Receiving Party"),
21        description: "Confidentiality agreement (mutual or one-way) — protects information exchanged while exploring a deal.",
22        tags: &[
23            "confidential", "confidentiality", "secret", "nondisclosure", "non-disclosure",
24            "disclosure", "privacy", "protect", "information", "evaluate", "exploring",
25        ],
26    },
27    KindSpec {
28        kind: "ncnda",
29        prefix: "NCNDA",
30        roles: ("Party", "Party"),
31        description: "Non-circumvention + non-disclosure — stops a counterparty going around you to deal directly with contacts you introduce, and protects the information shared.",
32        tags: &[
33            "circumvent", "circumvention", "non-circumvention", "go-around", "bypass",
34            "introducer", "introduction", "intermediary", "broker", "middleman",
35            "commission", "protect-contacts", "steal", "poach", "direct-deal", "cut-out",
36        ],
37    },
38    KindSpec {
39        kind: "consulting",
40        prefix: "CTR",
41        roles: ("Consultant", "Client"),
42        description: "One engagement with defined deliverables and a fee — a self-contained services contract.",
43        tags: &[
44            "freelance", "project", "deliverables", "engagement", "gig", "advisory",
45            "consultant", "services", "scope", "fee",
46        ],
47    },
48    KindSpec {
49        kind: "msa",
50        prefix: "MSA",
51        roles: ("Provider", "Customer"),
52        description: "Master services agreement — umbrella terms with no fee or scope; SOWs hang off it per piece of work.",
53        tags: &[
54            "master", "framework", "umbrella", "long-term", "terms", "relationship",
55        ],
56    },
57    KindSpec {
58        kind: "sow",
59        prefix: "SOW",
60        roles: ("Provider", "Customer"),
61        description: "Statement of work under an MSA — scope, deliverables, milestones, and price for one project.",
62        tags: &[
63            "scope", "milestones", "work-order", "statement", "under-msa", "project",
64        ],
65    },
66    KindSpec {
67        kind: "service",
68        prefix: "SVC",
69        roles: ("Provider", "Customer"),
70        description: "Ongoing or recurring services — retainers, hosting, support, monthly reviews.",
71        tags: &[
72            "retainer", "ongoing", "recurring", "monthly", "hosting", "support",
73            "subscription", "maintenance", "managed",
74        ],
75    },
76    KindSpec {
77        kind: "loan",
78        prefix: "LOAN",
79        roles: ("Lender", "Borrower"),
80        description: "Loan agreement between two parties — principal, interest (or interest-free), repayment date, default, and optional security.",
81        tags: &[
82            "loan", "lend", "lending", "borrow", "money", "principal", "interest",
83            "repayment", "repay", "credit", "advance", "bridge", "facility",
84        ],
85    },
86];
87
88pub fn get(kind: &str) -> Option<&'static KindSpec> {
89    KINDS.iter().find(|k| k.kind == kind)
90}
91
92pub fn names() -> Vec<&'static str> {
93    KINDS.iter().map(|k| k.kind).collect()
94}
95
96pub fn prefix_for(kind: &str) -> &'static str {
97    get(kind).map(|k| k.prefix).unwrap_or("DOC")
98}
99
100/// Score a free-text description against a kind's tags + description.
101/// Deterministic local token matching — no guessing, callers decide.
102pub fn score(query: &str, description: &str, tags: &[&str]) -> f64 {
103    let q = query.to_lowercase();
104    let q_tokens: Vec<&str> = q
105        .split(|c: char| !c.is_alphanumeric() && c != '-')
106        .filter(|t| t.len() > 2)
107        .collect();
108    if q_tokens.is_empty() {
109        return 0.0;
110    }
111    let desc = description.to_lowercase();
112    let mut hits = 0.0;
113    for t in &q_tokens {
114        // exact tag hit is strongest; tag prefix and description hits count less
115        if tags.iter().any(|tag| tag == t) {
116            hits += 3.0;
117        } else if tags.iter().any(|tag| tag.starts_with(*t) || t.starts_with(*tag)) {
118            hits += 1.5;
119        } else if desc.contains(*t) {
120            hits += 1.0;
121        }
122    }
123    hits / q_tokens.len() as f64
124}