[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:
2 files changed
+71
-11
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user