fix(sync): סנכרון-הסטטוס יכתוב רק על issue-שורש פתוח, לא על sub-task שנסגר (#446)
הג'וב sync-case-status כתב על כל issue מקושר-לתיק שסטטוסו שונה מהיעד — השומר היחיד היה 'issue.status !== targetStatus'. לכן הוא החזיר sub-tasks שנסגרו (done) ל-in_progress, כפי שתועד ב-CMPA-140 / תיק 8125-09-24. בחירת-היעד חולצה ל-src/sync-target.ts: נבחר שורש יחיד (parentId === null) שאינו done/cancelled ואינו in_review/blocked; אפס או יותר-מאחד → לא נכתב דבר, ונרשם לוג עם הסיבה. שני הקבועים מובחנים בכוונה — CLOSED_ISSUE_STATUSES הוא ההגדרה היחידה של 'סגור' (מראה של legal-ai/web/paperclip_client.py:561), ו-NON_WRITABLE_STATUSES הוא 'מצב בבעלות Paperclip/בהמתנה-לאדם': כתיבת in_progress על issue ב-in_review מזמינה auto-block תוך דקה (legal-ai/docs/paperclip-quirks.md §3) וגונבת אותו מתור-הביקורת של היו"ר. הלולאה הכפולה הוחלפה במעבר אחד + קיבוץ למפת case_number→מועמדים; הקיבוץ נדרש כדי לבחור 'השורש היחיד', וכתוצר-לוואי O(NxM)→O(M) קריאות state.get. מבחן-רגרסיה (node --test, ללא תלויות חדשות) משחזר את 11 ה-issues שנמדדו לתיק 8125-09-24 ומוכיח את הדלתא: הכלל הישן היה כותב על CMPA-140, החדש לא. בבעלות ריפו זה בהחלטת פאנל — העברת הלוגיקה ל-legal-ai נפסלה כי היא מוסיפה UPDATE ישיר ל-DB של Paperclip, בהפרת INV-INT4 (GAP-24/25).
This commit is contained in:
151
src/worker.ts
151
src/worker.ts
@@ -4,6 +4,7 @@ import type {
|
||||
} from "@paperclipai/plugin-sdk";
|
||||
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
|
||||
import { LegalApi } from "./legal-api.js";
|
||||
import { pickSyncTargetIssue, type SyncCandidate } from "./sync-target.js";
|
||||
|
||||
// Hoisted so onWebhook can access the context after setup() completes.
|
||||
let pluginCtx: PluginContext | null = null;
|
||||
@@ -18,6 +19,43 @@ const CEO_AGENT_IDS: Record<string, string> = {
|
||||
|
||||
const DEFAULT_LEGAL_API_BASE = "http://localhost:8085";
|
||||
|
||||
// Map 13 legal-ai case statuses to Paperclip issue status (used by the
|
||||
// `sync-case-status` job).
|
||||
const CASE_STATUS_TO_ISSUE_STATUS: Record<
|
||||
string,
|
||||
"todo" | "in_progress" | "done"
|
||||
> = {
|
||||
new: "todo",
|
||||
uploading: "todo",
|
||||
processing: "in_progress",
|
||||
documents_ready: "in_progress",
|
||||
outcome_set: "in_progress",
|
||||
brainstorming: "in_progress",
|
||||
direction_approved: "in_progress",
|
||||
drafting: "in_progress",
|
||||
qa_review: "in_progress",
|
||||
drafted: "in_progress",
|
||||
exported: "in_progress",
|
||||
reviewed: "in_progress",
|
||||
final: "done",
|
||||
};
|
||||
|
||||
const CASE_STATUS_LABELS: Record<string, string> = {
|
||||
new: "תיק חדש",
|
||||
uploading: "העלאת מסמכים",
|
||||
processing: "עיבוד מסמכים",
|
||||
documents_ready: "מסמכים מוכנים — הזן תוצאה",
|
||||
outcome_set: "תוצאה הוזנה — נדרש סיעור מוחות",
|
||||
brainstorming: "גיבוש כיוון בתהליך",
|
||||
direction_approved: "כיוון אושר — מוכן לכתיבה",
|
||||
drafting: "כתיבת החלטה בתהליך",
|
||||
qa_review: "בדיקת איכות",
|
||||
drafted: "טיוטה מוכנה — בדוק ושלח לדפנה",
|
||||
exported: "DOCX נוצר — ממתין לדפנה",
|
||||
reviewed: "דפנה הגיהה — העלה גרסה סופית",
|
||||
final: "גרסה סופית — לולאת למידה",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve `legalApiBaseUrl` from company-scoped plugin config.
|
||||
*
|
||||
@@ -845,71 +883,60 @@ const plugin = definePlugin({
|
||||
const companyId = companies[0].id;
|
||||
const issues = await ctx.issues.list({ companyId });
|
||||
|
||||
// מעבר אחד על ה-issues: state.get אחד ל-issue (לא לכל צירוף תיק×issue),
|
||||
// וקיבוץ למפה case_number → מועמדים. הקיבוץ נדרש כדי לבחור "השורש היחיד".
|
||||
const byCase = new Map<string, SyncCandidate[]>();
|
||||
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,
|
||||
});
|
||||
byCase.set(linkedCase, list);
|
||||
}
|
||||
|
||||
for (const legalCase of cases) {
|
||||
for (const issue of issues) {
|
||||
const linkedCase = await ctx.state.get({
|
||||
scopeKind: "issue",
|
||||
scopeId: issue.id,
|
||||
stateKey: "legal-case-number",
|
||||
const targetStatus = CASE_STATUS_TO_ISSUE_STATUS[legalCase.status];
|
||||
if (!targetStatus) continue;
|
||||
|
||||
const candidates = byCase.get(legalCase.case_number) ?? [];
|
||||
const { target, reason, writableRoots } =
|
||||
pickSyncTargetIssue(candidates);
|
||||
|
||||
if (!target) {
|
||||
// אין בליעה שקטה: מדווח למה לא נכתב כלום.
|
||||
ctx.logger.info("sync-case-status: no sync target", {
|
||||
caseNumber: legalCase.case_number,
|
||||
reason,
|
||||
linked: candidates.length,
|
||||
writableRoots,
|
||||
});
|
||||
|
||||
if (linkedCase === legalCase.case_number) {
|
||||
// Map 13 legal-ai statuses to Paperclip issue status
|
||||
const statusMap: Record<string, "todo" | "in_progress" | "done"> =
|
||||
{
|
||||
new: "todo",
|
||||
uploading: "todo",
|
||||
processing: "in_progress",
|
||||
documents_ready: "in_progress",
|
||||
outcome_set: "in_progress",
|
||||
brainstorming: "in_progress",
|
||||
direction_approved: "in_progress",
|
||||
drafting: "in_progress",
|
||||
qa_review: "in_progress",
|
||||
drafted: "in_progress",
|
||||
exported: "in_progress",
|
||||
reviewed: "in_progress",
|
||||
final: "done",
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
new: "תיק חדש",
|
||||
uploading: "העלאת מסמכים",
|
||||
processing: "עיבוד מסמכים",
|
||||
documents_ready: "מסמכים מוכנים — הזן תוצאה",
|
||||
outcome_set: "תוצאה הוזנה — נדרש סיעור מוחות",
|
||||
brainstorming: "גיבוש כיוון בתהליך",
|
||||
direction_approved: "כיוון אושר — מוכן לכתיבה",
|
||||
drafting: "כתיבת החלטה בתהליך",
|
||||
qa_review: "בדיקת איכות",
|
||||
drafted: "טיוטה מוכנה — בדוק ושלח לדפנה",
|
||||
exported: "DOCX נוצר — ממתין לדפנה",
|
||||
reviewed: "דפנה הגיהה — העלה גרסה סופית",
|
||||
final: "גרסה סופית — לולאת למידה",
|
||||
};
|
||||
|
||||
const targetStatus = statusMap[legalCase.status];
|
||||
const label = statusLabels[legalCase.status] || legalCase.status;
|
||||
|
||||
if (targetStatus && issue.status !== targetStatus) {
|
||||
await ctx.issues.update(
|
||||
issue.id,
|
||||
{ status: targetStatus },
|
||||
companyId,
|
||||
);
|
||||
await ctx.issues.createComment(
|
||||
issue.id,
|
||||
`📋 ${label}`,
|
||||
companyId,
|
||||
);
|
||||
ctx.logger.info("Synced issue status", {
|
||||
issueId: issue.id,
|
||||
caseNumber: legalCase.case_number,
|
||||
newStatus: targetStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.status === targetStatus) continue;
|
||||
|
||||
const label =
|
||||
CASE_STATUS_LABELS[legalCase.status] || legalCase.status;
|
||||
|
||||
await ctx.issues.update(
|
||||
target.id,
|
||||
{ status: targetStatus },
|
||||
companyId,
|
||||
);
|
||||
await ctx.issues.createComment(target.id, `📋 ${label}`, companyId);
|
||||
ctx.logger.info("Synced issue status", {
|
||||
issueId: target.id,
|
||||
caseNumber: legalCase.case_number,
|
||||
newStatus: targetStatus,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.logger.info("Case status sync completed", {
|
||||
|
||||
Reference in New Issue
Block a user