הג'וב 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).
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
/**
|
||
* בחירת יעד-הסנכרון של הג'וב `sync-case-status` (issue #446).
|
||
*
|
||
* המקור לבאג: הג'וב הישן כתב על **כל** issue מקושר-לתיק, כולל sub-issues
|
||
* ו-issues בסטטוסים סגורים/בהמתנה-לאדם. זה החזיר CMPA-140 מ-`done` ל-`in_progress`
|
||
* (8125-09-24) — כי sub-task שהושלם עדיין נמנה כ"מקושר".
|
||
*
|
||
* המודול הזה טהור בכוונה — בלי import מה-SDK ובלי side effects — כדי שאפשר
|
||
* יהיה לייבא אותו ישירות בטסט בלי להריץ את `runWorker(plugin, import.meta.url)`
|
||
* שקורה בזמן import של worker.ts.
|
||
*/
|
||
|
||
/** "סגור" — ההגדרה היחידה. מראה של web/paperclip_client.py:561 ב-legal-ai. */
|
||
export const CLOSED_ISSUE_STATUSES: ReadonlySet<string> = new Set([
|
||
"done",
|
||
"cancelled",
|
||
]);
|
||
|
||
/**
|
||
* מצב בבעלות Paperclip / בהמתנה-לאדם — **לא** הגדרה שנייה של "סגור".
|
||
* `in_review` הוא מצב-ההמתנה-ליו"ר המכוון: CEO שמשאיר issue ב-`in_progress`
|
||
* מקבל auto-block מ-Paperclip תוך דקה, ולכן הוא מעביר ל-`in_review`
|
||
* (legal-ai/docs/paperclip-quirks.md §3). כתיבת-סטטוס אוטומטית עליו גונבת את
|
||
* ה-issue מתור-הביקורת של היו"ר ומזמינה קרב-סטטוסים.
|
||
* `blocked` — כתיבה עליו מסתירה חוסם קיים.
|
||
*/
|
||
export const NON_WRITABLE_STATUSES: ReadonlySet<string> = new Set([
|
||
"in_review",
|
||
"blocked",
|
||
]);
|
||
|
||
export interface SyncCandidate {
|
||
id: string;
|
||
status: string;
|
||
parentId: string | null;
|
||
}
|
||
|
||
export type SyncTargetReason =
|
||
| "ok"
|
||
| "no_linked_issues"
|
||
| "no_writable_root"
|
||
| "ambiguous_writable_roots";
|
||
|
||
export interface SyncTargetResult {
|
||
target: SyncCandidate | null;
|
||
reason: SyncTargetReason;
|
||
/** כמה שורשים ברי-כתיבה נמצאו — לצורך לוג. */
|
||
writableRoots: number;
|
||
}
|
||
|
||
export function isWritableStatus(status: string): boolean {
|
||
return (
|
||
!CLOSED_ISSUE_STATUSES.has(status) && !NON_WRITABLE_STATUSES.has(status)
|
||
);
|
||
}
|
||
|
||
export function pickSyncTargetIssue(
|
||
candidates: readonly SyncCandidate[],
|
||
): SyncTargetResult {
|
||
if (candidates.length === 0) {
|
||
return { target: null, reason: "no_linked_issues", writableRoots: 0 };
|
||
}
|
||
|
||
const roots = candidates.filter(
|
||
(c) => c.parentId === null && isWritableStatus(c.status),
|
||
);
|
||
|
||
if (roots.length === 1) {
|
||
return { target: roots[0], reason: "ok", writableRoots: 1 };
|
||
}
|
||
if (roots.length === 0) {
|
||
return { target: null, reason: "no_writable_root", writableRoots: 0 };
|
||
}
|
||
return {
|
||
target: null,
|
||
reason: "ambiguous_writable_roots",
|
||
writableRoots: roots.length,
|
||
};
|
||
}
|