mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
7 of 8 apply-yes simplify findings landed:
F2 — `generateInsight()` confidenceContext config-bag dropped. No
production caller passed the resolver; the wrapper now derives inputs
from the parsed payload directly (n from metricSource.n, recencyDays
=0, deviationStdRatio=null). Two confidence-wrapper tests rewritten
to test the parsed-payload path. v1.4.17 will re-introduce a
resolver injection when the feedback ratchet ships a real second
caller.
F3 — `aiRecommendationRationaleSchema.referenceId` speculative
optional field removed. No consumer reads it; rec-level referenceId
is the canonical citation pointer.
F4 — `applyLastWorkingCache` doc-comment trimmed: dropped the
"NEW array" defensive note (no caller compares the input ref).
F5 — `<RecommendationFeedback>` re-uses the canonical
`RecommendationFeedbackRequest` types from the validation schema
instead of hand-maintaining the same enum vocabulary. Removes a
silent-drift risk if v1.4.17 adds another time-range value.
F6 — `formatRelativeTime` doc-comment trimmed: dropped the stale
`Intl.RelativeTimeFormat` polyfill claim (baseline-supported since
2018) — kept the genuine WHY (i18n consistency).
F7 — `<RecommendationCard>` 26-line slot-architecture header trimmed
to a 4-line WHY. The slots being narrated as "future" were filled by
B5d / B5e in the same file.
F9 — `pickProviderType` doc-comment trimmed: removed the
mis-anchored PROMPT_VERSION snapshot paragraph (the function only
picks providerType — promptVersion is read elsewhere).
F10 — `legacy-payload.ts` `UnknownRec` interface removed; cast
inlined as `(rec as { rationale?: unknown })`.
F12 — `<ConfidenceMeter>` 20-line band-policy header trimmed to a
3-line WHY. The bandFor() helper + colour-by-band record below are
the documentation now.
(F1 + F8 deferred — both flagged "no" in simplify review and need
Marc's call.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
/**
|
|
* v1.4.16 phase B5e — server-side resolver for the provider/prompt
|
|
* attribution that gets stamped on every `RecommendationFeedback` row.
|
|
*
|
|
* The client cannot supply these fields — that would let any user
|
|
* poison the daily aggregator's slice (`(severity x provider x prompt)
|
|
* helpful-rate`) by claiming a thumbs-down was attributed to a rival
|
|
* provider. So the API endpoint reads the user's most recent
|
|
* `insights.generate` audit row, pulls `chainProviderType` (or its
|
|
* legacy fallback `providerType`) plus the current `PROMPT_VERSION`
|
|
* constant, and stamps those on the row.
|
|
*
|
|
* If no audit row exists yet (the user is rating a per-status insight
|
|
* that pre-dates the comprehensive generate flow), we fall back to
|
|
* "unknown" + the current PROMPT_VERSION so the row is still
|
|
* persistable and the aggregator can group "unknown" separately.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import { PROMPT_VERSION } from "@/lib/ai/prompts/insight-generator";
|
|
|
|
export interface FeedbackAttribution {
|
|
providerType: string;
|
|
promptVersion: string;
|
|
}
|
|
|
|
/** Default attribution when no `insights.generate` row exists yet. */
|
|
export const FEEDBACK_ATTRIBUTION_FALLBACK: Readonly<FeedbackAttribution> = {
|
|
providerType: "unknown",
|
|
promptVersion: PROMPT_VERSION,
|
|
};
|
|
|
|
interface AuditDetailsShape {
|
|
providerType?: unknown;
|
|
chainProviderType?: unknown;
|
|
}
|
|
|
|
function parseAuditDetails(raw: string | null): AuditDetailsShape {
|
|
if (!raw) return {};
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed && typeof parsed === "object") return parsed as AuditDetailsShape;
|
|
} catch {
|
|
// Treat unparseable details as absent — the fallback applies.
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Pick provider attribution from a parsed audit-row details blob.
|
|
* Priority: chainProviderType > providerType > "unknown".
|
|
*/
|
|
export function pickProviderType(details: AuditDetailsShape): string {
|
|
if (typeof details.chainProviderType === "string") {
|
|
return details.chainProviderType;
|
|
}
|
|
if (typeof details.providerType === "string") return details.providerType;
|
|
return FEEDBACK_ATTRIBUTION_FALLBACK.providerType;
|
|
}
|
|
|
|
/**
|
|
* Resolve the (providerType, promptVersion) tuple to stamp on a
|
|
* `RecommendationFeedback` row. Reads the latest `insights.generate`
|
|
* audit row for the user. Falls back to the safe defaults when no
|
|
* audit row exists.
|
|
*/
|
|
export async function resolveFeedbackAttribution(
|
|
userId: string,
|
|
): Promise<FeedbackAttribution> {
|
|
const latest = await prisma.auditLog.findFirst({
|
|
where: { userId, action: "insights.generate" },
|
|
orderBy: { createdAt: "desc" },
|
|
select: { details: true },
|
|
});
|
|
|
|
if (!latest) return { ...FEEDBACK_ATTRIBUTION_FALLBACK };
|
|
|
|
const details = parseAuditDetails(latest.details);
|
|
return {
|
|
providerType: pickProviderType(details),
|
|
promptVersion: PROMPT_VERSION,
|
|
};
|
|
}
|