Doormile AI: fix two silent-failure bugs in entity handling

Both were found while re-checking the branch, not from a failing build —
each fails quietly and would have looked like "the bot just doesn't know".

riderLookup read `status`, `phonenumber` and `vehicletype` off a miler.
None of those fields exist: api.js documents the confirmed live shape as
`availabilitystatus`, `phone` and `defaultvehicletype`, and states outright
that no `status` field is present. So "where is rider Kumar" always rendered
"status unknown" with no phone and no vehicle. Now reads the real names and
adds the hub.

correctTypos could rewrite a named entity into a vocabulary word. It runs
before matching, and orderQuery extracts tenant/rider names from the
corrected text, so a tenant called "Partnerz" (one edit from "partners") or
"Ordero" (one from "orders") was silently renamed and then failed to
resolve — with no indication that a filter had been dropped. Words
following rider/tenant/hub/vehicle/customer/partner/named/called/for are
now protected from correction; genuine keyword typos still get fixed.

The comment above correctTypos already claimed names were never touched.
Nothing enforced it. The L2 composite-query work made the gap matter,
because before it entity names barely reached the matcher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:58:03 +05:30
parent 1ea936c7d8
commit 5284fc75bc

View File

@@ -109,11 +109,37 @@ const KEYWORD_VOCAB = [
'available'
];
const correctTypos = (text) =>
text
// Words that form a NAMED ENTITY must never be "corrected". A tenant called
// "Partnerz" is one edit from "partners" and a rider called "Delivara" is two
// from "delivered"; rewriting either makes the name unresolvable, and
// orderQuery extracts tenant/rider names from the corrected text, so the
// failure is silent — the filter just never matches.
//
// The original comment here claimed order IDs and tenant names were never
// touched. Nothing enforced that; this does.
const ENTITY_NAME_SPAN = /\b(?:rider|tenant|hub|vehicle|customer|partner|named|called|for)\s+((?:[A-Za-z][A-Za-z0-9.'-]*\s*){1,4})/gi;
const protectedNameWords = (text) => {
const keep = new Set();
const re = new RegExp(ENTITY_NAME_SPAN.source, 'gi');
let m = re.exec(text);
while (m !== null) {
m[1]
.split(/\s+/)
.filter(Boolean)
.forEach((w) => keep.add(w.toLowerCase()));
m = re.exec(text);
}
return keep;
};
const correctTypos = (text) => {
const keep = protectedNameWords(text);
return text
.split(/\b/)
.map((token) => {
const word = token.toLowerCase();
if (keep.has(word)) return token;
if (!/^[a-z]+$/.test(word) || word.length < 5 || KEYWORD_VOCAB.includes(word)) return token;
// >=6 chars allows distance 2, which is what a single adjacent-letter
// transposition ("riedrs" for "riders") costs in plain Levenshtein
@@ -132,6 +158,7 @@ const correctTypos = (text) =>
return best && bestDist <= maxDist ? best : token;
})
.join('');
};
const TODAY = () => dayjs().format('YYYY-MM-DD');
@@ -789,9 +816,18 @@ const INTENTS = [
);
if (!found) return null;
return {
headline: `${found.displayname || found.name} — ${found.status || 'status unknown'}.`,
// Field names matter here: a miler has NO `status`, `phonenumber` or
// `vehicletype` field. The real ones are `availabilitystatus`,
// `phone` and `defaultvehicletype` (api.js documents the confirmed
// live shape). Reading the wrong names meant this answer always said
// "status unknown" and never showed a phone or vehicle.
headline: `${found.displayname || found.name} — ${found.availabilitystatus || 'availability unknown'}.`,
detail:
[found.phonenumber ? `Phone: ${found.phonenumber}` : null, found.vehicletype ? `Vehicle: ${found.vehicletype}` : null]
[
found.phone ? `Phone: ${found.phone}` : null,
found.defaultvehicletype ? `Vehicle: ${found.defaultvehicletype}` : null,
found.hubname ? `Hub: ${found.hubname}` : null
]
.filter(Boolean)
.join('\n') || undefined,
sourceCalls: [{ name: 'getMilers', target: '/admin/milers', status: 'complete', stats: `matched "${name}"` }]