feat(ui): דף /scripts — קטלוג סקריפטים read-only מ-SCRIPTS.md
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 8s

מגיש את scripts/SCRIPTS.md כדף ב-/scripts: שם · סוג · תפקיד · תזמון
לכל סקריפט בתיקיית scripts/. מקור-האמת היחיד נשאר SCRIPTS.md (G2 — אין
מסלול-תוכן מקביל); עריכה דרך git, לא מה-UI.

- web/app.py: GET /api/scripts/catalog קורא את הקובץ בזמן-ריצה (מחקה את
  דפוס get_curator_prompt; HTTPException על כשל — אין בליעה שקטה §6)
- Dockerfile: COPY scripts/SCRIPTS.md (לא הועתק לקונטיינר עד כה)
- web-ui: דף /scripts (AppShell + רכיב Markdown הקיים) + מודול api + קישור ניווט
- SCRIPTS.md: תיעוד ingest_bulletins.py — היה הקובץ היחיד מ-73 שלא תועד

Invariants: G2 (מקור-אמת יחיד), G12 (אין מגע-Paperclip), X6 (UI↔API).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 19:42:44 +00:00
parent 24480950f1
commit 8651529327
6 changed files with 136 additions and 0 deletions

View File

@@ -74,6 +74,9 @@ COPY skills/decision/SKILL.md ./skills/decision/SKILL.md
COPY docs/legal-decision-lessons.md ./docs/legal-decision-lessons.md
COPY docs/corpus-analysis.md ./docs/corpus-analysis.md
# Scripts catalog surfaced read-only at /scripts (GET /api/scripts/catalog).
COPY scripts/SCRIPTS.md ./scripts/SCRIPTS.md
# Make mcp-server source available to web/app.py (it does sys.path.insert for legal_mcp)
ENV PYTHONPATH=/app/mcp-server/src

View File

@@ -74,6 +74,7 @@
| `monitor_halacha_quality.py` | python | מנטר איכות חילוץ הלכות. בודק drift של `avg(confidence)` בין baseline היסטורי לחלון אחרון. מחזיר JSON מטריקות + alert ב-stderr אם drift > threshold (ברירת מחדל 5%). 2 סדרות: trusted (approved+published) ו-all_extracted. תומך `--window N` / `--threshold X` / `--min-sample N` / `--silent` / `--exit-on-alert`. רץ ב-container או מקומית עם `mcp-server/.venv` (אין תלות ב-LLM, רק SQL). **תזמון מומלץ**: `0 8 * * 1` (יום ראשון 08:00, שבועי) | `0 8 * * 1` (לתזמן) |
| `audit_training_corpus.py` | python | audit של `style_corpus` — לכל החלטה: שדות מטא-דאטה מאוכלסים (`summary`/`outcome`/`key_principles`/`appeal_subtype`/`subject_categories`), קישור ל-`documents` (FK + chunks + embeddings). מפיק `data/audit/corpus-YYYY-MM-DD.json` + summary בקונסול. דרוש `POSTGRES_URL` או POSTGRES_*. אין תלויות חיצוניות מלבד asyncpg. **רץ מהמכונה המקומית** (לא קונטיינר) — חיבור ישיר ל-Postgres :5433 | ידני / קדם-עבודה לפני enrichment של מטא-דאטה |
| `backfill_style_exemplars.py` | python | **T1 (style-acquisition)** — מאכלס `style_exemplars` מקורפוס דפנה (`style_corpus` + `internal_committee` chair=דפנה): מפצל לסעיפים (`chunker._split_into_sections`) → פסקאות (25-450 מילים) → embed (Voyage) → שמירה עם `section`/`outcome`/`practice_area`. מאפשר לכותב לאחזר פסקאות-בלוק אמיתיות של דפנה (T2/T3). מקור-סגנון בלבד (INV-LRN5). אידמפוטנטי (מנקה per-decision). `--dry-run` (default) / `--apply`. דורש POSTGRES_URL + Voyage. **רץ מקומית** (venv). | ידני (`python scripts/backfill_style_exemplars.py --apply`) |
| `ingest_bulletins.py` | python | קליטת ארכיון העלון החודשי **"עו"ד על נדל"ן"** לקורפוס-הגילוי (X12) — כל PDF ב-`data/bulletins/incoming/` מפוצל ע"י LLM למצביעי-פסיקה (`digest_kind='decision'`) + מאמרים (`digest_kind='article'`), עם tag `publication='עו"ד על נדל"ן'`, דרך `bulletin_library.ingest_bulletin`. idempotent (dedup per-item לפי content_hash; הרצה חוזרת מוסיפה רק חדשים); כשל בעלון אחד לא עוצר את ה-batch. **רץ מהמכונה המקומית** (LLM מקומי-בלבד) עם venv של mcp-server: `mcp-server/.venv/bin/python scripts/ingest_bulletins.py [--dir PATH] [--limit N]`. | ידני, per-batch |
## תיקיית `.archive/` — סקריפטים שהושלמו

