chore(int9): שער-סטטי ל-INV-INT9 — issues.update({status}) רק במסלול sync-case-status (legal-ai #618)

ל-`issue.status` כותב לגיטימי אחד בפלאגין: הג'וב `sync-case-status`
(`ctx.jobs.register("sync-case-status", …)` ב-`src/worker.ts`). עד היום
האכיפה הייתה code-review בלבד — וזה בדיוק מה שאפשר את הבאג של legal-ai
‏#446 (flip-flop `done → in_progress`), כותב שני שנכנס בלי שאיש שם לב.

`scripts/int9-guard.mjs` — Node ESM חסר-תלויות. טוקנייזר תו-תו
(מחרוזות/תבניות עם `${}` מקונן/הערות/רגקס-ליטרלים) בונה מסכת "קוד רגיל",
ומעליה: כל `jobs.register("sync-case-status", …)` מגדיר **טווח-תווים מותר**
(איזון-סוגריים), וכל `issues.update(…)` שרשימת-הארגומנטים שלו מכילה את
המזהה `status` חייב לשבת בתוכו.

הגרעיניות היא טווח-קוד ולא רשימת-קבצים (הדפוס של `leak_guard.py` ב-legal-ai)
במכוון: הקריאה המותרת היחידה וכל קריאה עתידית אסורה יושבות באותו
`worker.ts`. הטווח נגזר מהמבנה האמיתי, ולכן שורד מעבר של הג'וב לקובץ אחר —
ושינוי שם-הג'וב מפיל את השער בקול במקום לפתוח חור שקט.

חריגה מכוונת: `// noqa: INT9 — <נימוק>`. `noqa: INT9` בלי נימוק הוא הפרה
בעצמו — אין השתקה שקטה. כשל-פענוח (סוגר שלא נסגר) מפיל את השער ולא נבלע.

`--self-test` מריץ 5 פיקסצ'רים (`scripts/fixtures/int9/`, מחוץ ל-`src/`
בכוונה) ומאמת ספירת-הפרות מדויקת — הוכחה שהשער נושך.

**ללא חיווט-CI.** ה-runner היחיד על nautilus רשום בהיקף-repo ל-`legal-ai`
בלבד (`action_runner.repo_id = 6`), ולכן job של הריפו הזה לא נאסף לעולם
(נמדד: ריצה 3200 / job 3250 תקוע ב-`queued`, `started_at: 1970-01-01`).
קובץ ה-workflow יושב ב-PR נפרד וחסום, כדי שלא ייכנס ל-`main` שער שמייצר
check תקוע-לנצח בכל PR עתידי. עד אז: `npm run int9:guard` /
`npm run int9:guard:self-test`.

מגבלה מוצהרת: תיל-מעידה סטטי, לא הוכחה — `issues.update(id, patch)` שבו
אובייקט-העדכון נבנה במקום אחר אינו נתפס.

Refs ezer-mishpati/legal-ai#618

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 04:07:19 +03:00
parent 314745d7a7
commit b24d816cc0
7 changed files with 614 additions and 1 deletions

View File

@@ -0,0 +1,16 @@
// פיקסצ'ר ל-scripts/int9-guard.mjs --self-test. לא קוד-ריצה.
//
// קריאה ל-issues.update({status}) בתוך jobs.register("sync-case-status", …)
// — המסלול המותר היחיד. כולל תבנית עם סוגריים לא-מאוזנים בתוך תבנית-מחרוזת
// והערה, כדי לבחון בפועל את הטוקנייזר (מחרוזת/הערה לא נספרות ב-matchParen).
export function register(ctx: any, api: any) {
ctx.jobs.register("sync-case-status", async (job: unknown) => {
// סוגר בודד בהערה — לא אמור לבלבל את חישוב-הטווח: )
const label = `📋 (${api.name}`;
const target = await api.pickTarget();
await ctx.issues.update(target.id, { status: label }, target.companyId);
});
}

View File

@@ -0,0 +1,15 @@
// פיקסצ'ר ל-scripts/int9-guard.mjs --self-test. לא קוד-ריצה.
//
// שתי קריאות ל-issues.update שאינן נוגעות ב-status כלל — לא הפרה.
// השנייה בודקת שגבול-מילה אמיתי: "statusLabel" אינו תואם ל-/\bstatus\b/
// (אין גבול-מילה בין ה-s של status ל-L של Label).
export async function renameOnly(ctx: any, target: any, statusLabel: string) {
await ctx.issues.update(target.id, { title: "x" }, target.companyId);
await ctx.issues.update(
target.id,
{ description: statusLabel },
target.companyId,
);
}

View File

@@ -0,0 +1,9 @@
// פיקסצ'ר ל-scripts/int9-guard.mjs --self-test. לא קוד-ריצה.
//
// קריאה ל-issues.update({status}) מחוץ ל-sync-case-status, עם noqa בלי נימוק —
// עדיין הפרה (AC3: חריגה חייבת להיות מנומקת בכתב).
export async function sloppyBackfill(ctx: any, target: any) {
// noqa: INT9
await ctx.issues.update(target.id, { status: "done" }, target.companyId);
}

View File

@@ -0,0 +1,9 @@
// פיקסצ'ר ל-scripts/int9-guard.mjs --self-test. לא קוד-ריצה.
//
// קריאה ל-issues.update({status}) מחוץ ל-sync-case-status, עם חריגה מנומקת —
// מותר.
export async function migrationBackfill(ctx: any, target: any) {
// noqa: INT9 — מיגרציה חד-פעמית לתיקון סטטוסים תקועים מלפני #618, מאושרת ע"י דפנה
await ctx.issues.update(target.id, { status: "done" }, target.companyId);
}

View File

@@ -0,0 +1,16 @@
// פיקסצ'ר ל-scripts/int9-guard.mjs --self-test. לא קוד-ריצה.
//
// קריאה ל-issues.update({status}) מחוץ לכל jobs.register("sync-case-status", …)
// — זו בדיוק ההפרה שהשער אמור לתפוס.
export async function badHandler(ctx: any, target: any) {
await ctx.jobs.register("stale-case-reminder", async (_job: unknown) => {
ctx.logger.info("stale-case-reminder: starting");
});
await ctx.issues.update(
target.id,
{ status: "in_progress" },
target.companyId,
);
}