Compare commits
5 Commits
fix/446-sy
...
41305fbb36
| Author | SHA1 | Date | |
|---|---|---|---|
| 41305fbb36 | |||
| 511d5c6e34 | |||
| 10d6216bec | |||
| 0a134d0134 | |||
| 602b42ca5e |
@@ -36,6 +36,10 @@ export class LegalApi {
|
||||
return this.request("/api/cases");
|
||||
}
|
||||
|
||||
async getStatusModel(): Promise<StatusModel> {
|
||||
return this.request("/api/status-model");
|
||||
}
|
||||
|
||||
async getCase(caseNumber: string): Promise<CaseDetails> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/details`);
|
||||
}
|
||||
@@ -184,6 +188,21 @@ export interface CaseSummary {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface StatusModelEntry {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
phase: string;
|
||||
selectable: boolean;
|
||||
terminal: boolean;
|
||||
on_enter: string | null;
|
||||
}
|
||||
|
||||
export interface StatusModel {
|
||||
statuses: StatusModelEntry[];
|
||||
phases: Array<{ key: string; label: string }>;
|
||||
}
|
||||
|
||||
export interface CaseDetails {
|
||||
id: string;
|
||||
case_number: string;
|
||||
|
||||
@@ -5,7 +5,14 @@ import { test } from "node:test";
|
||||
// Excluded from tsc (tsconfig.json) — never bundled/emitted, run natively via
|
||||
// `node --test`, which requires the literal `.ts` extension (Node's ESM
|
||||
// resolver does not remap `.js` specifiers to `.ts` files at runtime).
|
||||
import { pickSyncTargetIssue, type SyncCandidate } from "./sync-target.ts";
|
||||
import type { StatusModelEntry } from "./legal-api.ts";
|
||||
import {
|
||||
isWritableStatus,
|
||||
labelFor,
|
||||
pickSyncTargetIssue,
|
||||
resolveIssueStatus,
|
||||
type SyncCandidate,
|
||||
} from "./sync-target.ts";
|
||||
|
||||
/**
|
||||
* הכלל הישן (worker.ts:894 לפני #446): כל issue מקושר שסטטוסו שונה מהיעד —
|
||||
@@ -25,17 +32,27 @@ test("רגרסיית 8125-09-24 / CMPA-140: 11 ה-issues הממשיים של ה
|
||||
// רשומת legal-case-number ב-plugin_state, ולכן הוא לא מגיע כמועמד כלל —
|
||||
// לילד יש הורה שאינו מקושר.
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "CMPA-116", status: "cancelled", parentId: null },
|
||||
{ id: "CMPA-84", status: "cancelled", parentId: null },
|
||||
{ id: "CMPA-86", status: "cancelled", parentId: null },
|
||||
{ id: "CMPA-140", status: "done", parentId: "CMPA-139" }, // זה שנדרס בפועל
|
||||
{ id: "CMPA-141", status: "cancelled", parentId: "CMPA-139" },
|
||||
{ id: "CMPA-85", status: "done", parentId: "CMPA-84" },
|
||||
{ id: "CMPA-146", status: "done", parentId: "CMPA-84" },
|
||||
{ id: "CMPA-147", status: "done", parentId: "CMPA-84" },
|
||||
{ id: "CMPA-87", status: "done", parentId: "CMPA-86" },
|
||||
{ id: "CMPA-88", status: "done", parentId: "CMPA-86" },
|
||||
{ id: "CMPA-89", status: "cancelled", parentId: "CMPA-86" },
|
||||
{ id: "CMPA-116", status: "cancelled", parentId: null, companyId: "c1" },
|
||||
{ id: "CMPA-84", status: "cancelled", parentId: null, companyId: "c1" },
|
||||
{ id: "CMPA-86", status: "cancelled", parentId: null, companyId: "c1" },
|
||||
{ id: "CMPA-140", status: "done", parentId: "CMPA-139", companyId: "c1" }, // זה שנדרס בפועל
|
||||
{
|
||||
id: "CMPA-141",
|
||||
status: "cancelled",
|
||||
parentId: "CMPA-139",
|
||||
companyId: "c1",
|
||||
},
|
||||
{ id: "CMPA-85", status: "done", parentId: "CMPA-84", companyId: "c1" },
|
||||
{ id: "CMPA-146", status: "done", parentId: "CMPA-84", companyId: "c1" },
|
||||
{ id: "CMPA-147", status: "done", parentId: "CMPA-84", companyId: "c1" },
|
||||
{ id: "CMPA-87", status: "done", parentId: "CMPA-86", companyId: "c1" },
|
||||
{ id: "CMPA-88", status: "done", parentId: "CMPA-86", companyId: "c1" },
|
||||
{
|
||||
id: "CMPA-89",
|
||||
status: "cancelled",
|
||||
parentId: "CMPA-86",
|
||||
companyId: "c1",
|
||||
},
|
||||
];
|
||||
|
||||
// סטטוס-התיק היה drafted → in_progress (CASE_STATUS_TO_ISSUE_STATUS ב-worker.ts).
|
||||
@@ -53,7 +70,7 @@ test("רגרסיית 8125-09-24 / CMPA-140: 11 ה-issues הממשיים של ה
|
||||
|
||||
test("sub-task יחיד ב-done ומקושר: sub-task לעולם לא נבחר", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "sub-1", status: "done", parentId: "some-root" },
|
||||
{ id: "sub-1", status: "done", parentId: "some-root", companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -64,8 +81,8 @@ test("sub-task יחיד ב-done ומקושר: sub-task לעולם לא נבחר"
|
||||
|
||||
test("sub-task יחיד פתוח (in_progress) + שורש cancelled: פתוח אינו מספיק, חייב שורש", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "cancelled", parentId: null },
|
||||
{ id: "sub-1", status: "in_progress", parentId: "root-1" },
|
||||
{ id: "root-1", status: "cancelled", parentId: null, companyId: "c1" },
|
||||
{ id: "sub-1", status: "in_progress", parentId: "root-1", companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -76,7 +93,7 @@ test("sub-task יחיד פתוח (in_progress) + שורש cancelled: פתוח א
|
||||
|
||||
test("שורש יחיד ב-todo: נבחר", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "todo", parentId: null },
|
||||
{ id: "root-1", status: "todo", parentId: null, companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -86,20 +103,23 @@ test("שורש יחיד ב-todo: נבחר", () => {
|
||||
assert.equal(result.writableRoots, 1);
|
||||
});
|
||||
|
||||
test("שורש יחיד ב-in_review: לא נכתב (הגנת auto-block)", () => {
|
||||
// הכרעת #626 (2026-08-26): in_review כבר לא ב-NON_WRITABLE_STATUSES —
|
||||
// שורש יחיד ב-in_review עכשיו נבחר כיעד-כתיבה, במקום להיחסם.
|
||||
test("שורש יחיד ב-in_review: נבחר (הכרעת #626 — 2026-08-26)", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "in_review", parentId: null },
|
||||
{ id: "root-1", status: "in_review", parentId: null, companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
|
||||
assert.equal(result.target, null);
|
||||
assert.equal(result.reason, "no_writable_root");
|
||||
assert.equal(result.target?.id, "root-1");
|
||||
assert.equal(result.reason, "ok");
|
||||
assert.equal(result.writableRoots, 1);
|
||||
});
|
||||
|
||||
test("שורש יחיד ב-blocked: לא נכתב", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "blocked", parentId: null },
|
||||
{ id: "root-1", status: "blocked", parentId: null, companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -108,10 +128,18 @@ test("שורש יחיד ב-blocked: לא נכתב", () => {
|
||||
assert.equal(result.reason, "no_writable_root");
|
||||
});
|
||||
|
||||
test("isWritableStatus: הגבול המלא בין בר-כתיבה ללא-בר-כתיבה", () => {
|
||||
assert.equal(isWritableStatus("in_review"), true);
|
||||
assert.equal(isWritableStatus("blocked"), false);
|
||||
assert.equal(isWritableStatus("done"), false);
|
||||
assert.equal(isWritableStatus("cancelled"), false);
|
||||
assert.equal(isWritableStatus("todo"), true);
|
||||
});
|
||||
|
||||
test("שני שורשים ברי-כתיבה: ambiguous, לא נכתב", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "todo", parentId: null },
|
||||
{ id: "root-2", status: "in_progress", parentId: null },
|
||||
{ id: "root-1", status: "todo", parentId: null, companyId: "c1" },
|
||||
{ id: "root-2", status: "in_progress", parentId: null, companyId: "c1" },
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -123,8 +151,13 @@ test("שני שורשים ברי-כתיבה: ambiguous, לא נכתב", () => {
|
||||
|
||||
test("שורש todo + ילד in_progress: השורש נבחר", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{ id: "root-1", status: "todo", parentId: null },
|
||||
{ id: "child-1", status: "in_progress", parentId: "root-1" },
|
||||
{ id: "root-1", status: "todo", parentId: null, companyId: "c1" },
|
||||
{
|
||||
id: "child-1",
|
||||
status: "in_progress",
|
||||
parentId: "root-1",
|
||||
companyId: "c1",
|
||||
},
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
@@ -132,3 +165,174 @@ test("שורש todo + ילד in_progress: השורש נבחר", () => {
|
||||
assert.equal(result.target?.id, "root-1");
|
||||
assert.equal(result.reason, "ok");
|
||||
});
|
||||
|
||||
/**
|
||||
* פיקסצ'ר ריאלי של `/api/status-model` (case_status_model.py) — הסדר קובע
|
||||
* מי הוא "todo" (האינדקס הראשון), ו-`terminal: true` קובע מי "done".
|
||||
*/
|
||||
const STATUS_MODEL: StatusModelEntry[] = [
|
||||
{
|
||||
key: "new",
|
||||
label: "חדש",
|
||||
description: "תיק נפתח, טרם הועלו מסמכים",
|
||||
phase: "intake",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "processing",
|
||||
label: "בעיבוד",
|
||||
description: "מסמכים בעיבוד",
|
||||
phase: "intake",
|
||||
selectable: false,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "documents_ready",
|
||||
label: "מסמכים מוכנים",
|
||||
description: "העיבוד הושלם",
|
||||
phase: "intake",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "analyst_verified",
|
||||
label: 'אומת ע"י אנליסט',
|
||||
description: "אנליסט אימת את התוכן",
|
||||
phase: "analysis",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "research_complete",
|
||||
label: "מחקר הושלם",
|
||||
description: "מחקר תקדימים הושלם",
|
||||
phase: "analysis",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "outcome_set",
|
||||
label: "תוצאה נקבעה",
|
||||
description: "תוצאה הוזנה",
|
||||
phase: "analysis",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "direction_approved",
|
||||
label: "כיוון אושר",
|
||||
description: "כיוון הכתיבה אושר",
|
||||
phase: "drafting",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "qa_review",
|
||||
label: "בדיקת איכות",
|
||||
description: "בבדיקת איכות",
|
||||
phase: "drafting",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "drafted",
|
||||
label: "טיוטה מוכנה",
|
||||
description: "טיוטה נכתבה",
|
||||
phase: "drafting",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "exported",
|
||||
label: "יוצא ל-DOCX",
|
||||
description: "יוצא כקובץ DOCX",
|
||||
phase: "review",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "reviewed",
|
||||
label: 'נבדק ע"י דפנה',
|
||||
description: "דפנה הגיהה",
|
||||
phase: "review",
|
||||
selectable: true,
|
||||
terminal: false,
|
||||
on_enter: null,
|
||||
},
|
||||
{
|
||||
key: "final",
|
||||
label: "סופי",
|
||||
description: "גרסה סופית",
|
||||
phase: "final",
|
||||
selectable: true,
|
||||
terminal: true,
|
||||
on_enter: null,
|
||||
},
|
||||
];
|
||||
|
||||
test("resolveIssueStatus: הסטטוס הראשון ברשימה (new) → todo", () => {
|
||||
assert.equal(resolveIssueStatus(STATUS_MODEL, "new"), "todo");
|
||||
});
|
||||
|
||||
test("resolveIssueStatus: סטטוס terminal (final) → done", () => {
|
||||
assert.equal(resolveIssueStatus(STATUS_MODEL, "final"), "done");
|
||||
});
|
||||
|
||||
test("resolveIssueStatus: analyst_verified (חסר במפה הישנה) → in_progress", () => {
|
||||
assert.equal(
|
||||
resolveIssueStatus(STATUS_MODEL, "analyst_verified"),
|
||||
"in_progress",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveIssueStatus: research_complete → in_progress", () => {
|
||||
assert.equal(
|
||||
resolveIssueStatus(STATUS_MODEL, "research_complete"),
|
||||
"in_progress",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveIssueStatus: סטטוס לא-מוכר → null (לא בליעה שקטה)", () => {
|
||||
assert.equal(
|
||||
resolveIssueStatus(STATUS_MODEL, "totally_unknown_status"),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("labelFor: מחזיר את התווית מהפיקסצ'ר, ו-null לסטטוס לא-מוכר", () => {
|
||||
assert.equal(labelFor(STATUS_MODEL, "new"), "חדש");
|
||||
assert.equal(labelFor(STATUS_MODEL, "totally_unknown_status"), null);
|
||||
});
|
||||
|
||||
test("רגרסיה: companyId של המועמד-הנבחר שורד את pickSyncTargetIssue (לא companyId חיצוני קבוע)", () => {
|
||||
const candidates: SyncCandidate[] = [
|
||||
{
|
||||
id: "issue-a",
|
||||
status: "cancelled",
|
||||
parentId: null,
|
||||
companyId: "company-a",
|
||||
},
|
||||
{
|
||||
id: "issue-b",
|
||||
status: "todo",
|
||||
parentId: null,
|
||||
companyId: "company-b",
|
||||
},
|
||||
];
|
||||
|
||||
const result = pickSyncTargetIssue(candidates);
|
||||
|
||||
assert.equal(result.target?.id, "issue-b");
|
||||
assert.equal(result.target?.companyId, "company-b");
|
||||
});
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
*
|
||||
* המודול הזה טהור בכוונה — בלי import מה-SDK ובלי side effects — כדי שאפשר
|
||||
* יהיה לייבא אותו ישירות בטסט בלי להריץ את `runWorker(plugin, import.meta.url)`
|
||||
* שקורה בזמן import של worker.ts.
|
||||
* שקורה בזמן import של worker.ts. ה-import היחיד למטה הוא type-only (`import
|
||||
* type`), כך שהוא לא נדרס בזמן ריצה ולא שובר את הטוהר הזה.
|
||||
*/
|
||||
|
||||
import type { StatusModelEntry } from "./legal-api.ts";
|
||||
|
||||
/** "סגור" — ההגדרה היחידה. מראה של web/paperclip_client.py:561 ב-legal-ai. */
|
||||
export const CLOSED_ISSUE_STATUSES: ReadonlySet<string> = new Set([
|
||||
"done",
|
||||
@@ -17,22 +20,28 @@ export const CLOSED_ISSUE_STATUSES: ReadonlySet<string> = new Set([
|
||||
]);
|
||||
|
||||
/**
|
||||
* מצב בבעלות Paperclip / בהמתנה-לאדם — **לא** הגדרה שנייה של "סגור".
|
||||
* `in_review` הוא מצב-ההמתנה-ליו"ר המכוון: CEO שמשאיר issue ב-`in_progress`
|
||||
* מקבל auto-block מ-Paperclip תוך דקה, ולכן הוא מעביר ל-`in_review`
|
||||
* (legal-ai/docs/paperclip-quirks.md §3). כתיבת-סטטוס אוטומטית עליו גונבת את
|
||||
* ה-issue מתור-הביקורת של היו"ר ומזמינה קרב-סטטוסים.
|
||||
* `blocked` — כתיבה עליו מסתירה חוסם קיים.
|
||||
* מצב בבעלות Paperclip — **לא** הגדרה שנייה של "סגור". `blocked` — כתיבה
|
||||
* עליו מסתירה חוסם קיים.
|
||||
*
|
||||
* `in_review` **הוסר מהסט** בהכרעת חיים מ-2026-08-26 (legal-ai issue #626):
|
||||
* נשאל במפורש אם השחרור נקודתי או שיטתי, והשיב "התכוונתי לשיטתי".
|
||||
*
|
||||
* המחיר, בכנות: `in_review` הוא מצב-ההמתנה-ליו"ר המכוון — CEO שמשאיר issue
|
||||
* ב-`in_progress` מקבל auto-block מ-Paperclip תוך דקה, ולכן מעביר אותו
|
||||
* ל-`in_review` כדי לחמוק מזה (legal-ai/docs/paperclip-quirks.md §3). מעתה
|
||||
* issue שממתין לביקורת היו"ר עשוי להידרס בחזרה ל-`in_progress` בריצת-
|
||||
* הסנכרון הבאה (כל 15 דק') ולרדת מתור-הביקורת. זה מכוון ומתועד, לא תקלה.
|
||||
*
|
||||
* מסלול-חזרה: להחזיר `"in_review"` לסט למטה + build + התקנה +
|
||||
* `pm2 restart paperclip`.
|
||||
*/
|
||||
export const NON_WRITABLE_STATUSES: ReadonlySet<string> = new Set([
|
||||
"in_review",
|
||||
"blocked",
|
||||
]);
|
||||
export const NON_WRITABLE_STATUSES: ReadonlySet<string> = new Set(["blocked"]);
|
||||
|
||||
export interface SyncCandidate {
|
||||
id: string;
|
||||
status: string;
|
||||
parentId: string | null;
|
||||
companyId: string;
|
||||
}
|
||||
|
||||
export type SyncTargetReason =
|
||||
@@ -77,3 +86,32 @@ export function pickSyncTargetIssue(
|
||||
writableRoots: roots.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the coarse Paperclip issue status for a legal-ai case status, using
|
||||
* only generic fields already returned by legal-ai's `/api/status-model`
|
||||
* (the canonical `case_status_model.py` registry) — never a hardcoded list of
|
||||
* case-status keys. This is what makes a new case status "just work" without
|
||||
* editing this plugin: `terminal: true` → done; the very first status in the
|
||||
* ordered list → todo (nothing has started yet); everything else → in_progress.
|
||||
* Returns `null` when `caseStatus` is not present in `statuses` at all (a
|
||||
* genuinely unknown status — the caller must log this loudly, never swallow
|
||||
* it silently; see legal-ai issue #604).
|
||||
*/
|
||||
export function resolveIssueStatus(
|
||||
statuses: readonly StatusModelEntry[],
|
||||
caseStatus: string,
|
||||
): "todo" | "in_progress" | "done" | null {
|
||||
const index = statuses.findIndex((s) => s.key === caseStatus);
|
||||
if (index === -1) return null;
|
||||
if (statuses[index].terminal) return "done";
|
||||
return index === 0 ? "todo" : "in_progress";
|
||||
}
|
||||
|
||||
/** Hebrew label for a case status from the canonical model, or null if unknown. */
|
||||
export function labelFor(
|
||||
statuses: readonly StatusModelEntry[],
|
||||
caseStatus: string,
|
||||
): string | null {
|
||||
return statuses.find((s) => s.key === caseStatus)?.label ?? null;
|
||||
}
|
||||
|
||||
113
src/worker.ts
113
src/worker.ts
@@ -4,7 +4,12 @@ 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";
|
||||
import {
|
||||
labelFor,
|
||||
pickSyncTargetIssue,
|
||||
resolveIssueStatus,
|
||||
type SyncCandidate,
|
||||
} from "./sync-target.js";
|
||||
|
||||
// Hoisted so onWebhook can access the context after setup() completes.
|
||||
let pluginCtx: PluginContext | null = null;
|
||||
@@ -19,43 +24,6 @@ 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.
|
||||
*
|
||||
@@ -879,32 +847,48 @@ const plugin = definePlugin({
|
||||
const cases = await api.listCases();
|
||||
const companies = await ctx.companies.list();
|
||||
if (!companies.length) return;
|
||||
const statusModel = await api.getStatusModel();
|
||||
|
||||
const companyId = companies[0].id;
|
||||
const issues = await ctx.issues.list({ companyId });
|
||||
|
||||
// מעבר אחד על ה-issues: state.get אחד ל-issue (לא לכל צירוף תיק×issue),
|
||||
// וקיבוץ למפה case_number → מועמדים. הקיבוץ נדרש כדי לבחור "השורש היחיד".
|
||||
// מעבר אחד על ה-issues של כל חברה: state.get אחד ל-issue (לא לכל
|
||||
// צירוף תיק×issue), וקיבוץ למפה case_number → מועמדים. הקיבוץ נדרש
|
||||
// כדי לבחור "השורש היחיד". סורק את **כל** החברות (לא רק הראשונה) —
|
||||
// legal-ai issue #604.
|
||||
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 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 = CASE_STATUS_TO_ISSUE_STATUS[legalCase.status];
|
||||
if (!targetStatus) continue;
|
||||
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,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidates = byCase.get(legalCase.case_number) ?? [];
|
||||
const { target, reason, writableRoots } =
|
||||
@@ -924,14 +908,19 @@ const plugin = definePlugin({
|
||||
if (target.status === targetStatus) continue;
|
||||
|
||||
const label =
|
||||
CASE_STATUS_LABELS[legalCase.status] || legalCase.status;
|
||||
labelFor(statusModel.statuses, legalCase.status) ??
|
||||
legalCase.status;
|
||||
|
||||
await ctx.issues.update(
|
||||
target.id,
|
||||
{ status: targetStatus },
|
||||
companyId,
|
||||
target.companyId,
|
||||
);
|
||||
await ctx.issues.createComment(
|
||||
target.id,
|
||||
`📋 ${label}`,
|
||||
target.companyId,
|
||||
);
|
||||
await ctx.issues.createComment(target.id, `📋 ${label}`, companyId);
|
||||
ctx.logger.info("Synced issue status", {
|
||||
issueId: target.id,
|
||||
caseNumber: legalCase.case_number,
|
||||
|
||||
Reference in New Issue
Block a user