View File

@@ -0,0 +1,86 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Markdown } from "@/components/ui/markdown";
import { fetchScriptsCatalog } from "@/lib/api/scripts";
/*
* /scripts — read-only catalog of everything under scripts/.
*
* The content is `scripts/SCRIPTS.md` verbatim (active · archived · deleted
* tables), served by GET /api/scripts/catalog. SCRIPTS.md is the single
* source of truth — CLAUDE.md mandates updating it on every script change —
* so we render it directly rather than re-describing the scripts here.
*/
export default function ScriptsPage() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["scripts-catalog"],
queryFn: ({ signal }) => fetchScriptsCatalog(signal),
});
const lastModified =
data?.last_modified != null
? new Date(data.last_modified * 1000).toLocaleDateString("he-IL", {
year: "numeric",
month: "long",
day: "numeric",
})
: null;
return (
<AppShell>
<section className="space-y-6">
<div className="flex items-end justify-between gap-4">
<div>
<h1 className="text-xl font-bold text-navy">סקריפטים</h1>
<p className="text-sm text-ink-muted mt-1">
קטלוג כל הסקריפטים בתיקיית{" "}
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
scripts/
</code>{" "}
שם, סוג, תפקיד ותזמון. מקור-האמת הוא{" "}
<code className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem]">
scripts/SCRIPTS.md
</code>
; עריכה דרך git, לא מכאן.
</p>
</div>
{data?.gitea_url ? (
<a
href={data.gitea_url}
target="_blank"
rel="noreferrer"
className="shrink-0 text-sm text-gold-deep hover:text-gold underline underline-offset-2"
>
מקור ב-Gitea
</a>
) : null}
</div>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
{isLoading ? (
<p className="text-sm text-ink-muted">טוען קטלוג</p>
) : isError ? (
<p className="text-sm text-danger">
שגיאה בטעינת הקטלוג: {(error as Error)?.message ?? "לא ידוע"}
</p>
) : data ? (
<>
<Markdown content={data.content} />
{lastModified ? (
<p className="mt-6 pt-3 border-t border-rule text-xs text-ink-muted">
עודכן לאחרונה: {lastModified}
</p>
) : null}
</>
) : null}
</CardContent>
</Card>
</section>
</AppShell>
);
}

View File

@@ -72,6 +72,7 @@ const KNOWLEDGE_MENUS: NavMenuDef[] = [
const ADMIN_ITEMS: NavItem[] = [
{ href: "/skills", label: "מיומנויות" },
{ href: "/operations", label: "תפעול" },
{ href: "/scripts", label: "סקריפטים" },
{ href: "/diagnostics", label: "אבחון" },
{ href: "/settings", label: "הגדרות" },
];

View File

@@ -0,0 +1,18 @@
import { apiRequest } from "./client";
// ── Scripts catalog ───────────────────────────────────────────────
// Surfaces the maintained `scripts/SCRIPTS.md` (single source of truth for
// every script under scripts/) read-only at /scripts. Edits go through
// git/Gitea, not the UI — the backend just reads the file at runtime.
export type ScriptsCatalog = {
content: string;
filename: string;
bytes: number;
last_modified: number;
gitea_url: string;
};
export function fetchScriptsCatalog(signal?: AbortSignal) {
return apiRequest<ScriptsCatalog>("/api/scripts/catalog", { signal });
}

View File

@@ -1256,6 +1256,33 @@ async def get_curator_prompt():
}
@app.get("/api/scripts/catalog")
async def get_scripts_catalog():
"""Return the maintained ``scripts/SCRIPTS.md`` catalog (read-only).
``scripts/SCRIPTS.md`` is the single source of truth describing every
script under ``scripts/`` — its role, type, and schedule (CLAUDE.md
mandates updating it on any add/remove/change). The UI just surfaces it
at ``/scripts`` for transparency; edits go through git/Gitea, not here.
"""
path = Path(__file__).resolve().parent.parent / "scripts" / "SCRIPTS.md"
if not path.exists():
raise HTTPException(404, f"scripts catalog not found at {path}")
try:
content = path.read_text(encoding="utf-8")
stat = path.stat()
except OSError as e:
raise HTTPException(500, f"failed to read scripts catalog: {e}")
gitea_url = f"{_GITEA_REPO_BASE}/src/branch/main/scripts/SCRIPTS.md"
return {
"content": content,
"filename": path.name,
"bytes": stat.st_size,
"last_modified": stat.st_mtime,
"gitea_url": gitea_url,
}
@app.get("/api/training/curator/style-analyzer-prompt")
async def get_style_analyzer_prompt():
"""Return the system prompt that style_analyzer.py uses to extract patterns.