A B2B company, person and contact intelligence platform — and an engineering case study in building a system that is allowed to say "I don't know".
Two production-grade engines, 262 386 lines of Python and 3 613 test functions, built around one idea: in contact intelligence the expensive failure is not a missing answer, it is a confident wrong one.
This repository contains no source code and no data. It documents the architecture, the design decisions and the measurement discipline of a private system. The reasons are in Why the source is not in this repository, and they are part of the case study rather than a footnote to it.
A B2B seller wants to reach the procurement lead at a manufacturer. The public web contains the answer in pieces: a company site that may or may not be the right legal entity, a team page that lists people who may or may not still work there, a name that may belong to three different humans, and an email address that may be a real mailbox, a catch-all, or a guess that happens to be syntactically plausible.
Every commercial tool in this space answers anyway. The industry metric is coverage, and coverage rewards guessing: a wrong address still counts as a row delivered. The cost lands on the buyer, who emails the wrong person, and on the person who receives it.
MİHENK is built on the opposite bet — that a system which refuses to answer when the evidence is thin is worth more than one that always answers.
The engine's product contract states nine invariants. Each one is a mistake that a naive implementation makes by default, and each is enforced somewhere in the pipeline:
| The tempting shortcut | Why it is wrong |
|---|---|
| person mention = employee | A name on a page is a mention, not an employment relationship |
| same name = same human | Two people share a name; merging them corrupts both records |
| historical evidence = current employment | An archived team page proves the past, not the present |
| parent/subsidiary/brand = identity | A brand page is not its parent company's legal identity |
| inferred email = observed email | A pattern-derived address is a hypothesis, not an observation |
| SMTP acceptance = person ownership | A server accepting mail says nothing about who reads it |
| accept-all = verified mailbox | A catch-all domain accepts everything, including nonsense |
| provider failure = absence proof | A timeout is not evidence that something does not exist |
| conflicted authority = FOUND | Two sources disagreeing is a reason to abstain, not to pick |
The last one is the hardest to hold. When two first-party sources disagree about a
person's title, the profitable move is to pick the more recent one and ship a row. The
engine returns INSUFFICIENT instead.
Two engines with a deliberate seam between them. The factory decides which companies exist and may be approached; the forge decides what is true about one of them.
flowchart TB
subgraph F["B2B Data Factory — supply"]
direction TB
A1[Source discovery<br/>public indexes, directories] --> A2[Source Atlas<br/>candidate surfaces]
A2 --> A3{Governance<br/>fail-closed}
A3 -->|rights + purpose + capability| A4[Admitted sources]
A3 -->|any gate denies| AX[Rejected · recorded]
A4 --> A5[Canonical company universe]
end
subgraph C["ContactForge — truth about one company"]
direction TB
B1[Organization + domain resolution] --> B2[Bounded first-party crawl]
B2 --> B3[Contact + person extraction]
B3 --> B4[Identity resolution<br/>same-name containment]
B4 --> B5[Current-employment separation]
B5 --> B6[Email: observed vs inferred]
B6 --> B7{Evidence sufficient?}
B7 -->|yes| B8[CONTACTFORGE_RESULT_V1]
B7 -->|no| B9[INSUFFICIENT · with reason]
end
A5 --> B1
B8 --> E[(Evidence lineage<br/>provenance + confidence)]
B9 --> E
Every arrow that produces a claim also writes provenance: which source, which page, which extraction method, what confidence, observed when. The lineage is retained internally even though the default product output stays compact — because the question "why does the system believe this?" has to be answerable after the fact, not reconstructed from logs.
The system operates under KVKK (Turkey) and GDPR. Both were treated as a design input rather than a legal review at the end, which produced a three-question authorization model: may we use this source at all, why, and how.
flowchart LR
Q[Requested action] --> G1{Global source rights<br/>usable at all?}
G1 -->|no| D[Denied · recorded]
G1 -->|yes| G2{Purpose rights<br/>why is it used?}
G2 -->|no| D
G2 -->|yes| G3{Capability<br/>how is it accessed?}
G3 -->|no| D
G3 -->|yes| G4{Runtime access<br/>robots · freshness}
G4 -->|no| D
G4 -->|yes| P[Permitted]
style D fill:#7f1d1d,color:#fff
style P fill:#14532d,color:#fff
Seven bounded capabilities are modelled separately — public page fetch, link discovery, directory enumeration, public search submission, public file download, public API call and internal evidence storage — because "we are allowed to read this page" and "we are allowed to enumerate this directory" are different permissions that a single boolean would conflate.
Three properties follow, and each is enforced rather than promised:
- Missing or stale decisions fail closed. An unreviewed source is
UNKNOWN, andUNKNOWNdoes not authorize anything. A review that has aged past its freshness bound stops authorizing without anyone having to revoke it. - Access boundaries never create rights.
robots.txtpermitting a fetch is not a legal basis for processing what the fetch returns; the two gates are evaluated separately and both must pass. - The engine does not scrape authenticated profiles. LinkedIn profile fetching is outside the product boundary. Public professional-profile intelligence is derived from first-party published links only.
The benchmark methodology starts from one sentence: a system prediction is never its own ground truth, and a competitor prediction is never ground truth either.
That rules out the cheap evaluation everyone runs — comparing the engine to itself with a looser threshold, or to a vendor whose output you cannot audit. What remains is harder and slower: ground truth reviewed independently from first-party authoritative public evidence, and eight metrics reported separately rather than folded into one score:
organization correctness · person precision and recall · current-employment precision · profile-owner precision · public-email recall · person-email attribution precision · verified usable-contact yield · severe wrong attribution
Proportions carry Wilson 95% confidence intervals. Cold and warm runs are reported separately, because a live crawler and a pre-indexed vendor database are not the same latency workload and averaging them would flatter the wrong one.
And when a competitor cannot be measured, the methodology requires writing NOT_MEASURED
rather than inferring superiority. A benchmark that cannot embarrass its author is
decoration.
| ContactForge | B2B Data Factory | Total | |
|---|---|---|---|
| Source | 66 186 | 84 469 | 150 655 |
| Tests | 36 234 | 31 810 | 68 044 |
| Tooling / scripts | 16 995 | 20 068 | 37 063 |
| Project / vendored | — | 6 624 | 6 624 |
| Python lines | 119 415 | 142 971 | 262 386 |
| Python files | 449 | 438 | 887 |
| Test functions | 1 665 | 1 948 | 3 613 |
| Test files | 236 | 111 | 347 |
| Schema version | 43 | 057 | — |
| Phase records | 3 | 102 | 105 |
Counted with find … -name '*.py', excluding .venv, venv, build, dist, runs,
backups, __pycache__, *egg-info and site-packages — the exclusions are listed
because a line count without them is a statement about a backup directory. Documentation
across both engines is a further 7 906 lines in 105 files, and the B2B engine carries
63 schema migrations and 81 patch application scripts, each with its own
acceptance record.
For scale, ~26 % of the Python is tests, and the test-to-source ratio is roughly 0.45:1.
A case study that only lists wins is a brochure. The engineering history records the opposite as carefully, and these are quoted from it rather than softened:
- Overall P12B closure:
NOT_COMPLETE. The durable/parallel execution foundation landed; the closure gate above it did not. - A frozen real-world provider baseline of 12/24 challenge completion, with 0/12 known-positive routed recall, 0 accepted open-web person assertions and 0 public observed person emails on that corpus. That baseline is kept frozen precisely so later work cannot quietly redefine the bar it failed to clear.
- One company remains a measured zero-yield recall blocker and is named as such in the current phase notes, rather than being dropped from the benchmark set.
- Company-level wall-clock deadline enforcement was deliberately deferred from one phase and closed in the next — recorded as a deferral, not discovered later as a gap.
Two production failures are worth naming because the fixes are the interesting part. Title-cased institutional prose was being mistaken for people's names, and one ambiguous normalized person name could terminate an entire company's resolution run. The first was closed by context-aware institutional entity-shape rejection; the second by per-person fail-closed ambiguity containment — one ambiguous person now fails alone instead of taking the company with it.
Two reasons, and both are engineering decisions rather than modesty.
The corpus contains personal data. The working databases hold thousands of extracted contact rows, including personal mailbox addresses and phone numbers of identifiable individuals gathered from public company pages. "Publicly observable" and "lawful to republish in bulk" are different questions under KVKK and GDPR, and the second one answers no. A system built to treat provenance and lawful basis as first-class cannot have its own corpus dumped into a public repository as an afterthought.
The engine is a commercial product. MİHENK has a written go-to-market plan with unit economics, and its defensibility rests on exactly the compliance architecture described above. Publishing the implementation would give away the position it was built to hold.
What is publishable is the reasoning: the invariants, the governance model, the measurement discipline and the failures. That is what this repository is.