Add configurable evidence guard for resume tailoring

This commit is contained in:
2026-08-02 23:10:13 +03:30
parent 1c75afe0fd
commit a7a02ef141
12 changed files with 664 additions and 37 deletions
@@ -113,6 +113,93 @@
</p>
</div>
<div class="space-y-3 rounded-lg border p-3">
<div>
<div class="font-medium">Tailoring configuration</div>
<p class="mt-1 text-xs text-muted-foreground">
Set the app-wide evidence guard, then choose the mode for this rewrite.
</p>
</div>
<button
class="flex w-full items-center gap-3 rounded-md border p-3 text-left"
:class="
globalEvidenceGuard
? 'border-emerald-600 bg-emerald-50'
: 'border-orange-500 bg-orange-50'
"
type="button"
:aria-pressed="globalEvidenceGuard"
@click="toggleGlobalEvidenceGuard"
>
<span
class="relative h-5 w-9 shrink-0 rounded-full transition-colors"
:class="globalEvidenceGuard ? 'bg-emerald-600' : 'bg-orange-500'"
>
<span
class="absolute left-0.5 top-0.5 size-4 rounded-full bg-white shadow transition-transform"
:class="globalEvidenceGuard ? 'translate-x-4' : ''"
/>
</span>
<span>
<strong class="block text-xs">
{{
globalEvidenceGuard
? "App-wide evidence guard on"
: "App-wide evidence guard off"
}}
</strong>
<small class="mt-0.5 block text-xs text-muted-foreground">
{{
globalEvidenceGuard
? "Claim-level evidence rules are available across Signal."
: "All tailoring and revision uses profile-based rules."
}}
</small>
</span>
</button>
<button
class="flex w-full items-center gap-3 rounded-md border bg-muted/40 p-3 text-left"
:class="evidenceMode === 'profile' ? 'border-orange-500 bg-orange-50' : ''"
type="button"
:disabled="!globalEvidenceGuard"
:aria-pressed="evidenceMode === 'profile'"
:aria-disabled="!globalEvidenceGuard"
@click="evidenceMode = evidenceMode === 'strict' ? 'profile' : 'strict'"
>
<span
class="relative h-5 w-9 shrink-0 rounded-full transition-colors"
:class="evidenceMode === 'profile' ? 'bg-orange-500' : 'bg-gray-400'"
>
<span
class="absolute left-0.5 top-0.5 size-4 rounded-full bg-white shadow transition-transform"
:class="evidenceMode === 'profile' ? 'translate-x-4' : ''"
/>
</span>
<span>
<strong class="block text-xs">
{{
evidenceMode === "profile"
? "Flexible profile-based rewrite"
: "Strict source evidence"
}}
</strong>
<small class="mt-0.5 block text-xs text-muted-foreground">
{{
evidenceMode === "profile"
? globalEvidenceGuard
? "Uses the full resume profile; claim-level IDs are optional."
: "Forced by the app-wide setting; claim-level IDs are optional."
: "Every claim must cite a supporting profile fact."
}}
</small>
</span>
</button>
<p class="text-xs text-muted-foreground">
Flexible mode still excludes employers, skills, dates, metrics, and
achievements that are absent from this resume or your added context.
</p>
</div>
<UiAlert v-if="errorMessage" variant="destructive">
<UiAlertTitle>Signal could not complete the request</UiAlertTitle>
<UiAlertDescription>{{ errorMessage }}</UiAlertDescription>
@@ -185,6 +272,8 @@ const jobURL = ref("");
const jobText = ref("");
const about = ref("");
const tailoringStrength = ref(50);
const evidenceMode = ref<"strict" | "profile">("strict");
const globalEvidenceGuard = ref(true);
const busy = ref(false);
const errorMessage = ref("");
const successMessage = ref("");
@@ -209,10 +298,50 @@ const strengthDescription = computed(() => {
return "Fuller supported detail and stronger job-aligned wording without invention.";
});
onMounted(() => {
onMounted(async () => {
hasBackup.value = Boolean(localStorage.getItem(backupKey.value));
try {
const response = await fetch(`${apiBase.value}/api/settings`);
if (!response.ok) return;
const settings = (await response.json()) as { evidence_guard: boolean };
globalEvidenceGuard.value = settings.evidence_guard;
if (!settings.evidence_guard) evidenceMode.value = "profile";
} catch {
// The API error will be surfaced if the user starts a tailoring request.
}
});
const toggleGlobalEvidenceGuard = async () => {
const previous = globalEvidenceGuard.value;
const next = !previous;
globalEvidenceGuard.value = next;
if (!next) evidenceMode.value = "profile";
errorMessage.value = "";
try {
const response = await fetch(`${apiBase.value}/api/settings`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ evidence_guard: next })
});
const settings = (await response.json()) as {
evidence_guard?: boolean;
detail?: string;
};
if (!response.ok) {
throw new Error(settings.detail || "Could not update the Signal settings.");
}
globalEvidenceGuard.value = Boolean(settings.evidence_guard);
if (!globalEvidenceGuard.value) evidenceMode.value = "profile";
successMessage.value = globalEvidenceGuard.value
? "Evidence guard enabled across Signal."
: "Evidence guard disabled across Signal; profile truth rules remain active.";
} catch (error) {
globalEvidenceGuard.value = previous;
errorMessage.value = error instanceof Error ? error.message : "Unknown error.";
}
};
async function post<T>(path: string, payload: Record<string, unknown>): Promise<T> {
const response = await fetch(`${apiBase.value}${path}`, {
method: "POST",
@@ -300,6 +429,7 @@ const tailorResume = async () => {
markdown: data.markdown,
about: about.value,
tailoring_strength: tailoringStrength.value,
evidence_mode: evidenceMode.value,
...jobPayload()
});