# Animal Data Merge, Species Coverage, and Web Matcher Overlap — Diagnosis
Report date: 2026-04-20
Read-only diagnosis. No changes made.
---
## Part A — Animal Data Merge (SM + Profiler)
### How the matcher combines data
In `matchingEngine.ts:306-309`, the `findMatches()` function iterates over all animals from SM and fetches profiler notes separately:
```typescript
for (const animal of animals) { // SM data via fetchAnimals()
const notes = getBehaviorNotes(animal.id); // Profiler data from behavior_notes table
```
The two sources are **never merged into a single object for scoring**. Each scoring function receives `animal` (SM) and `notes` (profiler) as separate parameters and picks which to read per field. There is no "merge" step — it's a dual-source lookup pattern.
### Field-by-field: which source wins
| Attribute | SM has it? | Profiler has it? | Matcher reads from | Best source intention | Current matches intention? |
|---|---|---|---|---|---|
| Species | ✅ `animal.species` | ❌ | **Neither** — not used at all | SM | ❌ Not used |
| Color | ✅ `animal.color` | ✅ `notes.color` | **SM only** (`scoreColorMatch` reads `animal.color`) | Profiler (richer) | ❌ Reads SM, ignores profiler |
| Age | ✅ `animal.age`, `animal.ageInYears` | ❌ | **SM** (`scoreTraitMatch` reads `animal.ageInYears`) | SM | ✅ |
| Sex | ✅ `animal.sex` | ❌ | **Neither** — not scored | SM | ⚠️ Not scored at all |
| Size | ✅ `animal.size` | ❌ | **SM** (`scoreSizeMatch` reads `animal.size`) | SM | ✅ |
| Breed | ✅ `animal.breed` | ❌ | **SM** (`scoreTraitMatch` reads `animal.breed`) | SM | ✅ |
| Energy | ❌ | ✅ `notes.energyLevel` (text), `notes.energyLevel_match` (enum) | **Profiler text only** (`scoreEnergyMatch` reads `notes.energyLevel`, ignores enum) | Profiler enum | ⚠️ Reads text, ignores enum |
| Special needs | ❌ (only `additionalFlags` has "On Meds") | ✅ `notes.specialNeeds` | **Profiler** (`scoreSpecialNeeds` + `scoreExperience` read `notes.specialNeeds`) | Profiler | ✅ |
| Good w/ Cats | ❌ | ✅ `notes.goodWithCats_match` (enum), `notes.goodWithCats_text` | **Neither usable** — reads `notes.otherAnimalReaction` (legacy, always empty) | Profiler enum | ❌ Reads dead legacy field |
| Good w/ Dogs | ❌ | ✅ `notes.goodWithDogs_match` (enum), `notes.goodWithDogs_text` | **Neither usable** — reads `notes.otherAnimalReaction` (legacy, always empty) | Profiler enum | ❌ Reads dead legacy field |
| Good w/ Kids | ❌ | ✅ `notes.goodWithKids_match` (enum), `notes.goodWithKids_text` | **Neither usable** — reads `notes.kidBehavior` (legacy, always empty) | Profiler enum | ❌ Reads dead legacy field |
| People reaction | ❌ | ✅ `notes.peopleReaction` | **Profiler** (used in `scoreTraitMatch` personality keywords) | Profiler | ✅ |
| Backstory | ❌ | ✅ `notes.backstory` | **Profiler** (used in `scoreTraitMatch` + `generateExplanation`) | Profiler | ✅ |
### Summary of merge deviations
- **4 fields read correctly:** age, size, breed from SM; special needs, people reaction, backstory from profiler.
- **1 field reads wrong source:** color reads SM (less descriptive) instead of profiler (richer).
- **3 fields read dead legacy fields:** goodWithCats, goodWithDogs, goodWithKids all read empty legacy fields instead of the enum fields that have actual data.
- **1 field reads text instead of enum:** energy reads freeform text and re-parses it instead of using the pre-parsed enum.
- **2 fields not scored at all:** species, sex.
---
## Part B — Small Species Handling
### Does the ADOPTER_EXTRACTION_PROMPT handle small species?
**No.** The prompt (attributeParser.ts:107-116) does not list rabbit, guinea pig, gerbil, hamster, ferret, bird, reptile, or any small species as examples. The `preferredTraits` field examples are:
- "Young, playful cat"
- "Calm adult dog, medium size"
- "Any age, cuddly personality"
If the coordinator says "they want a rabbit," GPT-4o will likely produce `preferredTraits: "Rabbit"` or `preferredTraits: "Rabbit, calm, any age"` — burying species in the freeform traits string, same as cats and dogs. Since there's no structured species field, extraction handles all species identically (poorly).
### Species in the shelter database
From `animal_metadata`:
| Species | Count |
|---|---|
| Cat | 250 |
| Dog | 98 |
| Rabbit | 25 |
| Ferret | 1 |
| Guinea Pig | 1 |
SM returns each species distinctly — "Rabbit", "Guinea Pig", "Ferret" are separate `SPECIESNAME` values, not lumped under a generic "Small Animal" label.
### Small species in current Q1
Q1 text in staging-staff (index.html): **"Species, color, size and energy level"**
This is generic enough to cover any species. The problem isn't the question — it's the extraction and matching pipeline downstream.
---
## Part C — Current Q1 Text
Exact text from staging-staff/index.html:
```
1 Species, color, size and energy level
```
---
## Part D — Web Matcher Overlap
### Architecture
The Web Matcher is a **client-side filter app** — completely separate from the Adopter matcher's server-side scoring engine.
| Component | Adopter Matcher | Web Matcher |
|---|---|---|
| Location | `matchingEngine.ts` (server) | `matcher-web/app.js` (client) |
| Triggered by | `POST /api/match` or `POST /api/coordinator/process` | User clicks filter checkboxes |
| Data source | `fetchAnimals()` (SM) + `getBehaviorNotes()` (profiler) | `GET /api/animals` (SM + profiler notes + bios, merged server-side) |
| Species handling | **None** — no filter, no score | **Yes** — species tabs (Dog / Cat / Small Animals) |
| Matching method | Weighted scoring (7 factors, server-side) | Client-side checkbox filters (include/exclude) |
| Shared code | `matchingEngine.ts` | None — standalone JS |
### Web Matcher endpoints
- **Data:** `GET /api/animals` (server.ts:703) — returns all adoptable animals with `behaviorNotes` attached (including `_match` enum fields)
- **Static:** `/matcher` route (server.ts:6493) serves `matcher-web/` directory
- Does NOT use `/api/match` or `matchingEngine.ts` at all.
### Filter checkboxes (matcher-web/index.html)
| Filter | Values | Enabled? | Reads from |
|---|---|---|---|
| Species tabs | dog / cat / small | ✅ Enabled | `animal.species` via `matchesSpecies()` |
| Age | young / adult / senior | ✅ Enabled | `animal.ageInYears` (SM) |
| Sex | male / female | ✅ Enabled | `animal.sex` (SM) |
| Energy Level | low / medium / high | **Disabled** (HTML `disabled` attr) | `animal.behaviorNotes.energyLevel_match` (profiler **enum**) |
| Good with Cats | unknown / yes / somewhat / no | **Disabled** | `animal.behaviorNotes.goodWithCats_match` (profiler **enum**) |
| Good with Dogs | unknown / yes / somewhat / no | **Disabled** | `animal.behaviorNotes.goodWithDogs_match` (profiler **enum**) |
| Good with Kids | unknown / yes / somewhat / no | **Disabled** | `animal.behaviorNotes.goodWithKids_match` (profiler **enum**) |
| Special Needs | yes | **Disabled** | `animal.behaviorNotes.specialNeeds` (profiler text) |
| Color search | text input | ✅ Enabled | `animal.color` (SM) + `animal.behaviorNotes.color` (profiler — **both!**) |
### Critical finding: Web Matcher already uses the _match enums correctly
The Web Matcher's `applyFilters()` function (app.js:311-400) reads the **correct fields**:
```javascript
// Energy: reads energyLevel_match enum (not freeform text)
const energyMatch = animal.behaviorNotes?.energyLevel_match;
// Good with cats: reads goodWithCats_match enum (not legacy field)
const catsMatch = animal.behaviorNotes?.goodWithCats_match;
// Good with dogs: reads goodWithDogs_match enum
const dogsMatch = animal.behaviorNotes?.goodWithDogs_match;
// Good with kids: reads goodWithKids_match enum
const kidsMatch = animal.behaviorNotes?.goodWithKids_match;
```
Comments in the code explicitly say "no text fallback, only use _match field" (app.js:246, 255, 266, 277).
**The Web Matcher also reads BOTH color sources** for search (app.js:292):
```javascript
const bnColor = (animal.behaviorNotes?.color || '').toLowerCase();
```
So the Web Matcher already does what the Adopter matcher should be doing — it reads enum fields and checks both SM and profiler color.
### Small species in Web Matcher
The Web Matcher has a "Small Animals" tab (app.js:36, index.html:41) that filters on:
```javascript
const SMALL_SPECIES = ['rabbit', 'guinea pig', 'hamster', 'gerbil', 'ferret',
'chinchilla', 'rat', 'mouse', 'bird', 'reptile', 'small'];
```
So species filtering is already production-ready on the Web Matcher side, just not on the Adopter matching side.
### Shared code analysis
| Fix needed | Adopter matcher impact | Web Matcher impact |
|---|---|---|
| Add `preferredSpecies` to extraction | `attributeParser.ts` + `adopter_preferences` schema | No impact — Web Matcher doesn't use adopter preferences |
| Species pre-filter in matcher | `matchingEngine.ts` | No impact — Web Matcher filters client-side via `matchesSpecies()` |
| Read `_match` enums instead of legacy fields | `matchingEngine.ts` | No impact — Web Matcher already reads enums correctly |
| Read profiler color in addition to SM | `matchingEngine.ts` | No impact — Web Matcher already reads both |
**Conclusion: The Adopter matcher and Web Matcher share zero code.** Fixes to `matchingEngine.ts` and `attributeParser.ts` will not affect the Web Matcher at all — no auto-benefit, but also no risk of breaking it.
The Web Matcher can serve as a **reference implementation** for the correct field reads. Its `applyFilters()` function demonstrates the exact pattern the Adopter matcher should adopt.
---
## Summary of Findings
1. **Merge pattern:** No merge — dual-source lookup. 4 fields correct, 1 wrong source, 3 dead legacy reads, 1 text-instead-of-enum, 2 not scored.
2. **Small species:** SM tracks them distinctly (Rabbit, Guinea Pig, Ferret). Extraction prompt doesn't mention them but GPT-4o will likely handle them passably via `preferredTraits`. The real gap is species filtering in the matcher (none exists).
3. **Q1 text:** "Species, color, size and energy level" — generic enough for all species.
4. **Web Matcher vs Adopter Matcher:** Completely separate code. Web Matcher already correctly reads `_match` enums, both color sources, and has species tabs with small animal support. Adopter matcher fixes are isolated to `matchingEngine.ts` + `attributeParser.ts` — zero overlap, zero risk.
---
REPORT COMPLETE — 178 total lines.