feat(sync): מוני-ריצה מובחנים ל-sync-case-status — "אין מה לעשות" מובחן מ"שבור" (legal-ai #617) #12

Merged
chaim merged 1 commits from feat/617-sync-status-run-counters into main 2026-08-26 13:53:24 +00:00
Owner

סוגר את ezer-mishpati/legal-ai#617 ("‏sync-case-status: ‏13,872 ריצות ואי-אפשר להבדיל בין 'אין מה לעשות' ל'שבור'").
תכנון מלא + כל המדידות: ezer-mishpati/legal-ai#617 (comment)

מה זה עושה

הג'וב sync-case-status סיים עד כה בשורה אחת — casesChecked — שאינה מבחינה בין "אין מה לעשות", "נדחה בכוונה" ו**"נפל"**. מעתה כל ריצה פולטת:

sync-case-status run=ok scanned=42 matched=3 written=1 declined=39 [no_linked_issues=0 no_writable_root=12 ambiguous_writable_roots=0 unknown_status=0 already_matching=27]

הפליטה יושבת ב-finally וזורקת את השגיאה המקורית הלאה, כך שריצה שנפלה נשארת failed ב-plugin_job_runs וגם נושאת את המונים שהספיקה לצבור.

למה דווקא הערוצים האלה (‏AC3 — "לא לפלוט למקום מת")

נמדד מה בכלל צורך פלט של ג'וב:

ערוץ ממצא
plugin_job_runs completeRun בהצלחה מקבל רק {status,durationMs} — אין ערוץ-החזרה ל-handler. נשאר בינארי
plugin_job_runs.logs 0 מתוך 55,117 ריצות אינן [] — ערוץ מת
ctx.logger.* המארח תופס אותו כ-notification (plugin-worker-manager.js:289-317) ומחזיר לפני hostHandlers["log"] → לעולם לא מגיע ל-plugin_logs. רק stdout של pm2
ctx.metrics.write נכתב ל-plugin_logs ‏(level='metric'), והממשק מושך GET /plugins/:id/logs?limit=50 כל 30ש' ומרנדר פאנל "Recent Logs"
ctx.activity.log נדחה בג'וב מתוזמן (דורש companyIdInvocationScopeDenied)

לכן: ctx.logger.info (‏pm2) + ctx.metrics.write (‏plugin_logs) — שניהם משטחים קיימים. לא נוצרה טבלה, לא endpoint, ולא תור, ושורת-הסיכום הישנה הוחלפה ולא נוספה לצידה (G2).

⚠️ המונים בתוך מחרוזת-ההודעה ולא רק ב-meta, כי נמדד ששני המשטחים מתעלמים מ-meta: הפאנל מרנדר createdAt/level/message בלבד, ומ-pino נמדד ששדה {error:…} שהקוד מעביר נופל בפועל מהשורה.

הרשאה חדשה ב-manifest

נוספה "metrics.write" — הדרך היחידה לכתוב ל-plugin_logs. נבדק שזה בטוח: בדיקת-הסלמת-ההרשאות קיימת רק ב-upgradePlugin (‏plugin-loader.js:1026-1034), מסלול שאיננו משתמשים בו; מסלול-האתחול עובר ב-refreshPluginManifestFromPackage שמעדכן את ה-manifest בלי בדיקת-הסלמה. ‏metrics.write היא capability מוכרת (shared/dist/constants.js:958).
מסלול-חזרה: הסרת השורה → npm run buildpm2 restart paperclip.

Invariants

  • כלל-הנדסה §6 (אין בליעה שקטה) — "הצליח" שאינו מבחין בין הצלחה לאי-פעולה הוא בליעה שקטה. גם נתיב-הכשל של הפליטה עצמה אינו נבלע: metrics.write שנכשל מדווח ב-logger.error עם טקסט-השגיאה בתוך המחרוזת, ואינו מסווה את כשל-הריצה (הפונקציה לעולם אינה זורקת — היא נקראת מ-finally).
  • G9 (audit/תצפיתיות) — ריצה נעשית ניתנת-לשחזור-בדיעבד ב-SQL; זה מה שיאפשר את AC4 בעוד שבוע.
  • G2 — אין מסלול מקביל: plugin_logs הוא מאגר-הלוגים של המארח עם API ופאנל קיימים; ctx.metrics.write הוא הפרימיטיב שהמארח מייעד בדיוק לזה.
  • G1 — המונים נאגרים בנקודות-ההחלטה עצמן, לא משוחזרים בדיעבד מפרסור-לוגים.
  • ‏AC5 של ה-issuesrc/sync-target.ts לא נגע כלל (git diff ריק), ובכלל זה NON_WRITABLE_STATUSES (הכרעת-יו"ר, ‏#626).

אימות

npx tsc --noEmit          → פלט ריק
npx biome check src/      → Checked 13 files. No fixes applied.
node --test src/*.test.ts → tests 35 / pass 35 / fail 0   (29 קיימים + 6 חדשים)
git diff main -- src/sync-target.ts → ריק
git diff -w --stat        → manifest 4+ · worker 69+/5-   (שאר ה-diff הוא הזחה מעטיפת try/finally)

שני הטסטים המהותיים: ‏AC2 — שלוש ריצות (ריקה · דחתה-12 · נפלה), כולן written=0, מפיקות שלוש מחרוזות שונות. אינvariant — סימולציית-לולאה עם pickSyncTargetIssue/resolveIssueStatus האמיתיים מאמתת matched === written + already_matching ו-scanned === matched + סך-הדחיות-שאינן-already_matching.

אין Gitea Actions בריפו הזהpr-checks יחזיר "אין checks", וזה תקין ולא כשל.

פריסה ידנית (אין Coolify hook): git pull ב-/home/chaim/plugin-legal-ainpm run buildpm2 restart paperclip.

מה לא בתחום ה-PR הזה

הג'וב נופל כרגע בכל ריצה ב-resolveApiBase / ctx.issues.* שנדחים במנוע-ההרשאות — זהו #645 (p0-blocker נפרד). ה-PR הזה אינו נוגע בו, ולכן ריצה חיה תיראה run=failed scanned=0 … עד שייסגר. זו בדיוק ההבחנה שה-issue דרש: "נפלה" מובחנת מ"אין מה לעשות".

🤖 Generated with Claude Code

סוגר את **ezer-mishpati/legal-ai#617** ("‏sync-case-status: ‏13,872 ריצות ואי-אפשר להבדיל בין 'אין מה לעשות' ל'שבור'"). תכנון מלא + כל המדידות: https://gitea.nautilus.marcusgroup.org/ezer-mishpati/legal-ai/issues/617#issuecomment-5814 ## מה זה עושה הג'וב `sync-case-status` סיים עד כה בשורה אחת — `casesChecked` — שאינה מבחינה בין **"אין מה לעשות"**, **"נדחה בכוונה"** ו**"נפל"**. מעתה כל ריצה פולטת: ``` sync-case-status run=ok scanned=42 matched=3 written=1 declined=39 [no_linked_issues=0 no_writable_root=12 ambiguous_writable_roots=0 unknown_status=0 already_matching=27] ``` הפליטה יושבת ב-`finally` ו**זורקת את השגיאה המקורית הלאה**, כך שריצה שנפלה נשארת `failed` ב-`plugin_job_runs` *וגם* נושאת את המונים שהספיקה לצבור. ## למה דווקא הערוצים האלה (‏AC3 — "לא לפלוט למקום מת") נמדד מה בכלל צורך פלט של ג'וב: | ערוץ | ממצא | |---|---| | `plugin_job_runs` | ‏`completeRun` בהצלחה מקבל **רק** `{status,durationMs}` — אין ערוץ-החזרה ל-handler. נשאר בינארי | | `plugin_job_runs.logs` | ‏**0 מתוך 55,117** ריצות אינן `[]` — ערוץ מת | | `ctx.logger.*` | המארח תופס אותו כ-notification (`plugin-worker-manager.js:289-317`) ומחזיר **לפני** `hostHandlers["log"]` → לעולם לא מגיע ל-`plugin_logs`. רק stdout של pm2 | | **`ctx.metrics.write`** | ✅ נכתב ל-`plugin_logs` ‏(`level='metric'`), והממשק מושך `GET /plugins/:id/logs?limit=50` כל 30ש' ומרנדר פאנל "Recent Logs" | | `ctx.activity.log` | נדחה בג'וב מתוזמן (דורש `companyId` → `InvocationScopeDenied`) | לכן: **‏`ctx.logger.info` (‏pm2) + `ctx.metrics.write` (‏`plugin_logs`)** — שניהם משטחים **קיימים**. לא נוצרה טבלה, לא endpoint, ולא תור, ושורת-הסיכום הישנה **הוחלפה** ולא נוספה לצידה (G2). ⚠️ **המונים בתוך מחרוזת-ההודעה ולא רק ב-`meta`**, כי נמדד ששני המשטחים מתעלמים מ-`meta`: הפאנל מרנדר `createdAt`/`level`/`message` בלבד, ומ-pino נמדד ששדה `{error:…}` שהקוד מעביר **נופל בפועל** מהשורה. ## הרשאה חדשה ב-manifest נוספה `"metrics.write"` — הדרך היחידה לכתוב ל-`plugin_logs`. **נבדק שזה בטוח:** בדיקת-הסלמת-ההרשאות קיימת רק ב-`upgradePlugin` (‏`plugin-loader.js:1026-1034`), מסלול שאיננו משתמשים בו; מסלול-האתחול עובר ב-`refreshPluginManifestFromPackage` שמעדכן את ה-manifest **בלי** בדיקת-הסלמה. ‏`metrics.write` היא capability מוכרת (`shared/dist/constants.js:958`). **מסלול-חזרה:** הסרת השורה → `npm run build` → `pm2 restart paperclip`. ## Invariants - **כלל-הנדסה §6 (אין בליעה שקטה)** — "הצליח" שאינו מבחין בין הצלחה לאי-פעולה הוא בליעה שקטה. גם נתיב-הכשל של הפליטה עצמה אינו נבלע: `metrics.write` שנכשל מדווח ב-`logger.error` **עם טקסט-השגיאה בתוך המחרוזת**, ואינו מסווה את כשל-הריצה (הפונקציה לעולם אינה זורקת — היא נקראת מ-`finally`). - **G9 (audit/תצפיתיות)** — ריצה נעשית ניתנת-לשחזור-בדיעבד ב-SQL; זה מה שיאפשר את AC4 בעוד שבוע. - **G2** — אין מסלול מקביל: `plugin_logs` הוא מאגר-הלוגים של המארח עם API ופאנל קיימים; `ctx.metrics.write` הוא הפרימיטיב שהמארח מייעד בדיוק לזה. - **G1** — המונים נאגרים בנקודות-ההחלטה עצמן, לא משוחזרים בדיעבד מפרסור-לוגים. - **‏AC5 של ה-issue** — **‏`src/sync-target.ts` לא נגע כלל** (`git diff` ריק), ובכלל זה `NON_WRITABLE_STATUSES` (הכרעת-יו"ר, ‏#626). ## אימות ``` npx tsc --noEmit → פלט ריק npx biome check src/ → Checked 13 files. No fixes applied. node --test src/*.test.ts → tests 35 / pass 35 / fail 0 (29 קיימים + 6 חדשים) git diff main -- src/sync-target.ts → ריק git diff -w --stat → manifest 4+ · worker 69+/5- (שאר ה-diff הוא הזחה מעטיפת try/finally) ``` שני הטסטים המהותיים: **‏AC2** — שלוש ריצות (ריקה · דחתה-12 · נפלה), כולן `written=0`, מפיקות שלוש מחרוזות שונות. **אינvariant** — סימולציית-לולאה עם `pickSyncTargetIssue`/`resolveIssueStatus` האמיתיים מאמתת `matched === written + already_matching` ו-`scanned === matched + סך-הדחיות-שאינן-already_matching`. **אין Gitea Actions בריפו הזה** — `pr-checks` יחזיר "אין checks", וזה תקין ולא כשל. **פריסה ידנית** (אין Coolify hook): `git pull` ב-`/home/chaim/plugin-legal-ai` → `npm run build` → `pm2 restart paperclip`. ## מה **לא** בתחום ה-PR הזה הג'וב נופל כרגע בכל ריצה ב-`resolveApiBase` / `ctx.issues.*` שנדחים במנוע-ההרשאות — זהו **#645** (`p0-blocker` נפרד). ה-PR הזה אינו נוגע בו, ולכן ריצה חיה תיראה `run=failed scanned=0 …` עד שייסגר. זו בדיוק ההבחנה שה-issue דרש: "נפלה" מובחנת מ"אין מה לעשות". 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chaim added 1 commit 2026-08-26 13:53:16 +00:00
הג'וב סיים עד כה בשורה אחת — `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>
chaim merged commit a6bd02c237 into main 2026-08-26 13:53:24 +00:00
chaim deleted branch feat/617-sync-status-run-counters 2026-08-26 13:53:24 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ezer-mishpati/plugin-legal-ai#12