feat(sync): מוני-ריצה מובחנים ל-sync-case-status (legal-ai #617)
הג'וב סיים עד כה בשורה אחת — `casesChecked` — שאינה מבחינה בין "אין מה
לעשות", "נדחה בכוונה" ו"נפל". 13,872 ריצות דיווחו בריאות ירוקה, ובפועל
היו בהן 389 כתיבות בלבד; האבחנה ב-#604 נבנתה על היעדר-ראיה.
כל ריצה פולטת מעתה: scanned · matched · written · declined{no_linked_issues,
no_writable_root, ambiguous_writable_roots, unknown_status, already_matching}.
הפליטה יושבת ב-`finally` וזורקת את השגיאה המקורית הלאה, כך שריצה שנפלה
נשארת `failed` ב-`plugin_job_runs` **וגם** נושאת את המונים שהספיקה לצבור.
שני משטחים קיימים וצורכים, שנמדדו:
- `ctx.logger.info` → stdout של pm2 — שם בוצע בפועל האבחון של #626/#637.
- `ctx.metrics.write` → `plugin_logs` (level='metric') — הטבלה שפאנל
"Recent Logs" בדף-הפלאגין מרנדר, וניתנת לשאילתת-SQL (AC4).
המונים בתוך מחרוזת-ההודעה ולא רק ב-meta, כי שני המשטחים מתעלמים מ-meta
בפועל (הפאנל מרנדר createdAt/level/message בלבד; ומ-pino נמדד ששדה
`{error:…}` נופל). נדרשה הרשאת `metrics.write` ב-manifest — זו הדרך
היחידה לכתוב ל-`plugin_logs`.
אפס שינוי בהתנהגות-הכתיבה: `sync-target.ts` לא נגע כלל, ובכלל זה
`NON_WRITABLE_STATUSES` (הכרעת-יו"ר, #626).
Closes ezer-mishpati/legal-ai#617
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
226
src/worker.ts
226
src/worker.ts
@@ -11,6 +11,14 @@ import {
|
||||
runJobHandler,
|
||||
} from "./company-scope.js";
|
||||
import { LegalApi } from "./legal-api.js";
|
||||
import {
|
||||
formatSyncRunSummary,
|
||||
newSyncRunCounters,
|
||||
recordDecline,
|
||||
SYNC_RUN_SUMMARY_PREFIX,
|
||||
type SyncRunCounters,
|
||||
syncRunSummaryTags,
|
||||
} from "./sync-run-summary.js";
|
||||
import {
|
||||
labelFor,
|
||||
pickSyncTargetIssue,
|
||||
@@ -842,102 +850,154 @@ const plugin = definePlugin({
|
||||
|
||||
// ── Jobs ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* פולט את סיכום-ריצת `sync-case-status` לשני משטחים קיימים וצורכים:
|
||||
* 1. `ctx.logger.info` → stdout של pm2 (שם בוצע בפועל האבחון של #626/#637).
|
||||
* 2. `ctx.metrics.write` → טבלת `plugin_logs` (level='metric'), שאותה מרנדר
|
||||
* פאנל "Recent Logs" בדף-הפלאגין של Paperclip, וניתן לשאול ב-SQL.
|
||||
*
|
||||
* ⚠️ הפונקציה **לעולם אינה זורקת** — היא נקראת מתוך `finally`, וזריקה ממנה
|
||||
* הייתה מחליפה את שגיאת-הריצה המקורית ומסתירה את סיבת-הכשל האמיתית.
|
||||
* כשל של `metrics.write` מדווח בקול (`logger.error`) עם טקסט-השגיאה **בתוך
|
||||
* המחרוזת** — כי שדות-`meta` נופלים מפלט-pino (נמדד) — ולא נבלע.
|
||||
*/
|
||||
async function emitSyncRunSummary(
|
||||
counters: SyncRunCounters,
|
||||
failure: unknown,
|
||||
): Promise<void> {
|
||||
const outcome = failure === undefined ? "ok" : "failed";
|
||||
const error = failure === undefined ? undefined : String(failure);
|
||||
const line = formatSyncRunSummary(counters, { outcome, error });
|
||||
const tags = syncRunSummaryTags(counters, { outcome });
|
||||
|
||||
ctx.logger.info(line, tags);
|
||||
|
||||
try {
|
||||
await ctx.metrics.write(line, counters.written, tags);
|
||||
} catch (err) {
|
||||
ctx.logger.error(
|
||||
`${SYNC_RUN_SUMMARY_PREFIX}: failed to persist run summary metric: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.jobs.register("sync-case-status", async (job) => {
|
||||
ctx.logger.info("Starting case status sync", { runId: job.runId });
|
||||
|
||||
// אין `try/catch` בולע כאן — `runJobHandler` (company-scope.ts) הוא
|
||||
// שמלוגג ו**זורק מחדש**, כדי שהמתזמר ירשום `status:"failed"`
|
||||
// כשה-scope נדחה (legal-ai issue #637). ה-`try/catch` הישן כאן בלע
|
||||
// את הכשל, וה-run נרשם `succeeded` בלי לעשות כלום — 13,928 ריצות כוזבות.
|
||||
await runJobHandler("sync-case-status", ctx.logger, async () => {
|
||||
const cases = await api.listCases();
|
||||
const companies = await ctx.companies.list();
|
||||
if (!companies.length) return;
|
||||
const statusModel = await api.getStatusModel();
|
||||
const counters = newSyncRunCounters();
|
||||
let failure: unknown;
|
||||
|
||||
// מעבר אחד על ה-issues של כל חברה: state.get אחד ל-issue (לא לכל
|
||||
// צירוף תיק×issue), וקיבוץ למפה case_number → מועמדים. הקיבוץ נדרש
|
||||
// כדי לבחור "השורש היחיד". סורק את **כל** החברות (לא רק הראשונה) —
|
||||
// legal-ai issue #604.
|
||||
const byCase = new Map<string, SyncCandidate[]>();
|
||||
for (const company of companies) {
|
||||
const issues = await ctx.issues.list({ companyId: company.id });
|
||||
for (const issue of issues) {
|
||||
const linkedCase = await ctx.state.get({
|
||||
scopeKind: "issue",
|
||||
scopeId: issue.id,
|
||||
stateKey: "legal-case-number",
|
||||
});
|
||||
if (typeof linkedCase !== "string" || !linkedCase) continue;
|
||||
const list = byCase.get(linkedCase) ?? [];
|
||||
list.push({
|
||||
id: issue.id,
|
||||
status: issue.status,
|
||||
parentId: issue.parentId,
|
||||
companyId: company.id,
|
||||
});
|
||||
byCase.set(linkedCase, list);
|
||||
try {
|
||||
// אין `try/catch` בולע כאן — `runJobHandler` (company-scope.ts) הוא
|
||||
// שמלוגג ו**זורק מחדש**, כדי שהמתזמר ירשום `status:"failed"`
|
||||
// כשה-scope נדחה (legal-ai issue #637). ה-`try/catch` הישן כאן בלע
|
||||
// את הכשל, וה-run נרשם `succeeded` בלי לעשות כלום — 13,928 ריצות כוזבות.
|
||||
await runJobHandler("sync-case-status", ctx.logger, async () => {
|
||||
const cases = await api.listCases();
|
||||
// נקבע מיד — לפני כל קריאת-מארח שעלולה ליפול. אחרת ריצה
|
||||
// שנפלה באמצע (או `!companies.length`) הייתה מדווחת scanned=0
|
||||
// ומאבדת בדיוק את האבחנה שה-issue הזה נועד לתת.
|
||||
counters.scanned = cases.length;
|
||||
const companies = await ctx.companies.list();
|
||||
if (!companies.length) return;
|
||||
const statusModel = await api.getStatusModel();
|
||||
|
||||
// מעבר אחד על ה-issues של כל חברה: state.get אחד ל-issue (לא לכל
|
||||
// צירוף תיק×issue), וקיבוץ למפה case_number → מועמדים. הקיבוץ נדרש
|
||||
// כדי לבחור "השורש היחיד". סורק את **כל** החברות (לא רק הראשונה) —
|
||||
// legal-ai issue #604.
|
||||
const byCase = new Map<string, SyncCandidate[]>();
|
||||
for (const company of companies) {
|
||||
const issues = await ctx.issues.list({ companyId: company.id });
|
||||
for (const issue of issues) {
|
||||
const linkedCase = await ctx.state.get({
|
||||
scopeKind: "issue",
|
||||
scopeId: issue.id,
|
||||
stateKey: "legal-case-number",
|
||||
});
|
||||
if (typeof linkedCase !== "string" || !linkedCase) continue;
|
||||
const list = byCase.get(linkedCase) ?? [];
|
||||
list.push({
|
||||
id: issue.id,
|
||||
status: issue.status,
|
||||
parentId: issue.parentId,
|
||||
companyId: company.id,
|
||||
});
|
||||
byCase.set(linkedCase, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const legalCase of cases) {
|
||||
const targetStatus = resolveIssueStatus(
|
||||
statusModel.statuses,
|
||||
legalCase.status,
|
||||
);
|
||||
if (targetStatus === null) {
|
||||
ctx.logger.warn(
|
||||
"sync-case-status: unknown case status — not in /api/status-model (case_status_model.py)",
|
||||
{
|
||||
caseNumber: legalCase.case_number,
|
||||
status: legalCase.status,
|
||||
},
|
||||
for (const legalCase of cases) {
|
||||
const targetStatus = resolveIssueStatus(
|
||||
statusModel.statuses,
|
||||
legalCase.status,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (targetStatus === null) {
|
||||
recordDecline(counters, "unknown_status");
|
||||
ctx.logger.warn(
|
||||
"sync-case-status: unknown case status — not in /api/status-model (case_status_model.py)",
|
||||
{
|
||||
caseNumber: legalCase.case_number,
|
||||
status: legalCase.status,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidates = byCase.get(legalCase.case_number) ?? [];
|
||||
const { target, reason, writableRoots } =
|
||||
pickSyncTargetIssue(candidates);
|
||||
const candidates = byCase.get(legalCase.case_number) ?? [];
|
||||
const { target, reason, writableRoots } =
|
||||
pickSyncTargetIssue(candidates);
|
||||
|
||||
if (!target) {
|
||||
// אין בליעה שקטה: מדווח למה לא נכתב כלום.
|
||||
ctx.logger.info("sync-case-status: no sync target", {
|
||||
if (!target) {
|
||||
// reason כאן הוא SyncTargetReason שאינו "ok" (target===null) —
|
||||
// תת-קבוצה מובטחת-מהדר של SyncDeclineReason
|
||||
// (ראה sync-run-summary.ts: SyncTargetDeclineReason).
|
||||
recordDecline(counters, reason as Exclude<typeof reason, "ok">);
|
||||
// אין בליעה שקטה: מדווח למה לא נכתב כלום.
|
||||
ctx.logger.info("sync-case-status: no sync target", {
|
||||
caseNumber: legalCase.case_number,
|
||||
reason,
|
||||
linked: candidates.length,
|
||||
writableRoots,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
counters.matched++;
|
||||
|
||||
if (target.status === targetStatus) {
|
||||
recordDecline(counters, "already_matching");
|
||||
continue;
|
||||
}
|
||||
|
||||
const label =
|
||||
labelFor(statusModel.statuses, legalCase.status) ??
|
||||
legalCase.status;
|
||||
|
||||
await ctx.issues.update(
|
||||
target.id,
|
||||
{ status: targetStatus },
|
||||
target.companyId,
|
||||
);
|
||||
await ctx.issues.createComment(
|
||||
target.id,
|
||||
`📋 ${label}`,
|
||||
target.companyId,
|
||||
);
|
||||
counters.written++;
|
||||
ctx.logger.info("Synced issue status", {
|
||||
issueId: target.id,
|
||||
caseNumber: legalCase.case_number,
|
||||
reason,
|
||||
linked: candidates.length,
|
||||
writableRoots,
|
||||
newStatus: targetStatus,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.status === targetStatus) continue;
|
||||
|
||||
const label =
|
||||
labelFor(statusModel.statuses, legalCase.status) ??
|
||||
legalCase.status;
|
||||
|
||||
await ctx.issues.update(
|
||||
target.id,
|
||||
{ status: targetStatus },
|
||||
target.companyId,
|
||||
);
|
||||
await ctx.issues.createComment(
|
||||
target.id,
|
||||
`📋 ${label}`,
|
||||
target.companyId,
|
||||
);
|
||||
ctx.logger.info("Synced issue status", {
|
||||
issueId: target.id,
|
||||
caseNumber: legalCase.case_number,
|
||||
newStatus: targetStatus,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.logger.info("Case status sync completed", {
|
||||
casesChecked: cases.length,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
failure = err;
|
||||
throw err; // חובה — אחרת ה-run יירשם succeeded (זה בדיוק הבאג של #637).
|
||||
} finally {
|
||||
await emitSyncRunSummary(counters, failure);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.jobs.register("stale-case-reminder", async (_job) => {
|
||||
|
||||
Reference in New Issue
Block a user