[grade=B] fix(DC-163 rev2): strict merge semantics, contract-proving settings/test, valid enginecheck fixture

Judge r1 (C) blockers B1-B3 + polish both applied — see body of rev1 commit for feature scope.

urn:ump:l2tg66wmchkmd6fde4k3ojwc56zpxqh657spe37bu6dpvduswalq
This commit is contained in:
Hermes committed 2026-10-08 15:04:56 -07:00
1 parent e80e3273b7
commit 14160f9317
2 files changed
+71 -11

No files matched your search

+33 -11
View File
@@ -167,19 +167,34 @@ function validateParsed(parsed) {
}
// ─── Merge validated LLM fields onto the pattern result ────────────────────
// Pattern stays the structural base (known-good shapes); LLM contributes the
// semantic understanding. appId is never LOST silently when both engines
// disagree — the LLM candidate wins if it is a valid catalog id, because the
// LLM saw the full 78-app catalog while regex knows only its hardcoded list.
// Same-intent: pattern stays the structural base (known-good shapes) and
// the LLM adds semantic corrections (a valid catalog appId wins even when
// regex missed it). Different-intent: the pattern's intent-SPECIFIC fields
// (appId/serviceId/categories/response/action) belong to the OLD intent and
// are dropped — merged fields come exclusively from the validated LLM
// output, with `action` strictly ACTIONS_BY_INTENT[llm.intent] (null for
// 'unknown'). Stale pattern data can never ride along under a new intent.
function mergeIntent(patternResult, llm) {
const merged = { ...patternResult };
const intentChanged = llm.intent !== patternResult.intent;
merged.intent = llm.intent;
merged.action = ACTIONS_BY_INTENT[llm.intent] || patternResult.action || null;
// If the LLM routed to a DIFFERENT intent than the pattern did, the
// pattern's canned response object belongs to the old intent — drop it
// and let enrich() rebuild the shape for the new one (with llmReply text).
if (intentChanged) merged.response = null;
if (intentChanged) {
// Fields only ride along when they are meaningful for the NEW intent
// (a 'plex' appId under intent 'unknown' is meaningless — drop it).
return {
intent: llm.intent,
confidence: 0.85,
message: patternResult.message,
action: ACTIONS_BY_INTENT[llm.intent] ?? null,
response: null,
appId: (llm.intent === 'deploy' && llm.appId) ? llm.appId : undefined,
serviceId: (llm.intent === 'diagnose' && llm.serviceId) ? llm.serviceId : undefined,
categories: llm.intent === 'recommend'
? ((llm.categories && llm.categories.length) ? llm.categories : ['media-streaming'])
: undefined,
llmReply: llm.reply || undefined,
};
}
const merged = { ...patternResult };
merged.action = ACTIONS_BY_INTENT[llm.intent] ?? null;
if (llm.appId) merged.appId = llm.appId;
if (llm.serviceId && !merged.serviceId) merged.serviceId = llm.serviceId;
if (llm.intent === 'recommend') {
@@ -645,6 +660,13 @@ module.exports = function ({ asyncHandler, siteConfig = null, log = console, fet
const parsed = JSON.parse(repairParsed(content));
const validated = validateParsed(parsed);
if (!validated || !validated.intent) throw new Error('invalid classifier output');
// Contract proof: the fixed sample MUST classify as 'backup'. An
// 'unknown' here means the endpoint answers but the model/prompt is
// not actually doing DashCaddy classification — report it as a
// failure, not a pass.
if (validated.intent !== 'backup') {
return ok(res, { ok: false, engine: 'llm-error', reason: `sample misclassified: expected 'backup', got '${validated.intent}'` });
}
return ok(res, { ok: true, engine: 'llm', model: cfg.model, sample: { intent: validated.intent, reply: validated.reply } });
} catch (e) {
const reason = String((e && (e.name === 'AbortError' ? 'timeout' : e.message)) || 'error').slice(0, 200);