Country-aware C-Suite title ontology + deterministic rules validation engine
Find a file
brahm 925627424c feat: title-grammar rules — decide "is this a job title" by role-head grammar
The previous checker only knew string hygiene (HTML, placeholders, legal
suffixes, length, trigrams). It had no rule for what a job title *is*, so it
scored "Sales Department", "New York", "123 Main Street", "Test Test", "Male"
and "Manager Manager Manager" as VALID 78-100, while real titles like
"Prokurist" sat in QUESTIONABLE. Its 2.3% error rate was an artifact: the only
negatives in the benchmark were SEC placeholders, Norwegian company names and
character-level corruptions of real titles.

Rebuilt around one rule: a job title is a noun phrase whose HEAD is an
occupational role noun ([SENIORITY]* [DOMAIN]* HEAD [SCOPE]*). Language-
independent in form; only vocabulary changes.

- ontology/role_heads.json      2,645 role nouns across 24 language groups,
                                166 agent-noun suffixes, 113 canonical
                                multi-word roles, 35 role acronyms (authored,
                                not scraped, so the corpus stays independent)
- ontology/modifiers.json       426 domains, 127 seniority, 47 connectors
- ontology/negative_lexicon.json  13 named negative classes
- rules/title_grammar.py        the engine; every verdict carries a rule trace
- benchmark/hard_cases.jsonl    588 authored hard cases (person names, org
                                units, places, addresses, credentials,
                                statuses, brands, prose) — the dirt the old
                                evaluation never tested
- evaluate_grammar.py           scores both engines side by side

Policies: INVALID requires a named rule (no silent rejection); no head found
means REVIEW, never reject (no inventory covers every language).

Measured on 9,096 cases (8,000 held-out real titles, 508 real SEC/Brreg
strings, 588 authored):
  accuracy on decided cases  100.0%  (was 98.1%)
  hard errors                     0  (was 133)
  junk leaked as VALID            0  (was 130 of 744)
  sent to review              11.4%  (was 21.4%)
VALID threshold calibrated at 60 (cheapest cut with zero leakage).

Also fixed: \w tokenisation shredded Indic/Thai/Arabic titles; NFKC decomposed
Thai SARA AM out of the lexicon; accent stripping destroyed Indic vowels;
2-char suffixes made Equinor/Berlin/Okafor "role nouns"; keyboard-run detection
flagged "Property Underwriter"; "i am" substring made "estrattore di amido"
prose; Romance "responsable de" was treated as prose rather than a title head.

168 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGY4Qad6trDpppS7itm7FL
2026-07-30 18:44:57 +00:00
benchmark feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
data/real feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
data_sources feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
docs feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
ontology feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
results feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
rules feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
sample_data feat: C-Suite title ontology + deterministic rules engine (v0.2) 2026-07-30 15:32:16 +00:00
tests feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
.gitignore feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
build_benchmark.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
build_hard_benchmark.py feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
build_lexicon.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
cli.py feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
country_matrix.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
evaluate.py feat: C-Suite title ontology + deterministic rules engine (v0.2) 2026-07-30 15:32:16 +00:00
evaluate_grammar.py feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
evaluate_real.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
evaluate_title_validation.py feat: pivot to global title-validation business rules (no ML) 2026-07-30 17:34:01 +00:00
evaluate_validity.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
expand_ontology.py feat: v0.3 dual-axis engine, real-data benchmark, honest README 2026-07-30 17:03:54 +00:00
README.md feat: title-grammar rules — decide "is this a job title" by role-head grammar 2026-07-30 18:44:57 +00:00
requirements.txt feat: C-Suite title ontology + deterministic rules engine (v0.2) 2026-07-30 15:32:16 +00:00

Global job-title validation — explicit business rules

Product question: given a string from a contact record, filing or vendor feed, is it a real job title?

Answer: VALID / REVIEW / INVALID, a score 0100, and the named rule behind the call. Deterministic, inspectable, no ML.

$ python cli.py --single "Head of Growth" --explain
'Head of Growth' -> VALID (83) class=TITLE
  head: head  [en]
  T-UNICODE               clean  NFKC + whitespace
  T-HEAD                    +55  head 'head' [en]
  T-DOMAIN                  +10  business domain: growth
  T-FRAME                    +8  canonical title frame
  T-COVERAGE                +10  every token explained by the grammar
$ python cli.py --single "Sales Department" --explain
'Sales Department' -> INVALID (85) class=ORG_UNIT
  T-UNICODE               clean  NFKC + whitespace
  T-ORG-UNIT       HARD INVALID  org-unit noun with only business modifiers

Start here

