A small-scale matching system that scores candidates against companies on environmental fit — pace, autonomy, communication style, the things that actually predict whether someone will thrive somewhere. The point of difference: it models per-candidate preference curves rather than assuming "more match = better" on every attribute.
Click this link to try the live demo
Most matching systems treat fit as one-dimensional similarity: closer answers, better match. That's correct for product recommendations and roughly correct for hard skills, but it's the wrong model for environmental preferences. The same level of pressure energises one person and stresses another. The same level of autonomy is a gift to one and abandonment to another. A system that only knows how to compute distance can't tell these cases apart.
The brief from Safer Industries flagged this directly: "some variables may not always behave consistently — pressure may help one person thrive whilst causing stress to others." That observation, taken seriously, drives the entire design.
Two candidates give the same answer of 5 on the question "how much pressure helps you thrive."
- Candidate A also reports a remembered concrete experience of 5 on the triangulation item — they've worked in high-pressure environments and thrived there.
- Candidate B reports a remembered experience of 1 — they've never actually worked under pressure, but they're stating they want to.
Against the same high-pressure company:
| Candidate | pressure_level_c1 (preference) |
pressure_level_c2 (experience) |
Inferred curve | Fit |
|---|---|---|---|---|
| A | 5 | 5 | monotonic_increasing | 100% |
| B | 5 | 1 | peaked at midpoint | 42.9% |
A 57-percentage-point spread on a single attribute, for the same Likert answer, because the engine reads the triangulation item and infers different preference shapes. This is the thing the brief asked for, and the system does it correctly.
Five layers, each addressing a specific problem:
A taxonomy of 100 attributes. 45 paired (asked of both candidates and companies), 5 candidate-only (priority ranking, deal-breakers, free-text reflection), 5 company-only (org size, sector, regulation, etc). Organised into 8 categories. Each paired attribute carries metadata about which preference curve shapes are plausible for it, which constrains the inference engine downstream.
A 105-item survey. Designed against ten explicit principles — single construct per item, balanced anchors, behavioural framing, social-desirability awareness, forced-choice for high-clustering attributes. Five attributes get triangulation items where one Likert response can't disambiguate the preference shape.
A static survey app. Single-file HTML, no backend, mobile-first. Renders all 50+ questions, applies skip logic, captures the priority ranking and deal-breakers, and produces a candidate profile.
A matching engine. Computes per-attribute fit, aggregates by category using candidate-derived priority weights, applies deal-breaker hard gates, and ranks the demo companies. Six fictional companies span the design space deliberately — a boutique trading firm, a design studio, a government tech team, a healthcare nonprofit, an AI startup, a mid-size SaaS company.
A preference curve engine. For each paired numeric attribute, infers one of five curve shapes per candidate (monotonic_increasing, monotonic_decreasing, peaked, u_shaped, indifferent) using a four-rule cascade. This is what makes the flagship demo work.
An explainability layer. Headline reasons, category contribution bars sorted by candidate weight, inline curve visualisations showing where each company sits on each curve, plain-language curve descriptions. The whole pitch — that this matcher does something subtler than weighted similarity — only lands if a candidate can see what it's doing.
work-fit-taxonomy/
├── taxonomy.yaml 100 attributes across 8 categories
├── questions.yaml 105 survey items + design principles
├── companies.yaml 6 fictional company profiles
├── build.py compiles YAML into the survey app
├── survey.template.html app template with engine implementations
└── index.html generated; what gets deployed
Documentation:
├── README.md this file
├── TAXONOMY.md attribute design rationale
├── QUESTION_DESIGN.md question design principles applied
├── MATCHING.md matching engine design
├── CURVES.md preference curve engine design
├── EXPLAINABILITY.md explainability layer design
└── SURVEY.md deployment notes
The build pipeline is simple: python3 build.py validates the YAML, checks every company supplies a value for every attribute, and compiles a 130KB self-contained HTML file. The validator caught a real schema gap on first run — privately_held_partnership wasn't in the funding-type options — which is exactly what build-time validation should do.
A few decisions that shaped the system, written up properly in the relevant docs:
The matching engine uses weighted distance with hierarchical aggregation, not cosine similarity. Cosine assumes high-dimensional independent vectors with shared "more is better" semantics, which doesn't fit semantically-anchored paired attributes. Hierarchical-weighted-distance is the standard for this shape of data and the right shape to grow into the curve engine. (See MATCHING.md.)
The curve engine is rule-based, not learned. No real candidate or outcome data exists, so curve inference uses four explicit rules grounded in the literature on person-environment fit and the per-attribute curve constraints baked into the taxonomy. If real data appeared, the rules would be the obvious thing to replace with learned parameters. (See CURVES.md.)
Categories are sorted by candidate weight, not by fit, in the breakdown view. Sort-by-fit would foreground the categories the candidate cares least about (the ones most likely to differ wildly). Sort-by-weight foregrounds what they actually care about. (See EXPLAINABILITY.md.)
Compensation amount, hard skills, and demographics are deliberately excluded from the taxonomy. This system is about fit, not capability or transaction. They belong in separate layers. (See TAXONOMY.md.)
- It isn't piloted. Every question is a hypothesis until tested with real respondents. The standard validation work — distribution checks, test-retest reliability, internal consistency, predictive validity against actual job satisfaction — needs real data to do.
- It isn't a learned model. Curve inference is rule-based. Rule-based is the right starting point with no data; it's not where you'd want to stop.
- It isn't two-sided. Matching is candidate-against-company only. A real system would also match the company's needs against the candidate's profile.
- It isn't 500 attributes. The brief's eventual target is ~500 per side; the system is built around 50 per side as a clean trunk that subdivides into branches at scale.
TAXONOMY.mddiscusses what changes between 50 and 500.
These aren't excuses — they're the honest set of things a senior reviewer should know to evaluate the work properly.
It's a working demonstration of the harder version of matching, end-to-end. The design choices are explicit, the trade-offs are documented, and the flagship test case actually works. The next iteration of any part of it has a clear starting point.