fix: auto-fix code quality [skip ci] (#81)
This commit is contained in:
1 parent
b9bba643bd
commit
b5b370e6eb
4 files changed
+75
-57
No files matched your search
@@ -279,26 +279,37 @@ export type JevAnswers = Record<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/** Reads the per-message answer subset by id, missing → null. */
|
/** Per-message answer subset (nullable until validated — `answersOf`). */
|
||||||
function answersOf(
|
interface JevMessageAnswers {
|
||||||
answers: JevAnswers,
|
|
||||||
id: string,
|
|
||||||
): {
|
|
||||||
v?: { type: "noul"; noul: number };
|
v?: { type: "noul"; noul: number };
|
||||||
status?: { type: "choice"; choice: string; confidence: number };
|
status?: { type: "choice"; choice: string; confidence: number };
|
||||||
severity?: { type: "choice"; choice: string };
|
severity?: { type: "choice"; choice: string };
|
||||||
category?: { type: "choice"; choice: string };
|
category?: { type: "choice"; choice: string };
|
||||||
action?: { type: "choice"; choice: string };
|
action?: { type: "choice"; choice: string };
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
/** Reads the per-message answer subset by id, missing → undefined. */
|
||||||
|
function answersOf(answers: JevAnswers, id: string): JevMessageAnswers {
|
||||||
return {
|
return {
|
||||||
v: answers[`${id}__v`] as never,
|
v: answers[`${id}__v`] as JevMessageAnswers["v"],
|
||||||
status: answers[`${id}__status`] as never,
|
status: answers[`${id}__status`] as JevMessageAnswers["status"],
|
||||||
severity: answers[`${id}__severity`] as never,
|
severity: answers[`${id}__severity`] as JevMessageAnswers["severity"],
|
||||||
category: answers[`${id}__category`] as never,
|
category: answers[`${id}__category`] as JevMessageAnswers["category"],
|
||||||
action: answers[`${id}__action`] as never,
|
action: answers[`${id}__action`] as JevMessageAnswers["action"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Optionally-typed accessor for a choice answer's label ("" when missing). */
|
||||||
|
function labelOf(a: { type: "choice"; choice: string } | undefined): string {
|
||||||
|
return a?.type === "choice" ? a.choice : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set form of the vocab arrays for O(1) membership tests. */
|
||||||
|
const JEV_STATUS_SET = new Set<string>(JEV_STATUSES);
|
||||||
|
const JEV_SEVERITY_SET = new Set<string>(JEV_SEVERITIES);
|
||||||
|
const JEV_CATEGORY_SET = new Set<string>(JEV_CATEGORIES);
|
||||||
|
const JEV_ACTION_SET = new Set<string>(JEV_ACTIONS);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide per-message Jev acceptance. Requires ALL five questions present
|
* Decide per-message Jev acceptance. Requires ALL five questions present
|
||||||
* with valid labels and CROSS-CONSISTENT semantics:
|
* with valid labels and CROSS-CONSISTENT semantics:
|
||||||
@@ -331,39 +342,42 @@ export function isJevAccepted(
|
|||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!JEV_STATUSES.includes(status.choice as (typeof JEV_STATUSES)[number]))
|
// Valid label = one of the vocab const arrays. The membership guards
|
||||||
return false;
|
// (Set.has) reject anything unknown, then the labels are narrowed via the
|
||||||
|
// const-array includes so the downstream comparisons typecheck.
|
||||||
|
const statusLabel = labelOf(status);
|
||||||
|
const severityLabel = labelOf(severity);
|
||||||
|
const categoryLabel = labelOf(category);
|
||||||
|
const actionLabel = labelOf(action);
|
||||||
if (
|
if (
|
||||||
!JEV_SEVERITIES.includes(severity.choice as (typeof JEV_SEVERITIES)[number])
|
!JEV_STATUS_SET.has(statusLabel) ||
|
||||||
|
!JEV_SEVERITY_SET.has(severityLabel) ||
|
||||||
|
!JEV_CATEGORY_SET.has(categoryLabel) ||
|
||||||
|
!JEV_ACTION_SET.has(actionLabel)
|
||||||
)
|
)
|
||||||
return false;
|
return false; // unknown label — LLM fallback
|
||||||
if (
|
|
||||||
!JEV_CATEGORIES.includes(category.choice as (typeof JEV_CATEGORIES)[number])
|
const s = statusLabel as (typeof JEV_STATUSES)[number];
|
||||||
)
|
const sev = severityLabel as (typeof JEV_SEVERITIES)[number];
|
||||||
return false;
|
const cat = categoryLabel as (typeof JEV_CATEGORIES)[number];
|
||||||
if (!JEV_ACTIONS.includes(action.choice as (typeof JEV_ACTIONS)[number]))
|
const act = actionLabel as (typeof JEV_ACTIONS)[number];
|
||||||
return false;
|
|
||||||
|
|
||||||
const noulVal = a.v.noul;
|
const noulVal = a.v.noul;
|
||||||
|
|
||||||
// noul ↔ status consistency
|
// noul ↔ status consistency
|
||||||
if (status.choice === "clean" && noulVal >= 0.5) return false;
|
if (s === "clean" && noulVal >= 0.5) return false;
|
||||||
if (status.choice !== "clean" && noulVal < 0.5) return false;
|
if (s !== "clean" && noulVal < 0.5) return false;
|
||||||
// severity ↔ status: clean must be none; flagged/warn must NOT be none
|
// severity ↔ status: clean must be none; flagged/warn must NOT be none
|
||||||
if (status.choice === "clean" && severity.choice !== "none") return false;
|
if (s === "clean" && sev !== "none") return false;
|
||||||
if (status.choice !== "clean" && severity.choice === "none") return false;
|
if (s !== "clean" && sev === "none") return false;
|
||||||
// action ↔ status: clean must be none; flagged must NOT be none;
|
// action ↔ status: clean must be none; flagged must NOT be none;
|
||||||
// warn must not delete/escalate; clean must never delete/escalate
|
// warn must not delete/escalate; clean must never delete/escalate
|
||||||
if (status.choice === "clean" && action.choice !== "none") return false;
|
if (s === "clean" && act !== "none") return false;
|
||||||
if (status.choice === "flagged" && action.choice === "none") return false;
|
if (s === "flagged" && act === "none") return false;
|
||||||
if (
|
if (s === "warn" && (act === "delete" || act === "escalate")) return false;
|
||||||
status.choice === "warn" &&
|
|
||||||
(action.choice === "delete" || action.choice === "escalate")
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
// category ↔ status: clean must be none; flagged must NOT be none
|
// category ↔ status: clean must be none; flagged must NOT be none
|
||||||
if (status.choice === "clean" && category.choice !== "none") return false;
|
if (s === "clean" && cat !== "none") return false;
|
||||||
if (status.choice === "flagged" && category.choice === "none") return false;
|
if (s === "flagged" && cat === "none") return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -378,12 +392,16 @@ export function mapJevAnswersToResult(
|
|||||||
): AnalysisResult {
|
): AnalysisResult {
|
||||||
const a = answersOf(answers, messageId);
|
const a = answersOf(answers, messageId);
|
||||||
const v = a.v as { type: "noul"; noul: number };
|
const v = a.v as { type: "noul"; noul: number };
|
||||||
const st = a.status as { type: "choice"; choice: string; confidence: number };
|
const st = a.status as {
|
||||||
const sev = a.severity as { type: "choice"; choice: string };
|
type: "choice";
|
||||||
const cat = a.category as { type: "choice"; choice: string };
|
choice: string;
|
||||||
const act = a.action as { type: "choice"; choice: string };
|
confidence: number;
|
||||||
|
};
|
||||||
|
const sev = labelOf(a.severity);
|
||||||
|
const cat = labelOf(a.category);
|
||||||
|
const act = labelOf(a.action);
|
||||||
|
|
||||||
const status = st.choice as "clean" | "warn" | "flagged";
|
const status = st.choice as (typeof JEV_STATUSES)[number];
|
||||||
// Calibrated score: clean → 0; warn → 0.45; flagged → P(violates) clamped.
|
// Calibrated score: clean → 0; warn → 0.45; flagged → P(violates) clamped.
|
||||||
const rawNoul = typeof v.noul === "number" ? v.noul : 0;
|
const rawNoul = typeof v.noul === "number" ? v.noul : 0;
|
||||||
const score =
|
const score =
|
||||||
@@ -400,15 +418,15 @@ export function mapJevAnswersToResult(
|
|||||||
return {
|
return {
|
||||||
messageId,
|
messageId,
|
||||||
status,
|
status,
|
||||||
flags: cat.choice === "none" ? [] : [cat.choice],
|
flags: cat === "none" ? [] : [cat],
|
||||||
score,
|
score,
|
||||||
analysis:
|
analysis:
|
||||||
`[Jev] status=${status}, kategori=${cat.choice}, keparahan=${sev.choice}, ` +
|
`[Jev] status=${status}, kategori=${cat}, keparahan=${sev}, ` +
|
||||||
`keyakinan=${confidence.toFixed(2)}, tindakan=${act.choice}, p_melanggar=${rawNoul.toFixed(2)}`,
|
`keyakinan=${confidence.toFixed(2)}, tindakan=${act}, p_melanggar=${rawNoul.toFixed(2)}`,
|
||||||
categories: cat.choice === "none" ? [] : [cat.choice],
|
categories: cat === "none" ? [] : [cat],
|
||||||
severity: sev.choice as AnalysisResult["severity"],
|
severity: sev as AnalysisResult["severity"],
|
||||||
confidence,
|
confidence,
|
||||||
recommendedAction: act.choice as AnalysisResult["recommendedAction"],
|
recommendedAction: act as AnalysisResult["recommendedAction"],
|
||||||
policyVersion: JEV_POLICY_VERSION,
|
policyVersion: JEV_POLICY_VERSION,
|
||||||
evidence: [],
|
evidence: [],
|
||||||
};
|
};
|
||||||
@@ -483,7 +501,7 @@ export async function analyzeBatchWithJev(
|
|||||||
|
|
||||||
outcome.raw = systemOneResult;
|
outcome.raw = systemOneResult;
|
||||||
const answers = systemOneResult.answers as unknown as JevAnswers;
|
const answers = systemOneResult.answers as unknown as JevAnswers;
|
||||||
const minConfidence = config.AI_LLM_JEV_MIN_CONFIDENCE ?? 0.9;
|
const minConfidence = config.AI_LLM_JEV_MIN_CONFIDENCE;
|
||||||
|
|
||||||
for (const t of targets) {
|
for (const t of targets) {
|
||||||
if (isJevAccepted(answers, t.id, minConfidence)) {
|
if (isJevAccepted(answers, t.id, minConfidence)) {
|
||||||
|
|||||||
@@ -171,7 +171,11 @@ export async function runTextOnlyBatch(
|
|||||||
if (urlArr.length >= 10) break;
|
if (urlArr.length >= 10) break;
|
||||||
}
|
}
|
||||||
if (urlArr.length === 0) {
|
if (urlArr.length === 0) {
|
||||||
return { text: new Map(), image: new Map(), title: new Map() };
|
return {
|
||||||
|
text: new Map(),
|
||||||
|
image: new Map(),
|
||||||
|
title: new Map(),
|
||||||
|
} satisfies UrlFetchResult;
|
||||||
}
|
}
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
urlArr.map((url) => fetchUrlSafely(url)),
|
urlArr.map((url) => fetchUrlSafely(url)),
|
||||||
@@ -365,7 +369,7 @@ export async function runTextOnlyBatch(
|
|||||||
const messagesBlock = (
|
const messagesBlock = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workingSet.map(async (msg) => {
|
workingSet.map(async (msg) => {
|
||||||
const content = truncateForAi(getAnalysisContent(msg));
|
const content = analysisContentOf(msg);
|
||||||
const msgUrls = extractUrlsFromText(content);
|
const msgUrls = extractUrlsFromText(content);
|
||||||
const urlContexts = msgUrls
|
const urlContexts = msgUrls
|
||||||
.map((url) => {
|
.map((url) => {
|
||||||
|
|||||||
@@ -201,8 +201,8 @@ export const configSchema = z
|
|||||||
AI_LLM_JEV_ENABLED: z
|
AI_LLM_JEV_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(true),
|
.transform((v) => v === "true"),
|
||||||
AI_LLM_JEV_API_KEY: z.string().optional().default(""),
|
AI_LLM_JEV_API_KEY: z.string().optional().default(""),
|
||||||
AI_LLM_JEV_BASE_URL: z.string().url().default("http://127.0.0.1:4014"),
|
AI_LLM_JEV_BASE_URL: z.string().url().default("http://127.0.0.1:4014"),
|
||||||
AI_LLM_JEV_MODEL: z.string().default("oc/jev-1.13-free"),
|
AI_LLM_JEV_MODEL: z.string().default("oc/jev-1.13-free"),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { analyzeBatchWithJev } from "../src/modules/ai-moderation/jevAnalyzer.js";
|
import { analyzeBatchWithJev } from "../src/modules/ai-moderation/jevAnalyzer.js";
|
||||||
|
import type { AnalysisResult } from "../src/modules/message-capture/types.js";
|
||||||
|
|
||||||
const SMOKE = process.env.AI_LLM_JEV_SMOKE === "1";
|
const SMOKE = process.env.AI_LLM_JEV_SMOKE === "1";
|
||||||
const SKIP_REASON =
|
const SKIP_REASON =
|
||||||
@@ -75,13 +76,8 @@ describe("Jev live smoke (real 9router /v1/systemone)", () => {
|
|||||||
expect(byId.m_clean?.status).toBe("clean");
|
expect(byId.m_clean?.status).toBe("clean");
|
||||||
expect(byId.m_help?.status).toBe("clean");
|
expect(byId.m_help?.status).toBe("clean");
|
||||||
|
|
||||||
// Verdicts are calibration-honest
|
// Verdicts are calibration-honest (typed by the real pipeline shape)
|
||||||
const verdicts = outcome.results as Array<{
|
const verdicts = outcome.results as AnalysisResult[];
|
||||||
status: string;
|
|
||||||
confidence: number;
|
|
||||||
score: number;
|
|
||||||
analysis: string;
|
|
||||||
}>;
|
|
||||||
for (const r of verdicts) {
|
for (const r of verdicts) {
|
||||||
expect(r.confidence).toBeGreaterThanOrEqual(0.9);
|
expect(r.confidence).toBeGreaterThanOrEqual(0.9);
|
||||||
if (r.status === "clean") expect(r.score).toBe(0);
|
if (r.status === "clean") expect(r.score).toBe(0);
|
||||||
|
|||||||
Reference in new issue
Block a user