Doc What it is
docs/TITLE_VALIDATION_RULES.md The product. Every rule, its weight, its evidence, and what the rules honestly cannot do
rules/title_grammar.py the engine — one rule per named T-… id
ontology/*.json the three evidence files the rules read
evaluate_grammar.py scores the rules against real held-out titles, real dirt, and hard negatives

The rule everything hangs off

A job title is a noun phrase whose HEAD is an occupational role noun.

[SENIORITY]*   [DOMAIN]*    HEAD      [SCOPE]*
  Senior        Marketing    Manager    EMEA
  Chief         Financial    Officer
  Kaufmännischer            Leiter
  首席           财务          官

The form is language-independent; only the vocabulary changes. Germanic and Finnic languages fuse it into a compound (Vertriebs·leiter), CJK and Korean are head-final (영업·팀장), Romance and Semitic are head-first (مدير عام).

Two policies keep it honest:

  • A string is INVALID only when a named rule says so. Nothing is rejected for scoring low.
  • No head found ≠ junk. No inventory covers every language, so an unrecognised string goes to REVIEW, never to the bin.

Results

python evaluate_grammar.py — 9,096 cases: 8,000 held-out real titles (never seen by the lexicon), 508 pieces of real production dirt (SEC placeholders, Brønnøysund company names), 588 authored hard negatives.

grammar rules previous engine
Accuracy on decided cases 100.0% 98.1%
Hard errors (VALID ↔ INVALID) 0 133
Sent to review 11.4% 21.4%
Junk leaked as VALID (of 744) 0 130

Every rejection names a class, and every class is caught:

Class recall Class recall
ORG_UNIT 100% COMPANY 98.9%
PLACE 100% PERSON_NAME 97.7%
DEMOGRAPHIC 100% EMPLOYMENT_STATUS 97.0%
DESCRIPTION 100% CREDENTIAL 96.3%
TEST_DATA 100% GIBBERISH 94.4%
PLACEHOLDER 99.3% ADDRESS 93.3%

Review rate by language, held-out: en 2% · fr 3% · es 3% · it 6% · nl 15% · de 20% · fi 23% · hu 32% · mt 37% · da 41%. Where head coverage is thin the cost is review, never rejection — the fix is curation, not a model.

The 588 hard negatives were authored alongside the rules, so that slice is a design target. The independent evidence is the 8,000 held-out taxonomy titles and the 508 real SEC/Brreg strings — no rule was written against either.


Quick start

pip install -r requirements.txt

python cli.py --demo                                 # mixed real/junk table
python cli.py --single "Chief Banana Officer" --explain
python cli.py --input records.json --output out.json  # batch

python build_hard_benchmark.py    # rebuild the hard cases
python evaluate_grammar.py        # rescore both engines
python -m pytest tests/ -q        # 168 tests

What it does on the strings that break naive checkers

Input Verdict Score Why
Chief Executive Officer VALID 93 canonical multi-word head
Zimmerbereichsleiter VALID 100 German head-final compound
ผู้จัดการฝ่ายขาย VALID 77 Thai role morpheme, no word spaces
मुख्य कार्यकारी अधिकारी VALID 85 Devanagari, combining marks preserved
Freelance Photographer VALID 81 qualifier + role head
Sales Department INVALID 85 ORG_UNIT
John Smith INVALID 78 PERSON_NAME (beats the smith head)
New York INVALID 82 PLACE
Equinor ASA INVALID 92 COMPANY
Bachelor of Science INVALID 84 CREDENTIAL
Unemployed INVALID 90 EMPLOYMENT_STATUS
Male INVALID 88 DEMOGRAPHIC
Manager Manager Manager INVALID 93 GIBBERISH
See Remarks INVALID 98 PLACEHOLDER
addetto alla spa VALID 81 not S.p.A. — Italian spa attendant
Purple Elephant Officer REVIEW 57 valid head, two stacked unknown modifiers
Chief Banana Officer VALID 78 valid head, one free modifier — see below

What the rules cannot do

No rule separates Foundation Director from Chief Banana Officer: both are a valid head plus a modifier no lexicon lists, and real titles constantly invent modifiers (pharmacovigilance, growth, happiness). One free modifier is therefore normal; two stacked unknowns is the measurable signal of a made-up phrase. If the first case must be caught, that is where a semantic check belongs — and docs/TITLE_VALIDATION_RULES.md §8 says which kind, for each gap.

Need Recommendation
Refute a well-formed invented title LLM / embedding check on the REVIEW slice only
Thin head coverage (mt, da, hu) curate head nouns from ESCO — no model
Cross-lingual role mapping multilingual embeddings — different product
Company detection beyond suffixes + brands company-registry lookup or vendor NER
C-suite / seniority parked at rules/engine_v3.py — different question

Data

Source Rows Role here
ESCO (EU) 53,869 multilingual official occupations, 27+ languages
O*NET (US DOL) 11,560 official US titles
Wikidata 2,362 multilingual role labels (Asia, Africa, LatAm)
Brønnøysund (NO) 4,021 real roles and company names (company ≠ title)
SEC EDGAR 2,505 production dirt: placeholders, HTML entities

~74k observations · 54 countries · 44 languages · 6 continents. Pull scripts in data_sources/; rebuild the taxonomy lexicon (train split only) with python build_lexicon.py.

The role-head, modifier and negative lexicons are authored, not derived from this corpus — which is what keeps the corpus usable as an independent test set.


Repo map

rules/title_grammar.py      the engine (this is the product)
rules/title_validator.py    superseded string-hygiene checker, kept for comparison
ontology/role_heads.json    role nouns, morphology, canonical multi-word roles
ontology/modifiers.json     seniority / domain / connectors
ontology/negative_lexicon.json   the 13 things a title is not
benchmark/hard_cases.jsonl  authored hard negatives + global positives
evaluate_grammar.py         both engines, per-class metrics
results/real/               measured output, including threshold calibration
docs/TITLE_VALIDATION_RULES.md   the rulebook
rules/engine_v3.py          parked C-suite classifier (different product)

Run the old engine for comparison with python cli.py --demo --legacy.

License

MIT (see LICENSE if present). Upstream data under their own licenses (ESCO, O*NET CC BY, Wikidata CC0, Brønnøysund NLOD).