31 Commits

Author SHA1 Message Date
df4d28eb5c Merge branch 'ui-rewrite' 2026-04-13 12:43:12 +00:00
6b15f84fdb WIP: documents panel UI improvements 2026-04-13 12:43:07 +00:00
bffdfe3e9d Merge ui-rewrite into main: methodology + pipeline fixes
Major changes from ui-rewrite branch:
- Decision-writing methodology (decision-methodology.md) based on FJC, Garner, Posner
- 5 source books downloaded and processed (341K words)
- Methodology integrated into block-yod prompt
- All 8 Paperclip agents updated for methodology compliance
- DB schema V4: claim handling, standard of review, precedent hierarchy
- 15 pipeline gaps identified and fixed after test run on case 1130-25
- Negative checks layer added to CEO and QA agents
- HEARTBEAT: wakeup CEO on completion + blocked status
- Flexible claim handling (bundle/skip via chair_directions)

Conflicts resolved: all 5 files use ui-rewrite version (the latest).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:42:32 +00:00
ebecd87ad5 Analyst: split large documents before extraction to avoid timeout
Documents >15K chars must be split by chapter/section and extracted
in parts. If extract_claims times out, retry with chunks or extract
manually. This prevents the Matmon document issue (108K chars, 4x timeout).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:40:49 +00:00
b1ad67dc49 Fix 12 of 15 pipeline gaps found in 1130-25 test run
Test run on case 1130-25 revealed critical gaps. This commit fixes:

HEARTBEAT.md (#1, #11):
- Agents MUST wake CEO after completing any task (wakeup request)
- New "blocked" status option — agents cannot mark "done" if something failed
- Fallback: direct DB insert if API wake doesn't work

legal-analyst.md (#2):
- New step 6: completeness checks BEFORE finishing
- Verify all appeal/response documents extracted successfully
- Verify all extracted documents produced claims
- Verify classification is correct (no claims from committee)
- If any check fails → status = "blocked", not "done"

legal-ceo.md (#3, #6, #7, #12, #13, #14, #15):
- Step A rewritten with 3 sub-checks:
  A1: extraction completeness (no missing documents)
  A2: negative checks (wrong classification, abnormal counts, missing parties)
  A3: methodology compliance (syllogisms, CREAC prep, steel-man, etc.)
- Any failure blocks progress to step B

legal-qa.md (#6 reinforcement):
- New step 2b: negative checks on the written decision
- Missing issues, bare quotes, empty formulas, mixed findings/conclusions

Also:
- Synced all agent files to /home/chaim/legal-ai/ (Paperclip reads from there)
- Synced methodology + lessons + corpus docs
- Fixed claim classification in DB: 20 committee/applicant claims → response (#5)

Remaining gaps (3):
- #4: Paperclip cache may need restart to pick up new definitions
- #7: Matmon document retry (25K words, 0 claims extracted)
- #9: 53 appellant claims may need synthesis (high but not blocking)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:28:38 +00:00
6cf918ad79 Add DB schema V4: methodology alignment columns
New columns for methodology-aware decision pipeline:

claims table:
- claim_handling (address/bundle/skip) — per-claim handling mode
- bundle_group — group name for bundled claims
- handling_reason — explanation for skip/bundle

cases table:
- standard_of_review — review standard (independent discretion / etc.)
- subject_categories — JSONB array of topics in the appeal

case_law table:
- precedent_level — hierarchy (supreme/administrative/national/district)
- is_binding — binding holding vs. obiter dictum
- creac_role — how it serves reasoning (rule/explanation/analogy)

decisions table:
- issue_order — JSONB array of ordered issues with type
- claim_handling — JSONB overrides from chair_directions

Migration tested and applied successfully on production DB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:47:11 +00:00
444fb73681 Align all Paperclip agents with decision-methodology.md
All agents audited line-by-line against the new methodology.

legal-analyst (18 changes):
- Reads methodology before starting
- Issues formulated as syllogisms (rule + facts + question)
- SWOT replaced with analytical structure (rule/facts/open questions)
- Factual findings separated from legal conclusions
- Issue ordering: threshold → dispositive → secondary
- Claim handling section (bundle/skip recommendations)
- Standard of review field added
- CREAC preparation per issue
- Steel-man field per issue
- "הגוף המחליט" replaces "צד מיוצג"

legal-ceo (14 changes):
- Knows methodology exists, reads it before orchestrating
- Step B: asks claim handling (bundle/skip table) + appeal classification
- Step B: key questions as condensed syllogisms
- Step C: directions structured as syllogisms (rule + facts + conclusion)
- Step D: verifies chair_directions completeness before sending to writer
- Status map expanded with intermediate states
- Fallback conditions for every step
- Methodology consistency rule added

legal-researcher (4 changes):
- Reads methodology before starting
- Case law summaries include hierarchy level and CREAC role
- Plan mapping requires exact quotation + ambiguity detection
- Reporting structured by source type (text/precedent/policy)

legal-exporter (1 change):
- Verifies QA passed before export

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:43:42 +00:00
be9fa9e712 Add decision-writing methodology based on FJC, Garner, Posner sources
"בית ספר להחלטות" Phase 2 — the system now has formal analytical
methodology for building quasi-judicial decisions, separate from
Dafna's writing style (SKILL.md) and content checklists.

What was done:
- Downloaded 5 authoritative sources (~341K words): FJC Judicial
  Writing Manual (1991+2020), Garner Legal Writing in Plain English,
  Posner How Judges Think, Scalia/Garner Making Your Case
- Extracted principles from all sources into intermediate docs
- Synthesized into docs/decision-methodology.md (3,400 words,
  12 sections, 10 guiding principles)
- Integrated methodology into block-yod prompt via {methodology_guidance}
- Restructured legal-writer agent workflow to follow analytical stages
- Made "answer all claims" flexible (bundle/skip via chair_directions)
- Added methodology compliance check (#7) to legal-qa agent
- Updated all knowledge files (CLAUDE.md, SKILL.md, lessons, corpus)

Three-layer architecture:
1. Methodology (decision-methodology.md) — universal, how to think
2. Content checklists (lessons.py) — specific per appeal subtype
3. Style (SKILL.md) — Dafna's personal writing patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:29:16 +00:00
ed8502d46b Update knowledge files with corpus analysis and feedback system docs
- CLAUDE.md: added corpus-analysis.md to reference table, documented chair feedback system
- block-schema.md: added content_checklist constraint to block-yod
- legal-decision-lessons.md: added lessons 12-16 from corpus analysis (planning discussion, 5 subtypes, feedback system)
- SKILL.md: added section 12 (content checklists, planning discussion patterns, chair feedback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:25:54 +00:00
0fef20e272 Add content checklists for block-yod and chair feedback system
Addresses Dafna's observation that licensing decisions lack comprehensive
planning discussion. Systematic corpus analysis of all 24 training decisions
revealed the system learned writing style but not substantive content.

Changes:
- Corpus analysis of all 24 decisions (docs/corpus-analysis.md)
- 5 content checklists by appeal subtype injected into block-yod prompt
- chair_feedback DB table + API endpoints + MCP tools
- Feedback management page in Next.js UI (/feedback)
- Navigation updated with "הערות יו״ר" link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:58:28 +00:00
ca6ec48580 Render research prose as Markdown with RTL tables
The analysis-and-research.md content has markdown tables (ציר דיוני)
and inline formatting like **label:** strong runs, which were
rendering as raw pipes and dashes because the compose page used
whitespace-pre-line on plain text.

Add a reusable <Markdown> component backed by react-markdown +
remark-gfm with a custom `components` map that styles paragraphs,
lists, blockquotes, strong runs, and especially GFM tables for RTL
+ aligned columns:

- table: table-auto + border-collapse, wrapped in overflow-x-auto
  so very wide tables don't push the parent card out
- th: whitespace-nowrap so the header row sets column widths and
  every row border lines up row-to-row
- everything text-right + the whole block gets dir="rtl" at the root

Use it in three places on the compose screen:
- ProseSection (represented_party, procedural_background,
  agreed_facts, disputed_facts)
- Conclusions card
- SubsectionCard field content — threshold_claims and issues have
  the same markdown shape in their fields[]

react-markdown + remark-gfm added (~30KB gzipped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:43:37 +00:00
4e418787cf Full-width cards on compose
Drop the max-w-4xl wrapper so the compose cards fill the available
width of the AppShell main (which is already capped at 1400px
upstream). Threshold claim / issue cards with long Hebrew prose
now get the room they need without horizontal dead space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:33:08 +00:00
fdd12c6726 Move background + conclusions below the issues on compose
The right sidebar with "רקע לניתוח" and "מסקנות" was stealing
horizontal space and forced the editable cards (threshold claims,
issues, chair positions, now precedents) into a narrow column. Move
both cards into the main stack, after the issues list, so the chair
sees the decision points first and the surrounding context after.

Collapse the lg:grid-cols-[1fr_320px] layout into a single max-w-4xl
stack since there's no longer a sidebar. Bump the moved cards from
px-5 py-4 to px-6 py-5 to match the rest of the compose page's
card padding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:30:02 +00:00
e34d217345 Close first threshold claim by default on compose screen
Drop the defaultOpen={i===0} on the first threshold_claim — when a
case has a lot of material already on screen (research background
+ chair positions + now precedents), auto-opening the first card
creates a wall of text on page load. All cards now start collapsed,
same as the issues list already did.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:27:28 +00:00
6b8f002596 Precedent attachment UI in the compose screen
Surface the new POST/GET/DELETE /api/cases/{n}/precedents endpoints
in the compose screen as two insertion points:

1. A new case-level card "פסיקה כללית לדיון" at the top of the
   main column, for precedents that support the discussion intro
   rather than a specific threshold_claim / issue.
2. An inline "פסיקה תומכת" section inside each SubsectionCard,
   below the ChairEditor.

Both insertion points render a `<PrecedentsSection>` which shows a
list of `<PrecedentCard>` (citation + blockquote + optional chair
note + 📄 chip if a PDF was archived) followed by a `<PrecedentAttacher>`
popover trigger.

The Attacher is a Popover with cross-case typeahead: typing 2+
characters into the citation field hits /api/precedents/search and
shows distinct library matches; picking one prefills quote + chair
note but leaves them editable so customizing the quote for this
case doesn't mutate the library. An optional PDF/DOCX/DOC file can
be attached — it uploads first via POST .../upload-pdf and the
returned document_id is passed into the precedent create call.

The parent compose page issues a single useCasePrecedents query
and partitions the result by section_id into a Map so each
SubsectionCard renders its own slice without re-fetching.

shadcn Popover installed as a new primitive. sonner toasts wired
for success/error in both attach and delete flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:20:45 +00:00
aa0e608a4a Fix RTL alignment in DocumentsPanel
Force the title + meta column to the inline-start (visual right in
RTL) and the doc_type badge to the inline-end (visual left). The
previous layout used justify-between with an ambient dir="rtl"
inherited from <html>, but the Radix ScrollArea Viewport was eating
the direction context and flipping the row to LTR.

Fix: set dir="rtl" explicitly on both ScrollArea and the inner ul,
drop justify-between, and use ms-auto on the badge so it grows its
inline-start margin regardless of ambient direction. Title column
gets an explicit text-right for good measure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:34:09 +00:00
916360e9b2 Fix case detail: document fields, expected-outcome label, drop debug note
Three user-reported bugs on /cases/[caseNumber]:

1. Documents tab showed "9 מסמכים" in the count but rendered nothing —
   DocumentsPanel was reading filename/category/status/size_bytes/uploaded_at,
   but the real FastAPI payload (case_get → db.list_documents) returns
   title/doc_type/extraction_status/page_count/created_at. Rewrote the
   panel against the actual document row shape, added a CaseDocument
   type alias in lib/api/cases.ts, mapped doc_type to Hebrew labels
   (כתב ערר / כתב תשובה / ...) and extraction_status likewise.

2. The "פעולות" tab showed a debug-flavoured paragraph "עריכת פרטי התיק
   נשמרת מיד דרך PUT /api/cases/1033-25" — that was internal wording,
   not user copy. Removed.

3. Overview tab showed the raw enum value "full_acceptance" in the
   expected-outcome line. Mapped through the existing expectedOutcomes
   label array so it now reads "קבלה מלאה".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:00:24 +00:00
cbe9d60901 Phase 6: polish — error boundaries, a11y, smoke test doc
Close out the read-only surface before cutover with three families of
small fixes that the previous phases left unfinished:

- Error boundaries: add src/app/error.tsx (route-segment), global-error.tsx
  (root crash fallback with its own minimal html/body — no Providers
  dependency since those may be the thing that crashed), and not-found.tsx
  for a Hebrew 404 instead of the default Next page.

- Accessibility: wire usePathname() into AppShell so the current nav item
  gets aria-current="page" and a gold underline. Add aria-label + aria-hidden
  on the icon-only buttons that Phase 5 left text-less (corpus trash,
  parties-field Plus). Nav gets an aria-label of its own.

- Metadata template: title on each route now reads "X · עוזר משפטי" via
  the layout.tsx title.template. Description localized to Jerusalem.

- README: full E2E smoke test checklist covering all 9 screens, plus a
  backend contract table so future phases know which hook wraps which
  endpoint. Documents the known Gitea→Coolify webhook issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:43:59 +00:00
fb1f73fa25 Phase 5: secondary screens (diagnostics, skills, training)
Port the remaining read views from the vanilla UI to Next.js:

- /diagnostics — system health snapshot (DB connected, table counts,
  active tasks, failed and stuck documents). Uses the existing
  /api/system/diagnostics payload with a 10s refetchInterval so the
  page self-updates while the user watches.
- /skills — Paperclip skill inventory with sync status (DB-only,
  disk-only, synced, not-synced) as a card grid driven by
  /api/admin/skills.
- /training — Dafna's style portrait as three tabs on one page:
  * Report: corpus KPIs + CSS conic-gradient subject donut
    (SubjectDonut ported from index.html renderHero) + horizontal
    anatomy bars + top-12 signature phrases.
  * Corpus: TanStack Table of style_corpus rows with an inline
    delete mutation (useDeleteCorpusEntry invalidates both the
    corpus list and the style report so KPIs update).
  * Compare: two-decision selector backed by /api/training/compare,
    side-by-side panels plus shared / only-A / only-B pattern
    lists.

New API modules: lib/api/system.ts, lib/api/skills.ts,
lib/api/training.ts. All three use TanStack Query with staleTime
profiles tuned per endpoint (10s for diagnostics, 30s for skills,
60s for training reports).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:33:33 +00:00
ac0a5ee30b Phase 4.5: practice area integration in the Next.js UI
Backend commit 26d09d6 introduced a multi-tenant axis (practice_area +
appeal_subtype) that the vanilla UI picked up but the new Next.js
rewrite did not. Close the gap in the screens we already shipped so
future search/filter work in Phase 5 has the right data on screen.

- lib/practice-area.ts — new: enum + label maps + deriveSubtype(),
  mirrors mcp-server/src/legal_mcp/services/practice_area.py.
- lib/schemas/case.ts — two new z.enum fields on caseCreateSchema.
- lib/api/cases.ts — Case / CaseDetail gain practice_area and
  appeal_subtype as optional (cached pre-migration responses still
  parse cleanly).
- wizard/case-wizard.tsx — basics step gains a practice_area dropdown
  (future domains disabled with "(בקרוב)") and an appeal_subtype
  dropdown with auto-fill effect tracking a userTouchedSubtype ref,
  same behaviour as wireSubtypeAutofill() in the vanilla UI.
- cases/case-header.tsx — gold badge next to the status shows
  "ועדת ערר · רישוי ובנייה" when both fields are populated.
- cases/cases-table.tsx — new "תחום" column showing subtype label
  (dash for unknown). No filter yet — that's phase 5 when a second
  domain actually exists.

Plan: ~/.claude/plans/woolly-cooking-graham.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:15:48 +00:00
e483eba1a9 Allow two case-number formats: NNNN-YY and NNNN-MM-YY
Narrow the regex to exactly the two hyphen-separated shapes Dafna
uses in practice (1033-25, 1000-04-26). Wider shapes like "1033-foo"
are rejected too — they didn't exist in any real case and would
quietly widen the surface area for routing bugs later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:44:25 +00:00
d8a537e7aa Forbid slashes in case numbers
FastAPI path routing can't capture a slash inside a {case_number}
segment — %2F gets decoded before route matching, so any case created
with "1000/0426" becomes permanently unreachable at
GET /api/cases/{case_number}/details. Enforce the hyphen convention
used by existing prod cases (1033-25, 1130-25) at the zod layer so
the wizard rejects the bad shape before submit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:41:40 +00:00
75ea6825b2 Remove committee_type from case wizard and header
The committee_type field in FastAPI's CaseCreateRequest is a leftover
with no meaningful UI. Appeals against a ועדה מקומית / ועדה מחוזית are
legally distinguishable by case-number range, not by a picked committee
label; appeals against a ועדת ערר decision go to בית משפט לעניינים
מנהליים and never enter this system at all. The backend retains its
"ועדה מקומית" default, which is what we'd send anyway.

Drop the Select from the wizard's basics step and the "ועדה" row from
the case detail header. The Case TS type keeps the optional field so
existing API responses still parse cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:36:50 +00:00
10540a38b4 Phase 4c: bulk document upload with live SSE progress
New UploadSheet on the case detail page wraps react-dropzone + a
selector for doc_type. Files post to
POST /api/cases/{n}/documents/upload-tagged as multipart form-data;
the returned task_id is streamed via GET /api/progress/{task_id}
through the new lib/sse.ts EventSource wrapper.

Each upload row shows a per-file progress bar that transitions to
success/error on the terminal SSE payload. Closing the stream inside
the message handler avoids EventSource's auto-reconnect after EOF.

Phase 4 (task 86) is now complete end-to-end: create, upload, edit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:25:44 +00:00
b67dc47dc7 Phase 4b: Case create wizard
New /cases/new route with a 3-step wizard (basics / parties / details)
backed by react-hook-form + the caseCreateSchema. Each step validates
only its own fields so the user fixes errors in context. On success
useCreateCase invalidates the case list and the router pushes to the
freshly created case detail page.

PartiesField is a small chip-style editor for the appellants/respondents
arrays. The Home page now has a navy "+ תיק חדש" button that links to
the wizard.

Dropped .default() from the create schema — zod's input/output type
mismatch broke the RHF zodResolver generics; dropping the defaults is
simpler than plumbing z.input vs z.output through the mutation hook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:23:37 +00:00
9fcf4f2dc7 Phase 4a: shadcn form primitives + case inline edit
Add dialog/select/textarea/label/progress/sonner components and wire
a Toaster into Providers. New zod schemas in lib/schemas/case.ts
mirror CaseCreateRequest/CaseUpdateRequest and feed react-hook-form
validation.

CaseEditDialog on the case detail Actions tab posts PUT /api/cases/{n}
with optimistic cache patching via useUpdateCase, showing toast
feedback on success/error.

shadcn's <Form> registry entry skipped at init (missing from the
nova preset); the dialog uses RHF directly against the same Input/
Textarea/Select primitives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:21:21 +00:00
03b25bc273 Phase 3c: Compose view with chair-position editor
New /cases/[caseNumber]/compose route ports the research analysis +
chair-position editing flow from the vanilla UI onto the Next.js
stack. Reads /api/cases/{n}/research/analysis, renders background
prose in the side column and threshold claims + issues as collapsible
cards in the main column, each with a blur-autosaved chair editor
wired through a TanStack Query mutation with optimistic cache patching
(so concurrent reads don't steal editor focus).

Handles the common "analysis not yet generated" 404 with a dedicated
empty state rather than an error card.

Phase 3 task 85 is now ready for review end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:09:09 +00:00
d0daa0efe8 Phase 3b: Case Detail view with workflow timeline
New dynamic route /cases/[caseNumber] wired to the FastAPI details
endpoint, using Next 16's async params pattern with React's use()
hook. TanStack Query handles 5s refetchInterval so the page
self-updates during long-running processing without manual polling.

New components: CaseHeader (breadcrumb + meta), WorkflowTimeline
(5-phase RTL pipeline view), DocumentsPanel (categorized list).
Tabs split overview/documents/actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:06:27 +00:00
51064f3a03 Phase 3a: shadcn init + Home/Case List view
Initialize shadcn/ui (radix-nova preset) and wire its semantic tokens
to the editorial navy/cream/gold palette so primitives inherit the
judicial voice without per-component overrides.

Replace the Phase 2 live-probe with a real dashboard: KPI tiles,
conic-gradient status donut (ported from the vanilla renderHero),
and a TanStack Table cases list with search + sort. Add useCase(n)
hook with 5s staleTime/refetchInterval to replace the old manual
polling loop when Case Detail ships next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:04:30 +00:00
Chaim
0ee8e723bd Phase 2: API client, typed hooks, live probe
- Add api:types script (openapi-typescript against live FastAPI)
- Generate src/lib/api/types.ts (2972 lines, 55 paths, 16 schemas)
- lib/api/client.ts: typed apiRequest + ApiError + makeQueryClient
  (staleTime 5s, no refetchOnWindowFocus to preserve editor state)
- lib/providers.tsx: QueryClientProvider client component, useState
  singleton so App Router re-renders don't dump the cache
- lib/api/cases.ts: Case type + casesKeys + useCases hook (pragmatic
  hand-typed Case pending backend response-model annotations)
- layout.tsx: wrap children with <Providers>
- Smoke test: CasesLiveProbe component on home page hitting live FastAPI
  via /api/cases rewrite proxy

Phase 2 deliverable check: useCases() returns typed Case[] from the
production FastAPI through the Next.js proxy. End-to-end wiring proven.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:49:24 +00:00
Chaim
64724656af Phase 1: scaffold Next.js 16 web-ui + Coolify staging
- create-next-app with TypeScript, Tailwind v4, App Router
- Port design-system.css tokens into Tailwind @theme (navy/gold/parchment, Heebo)
- Install TanStack Query, react-hook-form, zod, lucide-react, react-dropzone
- layout.tsx: RTL Hebrew + Heebo via next/font/google
- AppShell component with navy header + gold rule + nav
- next.config.ts: output:standalone + rewrites to proxy /api/* to production FastAPI
- Dockerfile: multi-stage Node 20 Alpine build for Next.js standalone
  (branch-local override of the FastAPI Dockerfile; main is unaffected)
- Switch .taskmaster to claude-code provider (no API key required)
- Add 7 phase tasks (83-89) tracking the full rewrite plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:47:05 +00:00
121 changed files with 52812 additions and 487 deletions

View File

@@ -43,7 +43,7 @@ curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
**לפני שאתה מסיים, תמיד:**
פרסם comment על ה-issue:
### 4א. פרסם comment על ה-issue
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
@@ -51,7 +51,9 @@ curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-d '{"body": "סיכום העבודה..."}'
```
עדכן סטטוס issue:
### 4ב. קבע סטטוס — done או blocked
**אם המשימה הושלמה בהצלחה** (כל המסמכים חולצו, כל הבדיקות עברו, אין חסימות):
```bash
curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
@@ -59,6 +61,37 @@ curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-d '{"status": "done"}'
```
**אם המשימה נכשלה או חסומה** (מסמך לא חולץ, timeout, חוסר מידע, שגיאה שלא ניתנת לפתרון):
```bash
curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/issues/{issue-id}" \
-d '{"status": "blocked"}'
```
**אסור** לסיים issue כ-"done" אם יש כשל שלא טופל. "done" = הכל הושלם בהצלחה. אם משהו נכשל — "blocked".
### 4ג. העֵר את העוזר המשפטי (CEO) — חובה!
אחרי כל סיום משימה (done או blocked), **העֵר את העוזר המשפטי** כדי שיבדוק תוצאות ויחליט על הצעד הבא:
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "סוכן [שמך] סיים משימה [issue-id] בסטטוס [done/blocked]. נדרשת בדיקה והחלטה על הצעד הבא."}'
```
אם ה-API הזה לא עובד, השתמש ב-DB ישירות:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'סוכן סיים משימה — נדרשת בדיקה והחלטה על הצעד הבא',
'pending',
'agent'
);"
```
## 5. התראת מייל — כשנדרשת תשובה אנושית
**כשהתוצאה דורשת החלטה או תשובה של חיים**, שלח מייל:

View File

@@ -24,7 +24,13 @@ tools:
# מנתח ומחקר משפטי — סוכן ניתוח אסטרטגי והפקת שאלות מחקר
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיקי ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות אסטרטגיה משפטית, ולהפיק שאלות מחקר ממוקדות.
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיקי ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות ניתוח משפטי מובנה, ולהפיק שאלות מחקר ממוקדות.
## לפני שאתה מתחיל — קרא
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: איך לחשוב על החלטה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות
2. **`docs/block-schema.md`** — ארכיטקטורת 12 בלוקים
3. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות
## שפה
@@ -67,14 +73,16 @@ tools:
- **סוג ההליך**: ערר תכנוני, ערר היטל השבחה, ערעור מנהלי וכד'
- **הערכאה/הגוף**: ועדת ערר מחוזית, בית משפט לעניינים מנהליים וכד'
- **הצדדים**: מי העורר, מי המשיב, מי צד ג'
- **המסגרת הנורמטיבית**: חוקים, תקנות, תכניות רלוונטיות (רק מהמסמכים)
- **המסגרת הנורמטיבית**: חוקים, תקנות, תכניות רלוונטיות **קרא את המסמכים הנורמטיביים במלואם** (לא רק הסעיף הנטען; מילה בסעיף אחד מתפרשת לאור סעיפים אחרים באותו מסמך)
4. חלץ טענות/תשובות/תגובות (`extract_claims` עם doc_type ו-party_hint מתאימים)
- **מסמך גדול (>15,000 תווים):** פצל לחלקים לפי פרקים/סעיפים וחלץ מכל חלק בנפרד. אל תשלח מסמך שלם של 20K+ מילים בקריאה אחת — זה יגרום ל-timeout.
- **אם extract_claims נכשל (timeout):** נסה שוב עם חלק מהמסמך. אם עדיין נכשל — חלץ ידנית: קרא את הטקסט (`document_get_text`), זהה את הטענות המרכזיות, והכנס ל-DB.
5. וודא שכל פריט מסווג ל-claim_type הנכון
### שלב 2: ניתוח מעמיק
הצג במבנה הבא:
**צד מיוצג**: ועדת הערר (יו"ר — עו"ד דפנה תמיר). אנחנו צד ניטרלי שמכריע.
**הגוף המחליט**: ועדת הערר לתכנון ובניה, מחוז ירושלים (יו"ר — עו"ד דפנה תמיר). הוועדה היא גוף מעין-שיפוטי שמכריע בעררים על החלטות ועדות מקומיות. היא אינה מייצגת צד — היא מנתחת, שוקלת ומכריעה.
**רקע דיוני**: סוג ההליך, מספר תיק, תאריכים מרכזיים, היסטוריה דיונית, תכניות רלוונטיות.
@@ -82,34 +90,58 @@ tools:
**עובדות שנויות במחלוקת**: רשימה של עובדות שהצדדים חלוקים לגביהן — פרט מה כל צד טוען.
### שלב 3: טענות סף, סוגיות להכרעה ואסטרטגיה
### שלב 3: טענות סף, מפת דרכים, סוגיות להכרעה
**טענות סף** (אם קיימות):
חוסר סמכות, שיהוי, התיישנות, אי-מיצוי הליכים, חוסר יריבות, מעשה בית דין — הצג כל אחת עם עמדת שני הצדדים. אם אין — כתוב: "לא זוהו טענות סף."
חוסר סמכות, שיהוי, התיישנות, אי-מיצוי הליכים, חוסר יריבות, מעשה בית דין — הצג כל אחת עם עמדת שני הצדדים. לכל טענת סף הוסף **עמדת ועדת הערר** (שדה ריק ליו"ר). אם אין — כתוב: "לא זוהו טענות סף."
**תקן ביקורת**: ציין את תקן הביקורת של הוועדה בתיק זה — "הוועדה מפעילה שיקול דעת תכנוני עצמאי" (ברישוי) או "הוועדה בוחנת את תקינות השומה המכרעת" (בהיטל השבחה) או תקן אחר לפי סוג ההליך.
**מפת דרכים**: לאחר זיהוי טענות הסף ולפני הדיון בסוגיות — כתוב פסקת מפה: "X שאלות עומדות להכרעה: (1)...; (2)...; (3)..." — כדי שהקורא ידע מראש מה לצפות.
**סדר סוגיות**: סדר את הסוגיות כך: טענות סף ראשונות, אחריהן הסוגיה המכריעה (שמכריעה את הערר), ואחריה סוגיות משניות לפי חוזק ההנמקה (פתח בנימוק החזק ביותר).
**סוגיות להכרעה** — לכל סוגיה מרכזית:
1. **כותרת הסוגיה** — ניסוח תמציתי ומדויק
2. **טענה (claim)** — מה העוררים טוענים, על מה מסתמכים
3. **תשובה (response)** — מה הוועדה/משיבים עונים
4. **תגובה (reply)** — מה המבקשת מגיבה (אם קיימת)
5. **ניתוח אסטרטגי**:
- **חוזקות** — מה חזק בכל צד? מה מבוסס היטב?
- **חולשות** — מה חלש? מה לא מגובה בראיות?
- **הזדמנויות** — איפה יש פתח? מה הוועדה יכולה להישען עליו?
6. **שאלות משפטיות**צמד שאלות (ראה שלב 4)
7. **עמדת ועדת הערר** — שדה ריק שיו"ר הוועדה ימלא ידנית. **חובה להוסיף לכל סוגיה!** עמדה זו תשמש כהנחיה מחייבת לסוכן הכתיבה.
1. **כותרת הסוגיה** — ניסוח סילוגיסטי: הכלל + העובדות + שאלה חדה. לדוגמה: "תכנית X קובעת קו בניין של 3 מטרים; הבקשה כוללת בניה במרחק 1.5 מטרים — האם הבקשה תואמת את הוראות התכנית?"
2. **ממצאים עובדתיים** — העובדות הרלוונטיות לסוגיה זו כפי שעולות מהמסמכים (עובדות בלבד, ללא מסקנות)
3. **טענה (claim)** — מה העוררים טוענים, על מה מסתמכים
4. **תשובה (response)** — מה הוועדה/משיבים עונים
5. **תגובה (reply)** — מה המבקשת מגיבה (אם קיימת)
6. **ניתוח**:
- **הכלל החל** — הוראת תכנית, סעיף חוק, הלכה פסוקה, או עיקרון תכנוני
- **העובדות הרלוונטיות** — כיצד עובדות המקרה משתלבות בכלל
- **נקודות פתוחות** — מה עדיין לא ברור, מה דורש חקירה נוספת
- **הערכה ראשונית** — לאן נוטה הניתוח ומדוע
7. **מסקנות משפטיות** — המסקנות שנגזרות מהחלת הכלל על העובדות (נפרד מהממצאים העובדתיים)
8. **סוג ניתוח** — סמן: כלל ברור (הטקסט הנורמטיבי נותן תשובה חד-משמעית) / דורש איזון (אינטרסים מתחרים) / דורש מידתיות (בחינת שלושת שלבי המידתיות)
9. **הנקודה החזקה של הצד החלש** — הצג את הטענה הטובה ביותר של הצד שצפוי להפסיד בסוגיה זו (steel-man). מה עורך דין מוכשר היה מדגיש?
10. **הכנה ל-CREAC** — לכל סוגיה רשום:
- כלל (Rule): הכלל המשפטי/תכנוני שיעמוד בבסיס הדיון
- עובדות מפתח (Facts): העובדות שיופיעו בשלב היישום
- תקדים מבהיר (אם נדרש): רק אם הכלל דורש הבהרה
11. **שאלות משפטיות** — 1-3 שאלות לפי הצורך (ראה שלב 4)
12. **עמדת ועדת הערר** — שדה ריק שיו"ר הוועדה ימלא ידנית. **חובה להוסיף לכל סוגיה!** עמדה זו תשמש כהנחיה מחייבת לסוכן הכתיבה.
### שלב 3א: טיפול בטענות
לאחר ניתוח כל הסוגיות, הוסף סעיף "טיפול בטענות" עם המלצות:
- **טענות לקיבוץ**: טענות שמכוונות לאותה נקודה ואפשר לטפל בהן יחד ("באשר לטענות הנוספות בעניין X — לא מצאנו בהן ממש, ונפרט")
- **טענות לדילוג**: טענות שהועלו אך אינן נחוצות להכרעה ("נוכח מסקנתנו לעיל, אין צורך להכריע בטענה זו")
- **טענות שחייבות מענה פרטני**: טענות מרכזיות שהצד המפסיד חייב לראות שנשקלו
### שלב 4: הפקת שאלות מחקר
לכל סוגיה (כולל טענות סף), נסח **בדיוק שתי שאלות מחקר**:
לכל סוגיה (כולל טענות סף), נסח **1-3 שאלות מחקר לפי הצורך**:
**שאלה 1 — עקרונית (שאלת "האם")**:
**שאלה עקרונית (שאלת "האם")**:
בודקת עיקרון משפטי כללי בתחום התכנון והבניה.
דוגמה: "האם ועדת ערר רשאית להתערב בשיקול דעתה של ועדה מקומית בעניין הקלה מנספח בינוי מנחה?"
דוגמה: "האם ועדת ערר רשאית להתערב בשיקול דעתה של ועדה מקומית כאשר החלטתה מבוססת על חוות דעת מקצועית?"
**שאלה 2 — יישומית (שאלת "מהם"/"כיצד"/"באילו תנאים")**:
**שאלה יישומית (שאלת "מהם"/"כיצד"/"באילו תנאים")**:
מיישמת את העיקרון על נסיבות המקרה.
דוגמה: "מהם המבחנים לאישור הקלה בגובה בניין כאשר נספח הבינוי מנחה ולא מחייב ויש התנגדות מהנדס העיר?"
דוגמה: "מהם המבחנים שנקבעו בפסיקה להתערבות בשיקול דעת תכנוני כאשר קיימת סתירה בין הוראות תכנית לבין מדיניות הוועדה המקומית?"
**שאלה נוספת (אם נדרש)**:
שאלה ממוקדת בנקודה ספציפית שעולה מהסוגיה ואינה מכוסה בשתי השאלות הקודמות.
### כללים לשאלות מחקר
- ניתנות למחקר — אפשר למצוא תשובה בפסיקה, חקיקה, או ספרות
@@ -124,7 +156,34 @@ tools:
- `find_similar_cases` — תיקים דומים
הוסף תוצאות רלוונטיות תחת כל סוגיה כ-"תקדימים מהקורפוס הפנימי".
## שלב 6: שמירה ודיווח — חובה!
## שלב 6: בדיקת שלמות — לפני שמסיימים!
**לפני סיום, בצע את הבדיקות הבאות. אם בדיקה נכשלת — אל תסיים כ-"done".**
### 6א. שלמות חילוץ מסמכים
בדוק: **האם כל מסמך מסוג appeal/response/reply חולץ ויצר טענות?**
```
query: SELECT d.title, d.doc_type, d.extraction_status,
(SELECT count(*) FROM claims WHERE source_document LIKE '%' || d.title || '%' AND case_id = d.case_id) AS claim_count
FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'response', 'reply')
```
- אם יש מסמך עם extraction_status != 'completed' → **נסה שוב** (retry עם timeout ארוך, או פצל לחלקים)
- אם יש מסמך עם extraction_status = 'completed' אבל 0 טענות → **נסה לחלץ טענות שוב**
- אם ניסיון חוזר נכשל → **סטטוס issue = "blocked"**, לא "done". דווח מה נכשל ולמה.
### 6ב. בדיקת סיווג
בדוק: **האם הסיווג הגיוני?**
- אם יש claims (claim_type='claim') מצד ועדה מקומית או מבקשי היתר → **שגיאת סיווג**. תקן ל-response.
- אם יש יותר מ-30 טענות (claim_type='claim') מעורר אחד → **ייתכן חוסר סינתוז**. בדוק: האם טענות חוזרות? האם אפשר לאחד?
### 6ג. בדיקת צד חסר
בדוק: **האם כל צד מיוצג בטענות?**
- אם אין אף claim מהעוררים → חריגה
- אם אין אף response מהמשיבים → חריגה
## שלב 7: שמירה ודיווח — חובה!
**רק אם כל בדיקות שלב 6 עברו:**
1. **שמור** את הפלט המלא:
```
@@ -132,7 +191,8 @@ tools:
```
2. **פרסם comment** ב-Paperclip עם סיכום:
- כמה טענות, תשובות ותגובות חולצו
- כמה טענות חולצו (מפורט: X טענות עוררים, Y תשובות משיבים, Z תגובות)
- **האם כל המסמכים חולצו בהצלחה** (כן/לא — אם לא, פרט מה נכשל)
- הסוגיות המרכזיות (3-5 כותרות)
- כמה שאלות מחקר הופקו
- המלצה לשלב הבא
@@ -146,14 +206,17 @@ tools:
"סיכום: X סוגיות זוהו, Y שאלות מחקר הופקו. נדרשת ביקורתך לפני המשך."
```
**אם בדיקות שלב 6 נכשלו** — סטטוס issue = "blocked", פרסם comment עם פירוט מה נכשל, שלח מייל לחיים.
## מבנה הפלט המלא — analysis-and-research.md
```markdown
# ניתוח ומחקר משפטי — ערר {case_number}
תאריך: {date}
## 1. צד מיוצג
ועדת הערר לתכנון ובניה, מחוז ירושלים (יו"ר: עו"ד דפנה תמיר)
## 1. הגוף המחליט
ועדת הערר לתכנון ובניה, מחוז ירושלים (יו"ר: עו"ד דפנה תמיר).
הוועדה היא גוף מעין-שיפוטי שמכריע בעררים על החלטות ועדות מקומיות.
## 2. רקע דיוני
...
@@ -168,28 +231,56 @@ tools:
## 5. טענות סף
[אם קיימות — כולל שאלות משפטיות + עמדת ועדת הערר לכל טענה]
**תקן ביקורת:** [שיקול דעת עצמאי / בחינת תקינות השומה / אחר]
## 5א. מפת דרכים
X שאלות עומדות להכרעה:
1. ...
2. ...
3. ...
## 6. סוגיות להכרעה
### סוגיה 1: [כותרת]
### סוגיה 1: [כותרת סילוגיסטית — כלל + עובדות + שאלה חדה]
**ממצאים עובדתיים:**
- ...
**טענה (claim):** ...
**תשובה (response):** ...
**תגובה (reply):** ...
**ניתוח אסטרטגי:**
- חוזקות: ...
- חולשות: ...
- הזדמנויות: ...
**ניתוח:**
- הכלל החל: ...
- העובדות הרלוונטיות: ...
- נקודות פתוחות: ...
- הערכה ראשונית: ...
**מסקנות משפטיות:**
- ...
**סוג ניתוח:** כלל ברור / דורש איזון / דורש מידתיות
**הנקודה החזקה של הצד החלש:**
...
**הכנה ל-CREAC:**
- כלל (Rule): ...
- עובדות מפתח (Facts): ...
- תקדים מבהיר: ... (אם נדרש)
**שאלות משפטיות:**
1. [שאלה עקרונית — "האם..."]
2. [שאלה יישומית — "מהם..."]
3. [שאלה נוספת — אם נדרש]
**חיפוש תקדימים:**
- nevo (קלאסי): "ביטוי" ו "ביטוי" ו "ועדת ערר"
- nevo AI / law-mate: [השאלות המשפטיות מלמעלה — שאלה עקרונית + יישומית]
- nevo AI / law-mate: [השאלות המשפטיות מלמעלה]
**חקיקה רלוונטית:**
- סעיף X לחוק...
(הערה: התחל מלשון הטקסט הנורמטיבי. תקדים נדרש רק כשהטקסט עמום.)
**תקדימים מהקורפוס הפנימי:**
- [אם נמצאו]
@@ -201,8 +292,21 @@ tools:
### סוגיה 2: ...
## 7. מסקנות
סיכום האסטרטגיה, נקודות חוזק, סיכונים, סדר עדיפויות.
## 6א. טיפול בטענות
**טענות לקיבוץ:**
- ...
**טענות לדילוג:**
- ...
**טענות שחייבות מענה פרטני:**
- ...
## 7. סיכום
- **שאלות פתוחות**: שאלות שנותרו ללא מענה ודורשות מחקר או הנחיית יו
- **סדר דיון מומלץ**: הסדר המומלץ לדיון בסוגיות בהחלטה
- **תלויות**: סוגיות שהכרעתן תלויה בהכרעה בסוגיה אחרת
- **הערכה כללית**: לאן נוטה הניתוח ומהם הסיכויים הכלליים של הערר
```
## כללים קריטיים
@@ -213,3 +317,5 @@ tools:
4. **לא להמציא** — לא פסיקה, לא ציטוטים, לא מספרי תיקים שלא מופיעים במסמכים
5. **שאלות מחקר הן התוצר המרכזי** — הקדש להן תשומת לב מיוחדת
6. **אם חסר מידע** — ציין במפורש ובקש להעלות מסמכים נוספים
7. **היררכיית מקורות** — חקיקה/תכניות קודמים לתקדימים. התחל מלשון הטקסט הנורמטיבי; תקדים נדרש רק כשהטקסט עמום
8. **הפרדת עובדות ממסקנות** — ממצא עובדתי ("הבניה במרחק 1.5 מטרים") נפרד ממסקנה משפטית ("חריגה זו עולה כדי סטייה ניכרת"). אל תערבב

View File

@@ -35,6 +35,16 @@ tools:
אתה מתזמר את כל תהליך כתיבת ההחלטה. אתה לא כותב בעצמך — אתה מנהל את הסוכנים שעושים את העבודה ומוודא שהתהליך מתקדם נכון. **אתה עובד אינטראקטיבית מול חיים דרך Paperclip comments.**
## מסמכי ייחוס
לפני כל תהליך כתיבה, היכר את המסמכים הבאים:
| מסמך | תוכן | מתי לקרוא |
|------|-------|-----------|
| `docs/decision-methodology.md` | מתודולוגיה אנליטית — סילוגיזמים, סדר סוגיות, איזון | **לפני כל החלטה** |
| `docs/block-schema.md` | הגדרת 12 בלוקים — content model, constraints | **לפני כל החלטה** |
| `docs/legal-decision-lessons.md` | לקחים מ-3 החלטות — מה עבד, מה השתנה | **לפני כל החלטה** |
## הסוכנים שלך
| סוכן | Agent ID | תפקיד |
@@ -42,22 +52,46 @@ tools:
| מגיה מסמכים | 410c0167-27dc-485c-a51b-7aa8b9ff2217 | הגהת OCR — תיקון ראשי תיבות ושגיאות חילוץ |
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | חילוץ טענות, תשובות, תגובות |
| חוקר תקדימים | 35022af0-0498-4c3d-90ca-b0ab9e987198 | ניתוח פסיקה, תכניות, פרוטוקולים |
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יא (Opus) |
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יב (Opus) |
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
| מייצא טיוטה | d0dc703b-ca83-4883-bca7-c9449e8713cd | בדיקה סופית + ייצוא DOCX מגורסת |
## תהליך אינטראקטיבי — שלב אחר שלב
### שלב A: בדיקת מצב
### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
בכל heartbeat:
1. בדוק תיקים פעילים (`case_list`)
2. לכל תיק — בדוק סטטוס + מה כבר בוצע:
- יש טענות מחולצות? (`get_claims`)
- יש comments מחיים שממתינים לתגובה?
3. פעל לפי מפת הסטטוסים למטה
2. בדוק אם יש issues ב-"blocked" — אם כן, טפל בהם קודם
3. בדוק comments מחיים שממתינים לתגובה
4. **לפני מעבר לשלב B — בצע את כל הבדיקות למטה. אם בדיקה נכשלת — עצור.**
### שלב B: הכנת סיכום ושאלת תוצאה
#### A1. בדיקת שלמות חילוץ
- **כמה מסמכים בתיק?** (`document_list`) — ספור.
- **האם כל המסמכים מסוג appeal/response/reply חולצו?** — בדוק extraction_status. אם יש מסמך שנכשל → **עצור**. צור issue למנתח לתיקון.
- **האם כל מסמך שחולץ ייצר טענות?** — אם מסמך מסוג appeal/response ייצר 0 טענות → **עצור**. אין להמשיך עם מידע חלקי.
#### A2. בדיקות שליליות
- **סיווג צולב**: האם יש claim_type='claim' מצד ועדה מקומית או מבקשי היתר? → שגיאת סיווג. החזר למנתח.
- **כמות חריגה**: האם יש צד עם >30 טענות (claim_type='claim')? → ייתכן חוסר סינתוז. בדוק ודווח.
- **צד חסר**: האם יש צד שאין לו אף טענה? → חריגה.
- **מסמך ריק**: האם יש מסמך appeal/response עם טקסט שלא ייצר טענות ולא דווח ככשל?
#### A3. אימות תאימות מתודולוגיה
קרא את `analysis-and-research.md` ובדוק:
- [ ] סוגיות מנוסחות כסילוגיזם (כלל + עובדות + שאלה)?
- [ ] ממצאים עובדתיים מופרדים ממסקנות משפטיות?
- [ ] לכל סוגיה יש "סוג ניתוח" (כלל ברור / איזון / מידתיות)?
- [ ] לכל סוגיה יש "הכנה ל-CREAC" (כלל, עובדות, תקדים)?
- [ ] יש steel-man (הנקודה החזקה של הצד החלש)?
- [ ] יש סעיף "טיפול בטענות" (bundle/skip)?
- [ ] היררכיית מקורות: חקיקה לפני תקדימים?
**אם בדיקה כלשהי נכשלת → אל תמשיך לשלב B.** צור issue למנתח עם הנחיה ספציפית, ופרסם comment שמסביר מה חסר.
**עיקרון מנחה:** עדיף לעכב את התהליך מאשר לייצר החלטה על בסיס חלקי או פגום.
### שלב B: הכנת סיכום, סיווג, ושאלת תוצאה
**מתי:** כשיש טענות מחולצות + מחקר תקדימים, אבל אין תוצאה עדיין
@@ -66,18 +100,38 @@ tools:
```
## סיכום תיק {case_number} — מוכן להחלטה
### סיווג
- **סוג ערר:** {רישוי (1xxx) / היטל השבחה (8xxx) / פיצויים ס' 197 (9xxx)}
- **תקן ביקורת:** {שיקול דעת תכנוני עצמאי / ביקורת שומה מכרעת / ...}
### טענות מרכזיות של העוררים
[3-5 טענות עיקריות מ-get_claims עם claim_type=claim]
### תשובות המשיבים
[3-5 תשובות עיקריות מ-get_claims עם claim_type=response]
### עמדת הוועדה
[2-3 עמדות מ-get_claims עם claim_type=response ו-party_role=committee]
### החלטת הוועדה המקומית (=מושא הערר)
[ההחלטה שעליה מוגש הערר — מה הוועדה המקומית החליטה ומדוע]
### תגובת הוועדה המקומית (=ההגנה)
[עמדת הוועדה המקומית בהליך הערר — הנימוקים שלה מדוע החלטתה נכונה]
### תקדימים רלוונטיים
[מתוך comments קודמים של חוקר תקדימים]
### שאלות מרכזיות לדיון
[נסח כל שאלה כסילוגיזם מכווץ, בהתאם למתודולוגיה §א.3]
1. **{ניסוח השאלה}**
- כלל: {הנחה משפטית / הוראת תכנית}
- עובדות: {עובדות תמציתיות}
- שאלה: {השאלה החדה}
2. **{ניסוח השאלה}**
- כלל: ...
- עובדות: ...
- שאלה: ...
---
**מה התוצאה הצפויה?**
@@ -88,29 +142,94 @@ tools:
@chaim — הגב עם מספר (1/2/3) + הערות אם יש
```
### שלב C: קליטת תוצאה וסיעור מוחות
לאחר שחיים בחר תוצאה, שאל אותו לסמן טיפול בכל טענה:
**מתי:** חיים הגיב עם מספר תוצאה
```
## טיפול בטענות — {case_number}
סמן לכל טענה את סוג הטיפול:
| # | טענה | טיפול |
|---|------|-------|
| 1 | {טענה 1} | דיון מלא / קיבוץ / דילוג |
| 2 | {טענה 2} | דיון מלא / קיבוץ / דילוג |
| 3 | {טענה 3} | דיון מלא / קיבוץ / דילוג |
| ... | ... | ... |
**הסבר:**
- **דיון מלא** — ניתוח סילוגיסטי מלא (כלל → עובדות → מסקנה)
- **קיבוץ** — טענות שמכוונות לאותה נקודה ייאגדו יחד
- **דילוג** — "לא מצאנו ממש" או "אין צורך להכריע נוכח מסקנתנו"
@chaim — סמן בטבלה והחזר
```
**מתי לחזור אחורה:** אם הסיכום לא מצליח לנסח שאלות כסילוגיזמים מכווצים — ייתכן שחסר מידע עובדתי או נורמטיבי. חזור למנתח/חוקר להשלמה.
### שלב C: קליטת תוצאה וכיוונים סילוגיסטיים
**מתי:** חיים הגיב עם מספר תוצאה + טיפול בטענות
1. קרא את ה-comment של חיים
2. זהה את הבחירה (1=rejected, 2=partial, 3=accepted)
3. הרץ `set_outcome(case_number, outcome, reasoning)`
4. **בעצמך** חשוב על 2-3 כיוונים לנימוק — אתה כבר Claude, אתה יודע את הטענות והתקדימים. **אל תקרא ל-brainstorm_directions** (זה מפעיל claude בתוך claude ולוקח יותר מדי זמן).
5. פרסם comment:
4. **חשוב סילוגיסטית** על 2-3 כיוונים לנימוק — אתה כבר Claude, אתה יודע את הטענות והתקדימים. בנה כל כיוון כסילוגיזם מלא.
> **הערה טכנית:** אל תקרא ל-`brainstorm_directions` — זה מפעיל Claude בתוך Claude ולוקח יותר מדי זמן.
5. פרסם comment עם **סדר סוגיות מוצע**:
```
## כיוונים אפשריים לנימוק — {outcome_hebrew}
### סדר הסוגיות המוצע
1. {שאלת סף — אם רלוונטית}
2. {הסוגיה המכריעה}
3. {סוגיות נוספות לפי חוזק}
---
### כיוון 1: {title}
{description — 3-4 משפטים}
**כלל (הנחה עליונה):**
{הוראת תכנית / סעיף חוק / הלכה פסוקה}
**עובדות (הנחה תחתונה):**
{העובדות הספציפיות של הערר שנבחנות לאור הכלל}
**מסקנה:**
{התוצאה שנובעת מהחלת הכלל על העובדות}
**תקדימים תומכים:** {precedents}
---
### כיוון 2: {title}
{description}
**כלל (הנחה עליונה):**
{...}
**עובדות (הנחה תחתונה):**
{...}
**מסקנה:**
{...}
**תקדימים תומכים:** {precedents}
---
### כיוון 3: {title}
{description}
**כלל (הנחה עליונה):**
{...}
**עובדות (הנחה תחתונה):**
{...}
**מסקנה:**
{...}
**תקדימים תומכים:** {precedents}
---
@@ -119,18 +238,28 @@ tools:
אפשר גם לשלב כיוונים או להוסיף הערות.
```
**מתי לחזור אחורה:** אם לא ניתן לבנות סילוגיזם מלא (חסר כלל, חסרות עובדות, או המסקנה לא נובעת) — חזור לחוקר תקדימים או למנתח להשלמת החסר.
### שלב D: אישור כיוון והפעלת כתיבה
**מתי:** חיים הגיב עם בחירת כיוון
1. קרא את ה-comment של חיים
2. זהה כיוון (1/2/3) + הערות נוספות
3. הרץ `approve_direction(case_number, direction_index, additional_notes)`
4. צור issue חדש ב-Paperclip:
3. **אימות שלמות chair_directions** — לפני שליחה לכותב, ודא:
- [ ] טיפול בטענות (דיון מלא / קיבוץ / דילוג) מוגדר לכל טענה
- [ ] כיוון סילוגיסטי נבחר ומאושר
- [ ] סדר סוגיות מוגדר
- [ ] תקן ביקורת מצוין
- אם חסר פריט כלשהו — **שאל את חיים** לפני שממשיכים
4. הרץ `approve_direction(case_number, direction_index, additional_notes)`
5. צור issue חדש ב-Paperclip:
- כותרת: `[ערר {case_number}] כתיבת החלטה`
- הקצה ל: **כותב החלטה** (7ed8686f-24bc-49a3-bc02-67ca15b895a9)
5. פרסם comment: "כיוון אושר. הועבר לכותב החלטה."
6. עדכן סטטוס: `case_update(status=direction_approved)`
6. פרסם comment: "כיוון אושר. הועבר לכותב החלטה."
7. עדכן סטטוס: `case_update(status=direction_approved)`
**מתי לחזור אחורה:** אם חיים שינה דעתו לגבי התוצאה או הכיוון, או אם חסר מידע — חזור לשלב B או C בהתאם.
### שלב E: מעקב כתיבה
@@ -140,6 +269,8 @@ tools:
1. צור issue: `[ערר {case_number}] בדיקת איכות`
2. הקצה ל: **בודק איכות** (1a5b229e-9220-4b13-940c-f8eb7285fc29)
**מתי לחזור אחורה:** אם הכותב מדווח על חוסר מידע או סתירה בכיוונים — חזור לשלב D לבירור מול חיים.
### שלב F: QA וייצוא
**מתי:** בודק איכות סיים
@@ -149,19 +280,25 @@ tools:
3. פרסם comment: "החלטה מוכנה לביקורת דפנה. [קישור ל-DOCX]"
4. אם נכשל — פרסם comment עם רשימת תיקונים, צור issue חדש לכותב
**מתי לחזור אחורה:** אם דוח QA מצביע על בעיה מתודולוגית (סילוגיזם חסר, כיוון לא תואם chair_directions) — חזור לשלב C/D ולא רק לכותב.
## מפת סטטוסים
| סטטוס | פעולה |
|--------|-------|
| new + יש מסמכים + לא הוגהו | → צור issue למגיה מסמכים (410c0167) |
| new + מסמכים הוגהו + אין claims | → צור issue למנתח משפטי |
| new + יש claims + יש מחקר | → שלב B (סיכום + שאלת תוצאה) |
| outcome_set | → שלב C (brainstorm) |
| brainstorming + comment מחיים | → שלב D (approve + הפעל כותב) |
| direction_approved | → ודא שכותב עובד |
| new + יש claims + לא עבר אימות מנתח | → שלב A (אימות איכות פלט מנתח) |
| analyst_verified + יש claims + יש מחקר | → שלב B (סיכום + סיווג + שאלת תוצאה) |
| outcome_set + אין claim_handling | → שלב B המשך (טבלת טיפול בטענות) |
| outcome_set + יש claim_handling | → שלב C (כיוונים סילוגיסטיים) |
| brainstorming + comment מחיים | → שלב D (אימות שלמות + approve + הפעל כותב) |
| direction_approved + chair_directions שלם | → ודא שכותב עובד |
| direction_approved + chair_directions חסר | → חזור לשלב D (השלמה מול חיים) |
| drafted | → צור issue לבודק איכות |
| qa_review pass | → שלב F (export via מייצא טיוטה d0dc703b) |
| qa_review fail | → צור issue תיקון לכותב |
| qa_review fail — בעיה טכנית | → צור issue תיקון לכותב |
| qa_review fail — בעיה מתודולוגית | → חזור לשלב C/D |
## כללים
@@ -170,6 +307,7 @@ tools:
- **לא לכתוב בלוקים** — רק כותב ההחלטה
- **תמיד לדווח** — כל פעולה = comment ב-Paperclip
- **לשאול כשלא בטוח** — אם משהו לא ברור, שאל את חיים
- **ודא עקביות מתודולוגית** — כיוונים סילוגיסטיים (כלל + עובדות + מסקנה), chair_directions שלם (טיפול בטענות + כיוון + סדר סוגיות + תקן ביקורת), התאמה ל-`decision-methodology.md`
## איך לקרוא comments של חיים
@@ -182,5 +320,6 @@ curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
חפש ב-comment:
- מספר (1/2/3) → בחירה
- "כיוון" + מספר → אישור כיוון
- טבלת טיפול בטענות → סימון claim_handling
- שאלה → ענה
- הערה → שלב בתהליך

View File

@@ -43,7 +43,7 @@ tools:
### שלב 1: זיהוי התיק
1. קבל את מספר התיק מה-issue או מהמשתמש
2. קרא פרטי תיק (`case_get`)
3. בדוק סטטוס workflow (`workflow_status`) — ודא שהכתיבה הושלמה
3. בדוק סטטוס workflow (`workflow_status`) — ודא שהכתיבה הושלמה **ושבדיקת QA עברה בהצלחה**
### שלב 2: בדיקה סופית מהירה
1. הרץ `validate_decision` — בדוק שאין כשלים קריטיים
@@ -51,6 +51,7 @@ tools:
3. בדוק רצף מספור — שהמספור רציף מ-1 עד סוף ללא קפיצות או כפילויות
4. בדוק שאין placeholders ריקים (כמו `[...]`, `XXX`, `___`)
5. אם יש בעיות קריטיות — דווח למשתמש ואל תייצא
6. בדוק שסטטוס ה-QA הוא "passed" — אם ה-QA לא רץ או נכשל, **אל תייצא**
### שלב 3: ייצוא DOCX
1. קרא את סקייל legal-docx (SKILL.md) כדי להבין את דרישות העיצוב

View File

@@ -37,9 +37,10 @@ tools:
- רק עובדות: תיאור נכס, היסטוריה תכנונית, החלטת ועדה
### 3. כיסוי טענות (claims_coverage)
- כל טענה מבלוק ז נענתה בבלוק י
- גם אם בניסוח שונה — העיקר שנדונה
- **קריטי** — אם טענה לא נענתה, ה-QA נכשל
- כל טענה מהותית מבלוק ז קיבלה מענה בבלוק י (ישיר, קיבוץ, או ציון שנבחנה)
- טענות שסומנו [skip] ב-chair_directions — לא נספרות
- טענות שסומנו [bundle] — נבדקות כקבוצה: אם הנושא טופל, כולן עוברות
- **קריטי** — אם טענה מהותית ללא סימון לא נענתה, ה-QA נכשל
### 4. משקלות בטווח (weight_compliance)
- בלוק ו (רקע): 15-40%
@@ -56,6 +57,15 @@ tools:
- סעיפים 1, 2, 3... ללא איפוס בין בלוקים
- ללא כפילויות במספור
### 7. עמידה במתודולוגיה (methodology_compliance)
ראה `docs/decision-methodology.md` לעקרונות המלאים. בדוק:
- לכל סוגיה בבלוק י — ניתן לזהות מבנה סילוגיסטי: כלל + עובדות + מסקנה?
- ממצאים עובדתיים מופרדים ממסקנות משפטיות (לא מעורבבים)?
- טענה מרכזית של הצד המפסיד קיבלה מענה הוגן (Steel-Man — הוצגה בחוזקתה)?
- כשנדרש איזון — יש ניתוח מפורש (אינטרסים, השלכות, הכרעה)?
- אין "נוסחאות ריקות" (משפטים שמחיקתם לא משנה כלום)?
- ציטוטים עטופים בסנדוויץ' (הקדמה → ציטוט → ניתוח)?
## חומרה
| בדיקה | חומרה | משמעות |
@@ -66,6 +76,7 @@ tools:
| משקלות | warning | מדווח, לא חוסם |
| כפילות | warning | מדווח, לא חוסם |
| מספור | warning | מדווח, לא חוסם |
| מתודולוגיה | warning | מדווח, לא חוסם |
## תהליך עבודה
@@ -74,11 +85,19 @@ tools:
2. הרץ בדיקת איכות (`validate_decision`)
3. קבל מדדים (`get_metrics`)
### שלב 2: בדיקה ידנית
### שלב 2: בדיקה ידנית — חיובית
1. קרא את בלוק ו — בדוק ניטרליות
2. השווה טענות בבלוק ז מול דיון בבלוק י — בדוק כיסוי
3. בדוק מספור רציף
### שלב 2ב: בדיקות שליליות — מה חסר? מה לא הגיוני?
1. האם יש סוגיה מה-analysis-and-research.md שלא קיבלה מענה בדיון?
2. האם יש ציטוט ארוך ללא סנדוויץ' (הקדמה + ציטוט + ניתוח)?
3. האם יש "נוסחאות ריקות" — משפטים שמחיקתם לא משנה כלום?
4. האם יש פסקה בדיון ללא משפט נושא (פתיחה שלא מודיעה על הנקודה)?
5. האם יש ממצא עובדתי ומסקנה משפטית מעורבבים באותו משפט?
6. האם יש אנלוגיה לתקדים ללא הסבר מדיניות (למה הדמיון רלוונטי)?
### שלב 3: דיווח — חובה!
פרסם comment ב-Paperclip עם:
- תוצאת כל בדיקה (pass/fail)

View File

@@ -27,6 +27,11 @@ tools:
עבוד תמיד בעברית.
## לפני שאתה מתחיל — קרא!
1. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה)
2. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
## סוגי מסמכים שאתה מטפל בהם
| סוג מסמך | מה לעשות |
@@ -52,12 +57,18 @@ tools:
לכל פסק דין:
1. קרא את הטקסט (`document_get_text`)
2. סכם: עובדות, שאלה משפטית, הכרעה, רלוונטיות לתיק שלנו
3. הפק הפניות (`extract_references`)
3. בנוסף ציין:
- **רמת התקדים**: עליון / מנהלי / ועדת ערר ארצית / ועדת ערר מחוזית
- **הלכה מחייבת או אמרת אגב**
- **כיצד ישרת את מבנה ההנמקה**: כ"כלל" (הנחה עליונה), כ"הרחבה" (Explanation ב-CREAC), או כאנלוגיה
4. הפק הפניות (`extract_references`)
### שלב 3: מיפוי תכנית
1. קרא הוראות התכנית
1. קרא הוראות התכנית **במלואן** — לא רק את הסעיף הנטען
2. זהה סעיפים רלוונטיים למחלוקת
3. ציין: ייעוד, זכויות בנייה, מגבלות, חניה
3. **צטט את לשון ההוראות הרלוונטיות** — הנוסח המדויק, לא סיכום (המתודולוגיה דורשת: "התחל מלשון הטקסט")
4. סמן **עמימויות או סתירות** בין הוראות באותה תכנית
5. ציין: ייעוד, זכויות בנייה, מגבלות, תנאים
### שלב 4: סיכום פרוטוקולים והחלטות
1. קרא כל פרוטוקול והחלטת ביניים
@@ -68,7 +79,10 @@ tools:
- סיכום כל פסק דין (2-3 שורות לכל אחד)
- מיפוי הוראות תכנית רלוונטיות
- ציר זמן ההליך
- המלצה: אילו תקדימים הכי חזקים, אילו סעיפי תכנית מרכזיים
- **המלצה מובנית לפי מקורות הנמקה:**
- **טקסט**: אילו סעיפי תכנית/חוק מרכזיים (ציטוט הנוסח)
- **תקדים**: אילו פסקי דין הכי חזקים (עם ציון היררכיה ומעמד — הלכה/אגב)
- **מדיניות**: אילו שיקולים תכנוניים עולים מהחומר
## כללים
- **דיוק** — ציין מספרי סעיפים, תאריכים, שמות שופטים

View File

@@ -34,9 +34,10 @@ tools:
## לפני שאתה מתחיל — קרא!
1. מדריך סגנון: `skills/decision/SKILL.md`
2. ארכיטקטורת 12 בלוקים: `docs/block-schema.md`
3. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
1. **מתודולוגיה אנליטית: `docs/decision-methodology.md`** — איך לחשוב על החלטה
2. מדריך סגנון: `skills/decision/SKILL.md` — איך דפנה כותבת
3. ארכיטקטורת 12 בלוקים: `docs/block-schema.md`
4. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
## ארכיטקטורת 12 בלוקים
@@ -145,11 +146,35 @@ case_update(case_number, status="drafted")
## בלוק י — דיון (הבלוק החשוב ביותר)
- מבנה CREAC: מסקנה בפתיחה → כלל → הסבר → יישום → מסקנה
- ענה על כל טענה מבלוק ז
- השתמש בציטוטים ארוכים (200-600 מילים) מפסיקה
- אל תחזור על עובדות מבלוק ו
- אל תשתמש בכותרות משנה (למעט נושאים נפרדים לחלוטין)
**עקוב אחר `docs/decision-methodology.md` — שלבי הניתוח:**
### שלב א: פסקת מפה
פתח בפסקה שמודיעה מה ייבחן: "שלוש שאלות עומדות להכרעה: (1)...; (2)...; (3)..."
### שלב ב: סוגיות סף (אם רלוונטיות)
אם עולה שאלת סף — היא נדונה ראשונה. אם נדחית — פסקה אחת ועבור לגוף.
### שלב ג: לכל סוגיה — מבנה סילוגיסטי (CREAC)
1. **מסקנה** — פתח בתשובה
2. **כלל** — ציטוט הוראת תכנית/חוק (התחל מלשון הטקסט, לא מפסיקה)
3. **הרחבה** — תקדים רלוונטי אחד (טכניקת סנדוויץ': הקדמה→ציטוט→ניתוח)
4. **יישום** — החל את הכלל על העובדות. הפרד ממצא עובדתי ממסקנה משפטית. השתמש בנתונים (מספרים, מידות, אחוזים).
5. **Steel-Man** — הצג את הטענה הטובה ביותר של הצד המפסיד: "אמנם צודק העורר כי..., אולם..."
6. **מסקנה חוזרת** — סגור
### שלב ד: איזון (כשנדרש)
אם אין כלל ברור — בנה איזון: זהה אינטרסים קונקרטיים → בחן השלכות לכל כיוון → שקול השלכות מערכתיות → הכרע.
### שלב ה: טענות נותרות
- טענות מרכזיות ללא סימון: מענה פרטני
- טענות שסומנו [bundle] ב-chair_directions: קבץ ודון יחד
- טענות שסומנו [skip] ב-chair_directions: "נבחנה ולא מצאנו בה ממש"
- טענות חלשות: קיבוץ. "באשר לטענות הנוספות — לא מצאנו בהן ממש"
### כללים נוספים
- אל תחזור על עובדות מבלוק ו — הפנה: "כאמור בסעיף X לעיל"
- כל מילה עובדת — אין "לאחר ששקלנו את כלל השיקולים"
- כנות לגבי קושי — "הדבר אינו נקי מספקות, אולם..."
### חובה: שימוש בעמדות יו"ר מ-`get_chair_directions`

View File

@@ -1,20 +1,20 @@
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-opus-4-20250514",
"maxTokens": 64000,
"provider": "claude-code",
"modelId": "opus",
"maxTokens": 32000,
"temperature": 0.2
},
"research": {
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"maxTokens": 64000,
"provider": "claude-code",
"modelId": "opus",
"maxTokens": 32000,
"temperature": 0.1
},
"fallback": {
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
}

3
.taskmaster/state.json Normal file
View File

@@ -0,0 +1,3 @@
{
"migrationNoticeShown": true
}

View File

@@ -703,13 +703,218 @@
"status": "deferred",
"subtasks": [],
"updatedAt": "2026-04-03T10:19:26.779Z"
},
{
"id": "83",
"title": "Phase 1 — Project setup (legal-ai UI rewrite)",
"description": "הקמת scaffold של Next.js עם TypeScript + Tailwind v4 + App Router ב-web-ui/. התקנת כל התלויות: @tanstack/react-query, @tanstack/react-table, react-hook-form, @hookform/resolvers, zod, lucide-react, react-dropzone, openapi-typescript. העברת design-system.css tokens (navy/gold/parchment, Heebo) ל-Tailwind theme דרך @theme ו-CSS variables. הגדרת RTL עברית עם Heebo via next/font/google. בניית AppShell עם navy header + gold rule + nav.",
"status": "done",
"dependencies": [],
"priority": "high",
"details": "**השלמות (2026-04-11):**\n\n✅ **Scaffold:** Next.js 16.2.3 (חדש יותר מ-v15 שתוכנן), React 19.2.4, Tailwind v4, Turbopack.\n\n✅ **תלויות מותקנות** (`web-ui/package.json:11-21`):\n- @tanstack/react-query ^5.97.0\n- @tanstack/react-table ^8.21.3\n- react-hook-form ^7.72.1 + @hookform/resolvers ^5.2.2\n- zod ^4.3.6\n- lucide-react ^1.8.0\n- react-dropzone ^15.0.0\n- openapi-typescript ^7.13.0 (devDep)\n\n✅ **Design tokens** (`web-ui/src/app/globals.css:10-107`): Tailwind v4 @theme עם כל הצבעים (navy, cream, parchment, gold, ink, status colors), radii, shadows, fonts, dark mode preserved.\n\n✅ **RTL Hebrew** (`web-ui/src/app/layout.tsx:5-10, 23`): Heebo עם hebrew+latin subsets, `lang=\"he\" dir=\"rtl\"` on html.\n\n✅ **AppShell** (`web-ui/src/components/app-shell.tsx:29-70`): Navy header עם gold border-b-3, RTL nav, parchment body.\n\n✅ **Home page placeholder** (`web-ui/src/app/page.tsx`).\n\n✅ **Build:** `npm run build` עובר ב-3.8s, 0 errors, static.\n\n**נותר:** אישור ויזואלי של המשתמש עם `npm run dev`.\n\n**תוכנית מלאה:** `~/.claude/plans/joyful-marinating-sutton.md`",
"testStrategy": "1. `cd web-ui && npm run dev` — פתיחה ב-http://localhost:3000\n2. וידוא ויזואלי: Header navy עם gold rule, RTL rendering, פונט Heebo טעון\n3. השוואה ל-legal-ai.nautilus.marcusgroup.org — אותו מראה header\n4. בדיקת dark mode (toggle class על html)\n5. אישור סופי מהמשתמש",
"subtasks": [
{
"id": 1,
"title": "יצירת Next.js 16 scaffold עם TypeScript + Tailwind v4 + App Router",
"description": "הרצת create-next-app ב-web-ui/ עם App Router, TypeScript, Tailwind v4, ESLint",
"dependencies": [],
"details": "Next.js 16.2.3 (חדש יותר מ-v15), React 19.2.4, Tailwind v4, Turbopack. קבצים: web-ui/package.json, tsconfig.json, next.config.ts",
"status": "done",
"testStrategy": "npm run build succeeds",
"parentId": "undefined"
},
{
"id": 2,
"title": "התקנת כל התלויות הנדרשות",
"description": "npm install של @tanstack/react-query, @tanstack/react-table, react-hook-form, @hookform/resolvers, zod, lucide-react, react-dropzone, openapi-typescript",
"dependencies": [
1
],
"details": "ראה web-ui/package.json:11-21 לגרסאות המותקנות. כולל openapi-typescript כ-devDep לשלב 2.",
"status": "done",
"testStrategy": "npm ls shows all packages",
"parentId": "undefined"
},
{
"id": 3,
"title": "העברת design tokens ל-Tailwind v4 @theme",
"description": "פורט מלא של design-system.css ל-globals.css עם Tailwind v4 @theme syntax",
"dependencies": [
1
],
"details": "web-ui/src/app/globals.css:10-107 כולל כל הצבעים (navy, cream, parchment, gold, ink), status colors, radii, shadows, fonts, dark mode. CSS variables עובדים עם Tailwind classes.",
"status": "done",
"testStrategy": "Tailwind classes like bg-navy, text-gold work correctly",
"parentId": "undefined"
},
{
"id": 4,
"title": "הגדרת RTL Hebrew עם Heebo font",
"description": "next/font/google Heebo עם hebrew+latin, lang=he dir=rtl על html",
"dependencies": [
1
],
"details": "web-ui/src/app/layout.tsx:5-10 — Heebo עם weights 300-900, display swap. שורה 23: html lang=he dir=rtl.",
"status": "done",
"testStrategy": "Page renders RTL, Heebo font loaded",
"parentId": "undefined"
},
{
"id": 5,
"title": "בניית AppShell component עם navy header + gold rule",
"description": "רכיב shell עם header navy, gold border, RTL nav, parchment body",
"dependencies": [
3,
4
],
"details": "web-ui/src/components/app-shell.tsx:29-70 — Header עם bg-navy, border-b-3 border-gold, nav links (בית, העלאת מסמכים, אימון סגנון, מיומנויות, אבחון), main content area עם max-w-1400px.",
"status": "done",
"testStrategy": "Visual match to current header",
"parentId": "undefined"
},
{
"id": 6,
"title": "יצירת דף בית placeholder",
"description": "דף page.tsx עם AppShell ו-placeholder content",
"dependencies": [
5
],
"details": "web-ui/src/app/page.tsx:1-27 — דף בית עם כותרת 'עוזר משפטי', תיאור המערכת, gold gradient divider, כרטיס סטטוס.",
"status": "done",
"testStrategy": "npm run build succeeds (done: 3.8s, 0 errors)",
"parentId": "undefined"
},
{
"id": 7,
"title": "אישור ויזואלי מהמשתמש — npm run dev",
"description": "הרצת dev server ואישור סופי שה-UI תואם לציפיות — header, RTL, fonts, colors",
"dependencies": [
6
],
"details": "המשתמש צריך להריץ 'cd web-ui && npm run dev' ולאשר שהכל נראה כמו legal-ai.nautilus.marcusgroup.org. בדיקת dark mode אופציונלית.",
"status": "pending",
"testStrategy": "User confirms visual parity with current site, RTL works, Heebo font loads",
"parentId": "undefined"
}
],
"updatedAt": "2026-04-11T13:50:47.941Z"
},
{
"id": "84",
"title": "Phase 2 — API client + generated TypeScript types",
"description": "Add npm run api:types script that runs openapi-typescript against FastAPI's /openapi.json -> src/lib/api/types.ts. Build lib/api/client.ts (typed fetch wrapper + TanStack Query client with default retry/staleTime). Create one lib/api/<domain>.ts per endpoint category (cases, upload, compose, training, system), each exporting typed useQuery/useMutation hooks. Build lib/sse.ts as EventSource -> Query cache adapter. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 2 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "useCases() hook returns typed array from live FastAPI. TypeScript errors if backend endpoint changes without frontend update.",
"status": "done",
"dependencies": [
"83"
],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-11T15:51:34.020Z"
},
{
"id": "85",
"title": "Phase 3 — Core read views (home, case detail, compose)",
"description": "Port the 3 highest-value screens. Use the frontend-design Claude Code skill to generate layout + composition, passing design tokens (navy/gold/parchment, Heebo), editorial voice, and typed API hooks. Use shadcn Card/Badge/Tabs/Sheet/ScrollArea as primitives. Port the custom donut chart into <DonutChart> component. TanStack Query staleTime:5000 for case detail replaces manual 5s polling. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 3 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "Users can browse case list, open a case detail, and view the compose screen with live data from FastAPI. All 3 screens visually match the existing legal-ai identity.",
"status": "done",
"dependencies": [
"84"
],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-11T16:09:18.006Z"
},
{
"id": "86",
"title": "Phase 4 — Forms and wizards (new case, upload, inline edits)",
"description": "Port new case wizard, bulk upload, inline forms on case detail. Use react-hook-form + zod with schemas in lib/schemas/<entity>.ts. Build shared <WizardShell> from shadcn Card + Progress + Tabs. Build <DropZone> (react-dropzone + shadcn). Integrate SSE for upload progress via lib/sse.ts. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 4 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "Users can create a new case via the multi-step wizard (case appears in Gitea + Paperclip), upload documents with live SSE progress, and edit case fields inline.",
"status": "done",
"dependencies": [
"85"
],
"priority": "medium",
"subtasks": [],
"updatedAt": "2026-04-11T16:25:55.569Z"
},
{
"id": "87",
"title": "Phase 5 — Secondary screens (compare, training, style report, skills, diagnostics)",
"description": "Port the remaining 5 views. Use TanStack Table for training corpus and diagnostics lists. Port any charts/visualizations from current index.html. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 5 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "Feature parity with old legal-ai/web/static/index.html across all 10 views.",
"status": "done",
"dependencies": [
"86"
],
"priority": "medium",
"subtasks": [],
"updatedAt": "2026-04-11T17:33:42.976Z"
},
{
"id": "88",
"title": "Phase 6 — Polish & testing",
"description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
"status": "done",
"dependencies": [
"87"
],
"priority": "medium",
"subtasks": [],
"updatedAt": "2026-04-11T17:44:08.337Z"
},
{
"id": "89",
"title": "Phase 7 — Deployment & cutover",
"description": "Add multi-stage Dockerfile for web-ui/ (Node 20 build -> nginx serve of out/). Add web-ui as new app in Coolify project pointing to staging subdomain legal-ai-next.nautilus.marcusgroup.org. Run full smoke test against staging. Cutover: DNS flip legal-ai.nautilus.marcusgroup.org to new app, keep old on rollback subdomain for 1 week. Follow-up PR removes legal-ai/web/static/index.html + design-system.css once stable. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 7 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
"testStrategy": "legal-ai.nautilus.marcusgroup.org serves the new Next.js UI in production. Old UI accessible on rollback subdomain for 7 days. SSE streams working through Coolify proxy.",
"status": "pending",
"dependencies": [
"88"
],
"priority": "medium",
"subtasks": []
},
{
"id": "90",
"title": "Phase 4.5 — Practice area integration",
"description": "Add practice_area + appeal_subtype to the wizard, types, schema, case header, and cases table. Gap identified after backend commit 26d09d6 (multi-tenant axis) — new Next.js UI has zero integration while vanilla UI is fully wired. Plan: ~/.claude/plans/woolly-cooking-graham.md",
"details": "",
"testStrategy": "",
"status": "done",
"dependencies": [
"86"
],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-11T17:15:57.831Z"
},
{
"id": "91",
"title": "Precedent attachment in compose screen",
"description": "Add case_precedents table + FastAPI endpoints + MCP tools + Next.js compose UI for attaching legal precedents (quote + citation + optional archived PDF) to threshold_claims/issues and to the case as a whole. Plan: ~/.claude/plans/woolly-cooking-graham.md",
"details": "",
"testStrategy": "",
"status": "done",
"dependencies": [],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-11T19:20:56.040Z"
}
],
"metadata": {
"version": "1.0.0",
"lastModified": "2026-04-04T07:50:59.999Z",
"taskCount": 51,
"completedCount": 49,
"lastModified": "2026-04-11T19:20:56.040Z",
"taskCount": 60,
"completedCount": 57,
"tags": [
"master"
]

View File

@@ -42,6 +42,7 @@
| [`docs/block-schema.md`](docs/block-schema.md) | הגדרת 12 בלוקים — content model, constraints, processing params | **לפני כל כתיבת החלטה** |
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
| [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
| [`docs/decision-methodology.md`](docs/decision-methodology.md) | **מתודולוגיה אנליטית — איך לחשוב על החלטה מעין-שיפוטית** | **לפני כל כתיבת החלטה** |
| [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** |
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
@@ -108,6 +109,16 @@
3. **"ללא כפילות"** — בלוק י (דיון) מפנה לבלוקים קודמים, לא חוזר עליהם
4. **"טענות מקוריות בלבד"** — בלוק ז = מכתבי טענות מקוריים בלבד. השלמות → בלוק ח
5. **ארכיטקטורת 12 בלוקים** — ראה `docs/block-schema.md`
6. **צ'קליסט תוכן** — בלוק י מקבל צ'קליסט תוכן אוטומטי לפי סוג הערר (ראה `lessons.py: CONTENT_CHECKLISTS`)
## הערות יו"ר (Chair Feedback)
מנגנון לתיעוד הערות דפנה על טיוטות:
- **DB**: טבלת `chair_feedback` (case_id, block_id, feedback_text, category, lesson_extracted)
- **API**: `GET/POST /api/feedback`, `PATCH /api/feedback/{id}/resolve`
- **MCP tools**: `record_chair_feedback`, `list_chair_feedback`
- **UI**: דף ניהול ב-`/feedback` (ב-Next.js)
- **קטגוריות**: missing_content, wrong_tone, wrong_structure, factual_error, style, other
## יו"ר: עו"ד דפנה תמיר
- מדריך סגנון מלא: `skills/decision/SKILL.md`

View File

@@ -1,27 +1,40 @@
FROM python:3.12-slim
# ══════════════════════════════════════════════════════════════
# Dockerfile — Next.js 16 web-ui (ui-rewrite branch only)
#
# This file REPLACES the FastAPI Dockerfile on this branch so that
# Coolify's default /Dockerfile lookup builds the new Next.js staging
# UI. The FastAPI Dockerfile lives on `main` and is unaffected.
#
# When the rewrite is merged to main, decide between:
# (a) keeping both via separate Dockerfiles + dockerfile_location config, or
# (b) a multi-stage Dockerfile that serves both, or
# (c) fully replacing FastAPI's StaticFiles with this Next.js front end.
# ══════════════════════════════════════════════════════════════
FROM node:20-alpine AS deps
WORKDIR /app
COPY web-ui/package.json web-ui/package-lock.json ./
RUN npm ci --no-audit --no-fund
# System deps for PyMuPDF and document processing
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libmupdf-dev libfreetype6-dev libharfbuzz-dev libjpeg62-turbo-dev \
libopenjp2-7-dev curl git && rm -rf /var/lib/apt/lists/* && \
git config --global init.defaultBranch main
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY web-ui/ ./
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Copy Ezer Mishpati MCP server source
COPY mcp-server/pyproject.toml /app/mcp-server/pyproject.toml
COPY mcp-server/src/ /app/mcp-server/src/
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# Install MCP server + web deps
RUN pip install --no-cache-dir /app/mcp-server && \
pip install --no-cache-dir fastapi uvicorn python-multipart
# next.config.ts uses output: 'standalone', so we copy only the minimal runtime
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Copy web app
COPY web/ /app/web/
EXPOSE 3000
ENV PYTHONPATH=/app/mcp-server/src
ENV DOTENV_PATH=/dev/null
EXPOSE 8080
CMD ["uvicorn", "web.app:app", "--host", "0.0.0.0", "--port", "8080"]
CMD ["node", "server.js"]

View File

@@ -371,6 +371,7 @@ Conclusion → Rule → Explanation → Application → Conclusion.
- MUST: מסקנה בפתיחת הדיון (לא בסוף)
- MUST: מענה לכל טענה שהוצגה בבלוק ז
- MUST: ציטוט פסיקה בבלוקים ארוכים (200-600 מילים)
- MUST: **צ'קליסט תוכן** — הפרומפט מזריק `{content_checklist}` אוטומטית לפי סוג הערר (מתוך `lessons.py: CONTENT_CHECKLISTS`). ראה `docs/corpus-analysis.md` לדפוסי תוכן לפי סוג.
- ⚠️ **MUST NOT ("ללא כפילות"):** חזרה על עובדות/טענות מבלוקים קודמים. השתמש בהפניות: "כאמור בסעיף X לעיל", "כפי שפורט", "כפי שציינו"
- MUST NOT: כותרות משנה (חריג: נושאים נפרדים לחלוטין)
- Dependencies: **ALL** previous blocks (ה-ט)

View File

@@ -173,14 +173,12 @@
- טיפולוגיה/טופוגרפיה → רק זעיתר
- תכנית אב כמסגרת → רק בית הכרם + תורן
### 5.3 פער: הפרומפט הנוכחי לא מכיל "צ'קליסט תוכן"
הפרומפט של block-yod (שורות 198-234 ב-block_writer.py) אומר:
-CREAC methodology
-ענה על כל טענה
-צטט פסיקה
- **אין**: "בתיק רישוי, כסה את הנושאים התכנוניים הרלוונטיים"
-**אין**: צ'קליסט תוכן לפי סוג ערר
-**אין**: "הקשר תכנוני רחב" כמרכיב חובה
### 5.3 ~~פער: הפרומפט הנוכחי לא מכיל "צ'קליסט תוכן"~~ — **נסגר (2026-04-12)**
נוספו:
-צ'קליסטים תוכניים לפי סוג ערר (`lessons.py: CONTENT_CHECKLISTS`) — מוזרקים לפרומפט
-מתודולוגיה אנליטית (`docs/decision-methodology.md`) — מלמדת איך לחשוב, לא רק מה לכסות
-טיפול גמיש בטענות (bundle/skip דרך chair_directions)
- ✅ בדיקת QA חדשה (methodology compliance)
### 5.4 פער: הבחנה לא מספיקה בין תת-סוגי רישוי
תיקי רישוי שונים מאוד זה מזה:

View File

@@ -0,0 +1,409 @@
# מתודולוגיית כתיבת החלטות — מדריך אנליטי לוועדת ערר לתכנון ובניה
מסמך זה מלמד כיצד לחשוב, לנתח ולבנות החלטה מנומקת. הוא אינו עוסק בסגנון הכתיבה של דפנה (ראה SKILL.md) ולא בנושאים שיש לכסות (ראה צ'קליסטים תוכניים). הוא עוסק בשיטה — כיצד להפוך חומרי מקור להנמקה משכנעת שתעמוד בביקורת שיפוטית.
---
## א. שלב מקדים — הבנת התיק לפני שנכתבת מילה
### א.1 קרא הכל, סכם, ואז חשוב
לפני שנכתב משפט אחד — קרא את כל חומרי המקור: כתב הערר, תגובת הוועדה המקומית, תגובת מבקשי ההיתר (אם יש), פרוטוקול הדיון, חוות דעת מומחים, ומסמכי תכנון רלוונטיים (תכנית, נספחים, החלטות ועדה מקומית).
**מה לעשות:**
- סמן את הטענות המרכזיות של כל צד. אל תסמוך על סיכום הצד — קרא את הנוסח המלא.
- זהה מהן העובדות שאינן שנויות במחלוקת ומהן העובדות השנויות במחלוקת.
- זהה את המסמכים הנורמטיביים הרלוונטיים (תכניות, חוקים, תקנות) וקרא אותם במלואם — לא רק את הסעיף הנטען. מילה בסעיף אחד מתפרשת לאור סעיפים אחרים באותו מסמך.
### א.2 סווג את הערר
סוג הערר קובע את מסגרת הניתוח:
- **ערר רישוי (1xxx)**: שאלת שיקול דעת תכנוני; הוועדה מפעילה שיקול דעת עצמאי.
- **ערר היטל השבחה (8xxx)**: שאלת שמאות ומשפט; ביקורת על שומה.
- **ערר פיצויים — סעיף 197 (9xxx)**: דומה להיטל השבחה.
הסיווג משפיע על תקן הביקורת, על עומק הדיון התכנוני, ועל טון ההחלטה.
### א.3 נסח את השאלות לדיון — במילותיך
הוועדה אינה כבולה לניסוח של עורכי הדין. אם העוררים העלו שמונה טענות אבל באמת יש שתי שאלות מרכזיות — נסח שתי שאלות. ניסוח הסוגיות הוא אבן הפינה של ההחלטה: הוא קובע אילו עובדות מהותיות ואילו כללים חלים.
**מה לעשות:**
- נסח כל שאלה כסילוגיזם מכווץ: הנחה משפטית, עובדות תמציתיות, שאלה חדה. לדוגמה: "תכנית X קובעת קו בניין של 3 מטרים. הבקשה כוללת בניה במרחק 1.5 מטרים מגבול המגרש. האם הבקשה תואמת את הוראות התכנית?"
- ניסוח הסוגיות נכתב בגרסה סופית רק אחרי שהדיון מגובש — כדי לוודא שהשאלות תואמות את התשובות.
**מבוסס על:** FJC Judicial Writing Manual §§A5-A7; Garner, Making Your Case §36; Posner — ניסוח סוגיות כאבן פינה.
---
## ב. ניתוח סף — מתי לבדוק, מתי לדלג
### ב.1 שאלות סף תמיד קודמות
אם עולה שאלת סמכות, מועד הגשה, או עמידה בתנאי מוקדם — היא נדונה ראשונה. הלוגיקה פשוטה: אם אין סמכות לדון, כל שאר הדיון מיותר.
**מה לעשות:**
- אם שאלת הסף נדחית (כלומר, הוועדה מוסמכת / הערר הוגש בזמן) — ציין זאת בפסקה אחת ועבור לגוף הערר.
- אם שאלת הסף מתקבלת — ההחלטה מסתיימת בה. אין צורך לדון בגוף.
- אל תדון בשאלת סף שלא הועלתה על ידי אף צד ושאין לה בסיס בחומר.
### ב.2 ציון תקן הביקורת
בפתיחת חלק הדיון, ציין את תקן הביקורת של הוועדה: "הוועדה מפעילה שיקול דעת תכנוני עצמאי" (ברישוי) או "הוועדה בוחנת את תקינות השומה המכרעת" (בהיטל השבחה). בלי ציון תקן — הקורא לא יודע באיזה סטנדרט נבחנה ההחלטה, והנימוק נשאר עמום.
**מבוסס על:** FJC §B6; Posner — legalism works when the rule is clear.
---
## ג. סדר הסוגיות — מה קודם ולמה
### ג.1 עקרון הסדר
1. **שאלות סף** — תמיד ראשונות.
2. **הסוגיה המכריעה** — מיד אחריהן. הסוגיה שמכריעה את הערר באה לפני סוגיות משניות.
3. **סוגיות נוספות** — לפי חוזק ההנמקה. פתח בנימוק החזק ביותר. רושם ראשוני אי אפשר לבטל, ותשומת הלב של הקורא בשיאה בהתחלה.
4. **סוגיות שנויות אך לא נחוצות** — בסוף, או בכלל לא.
### ג.2 מתי לא לדון בטענה
ההחלטה צריכה לדון רק בסוגיות שיש לפתור כדי להכריע. אם העורר העלה שמונה טענות אבל שתיים מכריעות — הדיון מתמקד בשתיים. את השאר ניתן לטפל כך:
- טענה שהועלתה ברצינות אך אינה נחוצה: "טענה זו נבחנה על ידי הוועדה. נוכח מסקנתנו לעיל, אין צורך להכריע בה."
- טענות חלשות או חוזרות: ניתן לקבץ. "באשר לטענות הנוספות שהעלו העוררים — לא מצאנו בהן ממש."
- אל תתעלם לחלוטין מטענה מרכזית. הצד המפסיד חייב לראות שהוועדה שקלה את יסודות עמדתו.
### ג.3 פסקת מפה
בפתיחת הדיון, ספק מפת דרכים: "שלוש שאלות עומדות להכרעה: (1) האם הבקשה תואמת את הוראות התכנית לעניין קו הבניין; (2) האם ההקלה המבוקשת עומדת בתנאי סעיף 147; (3) מהו הסעד המתאים." הקורא יודע מראש מה לצפות, וההנמקה נתפסת כמאורגנת.
**מבוסס על:** FJC §§B2-B5; Garner, MYC §§7, 12; LWPE §27; Posner — narrow holdings, focus on what matters.
---
## ד. בניית הניתוח — הלב של ההחלטה
### ד.1 מבנה סילוגיסטי לכל סוגיה
כל סוגיה נבנית כסילוגיזם:
1. **הנחה עליונה (הכלל)** — סעיף בתכנית, הוראת חוק, הלכה פסוקה, או עיקרון תכנוני.
2. **הנחה תחתונה (העובדות)** — העובדות הספציפיות של הערר שנבחנות לאור הכלל.
3. **מסקנה** — התוצאה שנובעת בהכרח מהחלת הכלל על העובדות.
זהו השלד. כל הנמקה שאינה ניתנת לפירוק למבנה זה — חסרה חוליה. אם לא ניתן לזהות את הכלל — ההנמקה אינה מספקת. אם לא ניתן לזהות כיצד העובדות מקיימות את הכלל — ההנמקה קריפטית.
### ד.2 התחל מלשון הטקסט
כשהמקרה נשלט על ידי הוראת תכנית או סעיף חוק — פתח תמיד בציטוט ההוראה. לא בפסיקה, לא בעקרון כללי. המילים של הטקסט הן נקודת המוצא.
**מה לעשות:**
- הבא את לשון ההוראה הרלוונטית (ציטוט ישיר, קצר ככל האפשר).
- פרש מילים במשמעותן הרגילה.
- בדוק עקביות עם הוראות אחרות באותה תכנית.
- תן תוקף לכל מילה — מילה "מיותרת" בטקסט נורמטיבי אינה מיותרת.
- אם יש עמימות — השתמש בכלי פרשנות: הכלל הכללי מצטמצם לאור הפרט; מילה מתפרשת לאור הקשרה; הכללת דבר אחד מרמזת על הדרת אחרים.
### ד.3 שלושה מקורות להנחה העליונה
בעררי תכנון, הכלל נשאב משלושה מקורות:
- **טקסט**: הוראות התכנית, חוק התכנון והבניה, תקנות.
- **תקדים**: פסיקת בתי משפט, החלטות ועדת ערר ארצית, החלטות ועדות ערר מחוזיות.
- **מדיניות**: שיקולים תכנוניים — צפיפות, אופי סביבה, אינטרס ציבורי, השפעות כלכליות.
בחר את המקור החזק ביותר. אם יש הוראת תכנית ברורה — אין צורך בפסיקה כדי לתמוך בה. פסיקה נדרשת כשהטקסט עמום או כשצריך לקבוע כיצד ליישם עיקרון כללי.
### ד.4 ההנחה התחתונה היא המפתח
ברוב העררים, הכלל המשפטי אינו שנוי במחלוקת. השאלה היא כיצד העובדות משתלבות בכלל. זהו לב ההחלטה. ההנמקה חייבת להראות בפירוט — לא בהכרזה — כיצד העובדות הספציפיות מקיימות או אינן מקיימות את תנאי הכלל.
**מה לעשות:**
- השתמש בנתונים: מספרים, מידות, אחוזים, תאריכים (כשרלוונטיים). "הבקשה חורגת ב-1.5 מטרים מקו הבניין" — לא "הבקשה חורגת באופן משמעותי."
- הפרד בין ממצא עובדתי למסקנה משפטית. "הבניה במרחק 1.5 מטרים מגבול המגרש" — ממצא עובדתי. "חריגה זו עולה כדי סטייה ניכרת" — מסקנה משפטית. אל תערבב.
- כל מעבר מכלל לעובדה למסקנה צריך להיות מפורש. לא לכתוב "העובדות מלמדות כי הערר אינו מוצדק" בלי לפרט למה.
### ד.5 מבנה CREAC בפועל
לכל סוגיה, השתמש במבנה הבא:
1. **מסקנה** (Conclusion) — פתח בתשובה לשאלה. "הבקשה אינה תואמת את הוראות התכנית לעניין קו הבניין."
2. **כלל** (Rule) — הבא את הכלל. ציטוט הוראת התכנית או ההלכה.
3. **הרחבה** (Explanation) — אם הכלל דורש הבהרה, הבא תקדים רלוונטי אחד שמסביר כיצד הכלל יושם במקרה דומה.
4. **יישום** (Application) — החל את הכלל על עובדות המקרה. כאן נמצא לב ההנמקה.
5. **מסקנה חוזרת** (Conclusion) — סגור בתמצית. "לפיכך, הבקשה אינה עולה בקנה אחד עם הוראות התכנית."
הפתיחה במסקנה חיונית: הקורא יודע לאן הדיון מוביל, וכל עובדה שנקראת אחר כך מובנת בהקשרה. עובדות ללא מסגרת — נתפסות כאקראיות וחסרות משמעות.
**מבוסס על:** Garner, MYC §§22-27; FJC §§B1, B8; Posner — facts drive decisions; data over words; distinguish findings from conclusions.
---
## ה. איזון ומידתיות — מתי ואיך
### ה.1 מתי נדרש איזון
איזון נדרש כשהדין לא נותן תשובה חד-משמעית. כשהכלל ברור והעובדות מתאימות לו — אין צורך באיזון. אל תאזן כשאפשר להכריע לפי כלל. איזון הוא כלי לשעה שהכללים אוזלים, לא תחליף לניתוח נורמטיבי.
### ה.2 מבנה האיזון
כשאיזון נדרש, בנה אותו כך:
1. **זהה את האינטרסים** — מהם האינטרסים המתחרים. לא "אינטרס הציבור" מול "אינטרס העורר" באופן מעורפל, אלא אינטרסים קונקרטיים: "זכות הקניין של העורר לבנות על מגרשו" מול "שמירה על אופי מגורים צמודי קרקע בשכונה."
2. **בחן השלכות לכל כיוון** — מה קורה אם מקבלים? מה קורה אם דוחים? לא "מהו האינטרס החשוב יותר" אלא "מהן ההשלכות של כל תוצאה על כל אינטרס."
3. **שקול השלכות מערכתיות** — לא רק תוצאה לתיק זה, אלא גם האות שנשלח למערכת התכנון. קבלת הערר תיצור תקדים? תפתח פתח לבקשות דומות?
4. **הגע למסקנה** — ציין מפורשות מה מכריע את הכף ולמה.
### ה.3 מידתיות כמבחן
כשהוועדה מטילה מגבלה או תנאי — בדוק: (1) האם המגבלה משרתת תכלית ראויה; (2) האם יש אמצעי פוגע פחות; (3) האם הפגיעה מידתית ביחס לתועלת. שלושת השלבים צריכים להיות מפורשים בטקסט.
**מבוסס על:** Posner — balance as methodology; systemic vs. case-specific consequences; pragmatist approach within legal norms.
---
## ו. טיפול בטענות — כללים מעשיים
### ו.1 אל תהפוך את הדיון לוויכוח
ההחלטה מנתחת שאלה — לא מתווכחת עם עורכי דין. המבנה הנכון הוא: שאלה → כלל → עובדות → מסקנה. לא: "העורר טוען X — אין לקבל טענה זו — שכן Y."
הדיון לא מתנהל כ"תשובה לכתב הערר" אלא כניתוח עצמאי שבוחן את השאלות שהתעוררו. הוועדה מגיעה למסקנותיה מכוח הנימוק — לא מכוח דחיית טענות.
### ו.2 Steel-manning — הצג את הטענה הטובה ביותר של הצד המפסיד
לפני שדוחים טענה — הצג אותה בגרסה החזקה ביותר שלה. לא קריקטורה של הטענה, אלא הטענה כפי שעורך דין מוכשר היה מנסח אותה. אז הסבר למה היא נדחית.
**למה זה חשוב:** טענת קש קלה להפריך, אבל הקורא (ובמיוחד בית המשפט בביקורת שיפוטית) יזהה שלא התמודדת עם הטענה האמיתית. הצגה הוגנת של הטענה ודחייתה — משכנעת. הצגה מעוותת — מחשידה.
**מה לעשות:**
- כשנדרשת התמודדות עם טענת העורר, כתוב: "אמנם צודק העורר כי [נקודה שפועלת לטובתו], אולם [הנימוק לדחייה]."
- אם יש נקודה שאי אפשר להגן עליה — הכר בה בגלוי. "נכון כי המבנה הסמוך חורג מקו הבניין. אולם עובדה זו אינה מקנה זכות לחריגה נוספת, שכן..."
- טענה חלשה שאין בה ממש — מספיק משפט אחד. אל תפזר זמן על טענות שאינן ראויות לדיון.
### ו.3 מיקום ההתמודדות עם טענות נגדיות
באמצע הדיון — לא בהתחלה ולא בסוף. המבנה המומלץ לכל סוגיה:
1. הנחה משפטית (הכלל)
2. יישום על העובדות
3. מסקנה ראשונית
4. **טענה נגדית + תשובה**
5. **טענה נגדית נוספת + תשובה** (אם יש)
6. נקודה תומכת נוספת
7. משפט סיכום
פתיחה בטענות הצד השני מציבה את ההחלטה בעמדת הגנה. סיום בהן משאיר את המוקד על הצד המפסיד. האמצע הוא המקום הנכון.
### ו.4 קיבוץ טענות
כשיש טענות רבות שמכוונות לאותה נקודה — קבץ אותן. "העוררים העלו מספר טענות הנוגעות לאופן חישוב השטחים. לאחר בחינתן, לא מצאנו בהן ממש, ונפרט." זה עדיף על טיפול נקודתי בכל טענה, שמייצר תחושה של רשימת מכולת ולא של ניתוח.
**מבוסס על:** FJC §§B3-B4, E1-E2; Garner, MYC §§4, 8, 10-12; LWPE §30; Posner — honest engagement with counterarguments, avoid empty formulas.
---
## ז. ציטוטים ואזכורי פסיקה — פחות זה יותר
### ז.1 טכניקת הסנדוויץ'
כל ציטוט חייב להיות עטוף: משפט הקדמה → ציטוט → ניתוח.
**הקדמה גרועה:** "בית המשפט קבע כדלקמן:" (ריקה מתוכן).
**הקדמה טובה:** "בית המשפט קבע כי אין לקבל בקשות שהוגשו באיחור ללא טעם מיוחד:" (מודיעה על התוכן).
אל תניח שהקורא יקרא ציטוט ארוך. סכם את עיקרו לפניו, ולאחריו הוסף ניתוח שמסביר כיצד הציטוט רלוונטי למקרה הנדון.
### ז.2 כמה לצטט
- **הוראת תכנית/חוק**: ציטוט ישיר — המילים המדויקות חשובות כי ההנמקה נבנית עליהן.
- **הלכה פסוקה**: פרפרזה עדיפה. צטט ישירות רק כשהניסוח המקורי עושה נקודה שלא ניתן לבטא בפרפרזה. 1-2 משפטים לכל היותר.
- **כלל מוסדר**: מקור אחד מספיק. לא מחרוזות של "ראו: X; Y; Z; A; B." מחרוזת אזכורים אינה מוסיפה כוח — היא מעידה על חוסר ביטחון.
- **כלל חדש או שנוי במחלוקת**: כאן כן יש מקום לסקירת ההתפתחות בפסיקה, אבל ממוקדת ותכליתית.
### ז.3 היררכיית תקדימים
בעררי תכנון, סדר המשקל הוא:
1. פסיקת בית המשפט העליון
2. פסיקת בית משפט לעניינים מנהליים
3. החלטות ועדת ערר ארצית
4. החלטות ועדות ערר מחוזיות אחרות
5. ספרות משפטית/תכנונית
העדף תקדים עדכני. כשמאזכרים תקדים — ציין בדיוק מה נפסק ואם מדובר בהלכה מחייבת או אמרת אגב. אם התקדים שונה מהמקרה הנדון — אמור זאת במפורש.
### ז.4 הפניות ביבליוגרפיות
שלב את שם בית המשפט ושם התיק בגוף הטקסט ("כפי שקבע בית המשפט העליון בפרשת אליאב") והעבר את ההפניה המספרית להערת שוליים. הפניות בגוף הטקסט שוברות את מהלך המחשבה.
**מבוסס על:** FJC §§D1-D5; Garner, MYC §§26-27, 48, 50; LWPE §§28-29.
---
## ח. כתיבת חלק העובדות — ניטרלי, ממוקד, מדויק
### ח.1 רק עובדות הנחוצות להסברת ההחלטה
כל עובדה שמופיעה — הקורא יניח שהיא רלוונטית. אם היא לא רלוונטית — היא מסיחה דעת. אם היא רלוונטית ולא מופיעה — ההנמקה חסרה בסיס.
**מה לעשות:**
- כלול רק עובדות שמשמשות בדיון. מבחן: לכל עובדה בחלק הרקע, שאל — "האם אני מפנה לעובדה זו בחלק הדיון?" אם לא — שקול להסיר.
- תאריכים מדויקים רק כשהם מהותיים (מועד הגשה, תוקף תכנית, שאלת שיהוי). אחרת — "כחודש לאחר מכן", "בתחילת 2023."
- פרטים "מעניינים" שאינם רלוונטיים — השמט. היסטוריה של השכונה, נוף, תיאורים ציוריים — רק אם רלוונטיים להחלטה.
### ח.2 ניטרליות מוחלטת
חלק העובדות אינו טוען. אין בו מילות שיפוט ("למרבה הפליאה", "באופן מפתיע"). אין בו ציטוטים מצדדים (ציטוטים שייכים לחלק הטענות). הוא מציג עובדות — לא מפרש אותן.
אבל ניטרליות אינה הסתרה. אם יש עובדה שתומכת בצד המפסיד — היא חייבת להופיע. רקע ניטרלי כולל את כל העובדות המהותיות, לא רק את אלה שתומכות בתוצאה.
### ח.3 מבנה: סדר כרונולוגי, עובדות כלליות ואז ספציפיות
עקוב אחר ציר הזמן: הנכס, הבקשה, ההחלטה, הערר. אל תפתח בהחלטת הוועדה המקומית ואז תחזור לתיאור הנכס.
בתיקים רב-סוגייתיים — הגבל את חלק הרקע לעובדות כלליות ושלב עובדות ספציפיות בדיון בכל סוגיה. זה מונע כפילות ושומר על רלוונטיות.
### ח.4 דיוק מוחלט
אל תסמוך על עובדות כפי שמוצגות בכתבי הטענות. בדוק מול חומרי המקור (פרוטוקולים, תכניות, תצהירים). שגיאה עובדתית היא הדבר המזיק ביותר שיכול לקרות להחלטה — היא מערערת את סמכותה ופוגעת באמינותה.
**מבוסס על:** FJC §§C1-C6; Garner, LWPE §§3, 17, 23; MYC §36; Posner — data over words, facts drive decisions.
---
## ט. כתיבת חלק ההכרעה — ברור ואופרטיבי
### ט.1 התוצאה חייבת להיות חד-משמעית
"הערר נדחה." "הערר מתקבל." "הערר מתקבל בחלקו." לא "לאור כל האמור לעיל, הערר נדחה" — אלא סיכום קצר (2-3 משפטים) שמסביר את עיקר ההנמקה, ואז התוצאה.
### ט.2 הוראות אופרטיביות מפורטות
כשהערר מוחזר לוועדה המקומית — אל תדבר בחידות. "הערר מוחזר לוועדה המקומית לצורך דיון מחדש" — אינו מספיק. פרט: מה צריכה הוועדה המקומית לבחון? לפי איזו תכנית? האם לתת שימוע? מהם השיקולים שיש לשקול?
כשנקבעים תנאים — פרט כל תנאי באופן שהגוף המבצע יוכל ליישם בלי לפרש את ההחלטה.
### ט.3 שמירה על סמכות הערכאה הנמוכה
גם כשנמצא פגם בשיקול הדעת — ההחלטה מחזירה את העניין לוועדה המקומית כדי שתפעיל שיקול דעת מחדש. אל תכפה תוצאה ספציפית אלא אם הדין מחייב תוצאה אחת בלבד.
### ט.4 התייחסות לוועדה המקומית — ללא ביקורת מיותרת
כשהערר מתקבל — הוועדה המקומית טעתה. אבל ההנמקה מתמקדת ב"מה צריך להיות" — לא ב"כמה טעתה הוועדה המקומית." אין "באופן מפתיע", "למרבה הפליאה", "שגתה שגיאה חמורה". נמק את הפגם — אל תבקר את השופט.
**מבוסס על:** FJC §§E4, F1-F3; Garner, MYC §21; Posner — narrow holdings, constrained pragmatism.
---
## י. טכניקות כתיבה — ברמת הפסקה והמשפט
### י.1 משפט נושא בפתיחת כל פסקה
כל פסקה נפתחת במשפט שמודיע על הנקודה המרכזית שלה. לא באזכור פסק דין, לא בהפניה, לא בתיאור רקע. הנקודה — ואז התמיכה.
**לא:** "בעע"מ 1234/05 נקבע כי..." → הקורא לא יודע למה הוא קורא על פסק הדין הזה.
**כן:** "ועדת ערר אינה מוסמכת להתערב בשיקול דעת מקצועי של מהנדס העיר. כך נפסק ב..." → הקורא יודע את הנקודה, ופסק הדין תומך בה.
### י.2 גשרים בין פסקאות
כל פסקה חייבה להיות מחוברת לקודמתה. שלושה כלים:
- **מילות קישור מפורשות**: לפיכך, אולם, בנוסף, מנגד, אכן, עם זאת.
- **מילות הצבעה**: "בעניין זה", "נוכח קביעה זו", "מעבר לכך".
- **הדי הפסקה הקודמת**: חזרה על מונח מפתח מהפסקה הקודמת בפתיחת הפסקה הנוכחית.
### י.3 פסקה אחת — נקודה אחת
אם פסקה עוסקת גם בכלל המשפטי, גם ביישומו, וגם בטענה נגדית — חלק אותה. הפסקה היא יחידת החשיבה הבסיסית, ויחידה שמכילה שני רעיונות שונים — מבלבלת.
### י.4 כותרות אינפורמטיביות (כשמתאים)
כשיש כותרות משנה בדיון (בתיקים מורכבים עם סוגיות נפרדות) — כתוב כותרת שמודיעה על המסקנה, לא רק על הנושא.
- **לא:** "סוגיית קו הבניין"
- **כן:** "הבנייה בקו אפס אינה עולה בקנה אחד עם הוראות התכנית"
### י.5 בניין פעיל
"הוועדה המקומית דחתה את הבקשה" — לא "הבקשה נדחתה על ידי הוועדה המקומית." בניין פעיל קצר יותר, ברור יותר, ומזהה את הפועל. חריג: כשהפעולה חשובה יותר מהפועל ("ההיתר בוטל" — כשלא חשוב מי ביטל).
### י.6 דיוק ומשמעת לשונית
- **עקביות מינוחית**: אם כתבת "היתר בנייה" — אל תעבור ל"רישיון בנייה." עקביות חשובה מגיוון.
- **לא להגזים**: "הפסיקה חד-משמעית" — רק אם היא באמת חד-משמעית. "אין כל ספק" — רק אם באמת אין. הגזמה מערערת אמינות.
- **לא לנפח**: "במידה ו-" → "אם". "לאור העובדה ש-" → "מכיוון ש-". "על מנת ש-" → "כדי ש-". כל מילה שאינה עוזרת — מפריעה.
- **לא לכפול**: "לבטל ולהפקיע" → "לבטל". אם מילה אחת מספיקה — מילה שנייה מחייבת את הקורא לחפש הבדל שאינו קיים.
- **סיום חזק**: אל תסיים משפט בתאריך או בהפניה אלא אם הם חשובים. המילה האחרונה במשפט היא זו שנשארת.
### י.7 כנות לגבי קושי
כשהמקרה קשה — אמור זאת. "הדבר אינו נקי מספקות, אולם..." עדיף על פני הצגת מקרה קשה כקל. כנות לגבי הקושי מחזקת את אמינות ההחלטה — הקורא מבין שהוועדה התלבטה ובכל זאת הגיעה למסקנה מנומקת.
אבל — ההחלטה משקפת רק את התוצאה הסופית. לא לתעד כל צעד ומעד בדרך, לא להציג שני מסלולי חשיבה חלופיים. אם ההחלטה קשה — ניתן לומר זאת, ואז להציג את ההנמקה הסופית בביטחון.
### י.8 הימנעות מנוסחאות ריקות
כל משפט חייב לעשות עבודה. "לאחר ששקלנו את כלל השיקולים הרלוונטיים" — ריק. מה שקלתם? "בעניין זה יש לומר" — ריק. אמור מה יש לומר בלי ההקדמה. "הננו סבורים" — ריק. כתוב את מה שאתה סבור, בלי להכריז שאתה סבור.
מבחן: אם מוחקים את המשפט וההחלטה לא מאבדת מידע — המשפט מיותר.
**מבוסס על:** FJC §§G1-G6; Garner, LWPE §§5-17, 24-26; MYC §§6, 35, 39, 43; Posner — avoid empty formulas, candor about uncertainty.
---
## יא. אנלוגיה ותקדים — מתי ואיך
### יא.1 אנלוגיה דורשת הסבר מדיניות
"מקרה זה דומה לפרשת X" — ריק, אלא אם מסביר למה הדמיון רלוונטי. מה המדיניות שעמדה בבסיס ההחלטה ב-X? האם אותה מדיניות חלה כאן?
**מה לעשות:**
- כשמפנים לתקדים, ציין: (1) מה נפסק שם; (2) מה הנסיבות הדומות; (3) למה הרציונל חל גם כאן.
- כשמבחינים מתקדים: (1) מה שונה; (2) למה ההבדל משמעותי.
### יא.2 החזקות חלופיות — "אף בהנחה"
הימנע מ"אף בהנחה שצודקים העוררים בטענתם..." ו"גם אם היינו מקבלים..." — הם מחלישים את ההחזקה העיקרית. אם יש שני נימוקים — דון בנימוק המשני קודם ואז הצג את הנימוק העיקרי. כך שני הנימוקים עומדים בזכות עצמם, בלי שאחד מערער את השני.
**מבוסס על:** FJC §B7; Garner, MYC §§26, 48; Posner — analogy requires policy analysis, narrow holdings.
---
## יב. עריכה — רשימת ביקורת
לפני סיום ההחלטה, בצע את הבדיקות הבאות:
### ביקורת מבנית
- [ ] המבוא מכסה את כל הסוגיות שנדונו בהחלטה
- [ ] כל עובדה בחלק הרקע מופיעה בדיון (אין עובדות "יתומות")
- [ ] כל קביעה בדיון מבוססת על עובדה מחלק הרקע (אין עובדות חדשות בדיון)
- [ ] סדר הסוגיות לוגי: סף → מכריע → משני
- [ ] המסקנה נובעת מהדיון — לא מכריזה תוצאה שלא נומקה
### ביקורת אנליטית
- [ ] לכל סוגיה — ניתן לזהות כלל + עובדות + מסקנה (מבנה סילוגיסטי)
- [ ] הממצאים העובדתיים מופרדים מהמסקנות המשפטיות
- [ ] הטענה המרכזית של הצד המפסיד קיבלה מענה מנומק
- [ ] אין "נוסחאות ריקות" — כל משפט עושה עבודה
- [ ] אין הגזמה — "חד-משמעי", "ברי", "ללא ספק" רק כשמוצדקים
### ביקורת עקביות
- [ ] התוצאה בבלוק יא/יב תואמת את הסיכום בבלוק א/ב
- [ ] מינוח עקבי לאורך כל ההחלטה (אותם מונחים לאותם מושגים)
- [ ] הציטוטים מדויקים ובהקשרם
- [ ] אזכורי פסיקה נכונים (לא מייחסים לפסק דין יותר ממה שאמר)
**מבוסס על:** FJC §§G8-G10; Garner, MYC §6; Posner — precision, intellectual honesty.
---
## סיכום — עשרת העקרונות המנחים
1. **סילוגיזם תמיד**: כלל → עובדות → מסקנה. אין קיצורי דרך.
2. **התחל מהטקסט**: הוראת תכנית או חוק — לפני פסיקה, לפני עקרונות כלליים.
3. **עובדות מכריעות**: רוב המקרים מוכרעים על ידי העובדות, לא על ידי הדין.
4. **נתונים, לא תיאורים**: מספרים ומידות — לא "משמעותי", "ניכר", "מהותי."
5. **Steel-man**: הצג את הטענה הטובה ביותר של הצד המפסיד — ואז הסבר למה היא נדחית.
6. **כנות**: מקרה קשה — אמור שהוא קשה. אל תעמיד פנים שקל.
7. **כל מילה עובדת**: נוסחה ריקה, מילה מנופחת, כפילות — מחק.
8. **מסקנה קודם**: הקורא יודע לאן הדיון מוביל — העובדות מובנות בהקשרן.
9. **מקור אחד מספיק**: לנקודה מוסדרת — אזכור אחד. מחרוזות אזכורים = חולשה.
10. **הוראות ברורות**: הצד שמקבל את ההחלטה חייב לדעת בדיוק מה נדרש ממנו.
---
*מסמך זה מבוסס על שלושה מקורות מרכזיים: (1) Federal Judicial Center, Judicial Writing Manual (1991, 2020); (2) Garner, Legal Writing in Plain English (2001) ו-Scalia & Garner, Making Your Case (2008); (3) Posner, How Judges Think (2008). העקרונות סונתזו והותאמו להקשר של ועדת ערר לתכנון ובניה בישראל.*

View File

@@ -0,0 +1,610 @@
# עקרונות כתיבת החלטות מעין-שיפוטיות — מיצוי מתוך Judicial Writing Manual (FJC)
מקורות:
- **מהדורה ראשונה (1991)** — Judicial Writing Manual, Federal Judicial Center
- **מהדורה שנייה (2020)** — Judicial Writing Manual: A Pocket Guide for Judges, Second Edition
---
## A. מבנה כולל של ההחלטה — מה קודם, מה אחרון, רצף
### A1. חמישה מרכיבים חובה בהחלטה מלאה
**העיקרון:** החלטה מלאה חייבת לכלול חמישה אלמנטים בסדר הבא: (1) מבוא — טבע התיק ומצבו הפרוצדורלי; (2) ניסוח הסוגיות; (3) תיאור העובדות המהותיות; (4) דיון בעקרונות המשפטיים וביישומם; (5) התוצאה האופרטיבית וההוראות.
> "A full-dress opinion should contain five elements: (1) an introductory statement of the nature and procedural posture of the case; (2) a statement of the issues to be decided; (3) a description of the material facts; (4) a discussion of the governing legal principles and the resolution of the issues; and (5) the disposition and necessary instructions."
> — 1991, עמ' 13; 2020, עמ' 13
**יישום לוועדת ערר:** מתאים ישירות לארכיטקטורת 12 הבלוקים — בלוקים א-ג (מבוא/פרוצדורה), ד-ה (סוגיות), ו (עובדות), ז-י (דיון), יא-יב (תוצאה).
---
### A2. כותרות וכותרות-משנה — חובה
**העיקרון:** יש להשתמש בכותרות, כותרות-משנה, ומספור כדי לחשוף את ארגון ההחלטה לקורא. זה חיוני במיוחד כשההחלטה ארוכה והנושא מורכב.
> "The use of headings and subheadings, Roman numerals, or other means of disclosing the organization to the reader is always helpful, particularly where the opinion is long and the subject matter complex. These not only provide road signs for the reader, they also help to organize the writer's thoughts and test the logic of the opinion."
> — 1991, עמ' 13; 2020, עמ' 13
**יישום לוועדת ערר:** כל בלוק מקבל כותרת ברורה. בתוך בלוק הדיון (י) — כותרות-משנה לכל סוגיה. מאפשר לצדדים ולבית המשפט לנווט בהחלטה.
---
### A3. מבוא — מכוון את הקורא
**העיקרון:** מטרת המבוא היא לכוון (orient) את הקורא. הוא צריך לציין בקצרה: מהו התיק, מה הנושא המשפטי, ומה התוצאה. בנוסף, יש לזהות את הצדדים (רצוי בשם ולא בתואר פרוצדורלי), לתאר את המצב הפרוצדורלי, ולציין את הסוגיות.
> "The purpose of the introduction is to orient the reader to the case. It should state briefly what the case is about, the legal subject matter, and the result."
> — 1991, עמ' 13; 2020, עמ' 13
> "The parties should be identified, if not in the introduction then early in the opinion, preferably by name, and that identification should be used consistently throughout. The use of legal descriptions, such as 'appellant' and 'appellee,' tends to confuse, especially in multi-party cases."
> — 1991, עמ' 13; 2020, עמ' 13-14
**יישום לוועדת ערר:** בבלוק א — זיהוי הצדדים בשם (לא "העורר" ו"המשיבה" בלבד). ציון סוג הערר, נושאו, ותוצאתו כבר בפתיחה. שימוש עקבי באותו זיהוי לאורך כל ההחלטה.
---
### A4. סיכום ההחזקה בתחילת ההחלטה
**העיקרון:** סיכום התוצאה כבר בפתיחה חוסך זמן לקוראים, ומאלץ את הכותב לנסח את ההחזקה בדיוק ובתמציתיות. הגרסה הסופית של המבוא כדאי שתיכתב אחרי השלמת ההחלטה כולה.
> "Summarizing the holding at the outset can save time for readers, particularly researchers who will be able to determine immediately whether to read the rest of the opinion. Providing a terse summary of the holding at the start of the opinion also helps the writer to state it precisely and succinctly. The final version of the introduction may be best written after the opinion is completed."
> — 1991, עמ' 13; 2020, עמ' 14
**יישום לוועדת ערר:** בבלוק א לכתוב: "הערר נדחה/מתקבל" + משפט אחד על הנימוק המרכזי. המבוא נכתב אחרון (אחרי שהדיון מגובש).
---
### A5. ניסוח הסוגיות — אבן הפינה
**העיקרון:** ניסוח הסוגיות הוא אבן הפינה של ההחלטה. הוא קובע אילו עובדות הן מהותיות ואילו עקרונות משפטיים חלים. השופט לא כבול לניסוח של עורכי הדין — עליו לנסח את הסוגיות כפי שהוא רואה אותן.
> "The statement of issues is the cornerstone of the opinion; how the issues are formulated determines which facts are material and what legal principles govern. Judges should not be prisoners of the attorneys' analysis; they should frame the issues as they see them."
> — 1991, עמ' 14; 2020, עמ' 14
**יישום לוועדת ערר:** בלוקים ד-ה — הוועדה מנסחת את השאלות לדיון במילותיה, לא בניסוח העוררים. אם העוררים הגדירו שלוש שאלות אבל באמת יש שאלה מרכזית אחת — הוועדה מנסחת שאלה אחת.
---
### A6. סוגיות לפני/אחרי עובדות — גמישות
**העיקרון:** ניסוח הסוגיות יכול לבוא לפני או אחרי תיאור העובדות. הצבת הסוגיות קודם הופכת את תיאור העובדות למשמעותי יותר ומסייעת להתמקד בעובדות המהותיות. אך לפעמים לא ניתן לנסח את הסוגיה ללא שהקורא מכיר את העובדות.
> "Stating the issues first will make the fact statement more meaningful to the reader and help focus on material facts."
> — 1991, עמ' 14; 2020, עמ' 14
**יישום לוועדת ערר:** בארכיטקטורת 12 הבלוקים — בלוק ה (סוגיות) בא לפני בלוק ו (רקע עובדתי). זה מתאים לעיקרון.
---
### A7. ניסוח סוגיות ≠ פירוט טענות הצדדים
**העיקרון:** יש להפריד בין ניסוח הסוגיות לבין פירוט טענות הצדדים. פירוטים ארוכים של טענות אינם תחליף לניתוח ולנימוק, ויש להימנע מהם.
> "The statement of issues should not be confused with recitals of the parties' contentions. Lengthy statements of the parties' contentions, occasionally found in opinions, are not a substitute for analysis and reasoning and should be avoided."
> — 1991, עמ' 14-15; 2020, עמ' 14
**יישום לוועדת ערר:** בלוקים ז-ח (טענות הצדדים) הם נפרדים מבלוק ה (סוגיות). בלוק ה קצר וממוקד; בלוקים ז-ח מפרטים את הטענות; בלוק י מנתח — ולא חוזר על הטענות.
---
### A8. ההחלטה משקפת רק את התוצאה הסופית
**העיקרון:** הכתיבה צריכה לשקף רק את ההחלטה הסופית ואת הנימוקים שלה. כשההחלטה קשה — יש לומר זאת, אבל לא לתעד כל צעד ומעד בדרך.
> "The writing should reflect only the final decision and the reasons for it. Where the decision is a close one, the opinion should say so, but it should not record every step and misstep the writer took along the way."
> — 1991, עמ' 10; 2020, עמ' 9
**יישום לוועדת ערר:** הדיון בבלוק י לא מתעד את התלבטויות הוועדה. אם ההחלטה קשה — ניתן לכתוב "הדבר אינו נקי מספקות, אולם..." ולהמשיך בנימוק ברור לתוצאה.
---
## B. כתיבת חלק הדיון/ניתוח — לב ההחלטה
### B1. הדיון חייב להיות מבוסס על היגיון ולוגיקה, לא על טיעון
**העיקרון:** חלק הדיון הוא לב ההחלטה. הוא חייב להדגים שמסקנת בית המשפט מבוססת על שכל ישר ולוגיקה. הוא צריך לשכנע את הקורא בכוח הנימוק — לא באמצעות סנגוריה או טיעון.
> "The discussion of legal principles is the heart of the opinion. It must demonstrate that the court's conclusion is based on reason and logic. It should persuade the reader of the correctness of the result by the power of its reasoning, not by advocacy or argument."
> — 1991, עמ' 16; 2020, עמ' 16
**יישום לוועדת ערר:** בלוק י — הדיון לא "טוען" בעד התוצאה אלא בונה שרשרת נימוקים: כלל → עובדות → מסקנה. הטון ניטרלי-אנליטי, לא אדברסרי.
---
### B2. סוגיות מכריעות קודם
**העיקרון:** ככלל, סוגיות מכריעות (dispositive) צריכות להידון ראשונות. הסדר ייקבע על-ידי הלוגיקה של הנימוק. סוגיות שאינן מכריעות — אם בכלל נדונות — באות בסוף.
> "Generally, dispositive issues should be discussed first. The order in which those issues are taken up will be governed by the opinion's reasoning. If non-dispositive issues are addressed at all — for educational reasons or to guide further proceedings — discuss them near the end of the opinion."
> — 1991, עמ' 16-17; 2020, עמ' 16-17
**יישום לוועדת ערר:** אם יש טענת סף (אי-עמידה בתנאי, איחור) — נדונה קודם. אם נדחית, ממשיכים לגוף הערר. בתוך הדיון — הסוגיה שמכריעה את הערר קודמת.
---
### B3. לא לדון בכל מה שהצדדים העלו
**העיקרון:** ככלל, ההחלטה צריכה לדון רק בסוגיות שיש לפתור כדי להכריע בתיק. מה שהוועדה אינה צריכה להכריע — לא צריך לדון בו. אם הערכאה מגלה שסוגיה שהצדדים לא העלו היא מכריעה — עליה להודיע לצדדים ולאפשר להם לטעון.
> "An opinion should not range beyond the issues presented; it should address only the issues that need to be resolved to decide the case."
> — 1991, עמ' 17; 2020, עמ' 17
**יישום לוועדת ערר:** אם העורר העלה 8 טענות אבל 2 מכריעות — הדיון מתמקד ב-2. את השאר ניתן לציין בקצרה ("אין צורך להכריע בשאר הטענות" או "טענה זו נבחנה ונמצא כי אין בה ממש").
---
### B4. סוגיות שאינן נחוצות — מספיק להראות שנשקלו
**העיקרון:** סוגיות שאינן נחוצות להכרעה אך הצד המפסיד הציגן ברצינות — יש לדון בהן רק במידה הנדרשת כדי להראות שנשקלו. הקו בין מה שנחוץ למה שלא — לא תמיד ברור.
> "Issues not necessary to the decision but seriously urged by the losing party should be discussed only to the extent necessary to show that they have been considered."
> — 1991, עמ' 17; 2020, עמ' 17
**יישום לוועדת ערר:** טענה שהועלתה בכובד ראש אך אינה מכריעה — משפט עד פסקה. "טענה זו נבחנה על ידי הוועדה. נוכח מסקנתנו לעיל, אין צורך להכריע בה." או דיון קצר שמראה שהטענה נשקלה.
---
### B5. שיקולי יעילות — מתי לדון במה שלא חייבים
**העיקרון:** לפעמים שיקולי יעילות מצדיקים דיון בסוגיות שאינן נחוצות להכרעה — למשל, לתת הנחיות לערכאה הנמוכה בהחזרה. אך יש להיזהר מלהכריע בסוגיות שלא בפני הערכאה ומלתת חוות דעת מייעצות.
> "Considerations of economy and efficiency may argue in favor of addressing issues not necessary to the decision if the court can thereby provide useful guidance for the lower court on remand. In doing so, however, judges must be careful not to prejudge issues that are not before them and to avoid advisory opinions."
> — 1991, עמ' 17; 2020, עמ' 17
**יישום לוועדת ערר:** כשהערר מוחזר לוועדה המקומית — כדאי לתת הנחיות ברורות ("על הוועדה המקומית לבחון..." / "יש לשקול..."). אך לא להכריע בשאלות שלא נטענו.
---
### B6. הקדמת תקן הביקורת
**העיקרון:** ההחלטה צריכה לציין את תקן הביקורת (standard of review) בתחילת חלק הדיון. בלי זה — משמעות ההחלטה עלולה להיות עמומה. ציון התקן גם ממשמע את הניתוח.
> "The opinion should specify the controlling standard of review at the outset of the discussion of legal principles. Unless the reader is told whether review is under the de novo, the clearly erroneous, or the abuse of discretion standard, the meaning of the decision may be obscure."
> — 1991, עמ' 16; 2020, עמ' 16
**יישום לוועדת ערר:** בבלוק ט או תחילת בלוק י — ציון סמכות הוועדה ותקן הביקורת: "הוועדה רשאית להפעיל שיקול דעת עצמאי / הוועדה בוחנת את שיקול הדעת של הוועדה המקומית / ביקורת שיפוטית על שומה מכרעת" וכו'.
---
### B7. החזקות חלופיות — "גם אם" / "אף בהנחה"
**העיקרון:** ציון עילות נפרדות ועצמאיות להחלטה מחזק את ההחלטה אך מחליש את ערכה כתקדים. יש להימנע מ"גם אם" ו"בהנחת ארגומנדו" כי הם מערערים את סמכות ההחזקה. אלטרנטיבה: לטפל בעילה החלופית קודם ולציין את העילה העיקרית אחרונה.
> "Stating separate and independent grounds for a decision adds strength to the decision but diminishes its value as a precedent. Statements such as 'even if the facts were otherwise' or 'assuming arguendo that we had not concluded thus and so' undermine the authority of the holding."
> — 1991, עמ' 17; 2020, עמ' 17
> "Witkin suggests either limiting the 'even if' approach to situations where it is necessary to achieve a majority decision, or avoiding it completely by phrasing the opinion in such a manner that the alternative assumption is disposed of first and the substantial ground of the opinion stated last."
> — 1991, עמ' 17; 2020, עמ' 17
**יישום לוועדת ערר:** במקום לכתוב "גם אם היינו מקבלים את טענת העורר..." — עדיף לסדר את הדיון כך שהעילה המשנית נדונה קודם ונדחית, ואז העילה העיקרית מובאת כבסיס מוצק. אם בכל זאת משתמשים ב"אף בהנחה" — רק כשזה מחזק את ההחלטה משמעותית.
---
### B8. הניתוח לא יהיה קריפטי
**העיקרון:** אמנם תמציתיות רצויה, אבל השופט חייב לפרט את הנימוקים במידה מספקת כדי שהקורא יוכל לעקוב. החלטה שמדלגת על צעדים בנימוק — לא משיגה את מטרותיה.
> "While brevity is desirable, judges must elaborate their reasoning sufficiently so that the reader can follow. An opinion that omits steps in the reasoning essential to understanding will fail to serve its purposes."
> — 1991, עמ' 22; 2020, עמ' 22
**יישום לוועדת ערר:** בלוק י — כל מעבר מכלל לעובדה למסקנה צריך להיות מפורש. לא לכתוב "העובדות מלמדות כי הערר אינו מוצדק" בלי לפרט למה.
---
## C. טיפול בעובדות
### C1. רק עובדות הנחוצות להסברת ההחלטה
**העיקרון:** יש לכלול רק את העובדות הנחוצות להסברת ההחלטה. עם זאת, מה שנחוץ אינו תמיד מובן מאליו ותלוי בקהל היעד.
> "Only the facts that are necessary to explain the decision should be included, but what is necessary to explain the decision is not always obvious and may also vary depending on the audience."
> — 1991, עמ' 15; 2020, עמ' 15
**יישום לוועדת ערר:** בלוק ו — עובדות רלוונטיות בלבד. לא לפרט את כל תולדות המקרקעין אם רק עניין אחד רלוונטי. אבל "מבחן השופט" — לשופט שלא מכיר את התיק צריך לתת מספיק רקע.
---
### C2. פרטי עובדות מיותרים מסיחים דעת
**העיקרון:** פרטים עובדתיים מיותרים מסיחים דעת. תאריכים, למשל, נוטים לבלבל ואין לכלול אותם אלא אם הם מהותיים להחלטה.
> "Excessive factual detail can be distracting. Dates, for example, tend to confuse and should not be included unless material to the decision or helpful to its understanding."
> — 1991, עמ' 15; 2020, עמ' 15
**יישום לוועדת ערר:** בבלוק ו — לא לכתוב "ביום 15.3.2024 הגיש העורר בקשה, וביום 22.4.2024 הוועדה המקומית דנה, וביום 3.5.2024 ניתנה החלטה..." אלא אם הזמנים מהותיים (למשל, שאלת איחור).
---
### C3. עובדות הצד המפסיד — אסור להתעלם
**העיקרון:** תמציתיות ופשטות רצויים, אך הם משניים לצורך בהצגה מלאה והוגנת. אין להתעלם מעובדות משמעותיות שתומכות בצד המפסיד.
> "While brevity and simplicity are always desirable, they are secondary to the need for a full and fair statement. Facts significant to the losing side should not be ignored."
> — 1991, עמ' 15; 2020, עמ' 15
**יישום לוועדת ערר:** בבלוק ו — אם יש עובדה שתומכת בטענת העורר שנדחה, היא חייבת להופיע. רקע ניטרלי = כולל את הכול, לא רק את מה שתומך בתוצאה.
---
### C4. עובדות "צבעוניות" — סיכון
**העיקרון:** יש שופטים שאוהבים לכלול עובדות שאינן מהותיות אך מוסיפות צבע. הסכנה: הקורא עלול לחשוב שההחלטה מבוססת על עובדות אלה. גם הצדדים עלולים לראות בכך זלזול בתיק.
> "There is an obvious danger, however, that the reader may think the decision is based on these facts even though they are not material to the reasoning. Moreover, this style of writing — though appealing to the author — may be seen by the parties as trivializing the case."
> — 1991, עמ' 15; 2020, עמ' 15
**יישום לוועדת ערר:** בבלוק ו — לא לכלול פרטים "מעניינים" שאינם רלוונטיים. לא לתאר את נוף השכונה או היסטוריה שאינה נחוצה. כל עובדה שמופיעה — הקורא יניח שהיא רלוונטית להחלטה.
---
### C5. דיוק עובדתי — אין תחליף לבדיקת הרשומה
**העיקרון:** הצגת העובדות חייבת להיות מדויקת. אין להניח שעובדות כפי שמוצגות בכתבי הטענות נכונות. אין תחליף לבדיקה מול הרשומה.
> "Above all, the statement of facts must be accurate. The writer should not assume that the facts recited in the parties' briefs are stated correctly. There is no substitute for checking fact references against the record."
> — 1991, עמ' 15; 2020, עמ' 16
> "Misstating significant facts or authorities is a mark of carelessness and undermines the opinion's authority and integrity."
> — 1991, עמ' 1; 2020, עמ' 1
**יישום לוועדת ערר:** המערכת חייבת לוודא שעובדות בבלוק ו נלקחות מחומרי המקור (פרוטוקולים, תכניות, תצהירים) — לא מכתבי הטענות. שגיאה עובדתית = פגיעה בסמכות ההחלטה.
---
### C6. בתיקים רב-סוגייתיים — עובדות כלליות בהתחלה, ספציפיות בדיון
**העיקרון:** כשיש סדרת סוגיות ולא כל העובדות רלוונטיות לכולן, ניתן להגביל את תיאור העובדות ההתחלתי לרקע היסטורי נחוץ ולשלב עובדות ספציפיות בניתוח של כל סוגיה.
> "In such a case, the initial statement of facts may be limited to necessary historical background, leaving the specific decisional facts to be incorporated in the analysis of the issues on which they bear."
> — 1991, עמ' 15; 2020, עמ' 15
**יישום לוועדת ערר:** בלוק ו — רקע כללי (מיקום, תכנית רלוונטית, ההליך). בבלוק י — עובדות ספציפיות לכל סוגיה, עם הפניה לבלוק ו אם צריך. נמנעים מכפילות.
---
## D. ציטוטים ואזכורי פסיקה
### D1. אזכור מקרה אחד מספיק — לא מחרוזות
**העיקרון:** רוב הנקודות המשפטיות נתמכות היטב באזכור הפסק האחרון בעניין, או פסק-הדין הפורץ דרך. מחרוזות אזכורים ודיסרטציות על תולדות הכלל אינן מוסיפות כשהעניין מוסדר. יש להתנגד לפיתוי להרשים בלמדנות.
> "Most points of law are adequately supported by citation of the latest decision on point in the court's circuit or the watershed case, if there is one. String citations and dissertations on the history of the rule add nothing when the matter is settled."
> — 1991, עמ' 17; 2020, עמ' 18
> "Judges should resist the temptation of trying to impress people with their (or their law clerks') erudition."
> — 1991, עמ' 17; 2020, עמ' 18
**יישום לוועדת ערר:** לא לכתוב "ראו: עע"מ X; עע"מ Y; עע"מ Z; עת"מ A; עת"מ B" כשמספיק פסק אחד מנחה. מחרוזת אזכורים → מיותרת ומעמיסה. אזכור אחד + ציטוט רלוונטי = מספיק.
---
### D2. פריצת דרך — כן לסקור את המקורות
**העיקרון:** כאשר ההחלטה פורצת דרך חדשה, יש למרשל את המקורות הקיימים ולנתח את התפתחות הדין כדי לתמוך בכלל החדש.
> "If an opinion breaks new ground, however, the court should marshal existing authority and analyze the evolution of the law sufficiently to support the new rule."
> — 1991, עמ' 17; 2020, עמ' 18
**יישום לוועדת ערר:** כשהוועדה קובעת עמדה חדשה (למשל, פרשנות חדשה של סעיף בחוק) — יש לסקור את ההתפתחות בפסיקה ולהראות איך העמדה החדשה נגזרת מהדין הקיים.
---
### D3. מקורות משניים — במשורה ולמטרה
**העיקרון:** מקורות משניים (מאמרים, ספרים, מקורות לא-משפטיים) אינם סמכות ראשית ויש לאזכר אותם במשורה ורק לתכלית ברורה: הפניה לניתוח תומך, סמכות מוכרת בתחום, או שפיכת אור על שיקולי מדיניות.
> "Because law review articles, treatises, texts, and non-legal sources are not primary authority, they should be cited sparingly and only to serve a purpose."
> — 1991, עמ' 18; 2020, עמ' 18
**יישום לוועדת ערר:** ספרות תכנון, חוות דעת מומחים, מסמכי מדיניות — ניתן לאזכר אך רק כשתורמים ממשית לנימוק, לא כעיטור.
---
### D4. ציטוטים — קצרים, הוגנים, רק כשהם חשובים
**העיקרון:** אם משהו חשוב נאמר היטב לפני כן — ציטוט רלוונטי יכול להיות משכנע יותר מפרפרזה. אך ההשפעה של ציטוט יחס הפוך לאורכו. יש לצטט בקצרה, ורק כשהניסוח עושה נקודה חשובה. הציטוט חייב להיות הוגן — בהקשר ומשקף נאמנה את המקור.
> "If something important to the opinion has been said well before, quoting relevant language from a case on point can be more persuasive and informative than merely citing or paraphrasing it. The impact of a quote, however, is inversely proportional to its length. Quote briefly, and only when the language makes an important point."
> — 1991, עמ' 18; 2020, עמ' 18
> "While quotes should be short, they must also be fair. They must be in context and accurately reflect the tenor of their source."
> — 1991, עמ' 18; 2020, עמ' 18
**יישום לוועדת ערר:** לא להביא פסקאות שלמות מפסקי דין. ציטוט = 1-2 משפטים לכל היותר, ורק כשהניסוח המקורי חשוב (כלל מנחה, אמירה מכוננת). תמיד לוודא שהציטוט בהקשרו.
---
### D5. הערות שוליים — רק למידע שמפריע לזרימה
**העיקרון:** מטרת הערת שוליים היא להעביר מידע שיפריע לזרימת ההחלטה אם יכלל בטקסט. השאלה הראשונה: האם התוכן מוצדק בכלל. אם הוא לא חשוב מספיק לטקסט — צריכה להיות סיבה טובה לכלול אותו בהערה. הערות שוליים לא צריכות להיות מאגר של מידע שהכותב לא יודע מה לעשות איתו.
> "The first question to ask about a prospective footnote is whether its content is appropriate for inclusion in the opinion. If it is not important enough to go into the text, the writer must have some justification for including it in the opinion at all."
> — 1991, עמ' 24; 2020, עמ' 24
> "Footnotes should not be inserted for the writer's gratification or as a repository for information that the writer does not know what to do with."
> — 1991, עמ' 24
**יישום לוועדת ערר:** הערות שוליים רק לטקסט חקיקה, פרטי רקע נחוצים אך לא-מרכזיים, או דחיית טענה צדדית בקצרה. לא מאגר לחומר "מעניין".
---
## E. טיפול בצד המפסיד
### E1. דיון מספיק כדי להראות שהטענות נשקלו
**העיקרון:** השופט חייב להתמודד עם סמכות נוגדת לכאורה ועם טענות נגדיות. עליו להתעמת עם הסוגיות ישירות ובכנות. ההחלטה לא צריכה להתייחס לכל תיק וטענה, אך הדיון חייב להספיק כדי להדגים לצד המפסיד שהיסודות של עמדתו נשקלו במלואם.
> "The judge must deal with arguably contrary authority and opposing argument, and must confront the issues squarely and deal with them forthrightly. Although the opinion need not address every case and contention, the discussion must be sufficient to demonstrate to the losing party that the essentials of its position have been fully considered."
> — 1991, עמ' 16; 2020, עמ' 16
**יישום לוועדת ערר:** זהו עיקרון מפתח. כשהערר נדחה — הדיון חייב להראות שהוועדה הבינה את הטענה המרכזית וענתה עליה. לא צריך לענות על כל נקודה, אבל הטענה העיקרית של הצד המפסיד חייבת לקבל מענה מנומק.
---
### E2. לא להפוך לוויכוח עם עורכי הדין
**העיקרון:** בהתייחסות לטענות הצד המפסיד, ההחלטה לא צריכה להפוך לוויכוח בין השופט לעורכי הדין. אם הוצגו טענות מהותיות — יש להסביר למה נדחו. אבל אין צורך להפריך את טענות הצד המפסיד נקודה בנקודה או לאמץ טון עוין.
> "An opinion should not become an argument between the judge and the lawyers, or other judges on the court, or the court below. If the losing side has raised substantial contentions, the opinion should explain why they were rejected. But it need not refute the losing party's arguments point by point or adopt a contentious or adversarial tone."
> — 1991, עמ' 18; 2020, עמ' 18-19
**יישום לוועדת ערר:** הדיון לא מתנהל כ"תשובה לכתב הערר". הוועדה מנתחת את השאלה — לא מתווכחת עם הטוען. במקום "טענת העורר כי X — שגויה מיסודה" → "לאחר בחינת הסוגיה נמצא כי Y, ועל כן אין לקבל את הטענה".
---
### E3. הרשעה בלי להיות טרקט
**העיקרון:** החלטה יכולה — וצריכה — לשדר שכנוע בלי להפוך לחוברת. יש להניח בצד רגשות ותחושות אישיות, ולהימנע משימוש בשמות תואר ותארי פועל אלא אם הם מעבירים מידע מהותי.
> "An opinion can — and properly should — carry conviction without becoming a tract. Put aside emotion and personal feelings, and avoid using adjectives and adverbs unless they convey information material to the decision."
> — 1991, עמ' 18-19; 2020, עמ' 19
**יישום לוועדת ערר:** לא "בבירור" / "ללא ספק" / "ברי כי" אלא אם מדובר בעניין שבאמת ברור. הטון של דפנה — מקצועי, מרוסן, בטוח אך לא פומפוזי.
---
### E4. התייחסות לערכאה הנמוכה — ללא ביקורת מיותרת
**העיקרון:** ניתן ונדרש לתקן שגיאות של הערכאה הנמוכה, אך ללא ביקורת מיותרת, ללא תקיפת שיקול דעתה או גישתה, וללא ייחוס מניעים לא ראויים.
> "Appellate opinions can and should correct trial court errors and provide guidance on remand without embroidering on the circumstances or criticizing the court below. An appellate opinion need not attack a trial court's wisdom, judgment, or even its attitude in order to reverse its decision."
> — 1991, עמ' 19; 2020, עמ' 19
**יישום לוועדת ערר:** כשהערר מתקבל = הוועדה המקומית טעתה. אבל הנימוק צריך להתמקד ב"מה צריך להיות" — לא ב"כמה טעתה הוועדה המקומית". ללא ביטויים כמו "באופן מפתיע" / "למרבה הפליאה".
---
## F. ניסוח התוצאה / המסקנה
### F1. התוצאה היא החלק הכי חשוב
**העיקרון:** התוצאה האופרטיבית — וההוראות לערכאה הנמוכה או לגורם המנהלי — היא החלק הכי חשוב בפסקת הסיום.
> "Disposition of a case — and the mandate to the lower court or agency, when that is a part of the disposition — is the most important part of the conclusion."
> — 1991, עמ' 19; 2020, עמ' 19
**יישום לוועדת ערר:** בלוקים יא-יב — חייבים להיות ברורים ואופרטיביים. "הערר נדחה" / "הערר מתקבל" / "הערר מתקבל בחלקו". בהחזרה — הוראות מפורטות.
---
### F2. לא לדבר בחידות
**העיקרון:** אין לדבר בחידות. להחזיר תיק "להליכים נוספים בהתאם להחלטה זו" עלול להותיר את הערכאה הנמוכה בים. ההחלטה חייבת לפרט בבירור מה צפוי מהם — מבלי לפלוש לשיקול הדעת שנותר בידיהם.
> "Appellate courts should not speak in riddles. Simply to remand a case 'for further proceedings consistent with the opinion' may leave the court below at sea. Opinions must spell out clearly what the lower courts or agencies are expected to do without, however, trespassing on what remains entrusted to their discretion."
> — 1991, עמ' 19; 2020, עמ' 19
**יישום לוועדת ערר:** במקום "הערר מוחזר לדיון מחדש" → "הערר מוחזר לוועדה המקומית לצורך בחינה מחדש של [X] בהתאם לתכנית [Y], תוך מתן הזדמנות שימוע לעורר ובהתחשב ב[Z]." הוראות ספציפיות ואופרטיביות.
---
### F3. גם כשנמצא שימוש לרעה בשיקול דעת — הסמכות נשארת
**העיקרון:** גם כשנמצא שימוש לרעה בשיקול דעת, החלטת ערכאת הערעור היא בשאלת הדין. הערכאה הנמוכה או הגוף המנהלי בהחזרה שומרים על סמכותם להפעיל שיקול דעת כראוי.
> "Even where an abuse of discretion is found, the appellate court's decision is on the law, and the lower court or agency on remand retains the authority to exercise its discretion properly."
> — 1991, עמ' 19; 2020, עמ' 19
**יישום לוועדת ערר:** כשהוועדה המקומית לא שקלה שיקול רלוונטי — הערר מוחזר כדי שתשקול אותו. אין לכפות תוצאה ספציפית (אלא אם הדין מחייב).
---
## G. שפה, סגנון, עריכה עצמית
### G1. שלוש בעיות עיקריות — יתירות, חוסר דיוק, ארגון גרוע
**העיקרון:** הבעיות העיקריות בכתיבה שיפוטית: (א) יתירות — לא רק שימוש בשתי מילים כשמספיקה אחת, אלא ניסיון להעביר יותר מדי מידע, לכסות יותר מדי סוגיות, ופשוט לכתוב יותר מדי; (ב) חוסר דיוק ובהירות; (ג) ארגון גרוע.
> "Wordiness means not just verbosity — using two words when one will do — but trying to convey too much information, covering too many issues, and simply writing too much."
> — 1991, עמ' 21; 2020, עמ' 21
> "Often wordiness reflects the writer's failure (or inability) to separate the material from the immaterial and do the grubby work of editing."
> — 1991, עמ' 21; 2020, עמ' 21
**יישום לוועדת ערר:** עריכה קפדנית של כל בלוק. אם משפט לא מקדם את הנימוק — למחוק. אם סוגיה לא נחוצה — לקצר או להסיר.
---
### G2. דיוק — המטרה המרכזית
**העיקרון:** דיוק הוא המטרה המרכזית של כתיבה טובה. כדי לכתוב בבהירות ודיוק — הכותב חייב לדעת בדיוק מה הוא רוצה לומר, ולומר את זה ותו לא. שופטים כותבים לנצח — ברגע שהחלטה מוגשת, עורכי דין יקראו אותה עם עין למה שישרת את מטרתם.
> "To write with clarity and precision, the writer must know precisely what he or she wants to say and must say that and nothing else."
> — 1991, עמ' 21; 2020, עמ' 21
> "Precision in judicial writing is important not simply as a matter of style but also because judges write for posterity. Once an opinion is filed, lawyers and others will read it with an eye to how they can use it to serve their particular purpose."
> — 1991, עמ' 21; 2020, עמ' 21
**יישום לוועדת ערר:** כל משפט — "האם אמרתי בדיוק מה שרציתי? האם ניתן לקרוא את זה אחרת ממה שהתכוונתי?" מיוחד חשוב בהחלטות שקובעות תקדים.
---
### G3. השמטת מילים מיותרות — עיקרון סטרנק
**העיקרון:** כתיבה עזה היא תמציתית. כל מילה צריכה לעבוד.
> "Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all his sentences short, or that he avoid all detail and treat his subjects only in outline, but that every word tell."
> — Strunk & White, מצוטט ב-1991, עמ' 22-23; 2020, עמ' 22-23
**יישום לוועדת ערר:** בעריכה — לסמן כל מילה ולשאול: "האם היא נחוצה?" לא "קצר" — אלא "כל מילה עובדת". זהו הכלל המרכזי לסגנון דפנה.
---
### G4. תמציתיות ועמידה בנקודה
**העיקרון:** תמציתיות מקדמת בהירות. כתיבה שמגיעה לנקודה בקצרה — מובנת יותר. יש להשתמש במשפטים פשוטים ודקלרטיביים ובפסקאות קצרות, אך לגוון את אורך המשפט ומבנהו לצורכי הדגשה וניגוד. יש להעדיף לשון פעילה ולהימנע מבניות כמו "נטען כי", "הוטען כי".
> "Use simple, declarative sentences and short paragraphs most of the time, but vary sentence length and structure where necessary for emphasis, contrast, and reader interest. Prefer the active voice and avoid constructions such as 'it is said,' 'it is argued,' and 'it is well founded.'"
> — 1991, עמ' 23; 2020, עמ' 23
> "Weed out adjectives and eliminate adverbs such as 'clearly,' 'plainly,' and 'merely.'"
> — 1991, עמ' 23; 2020, עמ' 23
**יישום לוועדת ערר:** לא "נטען על-ידי העורר כי הוועדה המקומית טעתה" → "העורר טוען כי הוועדה המקומית טעתה". לא "ברי כי" / "מובן מאליו כי" — אם זה ברור, לא צריך לומר שזה ברור.
---
### G5. שפה פשוטה — אנגלית/עברית רגילה
**העיקרון:** אפילו רעיונות מורכבים ניתנים לביטוי בשפה פשוטה. יש להימנע מ"לשון משפטית", קלישאות, ביטויים שחוקים, ביטויים לטיניים, וז'רגון. כשמשתמשים במונחי מקצוע — לבדוק אם הם מובנים לקהל או דורשים הגדרה.
> "Even complex ideas can be expressed in simple language understandable by the general reader. To write in simple language requires that the writer understand the idea fully, enabling him or her to break it down into its essential components."
> — 1991, עמ' 23; 2020, עמ' 23
> "Avoid 'legalese,' clichés, hackneyed phrases ('as hereinabove set forth,' for example), Latin expressions ('vel non,' for example), and jargon."
> — 1991, עמ' 23; 2020, עמ' 23
**יישום לוועדת ערר:** לא "כדרישת הדין ולפיו" / "לאמור לעיל" / "כאמור" (מיותר). עברית פשוטה ובהירה. מונח תכנוני — להגדיר אם לא ברור ("תכנית בניין עיר" לא "תב"ע" ללא הגדרה ראשונית).
---
### G6. פומפוזיות — להימנע
**העיקרון:** כתיבה שיפוטית עלולה להיות פומפוזית. השופט חייב להיזהר: ביטויים ארכאיים או מליציים, שימוש ב"אנו" הקיסרי על-ידי שופט יחיד, סטיות ללמדנות שאינה רלוונטית.
> "The judge must be vigilant for evidence of pomposity, such as arcane or florid expressions, use of the imperial 'we' by a single district judge, or excursions into irrelevant erudition."
> — 1991, עמ' 22; 2020, עמ' 22
**יישום לוועדת ערר:** הוועדה = "הוועדה", לא "אנו סבורים" (אם יו"ר יחיד כותב). לא "למותר לציין כי" / "מן המפורסמות הוא כי". טון סמכותי אך פשוט.
---
### G7. הומור — סיכון שלא כדאי לקחת
**העיקרון:** הומור עובד טוב יותר בנאום מאשר בהחלטה. בעלי הדין — שלא סביר שיראו משהו מצחיק בהתדיינות — עלולים לראות בו סימן ליהירות וחוסר רגישות.
> "Although humor is sometimes rationalized as an antidote to pomposity, it works better in after-dinner speeches than in judicial opinions. In the latter it may strike the litigants — who are not likely to see anything funny in the litigation — as a sign of judicial arrogance and lack of sensitivity."
> — 1991, עמ' 22; 2020, עמ' 22
**יישום לוועדת ערר:** לא הומור, לא אירוניה, לא ציניות בהחלטות. גם אם הטענה נראית מגוחכת — להתייחס בכבוד.
---
### G8. עריכה — לא רק שפה, גם תוכן ומבנה
**העיקרון:** בעריכה, השופט צריך לבדוק: (א) עקביות פנימית; (ב) האם המבוא מכסה את כל הסוגיות; (ג) האם העובדות מכסות את כל מה שנחוץ להחלטה ולא יותר; (ד) האם הדיון מתייחס בסדר לוגי לכל הסוגיות; (ה) האם המסקנה נובעת מהדיון.
> "Judges must check for internal consistency. Go back to the introduction to see whether the opinion has addressed all of the issues and answered the questions as they were initially formulated. Reread the statement of facts to see whether it covers all the facts significant to the decision and no more. Review the legal discussion to see whether the opinion has addressed in logical order the issues that need to be addressed. Consider whether the conclusion follows from the discussion."
> — 1991, עמ' 25; 2020, עמ' 25-26
**יישום לוועדת ערר:** צ'קליסט עריכה אוטומטי: (1) עקביות בלוק א ↔ בלוק יב; (2) כל עובדה בבלוק ו מופיעה בדיון?; (3) סדר הסוגיות לוגי?; (4) המסקנה נובעת מהניתוח?
---
### G9. הנחת הטיוטה בצד ושיבה אליה
**העיקרון:** שיפור העריכה — על-ידי הנחת הטיוטה בצד ושיבה אליה מאוחר יותר. גם עיכוב של ימים ספורים מאפשר מבט אובייקטיבי יותר, תובנות חדשות, ורעיונות חדשים.
> "Although time constraints and mounting caseloads may make it difficult, delaying editing the opinion for even a few days may help the judge review things more objectively, gain new insights, and think of new ideas."
> — 1991, עמ' 25; 2020, עמ' 26
**יישום לוועדת ערר:** בתהליך העבודה עם המערכת — שלב "צינון" לפני עריכה סופית. הטיוטה נשמרת, יו"ר הוועדה חוזרת אליה לאחר זמן.
---
### G10. עריכה משפט-משפט
**העיקרון:** עריכה מדוקדקת ומהורהרת חיונית לכתיבה מדויקת. זה אומר לעבור על ההחלטה משפט אחרי משפט ולשאול: מה התכוונתי לומר כאן, והאם אמרתי את זה ולא יותר?
> "Painstaking and thoughtful editing is essential for precise writing. This means going over the opinion, sentence by sentence, and asking: What do I mean to say here, and have I said it and no more?"
> — 1991, עמ' 21-22; 2020, עמ' 21
**יישום לוועדת ערר:** כל בלוק — עריכה ברמת המשפט. כל משפט עומד בפני עצמו ומוסיף מידע חדש או נקודה חדשה.
---
## H. חידושים ייחודיים למהדורה השנייה (2020)
### H1. התייחסות לעידן הדיגיטלי
**העיקרון:** המהדורה השנייה מציינת שהחלטות שיפוטיות נקראות יותר ויותר בפורמט דיגיטלי, ולכן הבהירות חשובה אף יותר.
> "With so much of today's writing embedded in the truncated protocols of social media and other 'real time' forms of expression, the clarity and persuasive quality the authors of the first edition sought to teach are particularly important for judges' writing."
> — 2020, Foreword, עמ' ix
**יישום לוועדת ערר:** ההחלטות מתפרסמות באתר הוועדה ובמאגרי מידע — מותאמות לקריאה דיגיטלית. כותרות, מבנה, פסקאות קצרות.
---
### H2. ציטוט מ-Bryan Garner על שפה משפטית
**העיקרון:** המהדורה השנייה מוסיפה ציטוט מ-Garner על הימנעות מביטויים משפטיים מסורתיים:
> "[N]ever assume that traditional legal expressions are legally necessary. As often as not they are scars left by the law's verbal elephantiasis, which only lately has started into remission. Use words and phrases that you know to be both precise and as widely understood as possible."
> — Bryan Garner, מצוטט ב-2020, עמ' 23-24
**יישום לוועדת ערר:** ביטויים כמו "בכבוד רב", "מן הראוי", "למיטב הבנתנו" — לא "נחוצים משפטית". להחליף בשפה פשוטה ומדויקת.
---
### H3. מודעות לפרסום בלתי נשלט
**העיקרון:** המהדורה השנייה מוסיפה אזהרה שמפרסמים משפטיים (כמו Westlaw) מפרסמים לפעמים החלטות שסומנו כ"לא לפרסום" — על סמך שיקול דעתם שלהם.
> "Some legal publishers, including Westlaw, put certain district court orders and opinions on line whether or not the judge designates them for publication and even sometimes when a judge states that the order or opinion is 'not for publication.'"
> — 2020, עמ' 7
**יישום לוועדת ערר:** כל החלטה של ועדת הערר עלולה להתפרסם ולשמש תקדים — גם אם לא תוכננה לכך. יש לכתוב כל החלטה כאילו תפורסם.
---
### H4. הדגשת ניתוח קריפטי כבעיה נפרדת
**העיקרון:** המהדורה השנייה מבנה את "ניתוח קריפטי" כבעיה נפרדת (לא רק תת-סעיף) — מה שמדגיש את חשיבות פירוט הנימוקים.
**יישום לוועדת ערר:** בלוק י — כל צעד בנימוק חייב להיות מפורש. אסור "לדלג" מכלל למסקנה בלי ליישם על העובדות.
---
### H5. מבנה מעודכן — "Editing the Opinion" כפרק נפרד
**העיקרון:** במהדורה הראשונה, שפה/סגנון/עריכה היו פרק אחד. במהדורה השנייה, "Editing" הוא פרק נפרד (V), מה שמדגיש את חשיבות העריכה כתהליך עצמאי ולא כחלק מהכתיבה.
**יישום לוועדת ערר:** בתהליך העבודה — שלב עריכה מוגדר, נפרד מהכתיבה. המערכת מפעילה צ'קליסט עריכה אוטומטי אחרי יצירת הטיוטה.
---
### H6. הפניה ל-Aldisert על חשיבה לוגית לפני כתיבה
**העיקרון:** המהדורה השנייה מוסיפה ציטוט של שופט Aldisert:
> "If a judge wants to write clearly and cogently, with words parading before the reader in logical order, the judge must first think clearly and cogently, with thoughts laid out in neat rows."
> — Aldisert, Opinion Writing (2d ed. 2009), מצוטט ב-2020, עמ' 9
**יישום לוועדת ערר:** לפני שהמערכת כותבת — שלב "תכנון" חובה: מה התוצאה? מה הנימוקים? באיזה סדר? רק אחר-כך — כתיבה.
---
## סיכום כללי — עקרונות-על
1. **ההחלטה קיימת כדי להסביר ולשכנע** — לא רק להכריע, אלא להראות שההכרעה מבוססת, הוגנת, ומנומקת.
2. **כל מילה צריכה לעבוד** — תמציתיות היא לא קיצור אלא הסרת המיותר.
3. **הצד המפסיד צריך לראות שהוא נשמע** — הדיון חייב להדגים שהטענות המרכזיות נשקלו.
4. **דיוק הוא הדבר החשוב ביותר** — כל משפט נקרא לנצח וייקרא בדרכים שלא ציפית.
5. **מבנה ברור = חשיבה ברורה** — כותרות, סדר לוגי, וחמישה אלמנטים.
6. **לא סנגוריה** — ההחלטה משכנעת בכוח הנימוק, לא בטון.
7. **עובדות מדויקות והוגנות** — כולל עובדות שתומכות בצד המפסיד.
8. **ציטוטים קצרים, אזכורים מועטים** — אחד טוב > עשרה מיותרים.
9. **הוראות אופרטיביות ברורות** — לא חידות, לא עמימות.
10. **כתוב אחרון — ערוך ראשון** — המבוא נכתב אחרי הדיון; העריכה חשובה כמו הכתיבה.

View File

@@ -0,0 +1,625 @@
# עקרונות כתיבת החלטות מעין-שיפוטיות — מיצוי מספרי גארנר
מסמך מתודולוגי המבוסס על שני ספרים:
1. **Making Your Case: The Art of Persuading Judges** (Scalia & Garner, 2008)
2. **Legal Writing in Plain English** (Garner, 2001)
> **הערה חשובה**: "Making Your Case" נכתב עבור עורכי דין טוענים, לא שופטים. העקרונות כאן מותאמים לכתיבת החלטות — לא לטיעון תיק.
---
## א. חשיבה משפטית והנמקה (Making Your Case, פרקים 2227)
### א.1 חשיבה סילוגיסטית — מבנה כל טיעון משפטי
**עיקרון**: כל הנמקה משפטית חייבת להיבנות כסילוגיזם: הנחה עליונה (כלל משפטי) → הנחה תחתונה (עובדות המקרה) → מסקנה.
> "Leaving aside emotional appeals, persuasion is possible only because all human beings are born with a capacity for logical thought... The most rigorous form of logic, and hence the most persuasive, is the syllogism." (MYC §22)
> "If the major premise (the controlling rule) and the minor premise (the facts invoking that rule) are true... the conclusion follows inevitably." (MYC §22)
**יישום להחלטות ועדת ערר**: כל סוגיה בבלוק י (דיון) חייבת להיבנות כך:
- הנחה עליונה: הכלל התכנוני/המשפטי (סעיף בתוכנית, פסיקה, עקרון תכנוני)
- הנחה תחתונה: העובדות הספציפיות של הערר
- מסקנה: התוצאה לגבי סוגיה זו
**עיקרון משנה — שלושה מקורות להנחה עליונה**:
> "Legal argument generally has three sources of major premises: a text (constitution, statute, regulation, ordinance, or contract), precedent (caselaw, etc.), and policy (i.e., consequences of the decision)." (MYC §22)
**יישום**: בעררי תכנון, המקורות הם:
- טקסט: הוראות התוכנית, חוק התכנון והבניה, תקנות
- תקדים: החלטות ועדות ערר קודמות, פסיקת בתי משפט
- מדיניות: שיקולים תכנוניים (צפיפות, אופי הסביבה, אינטרס ציבורי)
**עיקרון משנה — ההנחה התחתונה היא המפתח**:
> "There is much to be said for the proposition that 'legal reasoning revolves mainly around the establishment of the minor premise.'" (MYC §22)
**יישום**: ברוב העררים, הכלל המשפטי אינו שנוי במחלוקת — השאלה היא כיצד העובדות משתלבות בכלל. ההחלטה חייבת להראות בפירוט כיצד העובדות הספציפיות מקיימות או אינן מקיימות את תנאי הכלל.
### א.2 פרשנות טקסטואלית — ניתוח הוראות תוכנית
**עיקרון ראשי**: לפני כל מסקנה לגבי משמעות טקסט — קרא את המסמך כולו.
> "Paramount rule: Before coming to any conclusion about the meaning of a text, read the entire document, not just the particular provision at issue. The court will be seeking to give an ambiguous word or phrase meaning in the context of the document in which it appears." (MYC §23)
**כללי פרשנות שיש לאמץ**:
> "Words are presumed to bear their ordinary meanings." (MYC §23)
> "Without some contrary indication, a word or phrase is presumed to have the same meaning throughout a document." (MYC §23)
> "The provisions of a document should be interpreted in a way that renders them harmonious, not contradictory." (MYC §23)
> "If possible, every word should be given effect; no word should be read as surplusage." (MYC §23)
**יישום**: כשההחלטה מפרשת הוראת תוכנית:
1. הצג את לשון ההוראה המלאה
2. פרש מילים במשמעותן הרגילה
3. בדוק עקביות עם הוראות אחרות באותה תוכנית
4. תן תוקף לכל מילה — אל תתעלם ממילים "מיותרות"
5. אם יש עמימות — השתמש בכלים הקאנוניים (הכלל הכללי מצטמצם לאור הפרט; מילה מתפרשת על פי הקשרה)
**כלים קאנוניים לפרשנות** (MYC §23):
- **Inclusio unius**: הכללת דבר אחד מרמזת על הדרת אחרים
- **Noscitur a sociis**: מילה מתפרשת לאור המילים הסמוכות לה
- **Ejusdem generis**: קטגוריה כללית שבאה אחרי רשימה מתייחסת לפריטים מאותו סוג
### א.3 התחל תמיד מלשון הטקסט
**עיקרון**: כשהמקרה נשלט על ידי טקסט משפטי — התחל תמיד מהמילים.
> "In cases controlled by governing legal texts, always begin with the words of the text to establish the major premise." (MYC §24)
**יישום**: בלוק י חייב לפתוח כל דיון בסוגיה בציטוט ישיר של ההוראה הרלוונטית מהתוכנית/חוק, ורק אז לעבור לניתוח ויישום על העובדות.
### א.4 משקל תקדימים — היררכיה ברורה
**עיקרון**: לסמכויות משפטיות שונות יש משקל שונה, וחובה להכיר בהיררכיה.
> "From a juridical point of view, case authorities are of two sorts: those that are governing (either directly or by implication) and those that are persuasive." (MYC §26)
> "Governing authorities are more significant and should occupy more of your attention." (MYC §26)
**היררכיה בעררי תכנון** (לפי סדר יורד של משקל):
1. פסיקת בית המשפט העליון
2. פסיקת בית משפט לעניינים מנהליים (שנותן ביקורת שיפוטית ישירה)
3. החלטות ועדת ערר ארצית
4. החלטות ועדות ערר מחוזיות אחרות
5. ספרות משפטית/תכנונית
**עיקרון משנה — עדיפות לתקדים עדכני**:
> "At least where opinions of governing courts are concerned, the more recent the citation the better. The judge wants to know whether the judgment you seek will be affirmed by the current court, not whether it would have been affirmed 30 years ago." (MYC §26)
### א.5 מצא ניסוח מפורש להנחה העליונה
**עיקרון**: אם אפשר, ציין בדיוק מהי ההנחה העליונה תוך ציטוט ישיר מסמכות מחייבת.
> "It is often quite easy to find a governing case with a passage that says precisely what you want your major premise to be." (MYC §27)
> "When direct quotation is not possible, set forth the major premise in your own words, supported by citation of a case from a governing court." (MYC §27)
**יישום**: בפתיחת דיון בכל סוגיה, ההנחה העליונה צריכה להופיע בצורה ברורה — אם אפשר כציטוט ישיר מפסק דין או מהוראת חוק/תוכנית.
---
## ב. מבנה וארגון (משני הספרים)
### ב.1 הצגת המסקנה מראש (Front-loading)
**עיקרון**: התחל תמיד בהצגת הסוגיה המרכזית לפני שמפרט עובדות.
> "Always start with a statement of the main issue before fully stating the facts." (MYC §14)
> "The facts one reads seem random and meaningless until one knows what they pertain to." (MYC §14)
> "The greatest mistake a lawyer can make either in briefing or oral argument is to keep the court in the dark as to what the case is about until after a lengthy discussion of dates, testimony of witnesses, legal authorities, and the like." (MYC §14, ציטוט השופט McAmis)
**עיקרון משלים מ-Legal Writing in Plain English**:
> "Virtually all analytical or persuasive writing should have a summary on page one—a true summary that capsulizes the upshot of the message. This upshot inevitably consists of three parts: the question, the answer, and the reasons." (LWPE §22)
**יישום**: בלוק א (כותרת) ובלוק ב (סיכום מנהלי) חייבים לגלות מיד את מהות הערר ואת התוצאה. הקורא לא צריך לקרוא 10 עמודים כדי להבין במה מדובר.
### ב.2 טכניקת ה-"Deep Issue" — סילוגיזם בשאלה
**עיקרון**: נסח את הסוגיה בצורת סילוגיזם מכווץ — עד 75 מילים, במספר משפטים.
> "The most persuasive form of an issue statement—the so-called deep issue—contains within it the syllogism that produces your desired conclusion." (MYC §36)
> "The better strategy is to break up the question into separate sentences totaling no more than 75 words. The first sentences follow a chronological order, telling a story in miniature. Then, emerging inevitably from the story, the pointed question comes at the end." (MYC §36)
**דוגמה מהספר**: במקום "האם דו"ח חקירת האירוע הפר כללי OSHA?" — כתוב:
> "כללי OSHA דורשים שכל דו"ח חקירת אירוע יכלול רשימת גורמים תורמים. הדו"ח על הפיצוץ במפעל פירט את הגורמים התורמים לא בגוף הדו"ח אלא בנספח נפרד. האם הדו"ח הפר את כללי OSHA?"
**יישום**: בלוק ב (סיכום מנהלי) צריך לנסח כל סוגיה בדרך זו — הנחה משפטית, עובדות תמציתיות, שאלה חדה.
### ב.3 שלושה חלקים: פתיחה, גוף, סיכום
**עיקרון**: כל כתיבה אנליטית חייבת שלושה חלקים — ורוב הכתיבה המשפטית מזניחה את הפתיחה והסיכום.
> "Virtually all expository writing should have three parts: an introduction, a main body, and a conclusion. You'd think everyone knows this. Not so: the orthodox method of brief-writing, and the way of many research memos, is to give only one part—a middle." (LWPE §21)
> "The conclusion should briefly sum up the argument. If you're writing as an advocate, you'll need to show clearly what the decision-maker should do and why." (LWPE §21)
**יישום**: ההחלטה חייבת פתיחה (בלוקים א–ב), גוף (בלוקים ג–י), וסיכום (בלוקים יא–יב). הסיכום אינו "לאור כל האמור לעיל" אלא חזרה תמציתית ורעננה על עיקרי ההנמקה.
### ב.4 סדר הסוגיות — החזק מתחיל
**עיקרון**: אם ההיגיון מאפשר — פתח בטיעון החזק ביותר.
> "If possible, lead with your strongest argument." (MYC §7)
> "Why? Because first impressions are indelible. Because when the first taste is bad, one is not eager to drink further. Because judicial attention will be highest at the outset." (MYC §7)
**חריג חשוב**: כשההיגיון דורש סדר אחר (למשל, שאלת סמכות לפני דיון בגוף)
> "Sometimes, of course, the imperatives of logical exposition demand that you first discuss a point that is not your strongest." (MYC §7)
**יישום**: בבלוק י, סדר הסוגיות צריך להיקבע לפי:
1. שאלות סף (סמכות, מועד) — תמיד ראשונות
2. הסוגיה המרכזית — מיד אחריהן
3. סוגיות משניות — לפי חוזק ההנמקה
### ב.5 כותרות אינפורמטיביות
**עיקרון**: השתמש בכותרות שהן משפטים מלאים המודיעים לא רק על הנושא אלא גם על העמדה.
> "Headings are most effective if they're full sentences announcing not just the topic but your position on the topic: Not 'I. Statute of Limitations' but 'I. The statute of limitations was tolled while the plaintiff suffered from amnesia.'" (MYC §40)
> "State and federal judges routinely emphasize this point at judicial-writing seminars. They say that headings and subheadings help them keep their bearings, let them actually see the organization, and afford them mental rest stops." (LWPE §4)
**יישום**: כל כותרת סעיף בהחלטה צריכה להודיע על המסקנה, לא רק על הנושא:
- לא: "סוגיית הבנייה בקו אפס"
- כן: "הבנייה בקו אפס אינה עולה בקנה אחד עם תוכנית המתאר"
### ב.6 פסקת מפה (Roadmap Paragraph)
**עיקרון**: ספק שלטי דרך ברורים — אמור מראש כמה נקודות יש ומה הן.
> "If there are three issues you're going to discuss, state them explicitly on page one. If there are four advantages to your recommended course of action, say so when introducing the list. And be specific: don't say that there are 'several' advantages. If there are four, say so." (LWPE §27)
**יישום**: בפתיחת בלוק י, כתוב: "הסוגיות שיש לדון בהן הן שלוש: (1) ...; (2) ...; (3) ...". זה מכין את הקורא ומאפשר לו לעקוב.
### ב.7 חלק וכבוש — חלוקה לסעיפים
**עיקרון**: חלק את המסמך לסעיפים ותתי-סעיפים עם כותרות.
> "Once you've determined the necessary order of your document, you should divide it into discrete, recognizable parts... The more complex your project, the simpler and more overt its structure should be." (LWPE §4)
**יישום**: ארכיטקטורת 12 הבלוקים כבר מספקת חלוקה מאקרו. בתוך בלוק י, יש לחלק לפי סוגיות עם כותרות וכותרות משנה.
---
## ג. טכניקות ברמת הפסקה (Legal Writing in Plain English)
### ג.1 משפט נושא בפתיחת כל פסקה
**עיקרון**: פתח כל פסקה במשפט שמודיע על הנושא המרכזי שלה.
> "By stating the controlling idea, a topic sentence will lend unity to a paragraph... readers who are in a hurry will get your point efficiently." (LWPE §24)
> "Good writers think of the paragraph—not the sentence—as the basic unit of thought." (LWPE §24)
**כלל מעשי**: אל תפתח פסקה באזכור תיק ללא הקשר:
> "Delaying the citation typically enables you to write a stronger topic sentence." (LWPE §24)
**יישום**: במקום "בעע"מ 1234/05 נקבע ש..." — כתוב "ועדת ערר אינה מוסמכת להתערב בשיקול דעת מקצועי של מהנדס העיר. כך נפסק ב..."
### ג.2 גשרים בין פסקאות (Echo Links)
**עיקרון**: כל פתיחת פסקה חייבת לכלול מילת קישור או הד לפסקה הקודמת.
> "Every paragraph opener should contain a transitional word or phrase to ease the reader's way from one paragraph to the next." (LWPE §25)
**שלושה כלים**:
> "Pointing words—that is, words like this, that, these, those, and the. Echo links—that is, words or phrases in which a previously mentioned idea reverberates. Explicit connectives—that is, words whose chief purpose is to supply transitions." (LWPE §25)
**רשימת מילות קישור** (LWPE §25):
- הוספה: גם, בנוסף, כמו כן, באופן דומה, יתרה מכך
- דוגמה: למשל, כדוגמה, לענייננו
- ניסוח מחדש: כלומר, במילים אחרות, בקצרה
- סיבה: מכיוון ש-, שכן, בשל
- תוצאה: לפיכך, אי לכך, כתוצאה מכך, משכך
- ניגוד: אולם, ואולם, לעומת זאת, מנגד, עם זאת
- ויתור: אמנם, נכון ש-, גם אם, אף ש-
- חיזוק: אכן, למעשה, ללא ספק
### ג.3 פסקה אחת — סוגיה אחת
**עיקרון**: כל פסקה צריכה לעסוק בנקודה אחת בלבד.
> "The topic sentence ensures that each paragraph has its own cohesive content. A good topic sentence centers the paragraph. It announces what the paragraph is about, while the other sentences play supporting roles." (LWPE §24)
**יישום**: אם פסקה עוסקת גם בכלל המשפטי וגם ביישומו על המקרה וגם בהתמודדות עם טענה נגדית — חלק אותה.
### ג.4 אורך פסקאות — קצר עדיף
**עיקרון**: פסקאות קצרות מגבירות קריאות.
> "Strive for an average paragraph of no more than 150 words—preferably far fewer—in three to eight sentences." (LWPE §26)
> "As with sentence length, you need variety in paragraph length: some slender paragraphs and some fairly ample ones." (LWPE §26)
**יישום**: בהחלטה, ממוצע של 100150 מילים לפסקה. פסקה של משפט אחד מותרת ואפילו רצויה לעתים — למשל, כמשפט סיכום חד.
---
## ד. בהירות ברמת המשפט (Legal Writing in Plain English)
### ד.1 בניין פעיל
**עיקרון**: העדף בניין פעיל על פני סביל.
> "In an active-voice construction, the subject does something (The court dismissed the appeal). In a passive-voice construction, something is done to the subject (The appeal was dismissed by the court)." (LWPE §8)
**ארבעה יתרונות**:
> "It usually requires fewer words. It better reflects a chronologically ordered sequence. It makes the reader's job easier because its syntax meets the English-speaker's expectation. It makes the writing more vigorous and lively." (LWPE §8)
**יישום**: במקום "הבקשה נדחתה על ידי הוועדה המקומית" — "הוועדה המקומית דחתה את הבקשה". חריג: כשהפועל חשוב מהפועל ("ההיתר בוטל" — כשלא חשוב מי ביטל).
### ד.2 קרבת נושא-נשוא-מושא
**עיקרון**: שמור את הנושא, הפועל והמושא קרובים זה לזה — ובתחילת המשפט.
> "Keep the subject, the verb, and the object together—toward the beginning of the sentence." (LWPE §7)
> "The reason you should put the subject and verb at or near the beginning is that readers approach each sentence by looking for the action." (LWPE §7)
**יישום**: במקום: "העורר, אשר רכש את הנכס בשנת 2018 ופנה לוועדה המקומית בבקשה להיתר בניה במרץ 2020, טוען כי..." — כתוב: "העורר טוען כי... [ההקשר העובדתי יובא בהמשך או בפסקה נפרדת]"
### ד.3 אורך משפטים — ממוצע 20 מילים
**עיקרון**: שמור על ממוצע של כ-20 מילים למשפט, עם גיוון.
> "Keep your average sentence length to about 20 words." (LWPE §6)
> "Not only do you want a short average; you also need variety. That is, you should have some 35-word sentences and some 3-word sentences, as well as many in between." (LWPE §6)
**יישום**: הימנע ממשפטים של 60+ מילים שנפוצים בכתיבה משפטית ישראלית. שבור משפטים ארוכים. משפט קצר ומפתיע ("הערר נדחה") יכול להעניק אפקט חזק.
### ד.4 הפוך שמות פעולה לפעלים
**עיקרון**: הימנע משמות פעולה (-tion words / שמות פעולה בעברית) כשאפשר להשתמש בפועל.
> "Turn -ion words into verbs when you can." (LWPE §14)
> "Write that someone has violated the law, not that someone was in violation of the law; that something illustrates something else, not that it provides an illustration of it." (LWPE §14)
**יישום**: במקום "ביצוע בחינה של" — "לבחון". במקום "קבלת החלטה" — "להחליט". במקום "מתן אישור" — "לאשר".
### ד.5 השמט מילים מיותרות
**עיקרון**: לחם נגד מילוי מילים. כל מילה שאינה עוזרת — מפריעה.
> "Three good things happen when you combat verbosity: your readers read faster, your own clarity is enhanced, and your writing has greater impact." (LWPE §5)
> "Every word that is not a help is a hindrance because it distracts. A judge who realizes that a brief is wordy will skim it; one who finds a brief terse and concise will read every word." (MYC §35)
**ביטויים מנופחים ותחליפיהם** (LWPE §15):
| מנופח | פשוט |
|---|---|
| במידה ו- | אם |
| בנסיבות אלה | לכן |
| לאור העובדה ש- | מכיוון ש- |
| בשלב הנוכחי | עתה |
| על מנת ש- | כדי ש- |
| בסמוך לאחר | אחרי |
| לא יאוחר מ- | עד |
### ד.6 סיים משפטים בחוזקה
**עיקרון**: המילה האחרונה במשפט היא החשובה ביותר.
> "Professional writers know that a sentence's final word, whatever it may be, should have a special kick." (LWPE §11)
**יישום**: אל תסיים משפט בתאריך או בהפניה אלא אם הם חשובים. במקום "הבקשה נדחתה ביום 15.3.2024" — "ביום 15.3.2024 נדחתה הבקשה". או אם התאריך לא חשוב — "הוועדה המקומית דחתה את הבקשה".
### ד.7 הימנע מז'רגון מיותר
**עיקרון**: אם יש מילה רגילה שאומרת אותו דבר — השתמש בה.
> "Learn to detest simplifiable jargon." (LWPE §12)
> "Legalisms should become part of your reading vocabulary, not part of your writing vocabulary." (LWPE §12)
**יישום**: במקום "הננו להורות" — "אנו מורים". במקום "דנא" — "כאן". במקום "המבקש דנן" — "העורר". במקום "כמפורט לעיל" — "כפי שצוין".
### ד.8 הימנע מכפילויות ושלישיות
**עיקרון**: אם מילה אחת מספיקה, אל תשתמש בשתיים או שלוש.
> "The idea isn't to say something in as many ways as you can, but to say it as well as you can." (LWPE §16)
**יישום**: במקום "לבטל ולהפקיע" — "לבטל". במקום "לפרש ולהבהיר" — "לפרש". כל מילה נוספת מחייבת את הקורא לחפש הבדל.
### ד.9 הקפד על הקבלה דקדוקית
**עיקרון**: רעיונות מקבילים דורשים מבנה דקדוקי מקביל.
> "Just as you should put related words together in ways that match the reader's natural expectations, you should also state related ideas in similar grammatical form." (LWPE §9)
**יישום**: ברשימות תנאים או נימוקים, שמור על מבנה אחיד. אם התנאי הראשון מתחיל בשם עצם — כולם יתחילו בשם עצם. אם הראשון פועל — כולם פועל.
### ד.10 הימנע מכפל שלילות
**עיקרון**: אם אפשר לנסח חיובית — עשה כן.
> "When you can recast a negative statement as a positive one without changing the meaning, do it. You'll save readers from needless mental exertion." (LWPE §10)
**יישום**: במקום "לא ניתן שלא להתעלם מ-" — ניסוח חיובי ברור. במקום "אין יסוד לטענה כי אין סמכות" — "לוועדה יש סמכות".
---
## ה. התמודדות עם טיעוני צד שכנגד (Making Your Case)
### ה.1 הכר את הצד השני — "Steel-manning"
**עיקרון**: אל תחליף את טענת היריב בטענת קש שקל להפריך.
> "Don't delude yourself. Try to discern the real argument that an intelligent opponent would make, and don't replace it with a straw man that you can easily dispatch." (MYC §4)
**יישום**: בבלוק י, כשמתמודדים עם טענות הצד שהפסיד — הצג את טענותיו בצורה הוגנת וחזקה לפני שדוחה אותן. זה מחזק את אמינות ההחלטה.
### ה.2 ויתור מפגין על שטח בלתי-ניתן להגנה
**עיקרון**: הודה בנקודות שנגדך — בגלוי ובנדיבות.
> "Don't try to defend the indefensible." (MYC §11)
> "Openly acknowledge the ones that are against you. In fact... raise them candidly and explain why they aren't dispositive." (MYC §11)
> "A weak argument does more than merely dilute your brief. It speaks poorly of your judgment and thus reduces confidence in your other points. As the saying goes, it is like the 13th stroke of a clock: not only wrong in itself, but casting doubt on all that preceded it." (MYC §11)
**יישום**: כשיש נקודה שפועלת לטובת העורר שהערר שלו נדחה — הכר בה מפורשות: "אמנם צודק העורר כי המבנה הסמוך חורג מקו הבניין, אולם עובדה זו אינה מקנה לו זכות לחרוג אף הוא, שכן..."
### ה.3 הפרכה מקדימה — באמצע, לא בהתחלה ולא בסוף
**עיקרון**: טפל בטענות נגדיות באמצע הדיון — לא בפתיחה (שמציבה אותך בעמדת הגנה) ולא בסיום (שמשאירה את המוקד על טענות הצד השני).
> "For the first to argue, refutation belongs in the middle. Aristotle observed that 'in court one must begin by giving one's own proofs, and then meet those of the opposition by dissolving them and tearing them up before they are made.'" (MYC §8)
**יישום בכתיבת החלטה**: מבנה מומלץ לכל סוגיה (מבוסס על LWPE §30):
1. הנחה משפטית (הכלל)
2. הנחה עובדתית (העובדות)
3. מסקנה ראשונית
4. **טענה נגדית אפשרית + תשובה**
5. **טענה נגדית נוספת + תשובה**
6. נקודה תומכת נוספת
7. משפט סיכום חד
> "An argument using this structure makes for convincing reading. And it's hard to rebut." (LWPE §30)
### ה.4 תפוס קרקע ניתנת להגנה
**עיקרון**: בחר את העמדה הקלה ביותר להגנה.
> "Select the most easily defensible position that favors your client. Don't assume more of a burden than you must." (MYC §10)
**יישום**: כשיש מספר נימוקים אפשריים לתוצאה, בחר את החזק ביותר ופתח בו. אל תנסה להגן על כל נימוק אפשרי.
### ה.5 היה ישר — גם כשזה לא נוח
**עיקרון**: הכר בנקודות חולשה. שכנע באמצעות הגינות, לא באמצעות הסתרה.
> "In dealing with counterarguments, be sure that you don't set out the opponent's points at great length before supplying an answer. Your undercut needs to be swift and immediate." (LWPE §30)
> "If you want to write convincingly, you should habitually ask yourself why the reader might arrive at a different conclusion from the one you're urging. Think of the reader's best objections to your point of view, and then answer those objections directly." (LWPE §30)
**יישום**: ההחלטה חייבת לעבור את "מבחן בית המשפט" — שופט בביקורת שיפוטית צריך לראות שכל טענה רצינית קיבלה מענה.
---
## ו. ציטוטים והפניות (משני הספרים)
### ו.1 צטט במשורה
**עיקרון**: ציטוטים ישירים צריכים להיות נדירים ומדויקים.
> "Quote authorities more sparingly still." (MYC §50)
> "A remarkably large number of lawyers seem to believe that their briefs are improved if each thought is expressed in the words of a governing case. The contrary is true." (MYC §50)
> "After you have established your major premise, it will be your reasoning that interests the court, and this is almost always more clearly and forcefully expressed in your own words." (MYC §50)
**יישום**: צטט ישירות רק כשהמילים המדויקות חשובות — הוראת תוכנית, קביעה מפתח בפסק דין. את השאר — פרפרז.
### ו.2 הימנע מציטוטים ארוכים בלוקים
**עיקרון**: ציטוט ארוך מוכנס (block quote) מזמין דילוג.
> "Be especially loath to use a lengthy, indented quotation. It invites skipping. In fact, many block quotes have probably never been read by anyone." (MYC §50)
> "Never let your point be made only in the indented quotation. State the point, and then support it with the quotation." (MYC §50)
**יישום**: אם חייבים ציטוט ארוך (למשל, הוראת תוכנית) — הקדם לו משפט שמסכם את עיקרו, ולאחריו הוסף ניתוח. אל תניח שהקורא יקרא את הציטוט.
### ו.3 טכניקת הסנדוויץ' — הקדמה → ציטוט → ניתוח
**עיקרון**: שלב ציטוטים בנרטיב — עם הקדמה ייעודית ומסקנה.
> "Weave quotations deftly into your narrative." (LWPE §29)
> "Say something specific. Assert something. Then let the quotation support what you've said." (LWPE §29)
**הקדמות גרועות** (LWPE §29):
- "בית המשפט קבע כדלקמן:"
- "החוק קובע בזו הלשון:"
**הקדמות טובות**:
- "בית המשפט פסק כי אין לקבל בקשות שהוגשו באיחור ללא טעם מיוחד:"
- "התוכנית מגבילה במפורש את השימוש למגורים בלבד:"
### ו.4 הפניות — תמציתיות, לא רשימות
**עיקרון**: הימנע מ-"string citations" — רשימות ארוכות של תקדימים.
> "Brevity means abandoning string cites with more than three cases." (MYC §36, חלק הArgument)
> "Obvious points can be made by citing a single governing case, a statute, or even a well-known treatise." (MYC §36)
**יישום**: לנקודה שאינה שנויה במחלוקת — מספיק מקור אחד. לנקודה מרכזית — דון בתקדים מוביל אחד לעומק, ואחריו "ראו גם" עם 12 מקורות נוספים.
### ו.5 תאר סמכויות בדיוק קפדני
**עיקרון**: אל תעוות תקדימים. אל תטען שפסק דין אומר יותר ממה שהוא באמת אומר.
> "Persuasive briefing induces the court to draw favorable conclusions from accurate descriptions of your authorities. It never distorts cases to fit the facts." (MYC §48)
> "When even one of your citations fails to live up to your introductory signal... all the rest of your citations inevitably become suspect." (MYC §48)
**יישום**: כשמצטטים פסק דין — ציין אם מדובר בהלכה מחייבת, אמרת אגב, או פסיקת ערכאה שאינה מחייבת. אם התקדים שונה מהמקרה הנדון — אמור זאת.
### ו.6 הזז הפניות ביבליוגרפיות להערות שוליים
**עיקרון**: הפניות (מספרי כרכים ועמודים) צריכות להיות בהערות שוליים, לא בגוף הטקסט.
> "Put citations—and generally only citations—in footnotes. And write in such a way that no reader would ever have to look at your footnotes to know what important authorities you're relying on." (LWPE §28)
> "Citations belong in a footnote: even one full citation... breaks the thought; two, three, or more in one massive paragraph are an abomination." (LWPE §28, ציטוט השופט Wisdom)
**יישום**: שלב את שם בית המשפט ושם התיק בגוף הטקסט ("כפי שקבע בית המשפט העליון בפרשת אליאב"), והעבר את ההפניה הביבליוגרפית להערת שוליים.
---
## ז. טכניקות שכנוע (Making Your Case)
### ז.1 פנה לצדק ולהיגיון בריא
**עיקרון**: הראה שהתוצאה לא רק נכונה משפטית אלא גם צודקת.
> "Appeal not just to rules but to justice and common sense." (MYC §15)
> "You need to give the court a reason you should win that the judge could explain in a sentence or two to a nonlawyer friend." (MYC §15)
**יישום**: בסיום הדיון בכל סוגיה, הוסף משפט שמסביר מדוע התוצאה הגיונית ומידתית — לא רק מדוע היא נכונה טכנית.
### ז.2 שלוט בשדה הסמנטי
**עיקרון**: המילים שבהן אתה משתמש מעצבות את תפיסת הקורא.
> "Labels are important... you should think through the terminology of your case. Use names and words that favor your side of the argument." (MYC §20)
**יישום**: בחר מונחים בקפידה. "סטייה מתוכנית" נשמע אחרת מ"גמישות תכנונית". "מבנה ותיק" נשמע אחרת מ"מבנה ללא היתר". המונחים צריכים לשקף את המסקנה.
### ז.3 סיים בחוזקה — אמור מפורשות מה התוצאה
**עיקרון**: הסיום חייב להיות ברור, חד, ולא פורמלי.
> "Persuasive argument neither comes to an abrupt halt nor trails off in a grab-bag of minor points." (MYC §21)
> "The trite phrase 'for all the foregoing reasons' is hopelessly feeble. Say something forceful and vivid to sum up your points." (MYC §21)
**יישום**: בלוק יא (הכרעה) צריך לחזור בתמציתיות על עיקר ההנמקה ואז לקבוע את התוצאה בצורה חד-משמעית. לא "לאור כל האמור לעיל, הערר נדחה" — אלא סיכום של 23 משפטים שמסבירים למה, ואז "הערר נדחה".
### ז.4 לעולם אל תגזים
**עיקרון**: דיוק קפדני חשוב יותר מהגזמה.
> "Never overstate your case. Be scrupulously accurate." (MYC §6)
> "Scrupulous accuracy consists not merely in never making a statement you know to be incorrect (that is mere honesty), but also in never making a statement you are not certain is correct." (MYC §6)
**יישום להחלטות**: אל תכתוב "הפסיקה חד-משמעית" אלא אם היא באמת חד-משמעית. אל תכתוב "אין כל ספק" אלא אם באמת אין. שפה מדויקת מחזקת אמינות; הגזמה מערערת אותה.
### ז.5 מרכז את האש — בחר את הטיעונים הטובים ביותר
**עיקרון**: בחר 23 נימוקים מרכזיים ופתח אותם לעומק. אל תפזר.
> "Pick your best independent reasons why you should prevail—preferably no more than three—and develop them fully." (MYC §12)
> "Scattershot argument is ineffective. It gives the impression of weakness and desperation, and it insults the intelligence of the court." (MYC §12)
> "We must not always burden the judge with all the arguments we have discovered, since by doing so we shall at once bore him and render him less inclined to believe us." (MYC §12, ציטוט קווינטיליאן)
**יישום**: בהחלטה, מרכז את ההנמקה ב-23 נימוקים חזקים. אם יש 7 טענות של העורר — אין צורך להתייחס לכל אחת באריכות. קבץ טענות חלשות, ותן מענה עמוק לעיקריות.
### ז.6 הבהר מושגים מופשטים באמצעות דוגמאות
**עיקרון**: דוגמה מבהירה יותר מכל הסבר תיאורטי.
> "Nothing clarifies [abstract concepts'] meaning as well as examples." (MYC §42)
**יישום**: כשהדיון נוגע לעקרונות תכנוניים מופשטים (כמו "אופי הסביבה" או "שיקולים מהותיים"), תן דוגמה קונקרטית מהמקרה הנדון.
### ז.7 בהירות מעל לכל
**עיקרון**: בהירות היא הערך העליון. כל ערך סגנוני אחר כפוף לה.
> "In brief-writing, one feature of a good style trumps all others. Literary elegance, erudition, sophistication of expression—these and all other qualities must be sacrificed if they detract from clarity." (MYC §39)
> "This means, for example, that the same word should be used to refer to a particular key concept, even if elegance of style would avoid such repetition in favor of various synonyms." (MYC §39)
**יישום**: אם השתמשת ב"היתר בנייה" — אל תעבור ל"רישיון בנייה" בפסקה הבאה כדי להימנע מחזרה. עקביות מינוחית חשובה יותר מגיוון לשוני.
### ז.8 עשה את הכתיבה מעניינת
**עיקרון**: כתיבה ברורה ותמציתית לא חייבת להיות משעממת.
> "To say that your writing must be clear and brief is not to say that it must be dull." (MYC §43)
> "Three simple ways to add interest to your writing are to enliven your word choices, to mix up your sentence structures, and to vary your sentence lengths." (MYC §43)
> "An occasional arrestingly short sentence can deliver real punch." (MYC §43)
**יישום**: גיוון אורך משפטים (משפטים קצרים וחדים בין משפטים ארוכים יותר); שימוש במטאפורה מדי פעם; סיפור עובדתי שזורם כרונולוגית.
### ז.9 השתמש בשמות, לא בתוויות
**עיקרון**: קרא לצדדים בשמם, לא בתוויות משפטיות.
> "Legal writers have traditionally spoiled their stories by calling people 'Plaintiff' and 'Defendant,' 'Appellant' and 'Appellee'... call people McInerny or Walker or Zook." (LWPE §17)
> "Refer to the bank or the company or the university... Then make sure your story line works." (LWPE §17)
**יישום**: בהחלטה, כתוב "משפחת כהן" או "העוררים" (ולא "המערער" או "העורר 1 והעורר 2"). כשאפשר — שם המשפחה או שם הפרויקט.
### ז.10 סדר כרונולוגי לעובדות
**עיקרון**: ספר את העובדות בסדר כרונולוגי. הימנע מקפיצות בזמן.
> "Order your material in a logical sequence. Use chronology when presenting facts." (LWPE §3)
> "Disruptions in the story line frequently result from opening the narrative with a statement of the immediately preceding steps in litigation." (LWPE §3)
**יישום**: בלוק ו (רקע עובדתי) חייב לעקוב אחר ציר הזמן. אל תפתח בהחלטת הוועדה המקומית ואז תחזור אחורה לתיאור הנכס. התחל מהנכס, המשך לבקשה, דרך ההחלטה, עד הגשת הערר.
### ז.11 הימנע מתאריכים מדויקים מיותרים
**עיקרון**: רוב התאריכים המדויקים מסיחים את דעת הקורא.
> "Never begin statement after statement with dates. A few dates will be important, but for the others simply say 'The next morning...,' 'That afternoon...,' etc." (MYC §36)
**דוגמה מ-LWPE §23**: במקום "ביום 12.2.1995 בשעה 15:00 בערך, במהלך מקלחת, התובעת נפלה..." — "בפברואר 1995, במהלך מקלחת, גב' ווקר נפלה..."
**יישום**: בבלוק ו, ציין תאריכים מדויקים רק כשהם משמעותיים (מועד הגשה, תוקף תוכנית). אחרת — "כחודש לאחר מכן", "בתחילת 2023".
### ז.12 הכל צריך להישמע טבעי
**עיקרון**: אם לא היית אומר את זה בעל פה — אל תכתוב את זה.
> "Here's a good test of naturalness: if you wouldn't say it, then don't write it." (LWPE §20)
> "Generally, the best approach in writing is to be relaxed and natural. That bespeaks confidence." (LWPE §20)
**יישום**: קרא את הטיוטה בקול רם. אם מילה או ביטוי גורמים לך להיתקע — החלף אותם.
---
## סיכום: 10 עקרונות העל
1. **חשוב סילוגיסטית**: כל נימוק = כלל + עובדות + מסקנה
2. **פתח בתמצית**: הקורא צריך לדעת מה התוצאה מהעמוד הראשון
3. **נסח בבהירות**: ממוצע 20 מילים למשפט, בניין פעיל, נושא-נשוא קרובים
4. **ארגן בהיגיון**: כותרות אינפורמטיביות, פסקת מפה, סדר מהחזק לחלש
5. **התמודד עם טענות נגדיות**: הכר בהן, הצג אותן בהגינות, הפרך באמצע
6. **צטט במשורה**: פרפרז עדיף; ציטוט רק כשהמילים המדויקות חשובות
7. **מרכז את ההנמקה**: 23 נימוקים חזקים, לא 7 חלשים
8. **ספר סיפור**: עובדות בסדר כרונולוגי, בשמות אמיתיים, ללא תאריכים מיותרים
9. **סיים בחוזקה**: סיכום רענן של ההנמקה, ואז תוצאה חד-משמעית
10. **לעולם אל תגזים**: דיוק קפדני בונה אמינות; הגזמה הורסת אותה

View File

@@ -161,3 +161,94 @@ Our skill was "over-indexed" on one case type (הכט = rejected appeal). The co
- Created `create-decision-structure.cjs` script for generating structure DOCX
- Key innovation from Arieli: "ההליכים בפני ועדת הערר" as separate section (Block ח)
- "Judge Test": every block written as if administrative court judge reads cold
---
## Lessons from Systematic Corpus Analysis (24 decisions, April 2026)
### Source
- All 24 proofread decisions in `/data/training/proofread/`
- Full analysis: [`docs/corpus-analysis.md`](corpus-analysis.md)
- Date: April 2026
### 12. System Learned Style but Not Substantive Content
- **Problem:** Dafna reviewed Kiryat Yearim draft and noted missing planning discussion in block-yod
- **Root cause:** The block-yod prompt taught CREAC methodology and "answer all claims" but never said "in licensing cases, include comprehensive planning discussion"
- **Fix:** Content checklists added to `lessons.py` (`CONTENT_CHECKLISTS`), injected into block-yod prompt via `{content_checklist}`
- **Applied to:** `lessons.py`, `block_writer.py`
### 13. Corpus Composition — All Licensing, No Betterment Levy
- All 24 training decisions are licensing/construction (1xxx)
- Zero betterment levy (8xxx) decisions in corpus
- Not a current priority gap — focusing on licensing first
### 14. Planning Discussion Patterns in Licensing Decisions
- **Always present** when the appeal reaches substantive planning questions
- **Never present** when the appeal is purely jurisdictional or property-based
- **Structure**: broad planning context → direct plan provision citations (200-600 words) → application to specific case → planning conclusion
- **Deepest planning**: פרומר (pure plan interpretation), לבנון (height/building appendix), בית הכרם (multi-plan TAMA 38)
- **No planning**: טלי-אביב (property only), גבאי (jurisdiction only)
### 15. Five Appeal Subtypes Identified (Not Just Three)
Licensing appeals are not homogeneous — the discussion structure varies significantly:
1. **Substantive licensing** — full planning discussion + legal analysis (majority of cases)
2. **Threshold/jurisdiction** — legal analysis only, no planning
3. **Property-focused** — תימוכין קנייניים, minimal planning
4. **TAMA 38** — balancing public interest + planning + neighbor impact
5. **Deviant use (שימוש חורג)** — deep plan interpretation across multiple plans
### 16. Chair Feedback System Established
- DB table `chair_feedback` records Dafna's comments on drafts
- Categories: missing_content, wrong_tone, wrong_structure, factual_error, style, other
- MCP tools + UI page for recording and reviewing feedback
- First entry: Kiryat Yearim — missing planning discussion (2026-04-12)
---
## Lessons from External Expertise Research (April 2026)
### Source
- Federal Judicial Center, *Judicial Writing Manual* (1991, 2nd ed. 2020)
- Bryan Garner, *Legal Writing in Plain English* (2001)
- Scalia & Garner, *Making Your Case: The Art of Persuading Judges* (2008)
- Richard Posner, *How Judges Think* (2008)
- Full texts stored in: `docs/sources/`
### 17. Methodology Document Created — Separating "How to Think" from "How to Write"
**Problem:** The system knew Dafna's STYLE (SKILL.md) and WHAT TOPICS to cover (content checklists), but had no formal methodology for HOW TO REASON through a decision — the analytical stages, when to balance, how to structure arguments, how to handle counterarguments.
**Fix:** Created `docs/decision-methodology.md` — a standalone analytical methodology document based on synthesis of all four external sources. 3,400 words, 12 sections, 10 guiding principles. Covers: pre-analysis, threshold questions, issue ordering, syllogistic structure (CREAC), balancing/proportionality, claims handling (steel-man, bundling), quotation technique (sandwich), factual findings vs. legal conclusions, disposition, writing techniques, analogy/precedent, editing checklist.
**Key principle:** Methodology is UNIVERSAL — it teaches how to think about any quasi-judicial decision. It does not contain case-specific content (parking, building lines, etc.). Case-specific content stays in the content checklists.
**Applied to:**
- `docs/decision-methodology.md` — new document
- `lessons.py` — new function `get_methodology_summary()` injected into block-yod prompt
- `block_writer.py` — new `{methodology_guidance}` placeholder in block-yod prompt
- `.claude/agents/legal-writer.md` — restructured block-yod workflow to follow methodology stages
- `.claude/agents/legal-qa.md` — new check #7 (methodology compliance)
### 18. "Answer All Claims" Made Flexible
**Problem:** The block-yod prompt hardcoded "answer every claim individually" and the QA check enforced it. But Dafna sometimes bundles weak claims, skips irrelevant ones, and focuses on what matters.
**Fix:**
- Block-yod prompt changed from "חובה לענות על כל אחת" to flexible handling: address substantive claims; bundle [bundle]; skip [skip]
- Chair can mark claims in `chair_directions` as bundle or skip
- QA check #3 updated to respect these markings
- Methodology teaches WHEN to address individually vs. bundle vs. skip (methodology §ו)
### 19. Source Library Established
Downloaded and converted to text 5 authoritative sources for the methodology:
- `docs/sources/fjc-judicial-writing-manual-1991.txt` (13,567 words)
- `docs/sources/fjc-judicial-writing-manual-2nd-ed-2020.txt` (15,912 words)
- `docs/sources/garner-legal-writing-plain-english.txt` (97,475 words)
- `docs/sources/posner-how-judges-think.txt` (156,789 words)
- `docs/sources/scalia-garner-making-your-case.txt` (54,683 words)
Total: ~340,000 words of source material.
Intermediate extraction documents also saved:
- `docs/fjc-principles-extraction.md` — 38 principles from FJC
- `docs/garner-methodology-extraction.md` — ~50 principles from Garner/Scalia

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,535 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="max-age=604800, must-revalidate">
<meta name="rating" content="general">
<!--<link href="/rss/index.php" rel="alternate" type="application/rss+xml" title="News" />-->
<link rel="shortcut icon" href="/img/favicon.ico" type="image/x-icon">
<title>Library Genesis</title>
<!--[if IE 6]>
<style>
body {behavior: url("/csshover3.htc");}
#menu li .drop {background:url("img/drop.gif") no-repeat right 8px;
</style>
<![endif]-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css">
<link href="/css/font.min.css" rel="stylesheet">
<style>
nav.navbar .dropdown:hover > .dropdown-menu {
display: block;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.panel-heading .accordion-toggle:after {
font-family: "Glyphicons Halflings";
content: "\e114";
float: right;
color: grey;
}
.panel-heading .accordion-toggle.collapsed:after {
content: "\e080";
}
.tooltip-inner {
max-width: 350px;
width: 350px;
}
h1 {
display: block;
font-size: 1.8rem;
font-weight: bold;
font-family: Georgia, "Times New Roman", Times, serif; color: #A00000;
}
#tablelibgen td {
font-family: "Pt Sans", Tahoma, Helvetica, sans-serif;
margin: 0;
padding: 0em 3px;
font-size: 1rem;
}
#tablelibgen1 td {
font-family: "Pt Sans", Tahoma, Helvetica, sans-serif;
margin: 0;
padding: 0em 3px;
font-size: 1rem;
}
.taghide {
display: none;
}
.taghide + label ~ div {
display: none;
}
/* оформляем текст label */
.taghide + label {
display: inline-block;
}
/* вид текста label при активном переключателе */
/* когда чекбокс активен показываем блоки с содержанием */
.taghide:checked + label + div {
display: block;
}
/*.navbar {
background-color: #BBBBBB;
}*/
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<style>p {margin: 0;}</style>
</head>
<body><script>
(function () {
var script = document.createElement('script');
var COOKIE_NAME = 'test_variant';
var valueFromCookie = getCookie(COOKIE_NAME);
var variant;
function getCookie(name) {
var cookiesList = document.cookie.split(';');
for (var i = 0, length = cookiesList.length; i < length; i += 1) {
var cookie = cookiesList[i].split('=');
if (cookie[0].trim() === name) {
return Number(cookie[1].trim());
}
}
return null;
}
function setCookie(name, value) {
document.cookie = [
name + '=' + value,
'SameSite=Lax',
'path=/',
'Expires=' +
new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000).toUTCString(),
].join(';');
}
if (valueFromCookie === null) {
variant = Math.random();
setCookie(COOKIE_NAME, variant);
} else {
variant = valueFromCookie;
}
if (variant < 0.5) {
script.setAttribute('data-domain', 'features-2562_0');
script.setAttribute('src', '//inopportunefable.com/7d/78/3d/7d783dc7f86db4429028d485a085a9b7.js');
window.addEventListener('DOMContentLoaded', function () {
if (
document.body.querySelector('script[data-domain="features-2562_0"]') ===
null
) {
document.body.appendChild(script);
}
});
} else {
script.setAttribute('data-domain', 'features-2562_1');
/* dynamic */ script.setAttribute('src', '//inopportunefable.com/imw/zIaHmB/0nCsRHnp/SCgHBcfS8hOrJa4/854J8Er1gxI1LoK32BBg/zk6iz1O4Lg/JiGAhxO4-ENw6/hJq3/4gzKxMG_mlKcbOl/08XbF_y6D5em/sH0oBrSV1A0hSBB/GxBx');
window.addEventListener('DOMContentLoaded', function () {
if (
document.body.querySelector('script[data-domain="features-2562_1"]') ===
null
) {
document.body.appendChild(script);
}
});
}
})();
</script>
<nav class="navbar navbar-expand-md navbar-dark bg-secondary mb-1">
<a class="navbar-brand" href="/index.php">
<img src="/img/logo.png" height="30" alt="">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/community/app.php/article/news">NEWS <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/community/">FORUM <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="/community/ucp.php?mode=login" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
LOGIN
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="/community/ucp.php?mode=register">Register</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
DOWNLOAD
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="/mirrors.php">Mirrors</a>
<a class="dropdown-item" href="http://libgenfrialc7tguyjywa36vtrdcplwpxaw43h6o63dmmwhvavo5rqqd.onion/">TOR</a>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">P2P</h6>
<a class="dropdown-item" href="/torrents/">Torrents</a>
<a class="dropdown-item" href="https://ipdl.cat/data/torrents.html">Torrents status</a>
<a class="dropdown-item" href="/nzb/">Usenet (*.nzb)</a>
<a class="dropdown-item" href="/soft/">Soft</a>
<!--https://phillm.net/libgen-stats-table.php-->
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">DB Dumps</h6>
<a class="dropdown-item" href="/dirlist.php?dir=dbdumps">Libgen</a>
<a class="dropdown-item" href="http://libgen.rs/dbdumps/">libgen.rs (gen.lib.rus.ec)</a>
<!--<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/magz0/">Unsorted magz</a>
<a class="dropdown-item" href="/fict0/">Unsorted fiction</a>
<a class="dropdown-item" href="/comics4/">Unsorted comics</a>
</div>-->
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="librarian.php" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
UPLOAD
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="ftp://ftp.libgen.bz/upload/">FTP</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="/index.php?req=fmode:last&topics1=all" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
LAST
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="/index.php?req=fmode:last&topics1=all"><b>Files</b></a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=l">Libgen</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=a">Scientific Articles</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=f">Fiction</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=c">Comics</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=m">Magazines</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=s">Standards</a>
<a class="dropdown-item" href="/index.php?req=fmode:last&topics%5B%5D=r">Fiction RUS</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/index.php?req=mode:last&curtab=e">Editions</a>
<a class="dropdown-item" href="/index.php?req=mode:last&curtab=s">Series</a>
<a class="dropdown-item" href="/index.php?req=mode:last&curtab=p">Publishers</a>
<!-- <a class="dropdown-item" href="/index.php?req=mode:last&curtab=f">Files</a> -->
<a class="dropdown-item" href="/index.php?req=mode:last&curtab=a">Authors</a>
<a class="dropdown-item" href="/index.php?req=mode:last&curtab=w">Works</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
OTHERS
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="json.php">API</a>
<a class="dropdown-item" href="rss.php">RSS</a>
<a class="dropdown-item" href="top.php">Top 100 users</a>
<a class="dropdown-item" href="stat.php">Stats</a>
<a class="dropdown-item" href="topics.php">Topics</a>
<a class="dropdown-item" href="batchsearchindex.php">Batch search</a>
<a class="dropdown-item" href="biblioservice.php">Bibliographic services</a>
<a class="dropdown-item" href="https://wiki.mhut.org/software:libgen_desktop">Libgen librarian for desktop</a>
<a class="dropdown-item" href="/code/">Source (PHP)</a>
<a class="dropdown-item" href="/soft/">LG soft</a>
<!--<a class="dropdown-item" href="/import/">Import local files in LG format</a>-->
<a class="dropdown-item" href="https://z-library.se/fulltext/">Full text search</a>
</div>
</li>
<!-- <li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="topics.php" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
Topics
</a>
</li>
-->
<li class="nav-item dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
LINKS
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="http://sci-hub.ru">Sci-hub</a>
<a class="dropdown-item" href="http://magzdb.org">Magzdb.org</a>
<a class="dropdown-item" href="http://nlr.ru/rlin/Periodika_rus.php">РНБ</a>
<a class="dropdown-item" href="http://rsl.ru/">РГБ</a>
<a class="dropdown-item" href="http://loc.gov/">LOC</a>
<a class="dropdown-item" href="https://comicvine.gamespot.com/">ComicVine</a>
<a class="dropdown-item" href="http://cyberleninka.ru/">Cyberleninka</a>
<a class="dropdown-item" href="http://lib.rus.ec/">Lib.rus.ec</a>
<a class="dropdown-item" href="http://flibusta.net/">Flibusta.net</a>
<a class="dropdown-item" href="http://goodreads.com/">Goodreads.com</a>
<a class="dropdown-item" href="http://worldcat.org/">Worldcat.org</a>
<a class="dropdown-item" href="https://wiki.archiveteam.org/">Archive team</a>
<a class="dropdown-item" href="https://www.reddit.com/r/libgen/">Reddit</a>
<a class="dropdown-item" href="http://annas-archive.org/">Anna's Archive</a>
<a class="dropdown-item" href="https://welib.org/">Welib</a>
<a class="dropdown-item" href="https://open-slum.org/">The Shadow Library Uptime Monitor</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="btn btn-secondary" href="index.php?req=mode:req&curtab=e" role="button" id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
WANTED
</a>
</li>
</ul>
</div>
<div class="nav-link">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="darkSwitch">
<label class="custom-control-label" for="darkSwitch">🌓</label>
</div>
<script src="/js/dark-mode-switch.js"></script>
</div>
<a class="navbar-brand" href="setlang.php?md5=1b1ba2439cfa9fa6f44bab813e9b7bab&lang=ru">RU</a>
</nav>
<span></span><table id=main align="center" border=1>
<tr>
<td align="left" valign="top" bgcolor="#F5F6CE" width=1 nowrap></td>
<td align="center" valign="top" bgcolor="#A9F5BC"><a href="get.php?md5=1b1ba2439cfa9fa6f44bab813e9b7bab&key=5TQ3IXLH0VDDKN79"><h2>GET</h2></a></td>
<td align="left" valign="top" bgcolor="#F5F6CE" width=1></td>
</tr>
<tr>
<td bgcolor="#F5F6CE" valign=top></td>
<td>
<table width=700 border=0>
<tr><td colspan=3 bgcolor="#F5F6CE" align="center"><nobr><script type="text/javascript">
atOptions = {
'key' : '8653b0dc857008353ad71d83dad80b6d',
'format' : 'iframe',
'height' : 90,
'width' : 728,
'params' : {}
};
document.write('<scr' + 'ipt type="text/javascript" src="http' + (location.protocol === 'https:' ? 's' : '') + '://inopportunefable.com/8653b0dc857008353ad71d83dad80b6d/invoke.js"></scr' + 'ipt>');
</script></nobr></td></tr>
<tr><td rowspan=2><a href="/covers/1586000/1b1ba2439cfa9fa6f44bab813e9b7bab.jpg"><img src="/covers/1586000/1b1ba2439cfa9fa6f44bab813e9b7bab.jpg" width=300></a></td><td>Title: Legal Writing in Plain English: A Text with Exercises<br>
Series: Chicago Guides to Writing, Editing, and Publishing<br>
Author(s): Bryan A. Garner<br>
Publisher: University Of Chicago Press<br>
Year: 2013<br>
ISBN: 0226283933; 9780226283937<br></td>
<tr><td><textarea rows='9' name='bibtext' id='bibtext' readonly cols='60'>@book{book:{92607912},
title = {Legal Writing in Plain English: A Text with Exercises},
author = {Bryan A. Garner},
publisher = {University Of Chicago Press},
isbn = {0226283933; 9780226283937},
year = {2013},
series = {Chicago Guides to Writing, Editing, and Publishing},
edition = {2},
url = {libgen.li/file.php?md5=1b1ba2439cfa9fa6f44bab813e9b7bab}}</textarea></td></tr>
<tr><td colspan=3><p style='text-align:center'>
<a href='https://www.worldcat.org/search?qt=worldcat_org_bks&q=Legal%20Writing%20in%20Plain%20English%3A%20A%20Text%20with%20Exercises&fq=dt%3Abks'>Search in WorldCat</a>
<a href='https://www.goodreads.com/search?utf8=✓&query=Legal%20Writing%20in%20Plain%20English%3A%20A%20Text%20with%20Exercises'>Search in Goodreads</a><br>
<a href='https://www.abebooks.com/servlet/SearchResults?tn=Legal%20Writing%20in%20Plain%20English%3A%20A%20Text%20with%20Exercises&pt=book&cm_sp=pan-_-srp-_-ptbook'>Search in AbeBooks</a></td></tr>
</table>
</td>
<td bgcolor="#F5F6CE" valign=top></td>
</tr>
<tr><td></td><td colspan=2></td></tr>
<tr><td colspan=3 bgcolor="#F5F6CE" align="center"><script type="text/javascript">
atOptions = {
'key' : '8653b0dc857008353ad71d83dad80b6d',
'format' : 'iframe',
'height' : 90,
'width' : 728,
'params' : {}
};
document.write('<scr' + 'ipt type="text/javascript" src="http' + (location.protocol === 'https:' ? 's' : '') + '://inopportunefable.com/8653b0dc857008353ad71d83dad80b6d/invoke.js"></scr' + 'ipt>');
</script><br><script type="text/javascript">
atOptions = {
'key' : '8653b0dc857008353ad71d83dad80b6d',
'format' : 'iframe',
'height' : 90,
'width' : 728,
'params' : {}
};
document.write('<scr' + 'ipt type="text/javascript" src="http' + (location.protocol === 'https:' ? 's' : '') + '://inopportunefable.com/8653b0dc857008353ad71d83dad80b6d/invoke.js"></scr' + 'ipt>');
</script></td></tr>
</table><nav class="navbar sticky-bottom navbar-expand-sm navbar-dark bg-secondary">
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#" data-toggle="modal" data-target="#dmcamodal">DMCA</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-toggle="modal" data-target="#aboutmodal">ABOUT</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-toggle="modal" data-target="#donatemodal" >DONATE</a>
</li>
</ul>
<span class="navbar-text">Users online 5949</span>
</div>
</nav>
<!-- Modal Donate -->
<div class="modal fade text-dark" id="donatemodal" tabindex="-1" aria-labelledby="donatemodalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="donatemodalLabel">Donate</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<a href="bitcoin://bc1qlv9lwa5vncm2jjrxyhddfcvu0z3u5vn0s9672r">Bitcoin</a>
<br>
<a href="monero:48WhyKv4D9x53SyDFNYuMsHsDzuHXEcht4mWoFtXtE3k4KZ3A7goi3CQWBQQZ3A8PSK7CpwnAFKLnfGiZTAbEpcaCQCghvN">Monero</a>
</div>
</div>
</div>
</div>
<!-- Modal About -->
<div class="modal fade text-dark" id="aboutmodal" tabindex="-1" aria-labelledby="aboutmodalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="aboutmodalLabel">About</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="about">
The Library Genesis aggregator is a community aiming at collecting and cataloging items descriptions for the most part of scientific,
scientific and technical directions, as well as file metadata. In addition to the descriptions,
the aggregator contains only links to third-party resources hosted by users.
All information posted on the website is collected from publicly available public Internet resources and is intended solely for informational purposes.
</div>
</div>
</div>
</div>
</div>
<!-- Modal DMCA -->
<div class="modal fade text-dark" id="dmcamodal" tabindex="-1" aria-labelledby="dmcamodalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="dmcamodalLabel">About</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="dmca">
Library Genesis - aggregator items is a website that collects and organizes online items from users.
Item aggregation is done for fact-finding purposes, and website Library Genesis respects the rights of copyright holders and respect dcma.
Removing Content From Library Genesis / DMCA Policy
Library Genesis respects the intellectual property of others.
</div>
<div class="dmca">
If you believe that your copyrighted work has been copied in a way that constitutes copyright infringement and is accessible on this site, you may notify our copyright agent, as set forth in the Digital Millennium Copyright Act of 1998 (DMCA). For your complaint to be valid under the DMCA, you must provide the following information when providing notice of the claimed copyright infringement:
</div>
<div class="dmca">
* A physical or electronic signature of a person authorized to act on behalf of the copyright owner Identification of the copyrighted work claimed to have been infringed <br />
* Identification of the material that is claimed to be infringing or to be the subject of the infringing activity and that is to be removed <br />
* Information reasonably sufficient to permit the service provider to contact the complaining party, such as an address, telephone number, and, if available, an electronic mail address <br />
* A statement that the complaining party "in good faith believes that use of the material in the manner complained of is not authorized by the copyright owner, its agent, or law" <br />
* A statement that the "information in the notification is accurate", and "under penalty of perjury, the complaining party is authorized to act on behalf of the owner of an exclusive right that is allegedly infringed" <br />
The above information must be submitted as a written, faxed or emailed notification to the following Designated Agent: ianzlib@protonmail.com. Appeals will be reviewed within 72 hours.</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.5/dist/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<script src="/js/form-validation.js"></script>
<script>
$('[data-toggle="tooltip"]').tooltip();
$('.btn-tooltip-bottom').tooltip({
placement: 'bottom'
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,261 @@
‫הנחיות יו"ר ועדת המשנה לעררים על החלטות הוועדה‬
‫המחוזית והולחוף‬
‫יחידה‬
‫ועדת המשנה לעררים‬
‫המועצה הארצית לתכנון‬
‫ולבניה‬
‫מס' נוהל‬
‫תאריך פרסום מקורי‬
‫תאריך פרסום עדכני‬
2019/1
11.03.2019
26.03.2024
‫הנחיות יו"ר ועדת המשנה לעררים על‬
‫החלטות הוועדה המחוזית והולחוף‬
‫על מנת לייעל את ההליכים לפני ועדת המשנה לעררים ולעמוד בלוחות הזמנים הקצובים‬
‫בתקנות התכנון והבניה (ערר בפני המועצה הארצית) ,‬התשל"ב‪ ,1972 -‬ובתקנות התכנון‬
‫והבניה ( סדרי דין בפני ועדת הערר למימי חופין) ,‬תש"ל‪( 1969-‬להלן ביחד‪ :‬תקנות העררים) ,
‫הוחלט לגבש את ההנחיות הבאות ולהביאן לידיעת הציבור‪.
‫ההנחיות יחולו על הליכי הערר החל ממועד פרסומן‪.
‫חשוב‪ :‬כל תגובה‪ ,‬בקשה או פניה בנוגע לערר‪ ,‬לרבות בקשה להתווסף לרשימת התפוצה‬
‫בדוא"ל או הסרה ממנה‪ ,‬יש להפנות למזכירות ועדת המשנה לעררים בכתובת הדוא"ל :
Arr@iplan.gov.ilלהלן ( :‬המזכירות) .‬הגשת פניה לגורם אחר או באמצעי אחר כמוה אי‪-
‫הגשה‪.
. 1הגשת בקשות‬
‫א‪.
‫כל בקשה המוגשת לוועדה ( לרבות‪ :‬בקשות להארכת מועד‪ ,‬בקשות לשינוי מועד דיון ,
‫בקשות לצירוף מסמכים‪ ,‬בקשות להצטרפות כמשיבים לערר וכדו') תוגש למזכירות‬
‫בליווי התייחסות יתר הצדדים להליך הערר כפי שקבעו תקנות העררים‪ .‬בקשות שיוגשו‬
‫ללא עמדת יתר הצדדים כאמור‪ ,‬או הסבר בנושא‪ ,‬יושבו למבקש על‪-‬ידי המזכירות‬
‫לצורך השלמה‪.
‫ב‪.
‫בקשה להארכת מועד להגשת ערר‬
‫על פי סעיף (110ד) לחוק התכנון והבניה‪ ,‬התשכ"ה‪( 1965-‬להלן‪ " :‬החוק") ,‬ערר יוגש‬
‫בתוך שלושים ימים מהיום שבו הומצאה לעורר החלטת הוועדה המחוזית‪ ,‬או הרשות‬
‫לערור‪ ,‬לפי העניין‪ .‬עררים שיוגשו באיחור וללא ארכה שאושרה על ידי יו"ר הוועדה‪,
‫יידחו על הסף‪.
( )1במקרה שבו נבצר מהעורר להגיש את הערר במועד‪ ,‬יש להגיש בקשה להארכת‬
‫מועד .‬בבקשה יש לציין את המועד שבו התקבלה החלטת הוועדה המחוזית או הרשות‬
‫לערור ,‬לפי העניין‪.
( )2בקשה להארכת מועד להגשת ערר ת היה מנומקת‪ ,‬ויצורפו לה תגובות הצדדים‬
‫לבקשה‪.
( )3במקרה שבו הבקשה מתבססת על טענות עובדתיות )כגון לעניין המועד שבו‬
‫הומצאה לעורר החלטת הוועדה המחוזית או הרשות לערור( ,‬יש לתמוך את הבקשה‬
‫בראיות מתאימות‪.
|1
‫הנחיות יו"ר ועדת המשנה לעררים על החלטות הוועדה‬
‫המחוזית והולחוף‬
‫ג‪.
‫יחידה‬
‫ועדת המשנה לעררים‬
‫המועצה הארצית לתכנון‬
‫ולבניה‬
‫מס' נוהל‬
‫תאריך פרסום מקורי‬
‫תאריך פרסום עדכני‬
2019/1
11.03.2019
26.03.2024
‫בקשות לשינוי מועד הדיון‬
( ) 1ככלל ,‬דיוני ועדת המשנה לעררים מתקיימים בימי חמישי‪.
( ) 2הכלל הוא כי הדיונים יתקיימו במועד שנקבע להם‪ .‬שיקולי נוחות‪ ,‬הסכמת‬
‫הצדדים ,‬קיום משא ומתן לפשרה‪ ,‬נסיבות אישיות או עומס עבודה אינם מהווים‪,
‫ככלל‪ ,‬הצדקה לדחיית הדיון‪ .‬במקרים של נסיבות אישיות חריגות ובלתי‪-‬צפויות‬
‫תישקל דחיית הדיון‪ ,‬תוך התחשב ות במאפייני התוכנית ובעיכוב שייגרם כתוצאה‬
‫מאישור הדחייה‪.
( ) 3בקשה לדחיית דיון בשל קיומו של דיון מקביל תוגש מיד עם קבלת הידיעה על‬
‫מועד הדיון‪ ,‬ותישקל בהתאם לנסיבות‪.
( ) 4כל בקשה לשינוי מועד הדיון בערר תכלול לפחות שלושה מועדים חלופיים לקיום‬
‫הדיון‪ ,‬שתואמו מבעוד מועד מול מזכירות הוועדה ומוסכמים על יתר הצדדים‬
‫לערר ,‬אין מניעה להגיש בקשה להקדמת הדיון בערר‪ ,‬הכול בכפוף ללוח הזמנים‬
‫של הוועדה .‬אין באמור לעיל כדי לגרוע מסמכות הוועדה לקבוע דיון במועד אחר‬
‫המתאים ליומנה‪.
.2‬המשיבים בערר‬
‫בכתב הערר יש לפרט את המשיבים בערר לפי תקנות הע ררים‪ ,‬ואותם בלבד‪ ,‬כאמור להלן‪:
‫א‪ .‬על פי תקנה 4לתקנות התכנון והבניה (ערר בפני המועצה הארצית) ,‬התשל"ב‪,1972 -
‫המשיבים בערר הם‪:
( ) 1בערר לפי סעיפים (78ב)( )1או (98ג) לחוק הוועדה המחוזית‪ ,‬הוועדה המקומית‬
‫הנוגעת בדבר ומגיש התוכנית;
( ) 2בערר לפי סעיף (110א) לחוק הוועדה המחוזית‪ ,‬הוועדה המקומית הנוגעת‬
‫בדבר ומגיש התוכנית; וכן‪ ,‬לפי העניין‪ ,‬מי שהתנגדותו לתוכנית נתקבלה ובעקבות‬
‫זאת הוגש הערר או מי שהשמיע טענות לפי סעיף (106ב) וטענותיו התקבלו‬
‫ובעקבות זאת הוגש הערר‪.
‫ב‪.
‫על פי תקנה 4לתקנות התכנון והבניה ( סדרי דין בפני ועדת הערר למימי חופין),
‫התש"ל‪ 1969-‬המשיבים בערר על החלטת הוועדה לשמירת הסביבה החופית הם‬
‫הוועדה לשמירת הסביבה החופית‪ ,‬וכן מי שהגיש תכנית שאושרה על ידיה לפי סעיף‬
4לתוספת השנייה לחוק‪ ,‬או מי שהגיש בקשה להיתר שאושרה על ידיה לפי סעיף 5
‫לתוספת השנייה לחוק‪.
‫ג‪.
‫ערר שיוגש שלא בהתאם ל רשימת המשיבים כאמור בתקנות הנ"ל יידרש בתיקון‬
‫רשימת המשיבים בהתאם להנחיות המזכירות‪ .‬המשיבים להליך יובהרו גם במסגרת‬
‫הזימון שיישלח לדיון‪ ,‬וראו סעיף (4ג) להלן‪.
|2
‫הנחיות יו"ר ועדת המשנה לעררים על החלטות הוועדה‬
‫המחוזית והולחוף‬
‫ד‪.
‫יחידה‬
‫ועדת המשנה לעררים‬
‫המועצה הארצית לתכנון‬
‫ולבניה‬
‫מס' נוהל‬
‫תאריך פרסום מקורי‬
‫תאריך פרסום עדכני‬
2019/1
11.03.2019
26.03.2024
‫משיבים נוספים הרואה עצמו משיב לערר שהוגש בשל קבלת התנגדותו‪ ,‬ולא צוין‬
‫ברשימת המשיבים לערר בזימון ל דיון‪ ,‬יגיש בקשת הצטרפות תוך ציון הסוגייה‬
‫בהתנגדות שהובילה להגשת הערר‪ .‬גורם שלא מופיע ברשימת המשיבים שנשלחה‬
‫במסגרת הזימון לדיון‪ ,‬ומבקש להיות משיב בערר‪ ,‬יגיש בקשה מנומקת בהתאם‬
‫להנחיות בסעיף 1לעיל.
. 3הגשת ערר על ידי רשות מקומית או ועדה מקומית הנוגעת בדבר לפי סעיף (110א)(()1ב)
‫לחוק‬
‫א‪.
‫בהתאם לחוק וההלכה הפסוקה‪ ,‬ערר לפי סעיף (110א)(()1ב) לחוק יוגש בליווי החלטת‬
‫מליאת הרשות‪/‬הוועדה המאשרת את הגשת הערר (להלן‪ :‬החלטת מליאה).
‫ב‪.
‫כאשר לוח הזמנים אינו מאפשר את כינוס מליאת הרשות‪/‬הוועדה קודם להגשת‬
‫הערר‪ ,‬יש לעדכן את מזכירות הוועדה מתי עתידה המליאה להתכנס בנדון‪ ,‬ובכל‬
‫מקרה החלטת מליאה תומצא למזכירות עד 30ימים לאחר הגשת הערר‪.
‫ג‪.
‫לא הומצאה החלטת המליאה לוועדה בתוך 30ימים מהגשת הערר‪ ,‬תישקל דחיית‬
‫הערר על הסף ללא התראה נוספת‪.
. 4איחוד עררים‪ ,‬הזימון לדיון והגשת תשובות לערר‬
‫א‪.
‫מזכירות הוועדה תוודא טרם שיבוץ ערר לדיון כי לא הוגשו עררים נוספים‪ ,‬בזכות או‬
‫בהתאם לרשות שניתנה על‪ -‬ידי יו"ר הוועדה המחוזית לפי סעיף (110א)( )2לחוק.
‫ב‪.
‫בהתאם לתקנות העררים‪ ,‬ככל שהוגשו כמה עררים בגין החלטה באותה התוכנית‪,
‫ככלל יאוחדו העררים לדיון אחד שייערך בעררים על תוכנית‪.
‫ג‪.
‫ז ימון לדיון בערר יישלח בדואר אלקטרוני לכלל הצדדים בערר וכן לבעלי עניין נוספים‬
‫לידיעה שייכתבו ברשימה בזימון לדיון‪ ,‬במצורף לכתב הערר‪.
‫ד‪.
‫הגשת תשובות לערר‪:
( ) 1בהתאם לתקנות העררים‪ ,‬על המשיבים להגיש תשובתם לערר בתוך 30ימים.
‫המועד להגשת התשובות ייכתב בזימון לדיון‪.
( ) 2ה גשת חומרים תיעשה באמצעות הדוא"ל כמופיע מטה לידי המזכירות‪ .‬עם זאת ,
‫המזכירות עשויה לפנות ולבקש הגשת חומרים גם באופן פיזי‪ ,‬בהתאם לשיקול‬
‫דעתה‪.
‫ה .‬הנגשת המידע מתיק הערר‪ :‬כתבי הערר‪ ,‬התשובות וחומרים נוספים שהוגשו מטעם‬
‫הצדדים יועלו לאתר מנהל התכנון‪ ,‬בדף הערר שקישור א ליו יישלח גם על‪-‬ידי‬
‫המזכירות .‬מצגות שהוצגו בדיון יועלו לאתר הערר לאחר הדיון‪ .‬המזכירות מעדכנת‬
‫את החומרים מעת לעת באתר הערר‪ ,‬ומומלץ לעקוב אחר מידע חדש שמתפרסם‪.
‫יתכן שהמזכירות תפיץ חלק מהחומרים הנ"ל גם באמצעות רשימת התפוצה בדוא"ל‪.
|3
‫הנחיות יו"ר ועדת המשנה לעררים על החלטות הוועדה‬
‫המחוזית והולחוף‬
‫יחידה‬
‫ועדת המשנה לעררים‬
‫המועצה הארצית לתכנון‬
‫ולבניה‬
‫מס' נוהל‬
‫תאריך פרסום מקורי‬
‫תאריך פרסום עדכני‬
2019/1
11.03.2019
26.03.2024
.5‬הדיון בערר‬
‫א‪.
‫הצדדים יתייצבו לדיון בערר בהתאם למועד בזימון לדיון‪.
‫ב‪.
‫הרכב ועדת המשנה לעררים ( בעררים על החלטות הוועדות המחוזיות והוולחו"ף )
‫נקבע בהחלטת מליאת המועצה הארצית מיום 10.06.2014:‬נציג שר המשפטים יהיה‬
‫היו"ר; נציג מנכ"ל מינהל התכנון; נציג השר הגנת הסביבה או נציג מנהל רשות הטבע‬
‫והגנים; נציג שר הבינוי והשיכון או נציג בעל הכשרה בשיכון ובניה; שני נציגי השלטון‬
‫המקומי‪ .‬בהחלטת המועצה הארצית הוגדרו גם ממלאי מקום לחברים .‬משכך‪ ,‬בהתאם‬
‫לסעיף (42א) לחוק‪ ,‬המניין החוקי בישיבות ועדת המשנה לעררים הוא .3
‫ג‪.
‫ככלל‪ ,‬הדיון בערר יתקיים באופן חזיתי ( פרונטלי) במשרדי מי נהל התכנון בירושלים‬
‫ועל הצדדים (בעלי דין‪ ,‬באי‪ -‬כוח ויועצים מקצועיים) להיערך להצגת הטענות באולם‬
‫הוועדה‪.
‫ד .‬מספר ימים טרם הדיון בערר תישלח המזכירות הודעת תזכורת לצדדים עם מיקום‬
‫הדיון במדויק (להלן בסעיף זה‪ :‬ההודעה) .‬ההודעה עשויה לכלול הנחיה לפיה הדיון‬
‫יתקיים גם בהיוועדות חזותית‪ .‬במקרה זה תכלול ההודעה מידע והנחיות נוספות‬
‫בהקשר זה‪.
‫ה .‬צד לדיון בערר שמבקש להציג מצגת יעביר למען הסדר הטוב את העתקה למזכירות‬
‫הוועדה לכל המאוחר ערב הדיון הקבוע בערר‪.
ו.
‫צד לדיון בערר אשר הגיש במהלך הדיון חומר נוסף שיו"ר הוועדה אישר הגשתו‪ ,‬יעביר‬
‫למזכירות הוועדה העתק במועד הדיון בערר לצורך הפצתו ליתר הצדדים‪.
‫מורן בראון‪,
‫עו"ד יו"ר ועדת המשנה לעררים‬
|4

View File

@@ -0,0 +1,220 @@
‫אגף תקצוב ורכש‬
‫הנחיות עזר להגשת עררים בועדת ערר מחוזיות לתכנון ובניה‬
‫הנחיות עזר להגשת ערר בנושא היתרי בניה‪:
‫כתב הערר יוגש תוך 30ימים מיום קבלת החלטת הועדה המקומית‬
.1‬הערר יוגש למזכירות ועדת הערר בכתב‪ ,‬בשישה עותקים‪ ,‬בצירוף עותקים נוספים לפי מספר‬
‫המשיבים‪.
.2
‫על הערר לכלול את כל אלה‪:
.2.1‬שם העורר‪ ,‬מספר ת‪.‬ז‪ ,‬מען‪ ,‬מספר טלפון וטלפון נייד‪ ,‬מספר פקס וכתובת מייל (במידה‬
‫ויש).
.2.2‬פרטי המשיבים‪ :‬שמותיהם ,‬מענם‪ ,‬מספר טלפון‪ ,‬מספר פקס וכתובת מייל (במידה ויש)
.2.2‬במידה והעורר מיוצג על ידי עורך דין‪ -‬שם ב"כ העורר‪ ,‬מען למסירת מסמכים‪ ,‬מספר‬
‫טלפון‪ ,‬מספר פקס‪ ,‬כתובת מייל וייפוי כוח‪.
.2.2‬פרטי הבקשה שלגביה ניתנה ההחלטה נושא הערר (פרטי המקרקעין‪/‬הנכס‪ -‬כתובת‪ ,‬מס'
‫גוש ומס' חלקה)
.2.2‬פרטי ההחלטה שעליה מוגש הערר והעתק מהודעת הועדה או הרשות על ההחלטה‪.
.2.2‬נימוקי הערר‬
.2.2‬עיקר הראיות שהעורר מבקש להביא בפני ועדת הערר‪.
.2.2‬כאשר הערר מוגש על ידי מבקש ההיתר‪ -‬עליו לצרף לכתב הערר עותק מהגרמושקה‬
‫נשוא ההחלטה‪.
.2.2‬כאשר העורר הוא מי שהגיש התנגדות לבקשה להיתר או מבקש ההיתר‪ ,‬על הועדת‬
‫המקומית לצרף לתגובתה עותק מודפס מהגרמושקה נשוא ההחלטה‪.
‫לתשומת ליבכם‪:
‫‪‬‬
‫הגשת הערר אינה כרוכה בתשלום אגרה‪.
‫‪‬‬
‫את הערר יש להגיש לועדת הערר במסירה ידנית או בדואר רשום ובלבד שעמד בכל דרישות‬
‫הדין להגשת הערר והגיע לועדת הערר במועד הקבוע בחוק להגשת ערר‪.
‫המועד בו נתקבל הערר בדואר רשום במזכירות הועדה ירשם כמועד בו נתקבל הערר‪.
‫ערר לא ניתן להעביר באמצעות פקס‪/‬מייל‪.
‫‪‬‬
‫ערר שהגיע לועדה שלא במועד‪ ,‬לא יתקבל אלא אם ניתנה החלטה המאשרת ארכה להגשתו‪.
‫‪‬‬
‫לבקשת עורר‪ ,‬תמציא לו הועדה המקומית את פרטי הצדדים להליך נושא הערר‪ ,‬שמותיהם‬
‫ומעניהם תוך שלושה ימים מיום הגשת הבקשה‪.
‫‪‬‬
‫שימו לב ❤ הערר צריך להיות חתום על ידי העורר‪.
‫הנחיות אלו כלליות ומשמשות כעזר לשירות הציבור‪ ,‬בכפוף לקבוע בדין ובתקנות‪ ,‬הגובר על האמור בהנחיות‬
‫אלה‪ ,‬ההנחיות אינן ממצות ואינן כוללות את כל הוראות הדין הרלוונטיות לעניין‪ .‬כמו כן ייתכן וקיימות דרישות‬
‫נוספות בוועדות הערר השונות והן ימסרו על ידי הועדה‪.
‫הנחיות אלו אינן מהוות תחליף לייעוץ משפטי‪.
‫עמוד 1
‫אגף תקצוב ורכש‬
‫הנחיות עזר להגשת ערר בעניין תכנית‪:
‫כתב הערר יוגש תוך 15ימים מיום קבלת ההחלטה‬
.1‬הערר יוגש למזכירות ועדת הערר בכתב‪ ,‬בשישה עותקים‪ ,‬בצירוף עותקים נוספים לפי מספר‬
‫המשיבים‪.
.2‬על הערר לכלול את כל אלה‪:
.2.1‬שם העורר‪ ,‬מענו‪ ,‬מספר טלפון וטלפון נייד‪ ,,‬מספר פקס וכתובת מייל (במידה ויש).
.2.2‬פרטי המשיבים‪ :‬שמותיהם ,‬מענם‪ ,‬מספר טלפון‪ ,‬מספר פקס וכתובת מייל (במידה ויש)
.2.2‬במידה והעורר מיוצג על ידי עורך דין‪ -‬שם ב"כ העורר‪ ,‬מען למסירת מסמכים‪ ,‬מספר‬
‫טלפון‪ ,‬מספר פקס‪ ,‬כתובת מייל וייפוי כוח‪.
.2.2‬פרטי התכנית שלגביה ניתנה ההחלטה נושא הערר (פרטי המקרקעין‪ /‬הנכס‪ -‬כתובת‪ ,‬מס'
‫גוש ומס' חלקה)
.2.2‬נימוקי הערר‬
.2.2‬עיקר הראיות שהעורר מבקש להביא בפני ועדת הערר (נספחים וכל מסמך הנוגע לערר)
.2.2‬החלטת הועדה המקומית לאשר‪/‬לדחות התכנית‪.
.2.2‬כאשר הערר מוגש על ידי מגיש התכנית‪ -‬עליו לצרף לכתב הערר עותק מתקנון ומתשריט‬
‫התכנית‪.
.2.2‬כאשר הערר מוגש על ידי מי שהגיש התנגדות לתכנית או מגיש התכנית -‬על הועדה‬
‫המקומית לצרף לתגובתה עותק מודפס מתקנון ומתשריט התכנית‪.
‫לתשומת ליבכם‪:
‫‪‬‬
‫הגשת הערר אינה כרוכה בתשלום אגרה‪.
‫‪‬‬
‫את הערר יש להגיש לועדת הערר במסירה ידנית או בדואר רשום ובלבד שעמד בכל דרישות‬
‫הדין להגשת הערר והגיע לועדת הערר במועד הקבוע בחוק להגשת ערר‪.
‫המועד בו נתקבל הערר בדואר רשום במזכירות הועדה ירשם כמועד בו נתקבל הערר‪.
‫ערר לא ניתן להעביר באמצעות פקס‪/‬מייל‪.
‫‪‬‬
‫ערר שהגיע לועדה שלא במועד‪ ,‬לא יתקבל אלא אם ניתנה החלטה המאשרת ארכה להגשתו‪.
‫‪‬‬
‫לבקשת עורר‪ ,‬תמציא לו הועדה המקומית את פרטי הצדדים להליך נושא הערר‪ ,‬שמותיהם‬
‫ומעניהם תוך שלושה ימים מיום הגשת הבקשה‪.
‫‪‬‬
‫שימו לב❤ הערר צריך להיות חתום על ידי העורר‪.
‫הנחיות אלו כלליות ומשמשות כעזר לשירות הציבור‪ ,‬בכפוף לקבוע בדין ובתקנות‪ ,‬הגובר על האמור בהנחיות‬
‫אלה‪ ,‬ההנחיות אינן ממצות ואינן כוללות את כל הוראות הדין הרלוונטיות לעניין‪ .‬כמו כן ייתכן וקיימות דרישות‬
‫נוספות בוועדות הערר השונות והן ימסרו על ידי הועדה‪.
‫הנחיות אלו אינן מהוות תחליף לייעוץ משפטי‪.
‫עמוד 2
‫אגף תקצוב ורכש‬
‫הנחיות עזר להגשת ערר בעניין תשריט חלוקה‬
‫כתב הערר יוגש תוך 30ימים מיום קבלת החלטת הועדה המקומית‬
.1‬הערר יוגש למזכירות ועדת הערר בכתב‪ ,‬בשישה עותקים‪ ,‬בצירוף עותקים נוספים לפי מספר‬
‫המשיבים‪.
.2‬על הערר לכלול את כל אלה‪:
.2.1‬שם העורר‪ ,‬מענו‪ ,‬מספר טלפון וטלפון נייד‪ ,‬מספר פקס וכתובת מייל (במידה ויש).
.2.2‬פרטי המשיבים‪ :‬שמותיהם ,‬מענם‪ ,‬מספר טלפון‪ ,‬מספר פקס וכתובת מייל (במידה ויש)
‫כאשר יש לציין בפרטי הועדה המקומית את תאריך הגשת הבקשה‪.
.2.2‬במידה והעורר מיוצג על ידי עורך דין‪ -‬שם ב"כ העורר‪ ,‬מספר רישיון‪ ,‬מען למסירת‬
‫מסמכים‪ ,‬מספר טלפון‪ ,‬מספר פקס‪ ,‬כתובת מייל וייפוי כוח‪.
.2.2‬פרטי הבקשה שלגביה ניתנה ההחלטה נושא הערר (פרטי המקרקעין‪ /‬הנכס‪ -‬כתובת‪ ,‬מס'
‫גוש ומס' חלקה)
.2.2‬פרטי ההחלטה שעליה מוגש הערר והעתק מהודעת הועדה או הרשות על ההחלטה‪.
.2.2‬נימוקי הערר‬
.2.2‬עיקר הראיות שהעורר מבקש להביא בפני ועדת הערר‪.
‫לתשומת ליבכם‪:
‫‪‬‬
‫הגשת הערר אינה כרוכה בתשלום אגרה‪.
‫‪‬‬
‫את הערר יש להגיש לועדת הערר במסירה ידנית או בדואר רשום ובלבד שעמד בכל דרישות‬
‫הדין להגשת הערר והגיע לועדת הערר במועד הקבוע בחוק להגשת ערר‪.
‫המועד בו נתקבל הערר בדואר רשום במזכירות הועדה ירשם כמועד בו נתקבל הערר‪.
‫ערר לא ניתן להעביר באמצעות פקס‪/‬מייל‪.
‫‪‬‬
‫ערר שהגיע לועדה שלא במועד‪ ,‬לא יתקבל אלא אם ניתנה החלטה המאשרת ארכה להגשתו‪.
‫‪‬‬
‫לבקשת עורר‪ ,‬תמציא לו הועדה המקומית את פרטי הצדדים להליך נושא הערר‪ ,‬שמותיהם‬
‫ומעניהם תוך שלושה ימים מיום הגשת הבקשה‪.
‫‪‬‬
‫שימו לב ❤ הערר צריך להיות חתום על ידי העורר‪.
‫הנחיות אלו כלליות ומשמשות כעזר לשירות הציבור‪ ,‬בכפוף לקבוע בדין ובתקנות‪ ,‬הגובר על האמור בהנחיות‬
‫אלה‪ ,‬ההנחיות אינן ממצות ואינן כוללות את כל הוראות הדין הרלוונטיות לעניין‪ .‬כמו כן ייתכן וקיימות דרישות‬
‫נוספות בוועדות הערר השונות והן ימסרו על ידי הועדה‪.
‫הנחיות אלו אינן מהוות תחליף לייעוץ משפטי‪.
‫עמוד 3
‫אגף תקצוב ורכש‬
‫הנחיות עזר להגשת ערר על הנחיות מרחביות‬
‫הערר יוגש תוך 30ימים מיום פרסום ההנחיות המרחביות‬
.1‬הערר יוגש למזכירות ועדת הערר בכתב‪ ,‬בשישה עותקים‪ ,‬בצירוף עותקים נוספים לפי מספר‬
‫המשיבים‪.
.2‬על הערר לכלול את כל אלה‪:
.2.1‬שם העורר‪ ,‬מענו‪ ,‬מספר טלפון וטלפון נייד‪ ,‬מספר פקס וכתובת מייל (במידה ויש).
.2.2‬פרטי המשיבים‪ :‬שמותיהם ,‬מענם‪ ,‬מספר טלפון‪ ,‬מספר פקס וכתובת מייל (במידה ויש)
.2.2‬במידה והעורר מיוצג על ידי עורך דין‪ -‬שם ב"כ העורר‪ ,‬מען למסירת מסמכים‪ ,‬מספר‬
‫טלפון‪ ,‬מספר פקס‪ ,‬כתובת מייל וייפוי כוח‪.
.2.2‬פרטי הבקשה שלגביה ניתנה ההחלטה נושא הערר (פרטי המקרקעין‪ /‬הנכס‪ -‬כתובת‪ ,‬מס'
‫גוש ומס' חלקה)
.2‬פרטי ההחלטה שעליה מוגש הערר‪ ,‬והעתק מהודעת הועדה או הרשות על ההחלטה‪.
.2.1‬נימוקי הערר;
.2.2‬עיקר הראיות שהעורר מבקש להביא בפני ועדת הערר‪.
‫לתשומת ליבכם‪:
‫‪‬‬
‫הגשת הערר אינה כרוכה בתשלום אגרה‪.
‫‪‬‬
‫את הערר יש להגיש לועדת הערר במסירה ידנית או בדואר רשום ובלבד שעמד בכל דרישות‬
‫הדין להגשת הערר והגיע לועדת הערר במועד הקבוע בחוק להגשת ערר‪.
‫המועד בו נתקבל הערר בדואר רשום במזכירות הועדה ירשם כמועד בו נתקבל הערר‪.
‫ערר לא ניתן להעביר באמצעות פקס‪/‬מייל‪.
‫‪‬‬
‫לבקשת עורר‪ ,‬תמציא לו הועדה המקומית את פרטי הצדדים להליך נושא הערר‪ ,‬שמותיהם‬
‫ומעניהם תוך שלושה ימים מיום הגשת הבקשה‪.
‫‪‬‬
‫ערר שהגיע לועדה שלא במועד‪ ,‬לא יתקבל אלא אם ניתנה החלטה המאשרת ארכה להגשתו‪.
‫‪‬‬
‫יש לציין תאריך המצאת ההחלטה לידי העורר‪.
‫‪‬‬
‫יש לציין באם הערר המוגש קשור לערר קודם שהוגש בעבר‪.
‫‪‬‬
‫שימו לב ❤ הערר צריך להיות חתום על ידי העורר‪.
‫הנחיות אלו כלליות ומשמשות כעזר לשירות הציבור‪ ,‬בכפוף לקבוע בדין ובתקנות‪ ,‬הגובר על האמור בהנחיות‬
‫אלה‪ ,‬ההנחיות אינן ממצות ואינן כוללות את כל הוראות הדין הרלוונטיות לעניין‪ .‬כמו כן ייתכן וקיימות דרישות‬
‫נוספות בוועדות הערר השונות והן ימסרו על ידי הועדה‪.
‫הנחיות אלו אינן מהוות תחליף לייעוץ משפטי‪.
‫עמוד 4
‫אגף תקצוב ורכש‬
‫הנחיות אלו כלליות ומשמשות כעזר לשירות הציבור‪ ,‬בכפוף לקבוע בדין ובתקנות‪ ,‬הגובר על האמור בהנחיות‬
‫אלה‪ ,‬ההנחיות אינן ממצות ואינן כוללות את כל הוראות הדין הרלוונטיות לעניין‪ .‬כמו כן ייתכן וקיימות דרישות‬
‫נוספות בוועדות הערר השונות והן ימסרו על ידי הועדה‪.
‫הנחיות אלו אינן מהוות תחליף לייעוץ משפטי‪.
‫עמוד 5

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ from uuid import UUID
from legal_mcp import config
from legal_mcp.services import db, embeddings, claude_session
from legal_mcp.services.lessons import get_content_checklist
from legal_mcp.services.lessons import get_content_checklist, get_methodology_summary
logger = logging.getLogger(__name__)
@@ -201,30 +201,22 @@ BLOCK_PROMPTS = {
## זהו הבלוק הקריטי ביותר — ליבת ההחלטה (ratio decidendi).
## אורך נדרש: **2,000-4,000 מילים לפחות**. זהו הבלוק הארוך ביותר בהחלטה (35-50%).
## מתודולוגיה — CREAC:
1. **C** (Conclusion) — פתח במסקנה: "לאחר שעיינו... מצאנו כי הערר [נדחה/מתקבל]"
2. **R** (Rule) — הצג את הכלל המשפטי הרלוונטי עם ציטוט פסיקה
3. **E** (Explanation) — צטט פסיקה שמסבירה את הכלל (200-600 מילים לכל ציטוט)
4. **A** (Application) — יישם על העובדות הספציפיות של התיק
5. **C** (Conclusion) — מסקנת ביניים
## כללים קריטיים:
- **מסקנה בפתיחה** — לא בסוף
- **מענה פרטני לכל טענה** שהוצגה בבלוק ז — עבור על כל טענה ברשימה והתייחס אליה בנפרד. אל תדלג על שום טענה.
- **ציטוטי פסיקה** — צטט לפחות 3-5 פסקי דין רלוונטיים. כל ציטוט עם שם התיק המלא.
- **ללא כפילות** — הפנה לבלוקים קודמים: "כאמור בסעיף X לעיל"
- **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין)
- מספור רציף
{methodology_guidance}
{content_checklist}
## כללים נוספים:
- **ללא כפילות** — הפנה לבלוקים קודמים: "כאמור בסעיף X לעיל"
- **מספור רציף** — המשך מספור מהבלוק הקודם
- מותרות כותרות-משנה כשיש נושאים נפרדים לחלוטין
## כיוון מאושר (חובה):
{direction_context}
## מבנה לפי תוצאה:
{structure_guidance}
## טענות שצריך לענות עליהן (חובה — כל טענה חייבת מענה):
## טענות:
{claims_context}
## חומרי מקור:
@@ -315,12 +307,15 @@ async def write_block(
# Content checklist — tells block-yod WHAT topics to cover
content_checklist = ""
methodology_guidance = ""
if block_id == "block-yod":
content_checklist = get_content_checklist(
appeal_type=case.get("appeal_type", ""),
subject=case.get("subject", ""),
subject_categories=case.get("subject_categories", []),
)
# Methodology guidance — tells block-yod HOW to reason (universal, not case-specific)
methodology_guidance = get_methodology_summary()
# Format prompt — per Anthropic long-context best practices:
# Place source documents FIRST (top of prompt), instructions LAST.
@@ -336,6 +331,7 @@ async def write_block(
discussion_context=discussion_context,
structure_guidance=structure_guidance,
content_checklist=content_checklist,
methodology_guidance=methodology_guidance,
)
# Restructure: sources first, then instructions
@@ -431,7 +427,7 @@ async def _build_claims_context(case_id: UUID) -> str:
lines.append(f"\n### {role_heb.get(current_role, current_role)}")
claim_num += 1
lines.append(f"טענה #{claim_num}: {c['claim_text'][:400]}")
lines.append(f"\n**סה\"כ {claim_num} טענות — חובה לענות על כל אחת.**")
lines.append(f"\n**סה\"כ {claim_num} טענות. ענה על כל טענה מהותית; טענות [bundle] — אגד; טענות [skip] — ציון קצר בלבד.**")
return "\n".join(lines)
@@ -489,17 +485,12 @@ async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
case = await db.get_case(case_id)
case_number = case.get("case_number", "") if case else ""
subject = case.get("subject", "") if case else ""
practice_area = case.get("practice_area") if case else None
appeal_subtype = case.get("appeal_subtype") if case else None
query = f"דיון משפטי בנושא {subject}" if subject else "דיון משפטי ועדת ערר"
query_emb = await embeddings.embed_query(query)
# Search 1: paragraph_embeddings (from other decisions by Dafna).
# Filter by practice_area + appeal_subtype so we don't pull a
# betterment-levy paragraph when writing a building-permit decision.
# Search 1: paragraph_embeddings (from other decisions by Dafna)
para_results = await db.search_similar_paragraphs(
query_embedding=query_emb, limit=10, block_type="block-yod",
practice_area=practice_area, appeal_subtype=appeal_subtype,
)
# Filter out same case
para_results = [r for r in para_results if r.get("case_number", "") != case_number]
@@ -667,6 +658,17 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
outcome = (decision or {}).get("outcome", "rejected")
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
# Content checklist + methodology for block-yod
content_checklist = ""
methodology_guidance = ""
if block_id == "block-yod":
content_checklist = get_content_checklist(
appeal_type=case.get("appeal_type", ""),
subject=case.get("subject", ""),
subject_categories=case.get("subject_categories", []),
)
methodology_guidance = get_methodology_summary()
formatted_prompt = prompt_template.format(
case_context=case_context,
source_context=source_context,
@@ -677,6 +679,8 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
style_context=style_context,
discussion_context=discussion_context,
structure_guidance=structure_guidance,
content_checklist=content_checklist,
methodology_guidance=methodology_guidance,
)
if instructions:

View File

@@ -200,110 +200,6 @@ CREATE TABLE IF NOT EXISTS appeal_type_rules (
ALTER TABLE decision_blocks ADD COLUMN IF NOT EXISTS image_placeholders JSONB DEFAULT '[]';
"""
# ── Phase 4: Practice area separation (multi-tenant axis) ──────────
SCHEMA_V4_SQL = """
-- ═══════════════════════════════════════════════════════════════════
-- practice_area = top-level legal domain (multi-tenant axis):
-- appeals_committee | national_insurance | labor_law | ...
-- appeal_subtype = refines within practice_area:
-- building_permit | betterment_levy | compensation_197 | unknown
-- Both columns are denormalized to documents/chunks/decisions/style_corpus
-- so vector searches can filter without expensive JOINs.
-- ═══════════════════════════════════════════════════════════════════
ALTER TABLE cases ADD COLUMN IF NOT EXISTS practice_area TEXT;
ALTER TABLE cases ADD COLUMN IF NOT EXISTS appeal_subtype TEXT;
ALTER TABLE documents ADD COLUMN IF NOT EXISTS practice_area TEXT;
ALTER TABLE documents ADD COLUMN IF NOT EXISTS appeal_subtype TEXT;
ALTER TABLE document_chunks ADD COLUMN IF NOT EXISTS practice_area TEXT;
ALTER TABLE document_chunks ADD COLUMN IF NOT EXISTS appeal_subtype TEXT;
ALTER TABLE decisions ADD COLUMN IF NOT EXISTS practice_area TEXT;
ALTER TABLE decisions ADD COLUMN IF NOT EXISTS appeal_subtype TEXT;
ALTER TABLE style_corpus ADD COLUMN IF NOT EXISTS practice_area TEXT;
ALTER TABLE style_corpus ADD COLUMN IF NOT EXISTS appeal_subtype TEXT;
CREATE INDEX IF NOT EXISTS idx_cases_practice
ON cases(practice_area, appeal_subtype);
CREATE INDEX IF NOT EXISTS idx_chunks_practice
ON document_chunks(practice_area);
CREATE INDEX IF NOT EXISTS idx_corpus_practice
ON style_corpus(practice_area, appeal_subtype);
CREATE INDEX IF NOT EXISTS idx_decisions_practice
ON decisions(practice_area);
-- Backfill (idempotent — only fills NULLs)
UPDATE cases SET practice_area = 'appeals_committee' WHERE practice_area IS NULL;
UPDATE cases SET appeal_subtype = CASE
WHEN case_number ~ '^1[0-9]{3}' THEN 'building_permit'
WHEN case_number ~ '^8[0-9]{3}' THEN 'betterment_levy'
WHEN case_number ~ '^9[0-9]{3}' THEN 'compensation_197'
ELSE 'unknown'
END WHERE appeal_subtype IS NULL;
UPDATE documents d
SET practice_area = c.practice_area, appeal_subtype = c.appeal_subtype
FROM cases c
WHERE d.case_id = c.id AND d.practice_area IS NULL;
UPDATE document_chunks dc
SET practice_area = c.practice_area, appeal_subtype = c.appeal_subtype
FROM cases c
WHERE dc.case_id = c.id AND dc.practice_area IS NULL;
UPDATE decisions de
SET practice_area = c.practice_area, appeal_subtype = c.appeal_subtype
FROM cases c
WHERE de.case_id = c.id AND de.practice_area IS NULL;
-- All existing style_corpus entries are דפנה's appeals-committee decisions
UPDATE style_corpus SET practice_area = 'appeals_committee' WHERE practice_area IS NULL;
-- Training corpus documents/chunks have case_id = NULL. All historical
-- training material is from דפנה's appeals committee, so default them.
UPDATE documents SET practice_area = 'appeals_committee'
WHERE case_id IS NULL AND practice_area IS NULL;
UPDATE document_chunks dc
SET practice_area = d.practice_area, appeal_subtype = d.appeal_subtype
FROM documents d
WHERE dc.document_id = d.id AND dc.practice_area IS NULL;
"""
# ── Phase 5: case_precedents (user-attached legal quotes) ──────────
SCHEMA_V5_SQL = """
-- ═══════════════════════════════════════════════════════════════════
-- case_precedents: legal support the chair attaches to a case / section
-- during the compose phase. Self-contained — quote + citation are
-- stored inline, with an optional FK to an archived PDF in documents.
-- Not linked to case_law (which has UNIQUE(case_number)) to keep the
-- citation as free-text. A backfill pass into case_law is a future
-- follow-up once the UI stabilizes.
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS case_precedents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE,
section_id TEXT, -- NULL = case-level
-- else "threshold_1" / "issue_3"
quote TEXT NOT NULL,
citation TEXT NOT NULL, -- free-text "מראה מקום"
chair_note TEXT DEFAULT '',
pdf_document_id UUID REFERENCES documents(id) ON DELETE SET NULL,
practice_area TEXT, -- denormalized from cases
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_case_precedents_case
ON case_precedents(case_id, section_id);
CREATE INDEX IF NOT EXISTS idx_case_precedents_library
ON case_precedents(citation);
CREATE INDEX IF NOT EXISTS idx_case_precedents_area
ON case_precedents(practice_area);
"""
# ── Phase 2: Decision + Knowledge + RAG layers ────────────────────
SCHEMA_V2_SQL = """
@@ -501,6 +397,51 @@ CREATE INDEX IF NOT EXISTS idx_case_law_embeddings_vec
"""
# ── Phase 4: Methodology alignment ──────────────────────────────
SCHEMA_V4_SQL = """
-- ═══════════════════════════════════════════════════════════════════
-- V4: Methodology alignment (decision-methodology.md)
-- ═══════════════════════════════════════════════════════════════════
-- claims: טיפול בטענות (bundle/skip) + סוג טענה
ALTER TABLE claims ADD COLUMN IF NOT EXISTS claim_type TEXT DEFAULT 'claim';
-- claim / response / reply
ALTER TABLE claims ADD COLUMN IF NOT EXISTS claim_handling TEXT DEFAULT 'address';
-- address (דיון מלא) / bundle (קיבוץ) / skip (דילוג)
ALTER TABLE claims ADD COLUMN IF NOT EXISTS bundle_group TEXT DEFAULT '';
-- שם הקבוצה לקיבוץ (למשל "פגמים פרוצדורליים")
ALTER TABLE claims ADD COLUMN IF NOT EXISTS handling_reason TEXT DEFAULT '';
-- נימוק לדילוג/קיבוץ (למשל "נבחנה ולא מצאנו ממש")
-- cases: תקן ביקורת + קטגוריות נושא
ALTER TABLE cases ADD COLUMN IF NOT EXISTS standard_of_review TEXT DEFAULT '';
-- "שיקול דעת תכנוני עצמאי" / "בחינת שומה מכרעת" / ...
ALTER TABLE cases ADD COLUMN IF NOT EXISTS subject_categories JSONB DEFAULT '[]';
-- ["חניה", "קווי בניין", "גובה", "שימוש חורג", ...]
-- case_law: רמת תקדים + מעמד
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS precedent_level TEXT DEFAULT '';
-- עליון / מנהלי / ועדת ערר ארצית / ועדת ערר מחוזית
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS is_binding BOOLEAN DEFAULT TRUE;
-- הלכה מחייבת (true) / אמרת אגב (false)
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS creac_role TEXT DEFAULT '';
-- rule (הנחה עליונה) / explanation (הרחבה) / analogy (אנלוגיה)
-- decisions: סדר סוגיות + תקן ביקורת
ALTER TABLE decisions ADD COLUMN IF NOT EXISTS issue_order JSONB DEFAULT '[]';
-- סדר הסוגיות שנקבע ע"י המנצח: [{"title": "...", "type": "threshold/dispositive/secondary"}]
ALTER TABLE decisions ADD COLUMN IF NOT EXISTS claim_handling JSONB DEFAULT '{}';
-- {"overrides": [{"claim_id": "...", "handling": "bundle", "group": "..."}]}
-- indexes
CREATE INDEX IF NOT EXISTS idx_claims_handling ON claims(claim_handling);
CREATE INDEX IF NOT EXISTS idx_claims_type ON claims(claim_type);
CREATE INDEX IF NOT EXISTS idx_case_law_level ON case_law(precedent_level);
"""
async def init_schema() -> None:
pool = await get_pool()
async with pool.acquire() as conn:
@@ -509,8 +450,7 @@ async def init_schema() -> None:
await conn.execute(SCHEMA_V2_SQL)
await conn.execute(SCHEMA_V3_SQL)
await conn.execute(SCHEMA_V4_SQL)
await conn.execute(SCHEMA_V5_SQL)
logger.info("Database schema initialized (v1 + v2 + v3 + v4 + v5)")
logger.info("Database schema initialized (v1 + v2 + v3 + v4)")
# ── Case CRUD ───────────────────────────────────────────────────────
@@ -527,8 +467,6 @@ async def create_case(
hearing_date: date | None = None,
notes: str = "",
expected_outcome: str = "",
practice_area: str = "appeals_committee",
appeal_subtype: str | None = None,
) -> dict:
pool = await get_pool()
case_id = uuid4()
@@ -536,43 +474,17 @@ async def create_case(
await conn.execute(
"""INSERT INTO cases (id, case_number, title, appellants, respondents,
subject, property_address, permit_number, committee_type,
hearing_date, notes, expected_outcome,
practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)""",
hearing_date, notes, expected_outcome)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)""",
case_id, case_number, title,
json.dumps(appellants or []),
json.dumps(respondents or []),
subject, property_address, permit_number, committee_type,
hearing_date, notes, expected_outcome,
practice_area, appeal_subtype,
)
return await get_case(case_id)
async def get_case_practice_area(case_id: UUID) -> tuple[str | None, str | None]:
"""Return (practice_area, appeal_subtype) for a case, or (None, None) if missing."""
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM cases WHERE id = $1", case_id
)
if row is None:
return None, None
return row["practice_area"], row["appeal_subtype"]
async def get_case_practice_area_by_number(case_number: str) -> tuple[str | None, str | None]:
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM cases WHERE case_number = $1",
case_number,
)
if row is None:
return None, None
return row["practice_area"], row["appeal_subtype"]
async def get_case(case_id: UUID) -> dict | None:
pool = await get_pool()
async with pool.acquire() as conn:
@@ -608,16 +520,6 @@ async def list_cases(status: str | None = None, limit: int = 50) -> list[dict]:
return [_row_to_case(r) for r in rows]
async def delete_case(case_id: UUID) -> bool:
"""Delete a case. Dependent rows in documents/document_chunks/qa_results
cascade automatically (schema-level ON DELETE CASCADE); audit_log rows
nullify their case_id reference. Returns True if a row was deleted."""
pool = await get_pool()
async with pool.acquire() as conn:
result = await conn.execute("DELETE FROM cases WHERE id = $1", case_id)
return result.endswith(" 1")
async def update_case(case_id: UUID, **fields) -> dict | None:
if not fields:
return await get_case(case_id)
@@ -648,34 +550,19 @@ def _row_to_case(row: asyncpg.Record) -> dict:
# ── Document CRUD ───────────────────────────────────────────────────
async def create_document(
case_id: UUID | None,
case_id: UUID,
doc_type: str,
title: str,
file_path: str,
page_count: int | None = None,
practice_area: str | None = None,
appeal_subtype: str | None = None,
) -> dict:
pool = await get_pool()
doc_id = uuid4()
async with pool.acquire() as conn:
# If practice_area not explicitly given, inherit from the parent case
# (for case-bound documents). Training corpus passes case_id=None and
# provides the practice_area directly.
if practice_area is None and case_id is not None:
case_row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM cases WHERE id = $1",
case_id,
)
if case_row:
practice_area = case_row["practice_area"]
appeal_subtype = case_row["appeal_subtype"]
await conn.execute(
"""INSERT INTO documents (id, case_id, doc_type, title, file_path,
page_count, practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)""",
"""INSERT INTO documents (id, case_id, doc_type, title, file_path, page_count)
VALUES ($1, $2, $3, $4, $5, $6)""",
doc_id, case_id, doc_type, title, file_path, page_count,
practice_area, appeal_subtype,
)
row = await conn.fetchrow("SELECT * FROM documents WHERE id = $1", doc_id)
return _row_to_doc(row)
@@ -731,113 +618,6 @@ def _row_to_doc(row: asyncpg.Record) -> dict:
return d
# ── case_precedents CRUD ───────────────────────────────────────────
def _row_to_precedent(row: asyncpg.Record) -> dict:
d = dict(row)
for k in ("id", "case_id"):
if d.get(k) is not None:
d[k] = str(d[k])
if d.get("pdf_document_id") is not None:
d["pdf_document_id"] = str(d["pdf_document_id"])
for ts in ("created_at", "updated_at"):
if d.get(ts) is not None:
d[ts] = d[ts].isoformat()
return d
async def create_case_precedent(
case_id: UUID,
quote: str,
citation: str,
section_id: str | None = None,
chair_note: str = "",
pdf_document_id: UUID | None = None,
practice_area: str | None = None,
) -> dict:
"""Insert a new attached precedent. practice_area is inherited from
the parent case when not explicitly supplied, so the cross-case
library search can filter without a JOIN."""
pool = await get_pool()
async with pool.acquire() as conn:
if practice_area is None:
row = await conn.fetchrow(
"SELECT practice_area FROM cases WHERE id = $1", case_id
)
practice_area = row["practice_area"] if row else None
inserted = await conn.fetchrow(
"""INSERT INTO case_precedents
(case_id, section_id, quote, citation, chair_note,
pdf_document_id, practice_area)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *""",
case_id, section_id, quote, citation, chair_note,
pdf_document_id, practice_area,
)
return _row_to_precedent(inserted)
async def list_case_precedents(case_id: UUID) -> list[dict]:
pool = await get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"SELECT * FROM case_precedents WHERE case_id = $1 "
"ORDER BY section_id NULLS FIRST, created_at",
case_id,
)
return [_row_to_precedent(r) for r in rows]
async def delete_case_precedent(precedent_id: UUID) -> bool:
pool = await get_pool()
async with pool.acquire() as conn:
result = await conn.execute(
"DELETE FROM case_precedents WHERE id = $1", precedent_id
)
return result.endswith(" 1")
async def search_precedent_library(
query: str, practice_area: str = "", limit: int = 10,
) -> list[dict]:
"""Cross-case typeahead for the citation field. Returns one row per
distinct citation so the user sees each precedent once even if they
previously attached it to multiple cases/sections. No embeddings —
simple ILIKE is fine at this scale."""
pool = await get_pool()
pattern = f"%{query}%"
async with pool.acquire() as conn:
if practice_area:
rows = await conn.fetch(
"""SELECT DISTINCT ON (citation)
id, citation, quote, chair_note, practice_area, created_at
FROM case_precedents
WHERE practice_area = $1
AND (citation ILIKE $2 OR quote ILIKE $2)
ORDER BY citation, created_at DESC
LIMIT $3""",
practice_area, pattern, limit,
)
else:
rows = await conn.fetch(
"""SELECT DISTINCT ON (citation)
id, citation, quote, chair_note, practice_area, created_at
FROM case_precedents
WHERE citation ILIKE $1 OR quote ILIKE $1
ORDER BY citation, created_at DESC
LIMIT $2""",
pattern, limit,
)
out = []
for r in rows:
d = dict(r)
d["id"] = str(d["id"])
if d.get("created_at"):
d["created_at"] = d["created_at"].isoformat()
out.append(d)
return out
# ── Claims ─────────────────────────────────────────────────────────
async def store_claims(case_id: UUID, claims: list[dict], source_document: str = "") -> int:
@@ -904,20 +684,12 @@ async def create_decision(
)
version = (existing["version"] + 1) if existing else 1
case_row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM cases WHERE id = $1", case_id
)
practice_area = case_row["practice_area"] if case_row else None
appeal_subtype = case_row["appeal_subtype"] if case_row else None
await conn.execute(
"""INSERT INTO decisions (id, case_id, version, outcome, outcome_summary,
outcome_reasoning, direction_doc,
practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
outcome_reasoning, direction_doc)
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
decision_id, case_id, version, outcome, outcome_summary,
outcome_reasoning, json.dumps(direction_doc) if direction_doc else None,
practice_area, appeal_subtype,
)
return await get_decision(decision_id)
@@ -991,37 +763,12 @@ async def store_chunks(
document_id: UUID,
case_id: UUID | None,
chunks: list[dict],
practice_area: str | None = None,
appeal_subtype: str | None = None,
) -> int:
"""Store document chunks with embeddings. Each chunk dict has:
content, section_type, embedding (list[float]), page_number, chunk_index.
practice_area defaults to the parent case's value, or — when case_id is
None (training corpus) — falls back to the parent document's value so
vector search can still filter cleanly.
content, section_type, embedding (list[float]), page_number, chunk_index
"""
pool = await get_pool()
async with pool.acquire() as conn:
# Resolve practice_area in priority order: explicit > case > document.
if practice_area is None:
if case_id is not None:
case_row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM cases WHERE id = $1",
case_id,
)
if case_row:
practice_area = case_row["practice_area"]
appeal_subtype = case_row["appeal_subtype"]
if practice_area is None:
doc_row = await conn.fetchrow(
"SELECT practice_area, appeal_subtype FROM documents WHERE id = $1",
document_id,
)
if doc_row:
practice_area = doc_row["practice_area"]
appeal_subtype = doc_row["appeal_subtype"]
# Delete existing chunks for this document
await conn.execute(
"DELETE FROM document_chunks WHERE document_id = $1", document_id
@@ -1029,16 +776,14 @@ async def store_chunks(
for chunk in chunks:
await conn.execute(
"""INSERT INTO document_chunks
(document_id, case_id, chunk_index, content, section_type,
embedding, page_number, practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
(document_id, case_id, chunk_index, content, section_type, embedding, page_number)
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
document_id, case_id,
chunk["chunk_index"],
chunk["content"],
chunk.get("section_type", "other"),
chunk["embedding"],
chunk.get("page_number"),
practice_area, appeal_subtype,
)
return len(chunks)
@@ -1048,15 +793,8 @@ async def search_similar(
limit: int = 10,
case_id: UUID | None = None,
section_type: str | None = None,
practice_area: str | None = None,
appeal_subtype: str | None = None,
) -> list[dict]:
"""Cosine similarity search on document chunks.
Filter by practice_area to keep precedents from the same legal domain
(e.g. don't surface betterment-levy chunks when working on building
permits). Uses the denormalized column on document_chunks — no JOIN.
"""
"""Cosine similarity search on document chunks."""
pool = await get_pool()
conditions = []
params: list = [query_embedding, limit]
@@ -1070,14 +808,6 @@ async def search_similar(
conditions.append(f"dc.section_type = ${param_idx}")
params.append(section_type)
param_idx += 1
if practice_area:
conditions.append(f"dc.practice_area = ${param_idx}")
params.append(practice_area)
param_idx += 1
if appeal_subtype:
conditions.append(f"dc.appeal_subtype = ${param_idx}")
params.append(appeal_subtype)
param_idx += 1
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
@@ -1110,8 +840,6 @@ async def add_to_style_corpus(
summary: str = "",
outcome: str = "",
key_principles: list[str] | None = None,
practice_area: str = "appeals_committee",
appeal_subtype: str | None = None,
) -> UUID:
pool = await get_pool()
corpus_id = uuid4()
@@ -1119,13 +847,11 @@ async def add_to_style_corpus(
await conn.execute(
"""INSERT INTO style_corpus
(id, document_id, decision_number, decision_date,
subject_categories, full_text, summary, outcome, key_principles,
practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)""",
subject_categories, full_text, summary, outcome, key_principles)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
corpus_id, document_id, decision_number, decision_date,
json.dumps(subject_categories), full_text, summary, outcome,
json.dumps(key_principles or []),
practice_area, appeal_subtype,
)
return corpus_id
@@ -1229,15 +955,8 @@ async def search_similar_paragraphs(
query_embedding: list[float],
limit: int = 10,
block_type: str | None = None,
practice_area: str | None = None,
appeal_subtype: str | None = None,
) -> list[dict]:
"""Search decision paragraphs by semantic similarity.
Filtering by practice_area uses the denormalized column on `decisions`
so we don't pull, e.g., betterment-levy paragraphs when writing a
building-permit decision.
"""
"""Search decision paragraphs by semantic similarity."""
pool = await get_pool()
conditions = []
params: list = [query_embedding, limit]
@@ -1247,14 +966,6 @@ async def search_similar_paragraphs(
conditions.append(f"db.block_id = ${param_idx}")
params.append(block_type)
param_idx += 1
if practice_area:
conditions.append(f"d.practice_area = ${param_idx}")
params.append(practice_area)
param_idx += 1
if appeal_subtype:
conditions.append(f"d.appeal_subtype = ${param_idx}")
params.append(appeal_subtype)
param_idx += 1
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""

View File

@@ -519,3 +519,65 @@ def get_content_checklist(
# Default: substantive licensing
return CONTENT_CHECKLISTS["licensing_substantive"]
# ── Methodology guidance (condensed from decision-methodology.md) ──
_METHODOLOGY_CORE = """## מתודולוגיה אנליטית — עקרונות מנחים לכתיבת הדיון
### מבנה סילוגיסטי לכל סוגיה
כל סוגיה נבנית כסילוגיזם: (1) הנחה עליונה = הכלל (הוראת תכנית, חוק, הלכה); (2) הנחה תחתונה = העובדות הספציפיות; (3) מסקנה. אם לא ניתן לזהות את הכלל — ההנמקה אינה מספקת. אם לא ניתן לזהות כיצד העובדות מקיימות את הכלל — ההנמקה קריפטית.
### התחל מלשון הטקסט
כשהמקרה נשלט על ידי הוראת תכנית או סעיף חוק — פתח בציטוט ההוראה. פרש מילים במשמעותן הרגילה. תן תוקף לכל מילה. אם יש עמימות — השתמש בכלי פרשנות.
### הפרד ממצא עובדתי ממסקנה משפטית
"הבניה במרחק 1.5 מטרים מגבול המגרש" = ממצא עובדתי. "חריגה זו עולה כדי סטייה ניכרת" = מסקנה משפטית. אל תערבב.
### CREAC לכל סוגיה
1. מסקנה — פתח בתשובה ("הבקשה אינה תואמת...")
2. כלל — ציטוט ההוראה
3. הרחבה — תקדים רלוונטי אחד (אם נדרש)
4. יישום — החלת הכלל על העובדות (לב ההנמקה)
5. מסקנה חוזרת — סגירה תמציתית
### Steel-Man — הצג טענה בחוזקתה לפני דחייה
לפני שדוחים טענה — הצג אותה בגרסה החזקה ביותר: "אמנם צודק העורר כי [נקודה לטובתו], אולם [הנימוק לדחייה]." טענת קש קלה להפריך אך לא משכנעת.
### טכניקת סנדוויץ' לציטוטים
כל ציטוט עטוף: משפט הקדמה (מודיע על התוכן) → ציטוט → ניתוח (מסביר כיצד רלוונטי למקרה). אל תניח שהקורא יקרא ציטוט ארוך ויפיק ממנו מסקנות בעצמו.
### נתונים, לא תיאורים
"הבקשה חורגת ב-1.5 מטרים מקו הבניין" — לא "הבקשה חורגת באופן משמעותי." מספרים, מידות, אחוזים.
### כנות לגבי קושי
כשהמקרה קשה — אמור זאת: "הדבר אינו נקי מספקות, אולם..." אל תעמיד פנים שמקרה קשה הוא קל.
### כל מילה עובדת
"לאחר ששקלנו את כלל השיקולים" — ריק, מחק. מבחן: אם מוחקים את המשפט וההחלטה לא מאבדת מידע — המשפט מיותר.
### איזון ומידתיות (כשהכלל לא נותן תשובה חד-משמעית)
כשנדרש איזון:
1. זהה אינטרסים קונקרטיים (לא "אינטרס הציבור" אלא "שמירה על אופי מגורים צמודי קרקע")
2. בחן השלכות לכל כיוון: מה קורה אם מקבלים? אם דוחים?
3. שקול השלכות מערכתיות: מה הסיגנל שנשלח למערכת?
4. ציין מה מכריע את הכף ולמה
כשמטילים מגבלה/תנאי — מבחן מידתיות: (1) תכלית ראויה?; (2) אמצעי פוגע פחות?; (3) פגיעה מידתית ביחס לתועלת?
### טיפול בטענות
- ההחלטה מנתחת שאלות — לא מתווכחת עם עו"ד. מבנה: שאלה→כלל→עובדות→מסקנה
- טענות שסומנו [bundle] ב-chair_directions: קבץ ודון יחד
- טענות שסומנו [skip] ב-chair_directions: ציון קצר בלבד
- טענות ללא סימון: ענה בנפרד עם מענה מנומק
- טענה מרכזית של הצד המפסיד חייבת מענה Steel-Man
- מיקום ההתמודדות עם טענות נגדיות: באמצע הדיון בסוגיה (לא בהתחלה ולא בסוף)
"""
def get_methodology_summary() -> str:
"""Return the condensed methodology guidance — always the same, always complete.
The methodology is universal: it teaches HOW to think, not WHAT to discuss.
Case-specific content (parking, building lines, significant deviation) belongs
in the content checklists, not here.
"""
return _METHODOLOGY_CORE

View File

@@ -203,6 +203,9 @@ description: This skill should be used when writing legal decisions (החלטו
## 6. השיטה האנליטית - "איך לחשוב" לפני "איך לכתוב"
> **מסמך המתודולוגיה המלא:** [`docs/decision-methodology.md`](../../docs/decision-methodology.md)
> מסמך זה עוסק בטכניקות הכתיבה של דפנה. לתאוריית ההחלטה הכללית — מבנה סילוגיסטי, איזון, steel-man, טכניקת סנדוויץ', הפרדת ממצאים ממסקנות — ראה את מסמך המתודולוגיה.
### 6.1 שומר הסף - שאלת הסף
לפני שנוגעים בטענה לגופה, השאלה הראשונה היא: "יש לעוררים בכלל זכות ערר?" זו לא רק שאלה פרוצדורלית - זו מסגרת הניתוח כולה. **סייג חשוב:** שאלת הסף היא כלי אסטרטגי, לא חובה. בתיקים עם שאלות מהותיות חזקות (חניה, שימור, קווי בניין), דפנה עשויה לדלג על שאלת הסף ולדון ישירות בגוף העניין — במיוחד בקבלה חלקית. ראה: בית הכרם 1126/25 — דילגה על ס' 152 לחלוטין.
@@ -499,3 +502,39 @@ description: This skill should be used when writing legal decisions (החלטו
| תיבות תמונה | מסגרת עם shading אפור בהיר (fill: "F0F0F0"), טקסט "📷 תמונה: [תיאור]" | ShadingType.CLEAR |
| חתימות | טבלה ללא גבולות (`visuallyRightToLeft: true`), 2 טורים | כמו בתבנית ב-create-legal-doc.js |
| כותרת מוסדית | טבלה ללא גבולות, 2 טורים: ימין=מוסד, שמאל=מספרי תיק | `visuallyRightToLeft: true` |
## 12. צ'קליסט תוכן לפי סוג ערר
> נוסף אפריל 2026 בעקבות ניתוח שיטתי של 24 החלטות. ראה: `docs/corpus-analysis.md`
הפרומפט של בלוק י מקבל **צ'קליסט תוכן** אוטומטי לפי סוג הערר (`lessons.py: CONTENT_CHECKLISTS`). זה מבטיח שהדיון יכסה את הנושאים הנדרשים — לא רק סגנון ומתודולוגיה, אלא תוכן ענייני.
### 12.1 חמישה תת-סוגי רישוי (לא שלושה)
ניתוח הקורפוס חשף שלתיקי רישוי יש 5 תת-סוגים שונים מבחינת מבנה הדיון:
| תת-סוג | מה בדיון | דוגמאות |
|---------|---------|---------|
| **רישוי מהותי** | דיון תכנוני מקיף + משפטי | רוב ההחלטות |
| **סף/סמכות** | משפטי בלבד, ללא תכנון | גבאי, ירושלים שקופה |
| **קנייני** | תימוכין קנייניים, מינימום תכנון | טלי-אביב, הראל 1043 |
| **תמ"א 38** | איזון אינטרסים + תכנון + שכנות | בית הכרם |
| **שימוש חורג** | פרשנות תכניות מרובות | תורן |
### 12.2 דיון תכנוני — מתי ואיך
**מתי חובה:** כשהערר מגיע לדיון מהותי (לא סף/סמכות, לא קנייני טהור).
**מבנה טיפוסי (מהקורפוס):**
1. הקשר תכנוני רחב — תכניות חלות, ייעוד, סביבה (2-8 סעיפים)
2. ציטוט ישיר מהוראות תכנית — בלוקים של 200-600 מילים עם "הדגשת הח"מ"
3. יישום על המקרה — הוראה → עובדה → מסקנה
4. מסקנה תכנונית — תואם/סוטה, מוצדק/לא
**נושאים שמופיעים בתדירות גבוהה:**
- חניה (8/24 החלטות) — הנושא התכנוני הנפוץ ביותר, עומק של 5-15 סעיפים
- קווי בניין (7/24) — כולל ניתוח סטייה ניכרת
- ניתוח הוראות תכנית (18/24) — כמעט תמיד
- פגיעה בשכנים (5/24) — צל, פרטיות, רעש
### 12.3 הערות יו
הערות דפנה על טיוטות מתועדות במערכת `chair_feedback` (DB + API + UI ב-`/feedback`). כל הערה מסווגת לקטגוריה ומפיקה לקח שמשפר את ההחלטות הבאות.

41
web-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

5
web-ui/AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
web-ui/CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

175
web-ui/README.md Normal file
View File

@@ -0,0 +1,175 @@
# עוזר משפטי — Web UI (Next.js rewrite)
The Next.js 16 rewrite of `legal-ai.nautilus.marcusgroup.org`, currently hosted side-by-side with the legacy vanilla `index.html` at:
- **Staging:** https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from `ui-rewrite` branch via Coolify)
- **Production FastAPI:** https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
The rewrite talks to the existing FastAPI via proxy rewrites in `next.config.ts` — no CORS setup, no duplicated backend.
## Stack
- Next.js 16.2.3 (App Router, Turbopack, `output: "standalone"`)
- React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
- TanStack Query v5 + TanStack Table v8
- react-hook-form + zod for mutations
- react-dropzone for uploads; EventSource for SSE progress
- Heebo via `next/font/google`; design tokens in `src/app/globals.css`
## Local development
```bash
npm install
npm run dev # http://localhost:3000
npm run build # full type check + production build
npm run lint
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
```
### API connection
By default the dev server proxies to production FastAPI (`https://legal-ai.nautilus.marcusgroup.org`). To point at a different backend, set:
```bash
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
npm run dev
```
## Project layout
```
src/
├── app/ # Route segments (App Router)
│ ├── layout.tsx # Root: Providers + RTL html + fonts
│ ├── page.tsx # Home: KPIs + status donut + cases table
│ ├── error.tsx # Route-segment error boundary
│ ├── global-error.tsx # Root crash fallback
│ ├── not-found.tsx # 404
│ ├── cases/
│ │ ├── new/ # 3-step create wizard
│ │ └── [caseNumber]/
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
│ │ └── compose/ # Research analysis + chair-position editor
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
│ ├── skills/ # Paperclip skills inventory
│ └── diagnostics/ # DB health + failed/stuck docs
├── components/
│ ├── app-shell.tsx # Header + nav with aria-current
│ ├── cases/ # Home + detail screens
│ ├── compose/ # Research analysis editor
│ ├── documents/ # UploadSheet
│ ├── training/ # Style report / corpus / compare panels
│ ├── wizard/ # Case create wizard + parties-field
│ └── ui/ # shadcn primitives
├── lib/
│ ├── api/ # Typed hooks per domain (cases, documents, research,
│ │ # system, skills, training)
│ ├── schemas/ # zod schemas (case create / update)
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
│ ├── sse.ts # EventSource wrapper
│ ├── providers.tsx # QueryClient + Toaster
│ └── utils.ts # cn()
```
## Smoke test (run after every deploy)
Use any browser at the staging URL. Every step should be doable **without console errors** and each mutation should produce a visible toast.
### 1. Home · `/`
- [ ] Header nav shows 5 items; the current page is underlined in gold
- [ ] 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
- [ ] Cases table lists existing cases; search filters by case number or title
- [ ] "פיזור סטטוסים" donut renders with a legend
- [ ] "+ תיק חדש" button in the top-left navigates to `/cases/new`
### 2. Create case · `/cases/new`
- [ ] 3-step wizard: פרטי יסוד → צדדים → השלמות
- [ ] Type `1500-25` → appeal_subtype auto-fills to "רישוי ובנייה"
- [ ] Type `8500-25` → subtype auto-fills to "היטל השבחה"
- [ ] Manually pick a different subtype → auto-fill stops
- [ ] Submitting with invalid case number shows a zod field error (no crash)
- [ ] Successful create → toast "תיק חדש נוצר" → router pushes to `/cases/{number}`
### 3. Case detail · `/cases/[caseNumber]`
- [ ] Header shows status badge + gold "ועדת ערר · X" practice-area badge
- [ ] Tabs switch cleanly: סקירה / מסמכים / פעולות
- [ ] Workflow timeline on the right shows the current phase highlighted in gold
- [ ] פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
- [ ] "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
### 4. Compose · `/cases/[caseNumber]/compose`
- [ ] If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
- [ ] Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
- [ ] If 404 (no analysis yet): empty state card renders, no error toast
### 5. Training · `/training`
- [ ] **Report tab:** headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
- [ ] **Corpus tab:** table of corpus decisions with a trash icon per row (aria-label present)
- [ ] Deleting a decision refreshes both the corpus table and the report KPIs
- [ ] **Compare tab:** two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
### 6. Skills · `/skills`
- [ ] Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
- [ ] Chars + file counts render; "לא ידוע" doesn't appear for installed skills
### 7. Diagnostics · `/diagnostics`
- [ ] DB status card shows "מחובר" in green
- [ ] Table counts populate for cases / documents / chunks / corpus / patterns
- [ ] Failed + stuck document lists render (empty states OK)
- [ ] Page self-refreshes every 10s — check the network tab for recurring calls
### 8. Error boundary
- [ ] Visit `/cases/NOT-REAL-999-99` → case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen)
- [ ] Visit `/anything-broken-xyz` → custom 404 page with "חזרה לבית" button
### 9. Keyboard + RTL
- [ ] Tab through the home page — focus rings are gold, visible
- [ ] Wizard progresses via Enter on the "הבא" button
- [ ] Screen reader announces nav items with "עמוד נוכחי" on the active one
## Deploy
```
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
```
> **Known issue:** the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (`mcp__coolify__deploy` with app UUID `l146g36mtlp0k03vrwkyrgkk`) or the Coolify UI until the webhook is fixed.
## Phase tracking
See `~/.claude/plans/joyful-marinating-sutton.md` for the 7-phase rewrite plan and `~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json` for the task board.
| Phase | Scope | Status |
|---|---|---|
| 1 | Scaffold + Coolify staging | ✅ |
| 2 | API client + typed hooks + probe | ✅ |
| 3 | Read views (home, case detail, compose) | ✅ |
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
| 4.5 | Practice-area integration | ✅ |
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
| 7 | DNS cutover to production | pending |
## Backend contract
The new UI consumes the existing FastAPI at `legal-ai/web/app.py`. Key endpoints currently relied on:
| Endpoint | Hook | Used by |
|---|---|---|
| `GET /api/cases?detail=true` | `useCases` | home table, KPIs |
| `GET /api/cases/{n}/details` | `useCase` | case detail |
| `POST /api/cases/create` | `useCreateCase` | wizard |
| `PUT /api/cases/{n}` | `useUpdateCase` | inline edit |
| `DELETE /api/cases?case_number=...` | (MCP only so far) | admin cleanup |
| `POST /api/cases/{n}/documents/upload-tagged` | `useUploadDocument` | upload sheet |
| `GET /api/progress/{task_id}` (SSE) | `useProgress` | upload progress |
| `GET /api/cases/{n}/research/analysis` | `useResearchAnalysis` | compose |
| `PATCH .../chair-position` | `useSaveChairPosition` | chair editor |
| `GET /api/training/style-report` | `useStyleReport` | training/report tab |
| `GET /api/training/corpus` | `useCorpus` | training/corpus tab |
| `GET /api/training/compare` | `useCompare` | training/compare tab |
| `DELETE /api/training/corpus/{id}` | `useDeleteCorpusEntry` | corpus tab |
| `GET /api/system/diagnostics` | `useDiagnostics` | diagnostics page |
| `GET /api/admin/skills` | `useSkills` | skills page |
Any new endpoint should get a typed hook in `src/lib/api/` — do not reach into `fetch` from component code.

25
web-ui/components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": true,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

18
web-ui/eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

33
web-ui/next.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { NextConfig } from "next";
/**
* Staging config — proxies /api/* and /openapi.json to the production FastAPI
* at legal-ai.nautilus.marcusgroup.org. This lets the new Next.js UI call the
* existing backend without CORS and without running a second FastAPI instance.
*
* When the rewrite branch is cut over to production, set NEXT_PUBLIC_API_BASE_URL
* and/or move the FastAPI in front of this app via traefik routing.
*/
const API_ORIGIN =
process.env.NEXT_PUBLIC_API_ORIGIN ??
"https://legal-ai.nautilus.marcusgroup.org";
const nextConfig: NextConfig = {
output: "standalone",
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${API_ORIGIN}/api/:path*`,
},
{
source: "/openapi.json",
destination: `${API_ORIGIN}/openapi.json`,
},
];
},
};
export default nextConfig;

13235
web-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
web-ui/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "web-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"api:types": "openapi-typescript https://legal-ai.nautilus.marcusgroup.org/openapi.json -o src/lib/api/types.ts"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "^5.97.0",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.8.0",
"next": "16.2.3",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-dropzone": "^15.0.0",
"react-hook-form": "^7.72.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.3",
"openapi-typescript": "^7.13.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
web-ui/public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
web-ui/public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
web-ui/public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
web-ui/public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
web-ui/public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,229 @@
"use client";
import { use } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { SubsectionCard } from "@/components/compose/subsection-card";
import { PrecedentsSection } from "@/components/compose/precedents-section";
import { Markdown } from "@/components/ui/markdown";
import { useCase } from "@/lib/api/cases";
import { useResearchAnalysis } from "@/lib/api/research";
import { useCasePrecedents } from "@/lib/api/precedents";
function ProseSection({ title, content }: { title: string; content?: string }) {
if (!content?.trim()) return null;
return (
<section className="space-y-2">
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
{title}
</h3>
<Markdown content={content.trim()} />
</section>
);
}
export default function ComposePage({
params,
}: {
params: Promise<{ caseNumber: string }>;
}) {
const { caseNumber } = use(params);
const caseQuery = useCase(caseNumber);
const analysis = useResearchAnalysis(caseNumber);
const precedentsQuery = useCasePrecedents(caseNumber);
/* Partition the flat list into scopes so each child renders its own slice
* without re-fetching. Done once at the page level. */
const allPrecedents = precedentsQuery.data ?? [];
const caseLevelPrecedents = allPrecedents.filter((p) => p.section_id === null);
const precedentsBySection = new Map<string, typeof allPrecedents>();
for (const p of allPrecedents) {
if (p.section_id) {
const existing = precedentsBySection.get(p.section_id) ?? [];
existing.push(p);
precedentsBySection.set(p.section_id, existing);
}
}
const practiceArea = caseQuery.data?.practice_area ?? null;
const isNotFound =
analysis.error instanceof Error &&
/404|לא נמצא|טרם בוצע/.test(analysis.error.message);
return (
<AppShell>
<section className="space-y-6">
{/* Header strip */}
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden>·</span>
<Link
href={`/cases/${caseNumber}`}
className="hover:text-gold-deep"
>
ערר {caseNumber}
</Link>
<span aria-hidden>·</span>
<span className="text-navy">עורך החלטה</span>
</nav>
<h1 className="text-navy mb-0">ניתוח משפטי וכתיבת עמדה</h1>
{caseQuery.data?.title && (
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
{caseQuery.data.title}
</p>
)}
</div>
<Button asChild variant="outline">
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
</Button>
</div>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{analysis.isPending ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-3">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-96" />
<Skeleton className="h-4 w-80" />
<Skeleton className="h-32 w-full" />
</CardContent>
</Card>
) : isNotFound ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-12 text-center space-y-3">
<div className="text-gold text-3xl" aria-hidden></div>
<h2 className="text-navy text-lg mb-0">
טרם בוצע ניתוח משפטי לתיק זה
</h2>
<p className="text-ink-muted text-sm max-w-md mx-auto">
לאחר שקובץ <code>analysis-and-research.md</code> ייווצר, תוכלי
לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה.
</p>
</CardContent>
</Card>
) : analysis.error ? (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-5 text-center">
<p className="text-danger">{analysis.error.message}</p>
</CardContent>
</Card>
) : analysis.data ? (
<div className="space-y-6">
{/* Case-level general precedents */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-xl mb-1">פסיקה כללית לדיון</h2>
<p className="text-[0.78rem] text-ink-muted mb-4">
ציטוטים התומכים בעמדה באופן רוחבי ישולבו בפתיחת בלוק י (דיון).
</p>
<PrecedentsSection
caseNumber={caseNumber}
sectionId={null}
precedents={caseLevelPrecedents}
practiceArea={practiceArea}
emptyHelperText="עדיין לא צורפה פסיקה כללית לתיק"
/>
</CardContent>
</Card>
{/* Threshold claims */}
{analysis.data.threshold_claims &&
analysis.data.threshold_claims.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<h2 className="text-navy text-xl mb-0">טענות סף</h2>
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
{analysis.data.threshold_claims.length}
</span>
</div>
<div className="space-y-3">
{analysis.data.threshold_claims.map((tc) => (
<SubsectionCard
key={tc.id}
caseNumber={caseNumber}
item={tc}
precedents={precedentsBySection.get(tc.id) ?? []}
practiceArea={practiceArea}
/>
))}
</div>
</div>
)}
{/* Issues */}
{analysis.data.issues && analysis.data.issues.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<h2 className="text-navy text-xl mb-0">סוגיות להכרעה</h2>
<span className="text-[0.72rem] rounded-full bg-gold-wash text-gold-deep px-2 py-0.5 border border-gold/40 tabular-nums">
{analysis.data.issues.length}
</span>
</div>
<div className="space-y-3">
{analysis.data.issues.map((iss) => (
<SubsectionCard
key={iss.id}
caseNumber={caseNumber}
item={iss}
precedents={precedentsBySection.get(iss.id) ?? []}
practiceArea={practiceArea}
/>
))}
</div>
</div>
)}
{(!analysis.data.threshold_claims?.length &&
!analysis.data.issues?.length) && (
<Card className="bg-surface border-rule">
<CardContent className="px-6 py-10 text-center text-ink-muted">
לא נמצאו טענות סף או סוגיות בניתוח זה.
</CardContent>
</Card>
)}
{/* Background prose — moved below the issues so it reads as
supporting context after the chair has seen the main
decision points, not as a wall of text beside them. */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-5">
<h2 className="text-navy text-xl mb-0">רקע לניתוח</h2>
<ProseSection
title="צד מיוצג"
content={analysis.data.represented_party}
/>
<ProseSection
title="רקע דיוני"
content={analysis.data.procedural_background}
/>
<ProseSection
title="עובדות מוסכמות"
content={analysis.data.agreed_facts}
/>
<ProseSection
title="עובדות במחלוקת"
content={analysis.data.disputed_facts}
/>
</CardContent>
</Card>
{analysis.data.conclusions?.trim() && (
<Card className="bg-gold-wash border-gold/40 shadow-sm">
<CardContent className="px-6 py-5 space-y-3">
<h2 className="text-gold-deep text-xl mb-0">מסקנות</h2>
<Markdown content={analysis.data.conclusions.trim()} />
</CardContent>
</Card>
)}
</div>
) : null}
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,136 @@
"use client";
import { use } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { CaseHeader } from "@/components/cases/case-header";
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { UploadSheet } from "@/components/documents/upload-sheet";
import { expectedOutcomes } from "@/lib/schemas/case";
import { useCase } from "@/lib/api/cases";
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
expectedOutcomes.map((o) => [o.value, o.label]),
);
/*
* Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component.
*/
export default function CaseDetailPage({
params,
}: {
params: Promise<{ caseNumber: string }>;
}) {
const { caseNumber } = use(params);
const { data, isPending, error } = useCase(caseNumber);
const expectedOutcomeLabel = data?.expected_outcome
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
: null;
return (
<AppShell>
<section className="space-y-6">
{error ? (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center space-y-3">
<p className="text-danger font-semibold">שגיאה בטעינת התיק</p>
<p className="text-sm text-ink-muted">{error.message}</p>
<Button asChild variant="outline">
<Link href="/">חזרה לרשימת התיקים</Link>
</Button>
</CardContent>
</Card>
) : (
<>
{isPending ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-6 w-96" />
</CardContent>
</Card>
) : (
<CaseHeader data={data} />
)}
<div className="grid gap-6 lg:grid-cols-[1fr_280px]">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<Tabs defaultValue="overview" dir="rtl">
<div className="flex items-center justify-between gap-3 mb-1">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="overview">סקירה</TabsTrigger>
<TabsTrigger value="documents">
מסמכים
{data?.documents && (
<span className="ms-1.5 text-[0.7rem] text-ink-muted tabular-nums">
({data.documents.length})
</span>
)}
</TabsTrigger>
<TabsTrigger value="actions">פעולות</TabsTrigger>
</TabsList>
<UploadSheet caseNumber={caseNumber} />
</div>
<TabsContent value="overview" className="mt-5 space-y-4">
<div>
<h3 className="text-navy text-base mb-2">תוצאה צפויה</h3>
<p className="text-ink-soft text-sm leading-relaxed">
{expectedOutcomeLabel ?? "לא נקבעה תוצאה צפויה."}
</p>
</div>
<div>
<h3 className="text-navy text-base mb-2">סיכום מהיר</h3>
<dl className="grid grid-cols-2 gap-y-2 gap-x-6 text-sm">
<dt className="text-ink-muted">מסמכים בתיק</dt>
<dd className="text-ink tabular-nums">
{data?.documents?.length ?? 0}
</dd>
<dt className="text-ink-muted">בעיבוד</dt>
<dd className="text-ink tabular-nums">
{data?.processing_count ?? 0}
</dd>
</dl>
</div>
</TabsContent>
<TabsContent value="documents" className="mt-5">
<DocumentsPanel data={data} />
</TabsContent>
<TabsContent value="actions" className="mt-5">
<div className="flex items-center gap-3 flex-wrap">
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href={`/cases/${caseNumber}/compose`}>
פתח בעורך ההחלטה
</Link>
</Button>
{data && <CaseEditDialog data={data} />}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm h-fit">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
<WorkflowTimeline status={data?.status} />
</CardContent>
</Card>
</div>
</>
)}
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,26 @@
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { CaseWizard } from "@/components/wizard/case-wizard";
export default function NewCasePage() {
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted flex items-center gap-2 mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden>·</span>
<span className="text-navy">תיק חדש</span>
</nav>
<h1 className="text-navy mb-0">יצירת תיק ערר</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
שלושה שלבים קצרים פרטי יסוד, צדדים, והשלמות. התיק ייווצר ב-DB
וב-Gitea מיד בשמירה.
</p>
</header>
<CaseWizard />
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,219 @@
"use client";
import Link from "next/link";
import { AlertTriangle, CheckCircle2, Clock, Database } from "lucide-react";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useDiagnostics, type DiagDoc } from "@/lib/api/system";
const TABLE_LABELS: Record<string, string> = {
cases: "תיקים",
documents: "מסמכים",
document_chunks: "chunks",
style_corpus: "קורפוס סגנון",
style_patterns: "דפוסי סגנון",
};
function formatRelativeTime(iso: string | null) {
if (!iso) return "—";
const then = new Date(iso);
const diffMs = Date.now() - then.getTime();
const min = Math.floor(diffMs / 60000);
if (min < 1) return "עכשיו";
if (min < 60) return `לפני ${min} דקות`;
const hr = Math.floor(min / 60);
if (hr < 24) return `לפני ${hr} שעות`;
const days = Math.floor(hr / 24);
if (days < 30) return `לפני ${days} ימים`;
return then.toLocaleDateString("he-IL");
}
function DocRow({ doc, tone }: { doc: DiagDoc; tone: "danger" | "warn" }) {
const cls =
tone === "danger" ? "bg-danger-bg/60 border-danger/30" : "bg-warn-bg/60 border-warn/30";
return (
<li
className={`rounded border px-3 py-2 flex items-center gap-3 text-sm ${cls}`}
>
<div className="flex-1 min-w-0">
<div className="text-ink font-medium truncate" title={doc.title}>
{doc.title || "(ללא כותרת)"}
</div>
<div className="text-[0.72rem] text-ink-muted flex gap-3 mt-0.5">
{doc.case_number && (
<Link href={`/cases/${doc.case_number}`} className="hover:text-gold-deep">
ערר {doc.case_number}
</Link>
)}
<span>{formatRelativeTime(doc.created_at)}</span>
</div>
</div>
<Badge variant="outline" className="text-[0.7rem]">{doc.status}</Badge>
</li>
);
}
export default function DiagnosticsPage() {
const { data, isPending, error } = useDiagnostics();
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">אבחון מערכת</span>
</nav>
<h1 className="text-navy mb-0">אבחון מערכת</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
מצב ה-DB, מסמכים שנכשלו או תקועים, ומשימות רקע פעילות. מתעדכן כל 10
שניות.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{error ? (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center text-danger">
{error.message}
</CardContent>
</Card>
) : (
<>
{/* DB status + table counts */}
<div className="grid gap-4 md:grid-cols-[240px_1fr]">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 flex flex-col items-start gap-2">
<div className="flex items-center gap-2 text-ink-muted text-[0.72rem] uppercase tracking-wider">
<Database className="w-3.5 h-3.5" />
מצב DB
</div>
{isPending ? (
<Skeleton className="h-6 w-24" />
) : data?.db_ok ? (
<div className="flex items-center gap-2 text-success font-semibold">
<CheckCircle2 className="w-5 h-5" />
<span>מחובר</span>
</div>
) : (
<div className="flex items-center gap-2 text-danger font-semibold">
<AlertTriangle className="w-5 h-5" />
<span>מנותק</span>
</div>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4">
<div className="text-ink-muted text-[0.72rem] uppercase tracking-wider mb-3">
ספירת טבלאות
</div>
<dl className="grid grid-cols-2 md:grid-cols-5 gap-y-2 gap-x-4">
{Object.keys(TABLE_LABELS).map((key) => (
<div key={key} className="space-y-0.5">
<dt className="text-[0.72rem] text-ink-muted">
{TABLE_LABELS[key]}
</dt>
<dd className="font-display font-bold text-navy text-xl tabular-nums">
{isPending ? "—" : (data?.tables[key] ?? "—")}
</dd>
</div>
))}
</dl>
</CardContent>
</Card>
</div>
{/* Active tasks */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-lg mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
משימות רקע פעילות
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.active_tasks.length ?? 0}
</Badge>
</h2>
{isPending ? (
<Skeleton className="h-16 w-full" />
) : data?.active_tasks.length === 0 ? (
<p className="text-ink-muted text-sm">אין משימות פעילות</p>
) : (
<ul className="space-y-2">
{data?.active_tasks.map((t) => (
<li
key={t.task_id}
className="flex items-center gap-3 text-sm rounded bg-rule-soft/60 border border-rule px-3 py-2"
>
<span className="flex-1 text-ink truncate">
{t.filename || t.task_id}
</span>
<Badge variant="outline" className="text-[0.7rem]">
{t.step || t.status}
</Badge>
</li>
))}
</ul>
)}
</CardContent>
</Card>
{/* Failed + stuck docs */}
<div className="grid gap-4 md:grid-cols-2">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-danger text-lg mb-3 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
מסמכים שנכשלו
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.failed_documents.length ?? 0}
</Badge>
</h2>
{isPending ? (
<Skeleton className="h-20 w-full" />
) : data?.failed_documents.length === 0 ? (
<p className="text-ink-muted text-sm">אין כשלונות</p>
) : (
<ul className="space-y-2">
{data?.failed_documents.map((d) => (
<DocRow key={d.id} doc={d} tone="danger" />
))}
</ul>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-warn text-lg mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
תקועים (&gt; 10 דק׳)
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{data?.stuck_documents.length ?? 0}
</Badge>
</h2>
{isPending ? (
<Skeleton className="h-20 w-full" />
) : data?.stuck_documents.length === 0 ? (
<p className="text-ink-muted text-sm">אין מסמכים תקועים</p>
) : (
<ul className="space-y-2">
{data?.stuck_documents.map((d) => (
<DocRow key={d.id} doc={d} tone="warn" />
))}
</ul>
)}
</CardContent>
</Card>
</div>
</>
)}
</section>
</AppShell>
);
}

57
web-ui/src/app/error.tsx Normal file
View File

@@ -0,0 +1,57 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
import { AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
/*
* Route-segment error boundary. Next 16 App Router convention: this file
* catches render-time errors thrown below the root layout. `reset` clears
* the error and re-renders the segment; we keep the user's nav in place
* (no AppShell here — the shell re-renders from the layout above us).
*/
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("Route error:", error);
}, [error]);
return (
<main className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10">
<div className="max-w-xl mx-auto text-center space-y-5 py-16">
<AlertTriangle className="w-12 h-12 text-danger mx-auto" aria-hidden="true" />
<div className="space-y-2">
<h1 className="text-navy">משהו השתבש</h1>
<p className="text-ink-muted leading-relaxed">
נכשלה טעינת המסך. זה עשוי להיות כשל זמני ברשת או באחד מה-endpoints
של FastAPI.
</p>
{error.digest && (
<p className="text-[0.72rem] text-ink-light tabular-nums">
error id: {error.digest}
</p>
)}
{error.message && (
<code className="text-[0.78rem] text-ink-soft bg-rule-soft/60 rounded px-3 py-1 inline-block mt-2">
{error.message}
</code>
)}
</div>
<div className="flex items-center justify-center gap-3">
<Button onClick={reset} className="bg-navy hover:bg-navy-soft text-parchment">
נסה שוב
</Button>
<Button variant="outline" asChild>
<Link href="/">חזרה לבית</Link>
</Button>
</div>
</div>
</main>
);
}

BIN
web-ui/src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,329 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
useFeedbackList,
useCreateFeedback,
useResolveFeedback,
CATEGORY_LABELS,
BLOCK_LABELS,
type FeedbackCategory,
} from "@/lib/api/feedback";
import { toast } from "sonner";
const CATEGORY_COLORS: Record<FeedbackCategory, string> = {
missing_content: "bg-amber-100 text-amber-800 border-amber-200",
wrong_tone: "bg-purple-100 text-purple-800 border-purple-200",
wrong_structure: "bg-blue-100 text-blue-800 border-blue-200",
factual_error: "bg-red-100 text-red-800 border-red-200",
style: "bg-emerald-100 text-emerald-800 border-emerald-200",
other: "bg-gray-100 text-gray-800 border-gray-200",
};
export default function FeedbackPage() {
const [showResolved, setShowResolved] = useState(false);
const [filterCategory, setFilterCategory] = useState<string>("");
const { data: feedbacks, isLoading } = useFeedbackList({
category: filterCategory || undefined,
unresolved_only: !showResolved,
});
const resolveMutation = useResolveFeedback();
function handleResolve(id: string) {
resolveMutation.mutate(
{ feedbackId: id, applied_to: [] },
{
onSuccess: () => toast.success("ההערה סומנה כמטופלת"),
onError: () => toast.error("שגיאה בעדכון"),
},
);
}
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">
בית
</Link>
<span aria-hidden> &middot; </span>
<span className="text-navy">הערות יו״ר</span>
</nav>
<h1 className="text-navy mb-0">הערות יו״ר על טיוטות</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
תיעוד הערות דפנה על טיוטות החלטות. כל הערה מנותחת ומשפיעה על שיפור
כתיבת ההחלטות העתידיות.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Toolbar */}
<div className="flex items-center gap-3 flex-wrap">
<NewFeedbackDialog />
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="rounded-md border border-rule bg-surface px-3 py-1.5 text-sm"
>
<option value="">כל הקטגוריות</option>
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
<label className="flex items-center gap-2 text-sm text-ink-muted cursor-pointer">
<input
type="checkbox"
checked={showResolved}
onChange={(e) => setShowResolved(e.target.checked)}
className="rounded border-rule"
/>
הצג גם מטופלות
</label>
{feedbacks && (
<span className="text-sm text-ink-muted me-auto">
{feedbacks.length} הערות
</span>
)}
</div>
{/* Feedback list */}
{isLoading ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-8 text-center text-ink-muted">
טוען...
</CardContent>
</Card>
) : !feedbacks?.length ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-8 text-center text-ink-muted">
אין הערות{!showResolved ? " פתוחות" : ""}
{filterCategory ? ` בקטגוריה ${CATEGORY_LABELS[filterCategory as FeedbackCategory]}` : ""}
</CardContent>
</Card>
) : (
<div className="space-y-3">
{feedbacks.map((fb) => (
<Card
key={fb.id}
className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}
>
<CardHeader className="border-b pb-3">
<div className="flex items-center gap-2 flex-wrap">
<Badge
className={`text-[0.7rem] border ${CATEGORY_COLORS[fb.category]}`}
>
{CATEGORY_LABELS[fb.category]}
</Badge>
<Badge variant="outline" className="text-[0.7rem]">
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
</Badge>
{fb.case_number && (
<Link
href={`/cases/${fb.case_number}`}
className="text-[0.7rem] text-gold-deep hover:underline"
>
תיק {fb.case_number}
</Link>
)}
{fb.resolved && (
<Badge className="bg-emerald-100 text-emerald-700 text-[0.7rem] border border-emerald-200">
טופל
</Badge>
)}
<span className="text-[0.7rem] text-ink-muted me-auto">
{fb.created_at
? new Date(fb.created_at).toLocaleDateString("he-IL")
: ""}
</span>
</div>
</CardHeader>
<CardContent className="px-6 py-4 space-y-3">
<p className="text-sm leading-relaxed">{fb.feedback_text}</p>
{fb.lesson_extracted && (
<div className="bg-gold/5 border border-gold/20 rounded-md px-4 py-3">
<p className="text-[0.7rem] font-semibold text-gold-deep mb-1">
לקח שהופק:
</p>
<p className="text-sm text-ink-muted leading-relaxed">
{fb.lesson_extracted}
</p>
</div>
)}
{!fb.resolved && (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleResolve(fb.id)}
disabled={resolveMutation.isPending}
>
סמן כמטופל
</Button>
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</section>
</AppShell>
);
}
/* ── New feedback dialog ─────────────────────────────────── */
function NewFeedbackDialog() {
const [open, setOpen] = useState(false);
const createMutation = useCreateFeedback();
const [caseNumber, setCaseNumber] = useState("");
const [blockId, setBlockId] = useState("block-yod");
const [category, setCategory] = useState<FeedbackCategory>("missing_content");
const [feedbackText, setFeedbackText] = useState("");
const [lesson, setLesson] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!feedbackText.trim()) return;
createMutation.mutate(
{
case_number: caseNumber || undefined,
block_id: blockId,
feedback_text: feedbackText,
category,
lesson_extracted: lesson || undefined,
},
{
onSuccess: () => {
toast.success("ההערה נרשמה בהצלחה");
setOpen(false);
setCaseNumber("");
setFeedbackText("");
setLesson("");
},
onError: () => toast.error("שגיאה ברישום ההערה"),
},
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>+ הערה חדשה</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg" dir="rtl">
<DialogHeader>
<DialogTitle>רישום הערת יו״ר</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="fb-case">מספר תיק (אופציונלי)</Label>
<Input
id="fb-case"
value={caseNumber}
onChange={(e) => setCaseNumber(e.target.value)}
placeholder="1130-25"
/>
</div>
<div>
<Label htmlFor="fb-block">בלוק</Label>
<select
id="fb-block"
value={blockId}
onChange={(e) => setBlockId(e.target.value)}
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
>
{Object.entries(BLOCK_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
</div>
<div>
<Label htmlFor="fb-category">קטגוריה</Label>
<select
id="fb-category"
value={category}
onChange={(e) => setCategory(e.target.value as FeedbackCategory)}
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
>
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="fb-text">ההערה</Label>
<Textarea
id="fb-text"
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
placeholder="מה דפנה אמרה? מה חסר, מה לא נכון, מה צריך לשנות..."
rows={4}
required
/>
</div>
<div>
<Label htmlFor="fb-lesson">לקח שהופק (אופציונלי)</Label>
<Textarea
id="fb-lesson"
value={lesson}
onChange={(e) => setLesson(e.target.value)}
placeholder="מה למדנו מההערה? מה צריך לשנות במערכת?"
rows={2}
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
ביטול
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "שומר..." : "שמור הערה"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,60 @@
"use client";
/*
* Root-level error boundary. Renders only when the root layout itself
* crashes, so it must include its own <html> / <body>. No AppShell here —
* the Providers that wrap AppShell are also above the crash boundary and
* may themselves be the thing that failed.
*/
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html lang="he" dir="rtl">
<body
style={{
fontFamily: "system-ui, sans-serif",
background: "#f5f1e8",
color: "#1a1a2e",
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "2rem",
}}
>
<div style={{ maxWidth: 520, textAlign: "center" }}>
<h1 style={{ color: "#0f172a", fontSize: "2rem", marginBottom: 8 }}>
שגיאה חמורה
</h1>
<p style={{ color: "#6b7280", lineHeight: 1.6, marginBottom: 16 }}>
האפליקציה נכשלה לטעון. נסה לרענן את הדף.
</p>
{error.digest && (
<p style={{ fontSize: 12, color: "#9ca3af", marginBottom: 16 }}>
error id: {error.digest}
</p>
)}
<button
onClick={reset}
style={{
background: "#0f172a",
color: "#fbf8f0",
border: 0,
padding: "0.6rem 1.25rem",
borderRadius: 6,
cursor: "pointer",
fontSize: "0.95rem",
}}
>
נסה שוב
</button>
</div>
</body>
</html>
);
}

248
web-ui/src/app/globals.css Normal file
View File

@@ -0,0 +1,248 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
/* ══════════════════════════════════════════════════════════════
* Ezer Mishpati — Design Tokens
* Ported from legal-ai/web/static/design-system.css.
* Editorial/judicial aesthetic for a Hebrew RTL judicial tool.
* Palette: Navy + Cream + Gold. Typography: Heebo.
*
* shadcn semantic tokens (background, foreground, primary, etc.)
* are mapped onto this palette in the @theme inline block + :root
* further down, so shadcn primitives inherit the editorial voice.
* ══════════════════════════════════════════════════════════════ */
@theme {
/* ── Colors ─────────────────────────────────────────── */
--color-navy: #0f172a;
--color-navy-soft: #1e293b;
--color-navy-dim: #334155;
--color-cream: #f5f1e8;
--color-cream-deep: #ede8d8;
--color-parchment: #fbf8f0;
--color-gold: #a97d3a;
--color-gold-deep: #8b6428;
--color-gold-soft: #c89a56;
--color-gold-wash: #fdf6e8;
--color-ink: #1a1a2e;
--color-ink-soft: #3a3a52;
--color-ink-muted: #6b7280;
--color-ink-light: #9ca3af;
--color-rule: #e5dfd0;
--color-rule-soft: #f0ead8;
--color-surface: #ffffff;
--color-surface-raised: #fbf8f0;
--color-bg: #f5f1e8;
/* Status colors — tuned to the palette */
--color-success: #4a7c59;
--color-success-bg: #e8efe7;
--color-warn: #b8894a;
--color-warn-bg: #faf0dc;
--color-danger: #a54242;
--color-danger-bg: #f5e6e6;
--color-info: #4e6a8c;
--color-info-bg: #e6ecf3;
/* ── Typography ─────────────────────────────────────── */
--font-sans: var(--font-heebo), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-display: var(--font-heebo), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-body: var(--font-heebo), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace;
/* ── Shadows — soft, editorial ──────────────────────── */
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow: 0 2px 6px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.05);
/* ── Transitions ────────────────────────────────────── */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}
/* shadcn/ui semantic tokens — wired to CSS vars defined in :root / .dark */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-heading: var(--font-display);
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
/*
* shadcn token values — mapped onto the editorial palette.
* background/foreground/primary all reference the navy-cream-gold system
* so shadcn primitives (Button, Card, Badge…) fit the judicial tone
* without per-component overrides.
*/
:root {
--radius: 0.5rem;
--background: var(--color-bg); /* cream */
--foreground: var(--color-ink); /* near-black */
--card: var(--color-surface); /* white */
--card-foreground: var(--color-ink);
--popover: var(--color-surface);
--popover-foreground: var(--color-ink);
--primary: var(--color-navy); /* navy buttons */
--primary-foreground: var(--color-parchment);
--secondary: var(--color-cream-deep);
--secondary-foreground: var(--color-navy);
--muted: var(--color-rule-soft);
--muted-foreground: var(--color-ink-muted);
--accent: var(--color-gold-wash); /* subtle gold backgrounds */
--accent-foreground: var(--color-gold-deep);
--destructive: var(--color-danger);
--border: var(--color-rule);
--input: var(--color-rule);
--ring: var(--color-gold); /* gold focus ring */
--chart-1: var(--color-navy);
--chart-2: var(--color-gold);
--chart-3: var(--color-info);
--chart-4: var(--color-success);
--chart-5: var(--color-warn);
--sidebar: var(--color-parchment);
--sidebar-foreground: var(--color-ink);
--sidebar-primary: var(--color-navy);
--sidebar-primary-foreground: var(--color-parchment);
--sidebar-accent: var(--color-gold-wash);
--sidebar-accent-foreground: var(--color-gold-deep);
--sidebar-border: var(--color-rule);
--sidebar-ring: var(--color-gold);
}
/* Dark theme — class-based, toggled on <html> or <body>.
Inverts navy ↔ parchment and keeps gold as accent. */
.dark {
--color-navy: #f5f1e8;
--color-navy-soft: #e8e0c8;
--color-navy-dim: #c7bc9a;
--color-cream: #0a0f1c;
--color-cream-deep: #121a2e;
--color-parchment: #161f36;
--color-gold: #d4a55a;
--color-gold-deep: #e8bc6f;
--color-gold-soft: #c89a56;
--color-gold-wash: rgba(212, 165, 90, 0.08);
--color-ink: #f5f1e8;
--color-ink-soft: #d8d2c0;
--color-ink-muted: #9a9380;
--color-ink-light: #6a6458;
--color-rule: #2a3352;
--color-rule-soft: #1e2a45;
--color-surface: #141b2f;
--color-surface-raised: #1a2238;
--color-bg: #0a0f1c;
--color-success: #5a9a6a;
--color-success-bg: rgba(90, 154, 106, 0.12);
--color-warn: #c79956;
--color-warn-bg: rgba(199, 153, 86, 0.12);
--color-danger: #c16565;
--color-danger-bg: rgba(193, 101, 101, 0.12);
--color-info: #6d8bab;
--color-info-bg: rgba(109, 139, 171, 0.12);
}
/* ══════════════════════════════════════════════════════════════
* Base layer — editorial typography + Hebrew prose defaults
* ══════════════════════════════════════════════════════════════ */
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
font-size: 16px;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-body);
font-weight: 400;
font-size: 0.95rem;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "kern", "liga", "clig", "calt";
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 700;
line-height: 1.25;
color: var(--color-navy);
letter-spacing: -0.01em;
}
h1 { font-size: 2.3rem; font-weight: 900; }
h2 { font-size: 1.8rem; }
h3 { font-size: 1.45rem; }
h4 { font-size: 1.2rem; }
h5 { font-size: 1.05rem; }
h6 { font-size: 0.95rem; }
/* Prose paragraphs — justified for Hebrew legal text */
p.prose,
.prose p {
text-align: justify;
text-justify: inter-word;
hyphens: auto;
line-height: 1.65;
}
/* Selection */
::selection {
background: var(--color-gold-wash);
color: var(--color-navy);
}
}

36
web-ui/src/app/layout.tsx Normal file
View File

@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Heebo, Geist } from "next/font/google";
import { Providers } from "@/lib/providers";
import "./globals.css";
import { cn } from "@/lib/utils";
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
const heebo = Heebo({
variable: "--font-heebo",
subsets: ["hebrew", "latin"],
weight: ["300", "400", "500", "600", "700", "800", "900"],
display: "swap",
});
export const metadata: Metadata = {
title: {
default: "עוזר משפטי — ניהול תיקים",
template: "%s · עוזר משפטי",
},
description: "מערכת סיוע בניסוח החלטות לוועדת ערר לתכנון ובנייה, ירושלים",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="he" dir="rtl" className={cn("h-full", "antialiased", heebo.variable, "font-sans", geist.variable)}>
<body className="min-h-full flex flex-col">
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,24 @@
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Button } from "@/components/ui/button";
export default function NotFound() {
return (
<AppShell>
<section className="max-w-xl mx-auto text-center py-16 space-y-5">
<div className="font-display text-gold text-6xl leading-none" aria-hidden="true">
404
</div>
<div className="space-y-2">
<h1 className="text-navy">הדף לא נמצא</h1>
<p className="text-ink-muted leading-relaxed">
הכתובת שביקשת אינה קיימת או שהוזזה לדף אחר.
</p>
</div>
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href="/">חזרה לבית</Link>
</Button>
</section>
</AppShell>
);
}

61
web-ui/src/app/page.tsx Normal file
View File

@@ -0,0 +1,61 @@
"use client";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { KPICards } from "@/components/cases/kpi-cards";
import { StatusDonut } from "@/components/cases/status-donut";
import { CasesTable } from "@/components/cases/cases-table";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useCases } from "@/lib/api/cases";
export default function HomePage() {
const { data, isPending, error } = useCases(true);
return (
<AppShell>
<section className="space-y-8">
<header className="flex items-end justify-between gap-6 flex-wrap">
<div className="space-y-1.5">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
ועדת ערר לתכנון ובנייה · ירושלים
</div>
<h1 className="text-navy">עוזר משפטי</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
לוח בקרה לניהול תיקי ערר, ניתוח סגנון, וכתיבת החלטות לפי ארכיטקטורת
12 הבלוקים.
</p>
</div>
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href="/cases/new">+ תיק חדש</Link>
</Button>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<KPICards cases={data} loading={isPending} />
<div className="grid gap-6 lg:grid-cols-[1fr_auto]">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-navy text-xl mb-0">רשימת תיקים</h2>
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
מעודכן חי
</span>
</div>
<CasesTable cases={data} loading={isPending} error={error} />
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm lg:w-[320px]">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-lg mb-4">פיזור סטטוסים</h2>
<StatusDonut cases={data} />
</CardContent>
</Card>
</div>
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,128 @@
"use client";
import Link from "next/link";
import { Plug, HardDrive, Database, FileText } from "lucide-react";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useSkills, type Skill } from "@/lib/api/skills";
function formatSize(bytes: number | null) {
if (bytes == null) return "—";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function statusBadge(s: Skill) {
if (s.not_in_db) {
return <Badge variant="outline" className="bg-warn-bg text-warn border-warn/40">לא סונכרן</Badge>;
}
if (s.db_markdown_chars > 0 && s.disk_exists) {
return <Badge variant="outline" className="bg-success-bg text-success border-success/40">מסונכרן</Badge>;
}
if (s.db_markdown_chars > 0) {
return <Badge variant="outline" className="bg-info-bg text-info border-info/40">DB בלבד</Badge>;
}
return <Badge variant="outline">לא ידוע</Badge>;
}
function SkillCard({ skill }: { skill: Skill }) {
const fileCount = skill.file_inventory?.length ?? 0;
return (
<Card className="bg-surface border-rule shadow-sm hover:shadow-md transition-shadow">
<CardContent className="px-5 py-4">
<div className="flex items-start justify-between gap-3 mb-2">
<div className="flex items-center gap-2 min-w-0">
<Plug className="w-4 h-4 text-gold-deep shrink-0" />
<div className="min-w-0">
<h3 className="text-navy font-semibold text-base mb-0 truncate">
{skill.name || skill.slug}
</h3>
<code className="text-[0.72rem] text-ink-muted tabular-nums">
{skill.slug}
</code>
</div>
</div>
{statusBadge(skill)}
</div>
<dl className="grid grid-cols-3 gap-2 text-[0.72rem] text-ink-muted mt-3">
<div className="flex items-center gap-1">
<FileText className="w-3 h-3" />
<span className="tabular-nums">{fileCount}</span>
<span>קבצים</span>
</div>
<div className="flex items-center gap-1">
<Database className="w-3 h-3" />
<span className="tabular-nums">
{(skill.db_markdown_chars / 1000).toFixed(1)}K
</span>
<span>תווים</span>
</div>
<div className="flex items-center gap-1">
<HardDrive className="w-3 h-3" />
<span className="tabular-nums">
{formatSize(skill.disk_skill_md_bytes)}
</span>
</div>
</dl>
{skill.updated_at && (
<p className="text-[0.7rem] text-ink-light mt-2">
עודכן: {new Date(skill.updated_at).toLocaleDateString("he-IL")}
</p>
)}
</CardContent>
</Card>
);
}
export default function SkillsPage() {
const { data, isPending, error } = useSkills();
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">מיומנויות</span>
</nav>
<h1 className="text-navy mb-0">מיומנויות Paperclip</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
רשימת ה-skills המותקנים במערכת Paperclip ומצב הסנכרון שלהם בין ה-DB
לדיסק.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{error ? (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-6 text-center text-danger">
{error.message}
</CardContent>
</Card>
) : isPending ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-32 w-full rounded-lg" />
))}
</div>
) : data?.length === 0 ? (
<Card className="bg-surface border-rule">
<CardContent className="px-6 py-12 text-center text-ink-muted">
<div className="text-gold text-3xl mb-2" aria-hidden></div>
אין skills מותקנים
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{data?.map((s) => <SkillCard key={s.slug} skill={s} />)}
</div>
)}
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { StyleReportPanel } from "@/components/training/style-report-panel";
import { CorpusPanel } from "@/components/training/corpus-panel";
import { ComparePanel } from "@/components/training/compare-panel";
export default function TrainingPage() {
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">אימון סגנון</span>
</nav>
<h1 className="text-navy mb-0">הפורטרט הסגנוני של דפנה</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
לוח בקרה של קורפוס האימון סטטיסטיקות, אנטומיית החלטה ממוצעת,
ביטויי חתימה, וכלי השוואה בין שתי החלטות.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<Tabs defaultValue="report" dir="rtl">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="report">פורטרט סגנון</TabsTrigger>
<TabsTrigger value="corpus">קורפוס</TabsTrigger>
<TabsTrigger value="compare">השוואה</TabsTrigger>
</TabsList>
<TabsContent value="report" className="mt-5">
<StyleReportPanel />
</TabsContent>
<TabsContent value="corpus" className="mt-5">
<CorpusPanel />
</TabsContent>
<TabsContent value="compare" className="mt-5">
<ComparePanel />
</TabsContent>
</Tabs>
</CardContent>
</Card>
</section>
</AppShell>
);
}

View File

@@ -0,0 +1,101 @@
"use client";
import type { ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
/**
* Ezer Mishpati navigation shell.
*
* Editorial/judicial aesthetic:
* - Navy header with a gold hairline rule (border-b-3)
* - Parchment/cream body background (set on <body> via globals.css)
* - Hebrew RTL throughout (set on <html> in layout.tsx)
*
* Nav items pick up an `aria-current="page"` and a gold underline when
* the current route matches, so screen readers announce the active
* section and sighted users can see where they are.
*/
type NavItem = {
href: string;
label: string;
};
const NAV_ITEMS: NavItem[] = [
{ href: "/", label: "בית" },
{ href: "/cases/new", label: "תיק חדש" },
{ href: "/training", label: "אימון סגנון" },
{ href: "/feedback", label: "הערות יו״ר" },
{ href: "/skills", label: "מיומנויות" },
{ href: "/diagnostics", label: "אבחון" },
];
function isActive(pathname: string, href: string): boolean {
if (href === "/") return pathname === "/";
return pathname === href || pathname.startsWith(`${href}/`);
}
export function AppShell({ children }: { children: ReactNode }) {
const pathname = usePathname();
return (
<>
<header
className="
relative z-10 flex items-center gap-4
px-10 py-[18px]
bg-navy text-parchment
border-b-[3px] border-gold
shadow-md
"
>
<Link href="/" className="flex items-baseline gap-3 hover:text-parchment">
<span className="font-display text-[1.45rem] font-bold tracking-[0.02em] text-parchment">
עוזר משפטי
</span>
<span className="text-gold-soft text-sm font-medium">ניהול תיקים</span>
</Link>
<nav
className="me-auto flex items-center gap-1"
aria-label="ניווט ראשי"
>
{NAV_ITEMS.map((item) => {
const active = isActive(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
aria-current={active ? "page" : undefined}
className={`
relative px-3 py-1.5 rounded text-sm transition-colors
${
active
? "text-parchment font-semibold bg-navy-soft/80"
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"
}
`}
>
{item.label}
{active && (
<span
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
aria-hidden="true"
/>
)}
</Link>
);
})}
</nav>
</header>
<main
id="main"
className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10"
>
{children}
</main>
</>
);
}

View File

@@ -0,0 +1,160 @@
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import {
Dialog, DialogContent, DialogDescription, DialogFooter,
DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { useUpdateCase } from "@/lib/api/cases";
import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case";
import type { CaseDetail } from "@/lib/api/cases";
/*
* Inline edit dialog for core case fields. Uses react-hook-form + zod
* directly (shadcn's <Form> registry entry wasn't available at init
* time, so the styling is reproduced by hand in a lean form layout).
*/
function FieldError({ message }: { message?: string }) {
if (!message) return null;
return <p className="text-[0.72rem] text-danger mt-1">{message}</p>;
}
export function CaseEditDialog({ data }: { data: CaseDetail }) {
const [open, setOpen] = useState(false);
const mutate = useUpdateCase(data.case_number);
const form = useForm<CaseUpdateInput>({
resolver: zodResolver(caseUpdateSchema),
defaultValues: {
title: data.title ?? "",
subject: data.subject ?? "",
hearing_date: data.hearing_date ?? "",
notes: "",
expected_outcome: data.expected_outcome ?? "",
},
});
/* Re-sync the form when the underlying case refetches after save */
useEffect(() => {
if (!open) return;
form.reset({
title: data.title ?? "",
subject: data.subject ?? "",
hearing_date: data.hearing_date ?? "",
notes: "",
expected_outcome: data.expected_outcome ?? "",
});
}, [open, data, form]);
const onSubmit = form.handleSubmit(async (values) => {
try {
await mutate.mutateAsync(values);
toast.success("פרטי התיק עודכנו");
setOpen(false);
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון התיק");
}
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
עריכת פרטי תיק
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg" dir="rtl">
<DialogHeader>
<DialogTitle>עריכת פרטי תיק {data.case_number}</DialogTitle>
<DialogDescription className="text-ink-muted">
השינויים נשמרים ישירות ל-FastAPI. השדות הריקים נשארים ללא שינוי.
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-4">
<div>
<Label htmlFor="title" className="text-navy">כותרת</Label>
<Input id="title" {...form.register("title")} className="mt-1" />
<FieldError message={form.formState.errors.title?.message} />
</div>
<div>
<Label htmlFor="subject" className="text-navy">נושא</Label>
<Input id="subject" {...form.register("subject")} className="mt-1" />
<FieldError message={form.formState.errors.subject?.message} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="hearing_date" className="text-navy">תאריך דיון</Label>
<Input
id="hearing_date"
type="date"
{...form.register("hearing_date")}
className="mt-1 tabular-nums"
/>
<FieldError message={form.formState.errors.hearing_date?.message} />
</div>
<div>
<Label className="text-navy">תוצאה צפויה</Label>
<Select
value={form.watch("expected_outcome") || "__none__"}
onValueChange={(v) =>
form.setValue("expected_outcome", v === "__none__" ? "" : v)
}
dir="rtl"
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{expectedOutcomes.map((o) => (
<SelectItem key={o.value || "none"} value={o.value || "__none__"}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="notes" className="text-navy">הערות (יתווספו לקיים)</Label>
<Textarea id="notes" rows={3} {...form.register("notes")} className="mt-1" />
<FieldError message={form.formState.errors.notes?.message} />
</div>
<DialogFooter className="gap-2">
<Button
type="button"
variant="ghost"
onClick={() => setOpen(false)}
disabled={mutate.isPending}
>
ביטול
</Button>
<Button
type="submit"
disabled={mutate.isPending}
className="bg-navy hover:bg-navy-soft text-parchment"
>
{mutate.isPending ? "שומר…" : "שמור שינויים"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,79 @@
import Link from "next/link";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/cases/status-badge";
import {
PRACTICE_AREA_LABELS,
APPEAL_SUBTYPE_LABELS,
} from "@/lib/practice-area";
import type { CaseDetail } from "@/lib/api/cases";
function formatDate(iso?: string | null) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso ?? "—";
}
}
export function CaseHeader({ data }: { data?: CaseDetail }) {
return (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden>·</span>
<span>תיקי ערר</span>
<span aria-hidden>·</span>
<span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span>
</nav>
<div className="flex items-start justify-between gap-6 flex-wrap">
<div className="space-y-2">
<div className="flex items-center gap-3 flex-wrap">
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
ערר {data?.case_number ?? "—"}
</span>
{data?.status && <StatusBadge status={data.status} />}
{data?.practice_area && (
<Badge
variant="outline"
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-gold-wash text-gold-deep border-gold/40"
>
{PRACTICE_AREA_LABELS[data.practice_area]}
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
)}
</Badge>
)}
</div>
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
{data?.title ?? "טוען…"}
</h1>
{data?.subject && (
<p className="text-ink-muted text-sm max-w-2xl leading-relaxed">
{data.subject}
</p>
)}
</div>
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
תאריך דיון
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
עודכן
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
</dl>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,201 @@
"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type SortingState,
} from "@tanstack/react-table";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { StatusBadge } from "@/components/cases/status-badge";
import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area";
import type { Case } from "@/lib/api/cases";
function formatDate(iso?: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso;
}
}
const columns: ColumnDef<Case>[] = [
{
accessorKey: "case_number",
header: "מס׳ ערר",
cell: ({ row }) => (
<Link
href={`/cases/${row.original.case_number}`}
className="text-navy font-semibold hover:text-gold-deep tabular-nums"
>
{row.original.case_number}
</Link>
),
},
{
accessorKey: "title",
header: "כותרת",
cell: ({ row }) => (
<div className="text-ink max-w-[420px] truncate" title={row.original.title}>
{row.original.title}
</div>
),
},
{
accessorKey: "status",
header: "סטטוס",
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
accessorKey: "appeal_subtype",
header: "תחום",
cell: ({ row }) => {
const s = row.original.appeal_subtype;
if (!s || s === "unknown") return <span className="text-ink-muted"></span>;
return <span className="text-ink-soft text-sm">{APPEAL_SUBTYPE_LABELS[s]}</span>;
},
},
{
accessorKey: "document_count",
header: "מסמכים",
cell: ({ row }) => (
<span className="tabular-nums text-ink-soft">
{row.original.document_count ?? "—"}
</span>
),
},
{
accessorKey: "updated_at",
header: "עודכן",
cell: ({ row }) => (
<span className="text-ink-muted text-sm">{formatDate(row.original.updated_at)}</span>
),
},
];
export function CasesTable({
cases,
loading,
error,
}: {
cases?: Case[];
loading?: boolean;
error?: Error | null;
}) {
const [sorting, setSorting] = useState<SortingState>([
{ id: "updated_at", desc: true },
]);
const [globalFilter, setGlobalFilter] = useState("");
const data = useMemo(() => cases ?? [], [cases]);
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: (row, _colId, filterValue: string) => {
if (!filterValue) return true;
const needle = filterValue.toLowerCase();
return (
row.original.case_number.toLowerCase().includes(needle) ||
row.original.title.toLowerCase().includes(needle)
);
},
});
return (
<div className="space-y-3">
<div className="flex items-center gap-3">
<Input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="חיפוש לפי מס׳ ערר או כותרת…"
className="max-w-sm bg-surface"
dir="rtl"
/>
<span className="text-sm text-ink-muted me-auto">
{table.getFilteredRowModel().rows.length} תיקים
</span>
</div>
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-rule-soft/60">
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="border-rule">
{hg.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="text-navy font-semibold cursor-pointer select-none text-right"
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: " ▲", desc: " ▼" }[header.column.getIsSorted() as string] ?? ""}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
Array.from({ length: 4 }).map((_, i) => (
<TableRow key={i} className="border-rule">
{columns.map((_c, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))
) : error ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center text-danger py-8">
שגיאה בטעינת תיקים: {error.message}
</TableCell>
</TableRow>
) : table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center text-ink-muted py-12">
<div className="text-gold text-2xl mb-2" aria-hidden></div>
{globalFilter ? "אין תיקים תואמים לחיפוש" : "עדיין אין תיקי ערר"}
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="border-rule hover:bg-gold-wash/40 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,193 @@
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { ScrollArea } from "@/components/ui/scroll-area";
import { CheckCircle2, Clock, Loader2, XCircle } from "lucide-react";
import type { CaseDetail } from "@/lib/api/cases";
/*
* Document list for the case detail "מסמכים" tab. Uses the real document
* row shape returned by the FastAPI case_get endpoint — see db.list_documents
* and the `documents` schema in legal_mcp/services/db.py:
* id · case_id · doc_type · title · file_path · extraction_status ·
* page_count · created_at · practice_area · appeal_subtype
*/
const DOC_TYPE_LABELS: Record<string, string> = {
appeal: "כתב ערר",
response: "כתב תשובה",
protocol: "פרוטוקול",
decision: "החלטת ועדה מקומית",
plan: "תכנית",
reference: "חומר רקע",
auto: "—",
};
function doctypeLabel(t: string): string {
return DOC_TYPE_LABELS[t] ?? t;
}
function doctypeTone(t: string): string {
switch (t) {
case "appeal": return "bg-info-bg text-info border-info/40";
case "response": return "bg-gold-wash text-gold-deep border-gold/40";
case "decision": return "bg-success-bg text-success border-success/40";
case "protocol": return "bg-warn-bg text-warn border-warn/40";
default: return "bg-rule-soft text-ink-muted border-rule";
}
}
const STATUS_LABELS: Record<string, string> = {
pending: "בהמתנה",
processing: "בעיבוד",
completed: "הושלם",
proofread: "הוגה",
failed: "נכשל",
error: "שגיאה",
};
/** Sort priority — lower = higher in list */
const STATUS_ORDER: Record<string, number> = {
failed: 0,
error: 0,
processing: 1,
pending: 2,
completed: 3,
proofread: 3,
};
function statusOrder(s: string): number {
return STATUS_ORDER[s] ?? 3;
}
function StatusIcon({ status }: { status: string }) {
switch (status) {
case "completed":
case "proofread":
return <CheckCircle2 className="w-4 h-4 text-success shrink-0" />;
case "processing":
return <Loader2 className="w-4 h-4 text-gold animate-spin shrink-0" />;
case "pending":
return <Clock className="w-4 h-4 text-ink-muted shrink-0" />;
case "failed":
case "error":
return <XCircle className="w-4 h-4 text-danger shrink-0" />;
default:
return <Clock className="w-4 h-4 text-ink-muted shrink-0" />;
}
}
function formatDate(iso: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL");
} catch {
return iso;
}
}
function filenameFromPath(path: string): string {
const parts = path.split("/");
return parts[parts.length - 1] || path;
}
export function DocumentsPanel({ data }: { data?: CaseDetail }) {
const docs = data?.documents ?? [];
if (docs.length === 0) {
return (
<div className="text-center py-12 text-ink-muted">
<div className="text-gold text-2xl mb-2" aria-hidden="true"></div>
<p className="text-sm">אין מסמכים בתיק זה</p>
</div>
);
}
const sorted = [...docs].sort(
(a, b) => statusOrder(a.extraction_status) - statusOrder(b.extraction_status),
);
const done = docs.filter(
(d) => d.extraction_status === "completed" || d.extraction_status === "proofread",
).length;
const processing = docs.filter((d) => d.extraction_status === "processing").length;
const pending = docs.filter((d) => d.extraction_status === "pending").length;
const failed = docs.filter(
(d) => d.extraction_status === "failed" || d.extraction_status === "error",
).length;
const hasIncomplete = processing > 0 || pending > 0 || failed > 0;
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
return (
<div className="space-y-3">
{hasIncomplete && (
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
<div className="flex items-center gap-4 text-[0.78rem] flex-wrap">
{done > 0 && (
<span className="flex items-center gap-1 text-success">
<CheckCircle2 className="w-3.5 h-3.5" />
{done} {STATUS_LABELS.completed}
</span>
)}
{processing > 0 && (
<span className="flex items-center gap-1 text-gold-deep">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
{processing} {STATUS_LABELS.processing}
</span>
)}
{pending > 0 && (
<span className="flex items-center gap-1 text-ink-muted">
<Clock className="w-3.5 h-3.5" />
{pending} {STATUS_LABELS.pending}
</span>
)}
{failed > 0 && (
<span className="flex items-center gap-1 text-danger">
<XCircle className="w-3.5 h-3.5" />
{failed} {STATUS_LABELS.failed}
</span>
)}
</div>
<Progress
value={pct}
className={failed > 0 && done === 0 ? "[&>div]:bg-danger" : ""}
/>
</div>
)}
<ScrollArea className="max-h-[520px]" dir="rtl">
<ul className="divide-y divide-rule" dir="rtl">
{sorted.map((doc) => {
const displayName = doc.title || filenameFromPath(doc.file_path);
return (
<li
key={doc.id}
className="py-3 flex items-start gap-3 hover:bg-gold-wash/30 transition-colors px-2 -mx-2 rounded"
>
<StatusIcon status={doc.extraction_status} />
<div className="flex-1 min-w-0 space-y-0.5 text-right">
<div className="text-ink font-medium truncate" title={displayName}>
{displayName}
</div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3 flex-wrap">
{doc.page_count != null && (
<span className="tabular-nums">{doc.page_count} עמ׳</span>
)}
{doc.created_at && <span>{formatDate(doc.created_at)}</span>}
</div>
</div>
{doc.doc_type && (
<Badge
variant="outline"
className={`rounded-full px-2 py-0.5 text-[0.7rem] shrink-0 ms-auto ${doctypeTone(doc.doc_type)}`}
>
{doctypeLabel(doc.doc_type)}
</Badge>
)}
</li>
);
})}
</ul>
</ScrollArea>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { Card, CardContent } from "@/components/ui/card";
import type { Case } from "@/lib/api/cases";
type Bucket = {
label: string;
caption: string;
value: number;
tone: "navy" | "gold" | "warn" | "success";
};
const TONE_STYLES: Record<Bucket["tone"], string> = {
navy: "before:bg-navy text-navy",
gold: "before:bg-gold text-gold-deep",
warn: "before:bg-warn text-warn",
success: "before:bg-success text-success",
};
function bucketize(cases: Case[] | undefined): Bucket[] {
const c = cases ?? [];
const inProgress = c.filter((x) =>
["processing", "documents_ready", "outcome_set", "brainstorming", "direction_approved"].includes(x.status),
).length;
const drafting = c.filter((x) =>
["drafting", "qa_review", "drafted"].includes(x.status),
).length;
const done = c.filter((x) =>
["exported", "reviewed", "final"].includes(x.status),
).length;
return [
{ label: "סה״כ תיקי ערר", caption: "בכל הסטטוסים", value: c.length, tone: "navy" },
{ label: "בהכנה", caption: "מסמכים וניתוח", value: inProgress, tone: "gold" },
{ label: "בכתיבה", caption: "טיוטות ו-QA", value: drafting, tone: "warn" },
{ label: "מוכנים", caption: "יוצאו או סופיים", value: done, tone: "success" },
];
}
export function KPICards({ cases, loading }: { cases?: Case[]; loading?: boolean }) {
const buckets = bucketize(cases);
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{buckets.map((b) => (
<Card
key={b.label}
className={`
relative overflow-hidden bg-surface shadow-sm border-rule
before:content-[''] before:absolute before:top-0 before:right-0 before:h-full before:w-[3px]
${TONE_STYLES[b.tone]}
`}
>
<CardContent className="px-5 py-4 flex flex-col gap-1">
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
{b.label}
</span>
<span className="font-display text-[2.3rem] font-black leading-none">
{loading ? "—" : b.value}
</span>
<span className="text-[0.78rem] text-ink-muted mt-0.5">{b.caption}</span>
</CardContent>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { Badge } from "@/components/ui/badge";
import type { CaseStatus } from "@/lib/api/cases";
const STATUS_LABELS: Record<CaseStatus, string> = {
new: "חדש",
uploading: "מעלה",
processing: "בעיבוד",
documents_ready: "מסמכים מוכנים",
outcome_set: "תוצאה נקבעה",
brainstorming: "סיעור מוחות",
direction_approved: "כיוון אושר",
drafting: "בכתיבה",
qa_review: "QA",
drafted: "טיוטה",
exported: "יוצא",
reviewed: "נבדק",
final: "סופי",
};
/* Status color groups:
* intake → new, uploading, processing (muted parchment)
* prep → documents_ready, outcome_set (info blue)
* thinking→ brainstorming, direction_approved (gold)
* writing → drafting, qa_review, drafted (warn amber)
* done → exported, reviewed, final (success green) */
const STATUS_TONE: Record<CaseStatus, string> = {
new: "bg-rule-soft text-ink-muted border-rule",
uploading: "bg-rule-soft text-ink-muted border-rule",
processing: "bg-info-bg text-info border-info/30",
documents_ready: "bg-info-bg text-info border-info/40",
outcome_set: "bg-info-bg text-info border-info/40",
brainstorming: "bg-gold-wash text-gold-deep border-gold/40",
direction_approved:"bg-gold-wash text-gold-deep border-gold/50",
drafting: "bg-warn-bg text-warn border-warn/40",
qa_review: "bg-warn-bg text-warn border-warn/40",
drafted: "bg-warn-bg text-warn border-warn/50",
exported: "bg-success-bg text-success border-success/40",
reviewed: "bg-success-bg text-success border-success/50",
final: "bg-success-bg text-success border-success/60 font-semibold",
};
export function StatusBadge({ status }: { status: CaseStatus }) {
return (
<Badge
variant="outline"
className={`rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium ${STATUS_TONE[status] ?? ""}`}
>
{STATUS_LABELS[status] ?? status}
</Badge>
);
}
export { STATUS_LABELS };

View File

@@ -0,0 +1,91 @@
"use client";
import type { Case, CaseStatus } from "@/lib/api/cases";
import { STATUS_LABELS } from "@/components/cases/status-badge";
/*
* Conic-gradient donut — ported from legal-ai/web/static/index.html renderHero().
* Kept deliberately dependency-free (no D3/recharts) — a single background-image.
* Five status groups map onto the navy/gold/info/warn/success palette.
*/
type GroupKey = "intake" | "prep" | "thinking" | "writing" | "done";
const GROUP_OF: Record<CaseStatus, GroupKey> = {
new: "intake", uploading: "intake", processing: "intake",
documents_ready: "prep", outcome_set: "prep",
brainstorming: "thinking", direction_approved: "thinking",
drafting: "writing", qa_review: "writing", drafted: "writing",
exported: "done", reviewed: "done", final: "done",
};
const GROUP_META: Record<GroupKey, { label: string; color: string }> = {
intake: { label: "חדש / בעיבוד", color: "var(--color-ink-muted)" },
prep: { label: "הכנה", color: "var(--color-info)" },
thinking: { label: "ניתוח וכיוון", color: "var(--color-gold)" },
writing: { label: "בכתיבה", color: "var(--color-warn)" },
done: { label: "מוכן", color: "var(--color-success)" },
};
export function StatusDonut({ cases }: { cases?: Case[] }) {
const counts: Record<GroupKey, number> = {
intake: 0, prep: 0, thinking: 0, writing: 0, done: 0,
};
(cases ?? []).forEach((c) => {
const g = GROUP_OF[c.status];
if (g) counts[g] += 1;
});
const total = Object.values(counts).reduce((a, b) => a + b, 0);
const segments: { key: GroupKey; start: number; end: number }[] = [];
let pct = 0;
(Object.keys(counts) as GroupKey[]).forEach((k) => {
if (counts[k] === 0) return;
const start = total === 0 ? 0 : (pct / total) * 360;
pct += counts[k];
const end = total === 0 ? 360 : (pct / total) * 360;
segments.push({ key: k, start, end });
});
const background =
total === 0
? "conic-gradient(var(--color-rule-soft) 0deg 360deg)"
: `conic-gradient(${segments
.map((s) => `${GROUP_META[s.key].color} ${s.start}deg ${s.end}deg`)
.join(", ")})`;
return (
<div className="flex items-center gap-6">
<div
className="relative w-[140px] h-[140px] rounded-full shadow-sm"
style={{ background }}
aria-label="פיזור תיקים לפי סטטוס"
>
<div className="absolute inset-[18px] bg-surface rounded-full flex flex-col items-center justify-center">
<span className="font-display text-2xl font-black text-navy leading-none">
{total}
</span>
<span className="text-[0.7rem] text-ink-muted mt-1">תיקים</span>
</div>
</div>
<ul className="flex flex-col gap-1.5 text-sm">
{(Object.keys(GROUP_META) as GroupKey[]).map((k) => (
<li key={k} className="flex items-center gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full"
style={{ background: GROUP_META[k].color }}
/>
<span className="text-ink-soft">{GROUP_META[k].label}</span>
<span className="text-ink-muted tabular-nums me-auto ms-1">
{counts[k]}
</span>
</li>
))}
</ul>
</div>
);
}
/* Exported for the legend tests / docs if ever needed */
export { STATUS_LABELS };

View File

@@ -0,0 +1,77 @@
"use client";
import type { CaseStatus } from "@/lib/api/cases";
import { STATUS_LABELS } from "@/components/cases/status-badge";
/*
* Vertical RTL workflow timeline showing the 13-status case pipeline.
* Groups the raw statuses into the 5 visual phases used across the app
* (intake → prep → thinking → writing → done) so the user sees
* "where am I in the process" rather than 13 micro-steps.
*/
type Phase = {
key: string;
label: string;
statuses: CaseStatus[];
};
const PHASES: Phase[] = [
{ key: "intake", label: "קליטה ועיבוד", statuses: ["new", "uploading", "processing"] },
{ key: "prep", label: "הכנת תיק", statuses: ["documents_ready", "outcome_set"] },
{ key: "thinking", label: "ניתוח וכיוון", statuses: ["brainstorming", "direction_approved"] },
{ key: "writing", label: "כתיבת טיוטה", statuses: ["drafting", "qa_review", "drafted"] },
{ key: "done", label: "סגירה", statuses: ["exported", "reviewed", "final"] },
];
function phaseIndexOf(status?: CaseStatus): number {
if (!status) return -1;
return PHASES.findIndex((p) => p.statuses.includes(status));
}
export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
const currentIdx = phaseIndexOf(status);
return (
<ol className="relative space-y-4">
<div
className="absolute top-2 bottom-2 right-[11px] w-px bg-rule"
aria-hidden
/>
{PHASES.map((phase, i) => {
const state =
currentIdx === -1 ? "pending"
: i < currentIdx ? "done"
: i === currentIdx ? "current"
: "pending";
const dotTone =
state === "done" ? "bg-success border-success"
: state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]"
: "bg-surface border-rule";
const labelTone =
state === "done" ? "text-ink-soft"
: state === "current" ? "text-navy font-semibold"
: "text-ink-muted";
return (
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
<span
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
aria-hidden
/>
<div className="flex flex-col">
<span className={`text-sm ${labelTone}`}>{phase.label}</span>
{state === "current" && status && (
<span className="text-[0.72rem] text-gold-deep mt-0.5">
{STATUS_LABELS[status]}
</span>
)}
</div>
</li>
);
})}
</ol>
);
}

View File

@@ -0,0 +1,96 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useSaveChairPosition } from "@/lib/api/research";
/*
* Chair-position editor for a single threshold claim or issue.
*
* Autosaves on blur, with an optimistic in-memory "last saved" value so the
* user sees immediate feedback. No debounced per-keystroke save — the user
* writes in long paragraphs and the backend writes to a file, so per-blur
* is the right granularity (matches the vanilla UI's behavior).
*/
type SaveState =
| { kind: "idle" }
| { kind: "saving" }
| { kind: "saved"; at: Date }
| { kind: "error"; message: string };
export function ChairEditor({
caseNumber,
sectionId,
initialValue,
}: {
caseNumber: string;
sectionId: string;
initialValue: string;
}) {
const [value, setValue] = useState(initialValue);
const [state, setState] = useState<SaveState>({ kind: "idle" });
const lastSaved = useRef(initialValue);
const mutate = useSaveChairPosition(caseNumber);
/* Reset when the upstream analysis refetches (e.g. after initial load) */
useEffect(() => {
setValue(initialValue);
lastSaved.current = initialValue;
}, [initialValue]);
const save = async () => {
const trimmed = value.trim();
if (trimmed === lastSaved.current.trim()) return;
setState({ kind: "saving" });
try {
await mutate.mutateAsync({ sectionId, position: trimmed });
lastSaved.current = trimmed;
setState({ kind: "saved", at: new Date() });
} catch (e) {
setState({
kind: "error",
message: e instanceof Error ? e.message : "שגיאה בשמירה",
});
}
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-[0.78rem] font-semibold text-navy">
עמדת ועדת הערר
</span>
<SaveIndicator state={state} />
</div>
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={save}
rows={6}
dir="rtl"
placeholder="כתבי כאן את עמדתך לגבי סוגיה זו. הטקסט נשמר אוטומטית כשעוזבת את השדה."
className="
w-full resize-y rounded border border-rule bg-parchment
px-3 py-2 text-sm leading-relaxed text-ink
shadow-inner focus:border-gold focus:outline-none
focus:ring-2 focus:ring-gold/30
"
/>
</div>
);
}
function SaveIndicator({ state }: { state: SaveState }) {
if (state.kind === "idle") return null;
if (state.kind === "saving") {
return <span className="text-[0.72rem] text-ink-muted"> שומר</span>;
}
if (state.kind === "saved") {
const time = state.at.toLocaleTimeString("he-IL", {
hour: "2-digit",
minute: "2-digit",
});
return <span className="text-[0.72rem] text-success"> נשמר {time}</span>;
}
return <span className="text-[0.72rem] text-danger"> {state.message}</span>;
}

View File

@@ -0,0 +1,229 @@
"use client";
import { useState } from "react";
import { Plus, Paperclip } from "lucide-react";
import { toast } from "sonner";
import {
Popover, PopoverContent, PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
useCreatePrecedent,
usePrecedentLibrarySearch,
uploadPrecedentPdf,
} from "@/lib/api/precedents";
import type { PracticeArea } from "@/lib/practice-area";
/*
* Inline form for adding a new precedent. Opens in a Popover adjacent
* to the trigger button so the user can see the surrounding context
* (the threshold_claim body, the chair editor) while they fill it in.
*
* The citation field has cross-case typeahead: once the user types
* 2+ characters, we hit /api/precedents/search and show distinct
* matches. Picking one prefills quote + chair_note but keeps them
* editable — the new row is a copy, so a customized quote for this
* case doesn't affect the library.
*/
export function PrecedentAttacher({
caseNumber,
sectionId,
practiceArea,
}: {
caseNumber: string;
sectionId: string | null;
practiceArea: PracticeArea | null | undefined;
}) {
const [open, setOpen] = useState(false);
const [citation, setCitation] = useState("");
const [quote, setQuote] = useState("");
const [chairNote, setChairNote] = useState("");
const [pdfFile, setPdfFile] = useState<File | null>(null);
const [submitting, setSubmitting] = useState(false);
const [picked, setPicked] = useState(false);
const create = useCreatePrecedent(caseNumber);
const library = usePrecedentLibrarySearch(
citation,
practiceArea,
/* pause typeahead once the user has picked one and we're just editing */
!picked,
);
const reset = () => {
setCitation("");
setQuote("");
setChairNote("");
setPdfFile(null);
setPicked(false);
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!quote.trim() || !citation.trim()) {
toast.error("ציטוט ומראה-מקום חובה");
return;
}
setSubmitting(true);
try {
let pdfDocumentId: string | undefined;
if (pdfFile) {
const res = await uploadPrecedentPdf(caseNumber, pdfFile);
pdfDocumentId = res.document_id;
}
await create.mutateAsync({
quote: quote.trim(),
citation: citation.trim(),
chair_note: chairNote.trim(),
section_id: sectionId ?? undefined,
pdf_document_id: pdfDocumentId,
});
toast.success("נוספה פסיקה");
reset();
setOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה בשמירה");
} finally {
setSubmitting(false);
}
};
return (
<Popover open={open} onOpenChange={(v) => { setOpen(v); if (!v) reset(); }}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="border-dashed border-gold/50 text-gold-deep hover:bg-gold-wash"
>
<Plus className="w-4 h-4 me-1" aria-hidden="true" />
הוסף פסיקה תומכת
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[520px] max-w-[90vw] p-5"
align="start"
dir="rtl"
>
<form onSubmit={onSubmit} className="space-y-3" dir="rtl">
<div>
<Label htmlFor="prec-citation" className="text-navy">
מראה מקום <span className="text-danger">*</span>
</Label>
<Input
id="prec-citation"
value={citation}
onChange={(e) => {
setCitation(e.target.value);
setPicked(false);
}}
placeholder="ערר (ירושלים) 1126-08-25 ... נ' ... (נבו 9.3.2026)"
autoComplete="off"
className="mt-1"
/>
{!picked && library.data && library.data.length > 0 && citation.length >= 2 && (
<ul
className="
mt-1 rounded border border-rule bg-surface shadow-sm
max-h-44 overflow-y-auto divide-y divide-rule
"
role="listbox"
>
{library.data.map((m) => (
<li key={m.id}>
<button
type="button"
onClick={() => {
setCitation(m.citation);
setQuote(m.quote);
setChairNote(m.chair_note || "");
setPicked(true);
}}
className="w-full text-right px-3 py-2 hover:bg-gold-wash/60 transition-colors"
>
<div className="text-[0.78rem] text-gold-deep font-semibold truncate">
{m.citation}
</div>
<div className="text-[0.72rem] text-ink-muted truncate">
{m.quote}
</div>
</button>
</li>
))}
</ul>
)}
</div>
<div>
<Label htmlFor="prec-quote" className="text-navy">
ציטוט <span className="text-danger">*</span>
</Label>
<Textarea
id="prec-quote"
value={quote}
onChange={(e) => setQuote(e.target.value)}
rows={5}
placeholder="הטקסט המדויק שישולב בהחלטה"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="prec-note" className="text-navy">
הערה (אופציונלי)
</Label>
<Textarea
id="prec-note"
value={chairNote}
onChange={(e) => setChairNote(e.target.value)}
rows={2}
placeholder="למה הציטוט הזה תומך בעמדה"
className="mt-1"
/>
</div>
<div>
<Label className="text-navy flex items-center gap-2">
<Paperclip className="w-3.5 h-3.5" aria-hidden="true" />
צירוף קובץ המקור (אופציונלי, לארכיון)
</Label>
<input
type="file"
accept=".pdf,.docx,.doc"
onChange={(e) => setPdfFile(e.target.files?.[0] ?? null)}
className="mt-1 w-full text-sm file:me-3 file:rounded file:border-0 file:bg-rule-soft file:px-3 file:py-1.5 file:text-navy file:text-sm hover:file:bg-rule"
/>
{pdfFile && (
<p className="text-[0.72rem] text-ink-muted mt-1">
{pdfFile.name} · {(pdfFile.size / 1024).toFixed(1)} KB
</p>
)}
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button
type="button"
variant="ghost"
onClick={() => { setOpen(false); reset(); }}
disabled={submitting}
>
ביטול
</Button>
<Button
type="submit"
disabled={submitting}
className="bg-navy hover:bg-navy-soft text-parchment"
>
{submitting ? "שומר…" : "שמור פסיקה"}
</Button>
</div>
</form>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import { Trash2, FileText } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { useDeletePrecedent, type CasePrecedent } from "@/lib/api/precedents";
/*
* Read-only display of a single attached precedent. Layout is:
*
* ┌───────────────────────────────────────────┐
* │ citation (gold semibold) [🗑] │
* │ ┌──┐ │
* │ │╎ │ "quote text…" │
* │ └──┘ │
* │ chair_note (muted) │
* │ 📄 קובץ מצורף │
* └───────────────────────────────────────────┘
*/
export function PrecedentCard({
caseNumber,
precedent,
}: {
caseNumber: string;
precedent: CasePrecedent;
}) {
const del = useDeletePrecedent(caseNumber);
const onDelete = async () => {
if (!window.confirm("להסיר פסיקה זו מהתיק?")) return;
try {
await del.mutateAsync(precedent.id);
toast.success("הפסיקה הוסרה");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה בהסרה");
}
};
return (
<article className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2">
<div className="flex items-start gap-3">
<p className="flex-1 text-[0.82rem] text-gold-deep font-semibold leading-snug">
{precedent.citation}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={onDelete}
disabled={del.isPending}
aria-label="הסר פסיקה זו"
className="text-danger hover:text-danger hover:bg-danger-bg shrink-0 -mt-1 -me-2"
>
<Trash2 className="w-4 h-4" aria-hidden="true" />
</Button>
</div>
<blockquote className="border-e-2 border-gold pe-3 text-sm text-ink leading-relaxed whitespace-pre-line">
{precedent.quote}
</blockquote>
{precedent.chair_note && (
<p className="text-[0.78rem] text-ink-muted italic">
{precedent.chair_note}
</p>
)}
{precedent.pdf_document_id && (
<div className="flex items-center gap-1.5 text-[0.72rem] text-ink-muted">
<FileText className="w-3 h-3" aria-hidden="true" />
<span>קובץ מצורף</span>
</div>
)}
</article>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import { PrecedentCard } from "@/components/compose/precedent-card";
import { PrecedentAttacher } from "@/components/compose/precedent-attacher";
import type { CasePrecedent } from "@/lib/api/precedents";
import type { PracticeArea } from "@/lib/practice-area";
/*
* Wrapper that renders the list of precedents for one scope — either
* case-level (sectionId=null) or a specific threshold_claim / issue.
* The parent page fetches useCasePrecedents(caseNumber) once and
* passes a pre-filtered slice down, so each section doesn't re-query.
*/
export function PrecedentsSection({
caseNumber,
sectionId,
precedents,
practiceArea,
emptyHelperText,
}: {
caseNumber: string;
sectionId: string | null;
precedents: CasePrecedent[];
practiceArea: PracticeArea | null | undefined;
emptyHelperText?: string;
}) {
return (
<div className="space-y-3">
{precedents.length === 0 ? (
emptyHelperText && (
<p className="text-[0.78rem] text-ink-muted">{emptyHelperText}</p>
)
) : (
<ul className="space-y-2">
{precedents.map((p) => (
<li key={p.id}>
<PrecedentCard caseNumber={caseNumber} precedent={p} />
</li>
))}
</ul>
)}
<PrecedentAttacher
caseNumber={caseNumber}
sectionId={sectionId}
practiceArea={practiceArea}
/>
</div>
);
}

View File

@@ -0,0 +1,108 @@
"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
import { ChairEditor } from "@/components/compose/chair-editor";
import { PrecedentsSection } from "@/components/compose/precedents-section";
import { Markdown } from "@/components/ui/markdown";
import type { ResearchSubsection } from "@/lib/api/research";
import type { CasePrecedent } from "@/lib/api/precedents";
import type { PracticeArea } from "@/lib/practice-area";
export function SubsectionCard({
caseNumber,
item,
defaultOpen = false,
precedents = [],
practiceArea,
}: {
caseNumber: string;
item: ResearchSubsection;
defaultOpen?: boolean;
precedents?: CasePrecedent[];
practiceArea?: PracticeArea | null;
}) {
const [open, setOpen] = useState(defaultOpen);
const isFilled = Boolean(item.chair_position?.trim());
return (
<article className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="
w-full flex items-center gap-3 px-4 py-3 text-right
hover:bg-gold-wash/30 transition-colors
focus:outline-none focus-visible:bg-gold-wash/40
"
aria-expanded={open}
>
<span
className="
inline-flex items-center justify-center shrink-0
w-7 h-7 rounded-full
bg-navy text-parchment font-display font-bold text-sm
tabular-nums
"
>
{item.number}
</span>
<span className="flex-1 text-navy font-semibold text-base leading-snug">
{item.title}
</span>
<span
className={`
text-[0.72rem] rounded-full px-2.5 py-0.5 border shrink-0
${
isFilled
? "bg-success-bg text-success border-success/40"
: "bg-rule-soft text-ink-muted border-rule"
}
`}
>
{isFilled ? "✓ עמדה נקבעה" : "ממתין לעמדה"}
</span>
<ChevronDown
className={`w-4 h-4 text-ink-muted transition-transform ${open ? "rotate-180" : ""}`}
aria-hidden
/>
</button>
{open && (
<div className="border-t border-rule px-5 py-4 space-y-4 bg-parchment/40">
{item.fields.length > 0 && (
<dl className="space-y-3">
{item.fields.map((f, i) => (
<div key={i}>
<dt className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-semibold mb-1">
{f.label}
</dt>
<dd>
<Markdown content={f.content} />
</dd>
</div>
))}
</dl>
)}
<ChairEditor
caseNumber={caseNumber}
sectionId={item.id}
initialValue={item.chair_position ?? ""}
/>
<div className="border-t border-rule pt-4 mt-2">
<h4 className="text-[0.78rem] uppercase tracking-wider text-gold-deep font-semibold mb-2">
פסיקה תומכת
</h4>
<PrecedentsSection
caseNumber={caseNumber}
sectionId={item.id}
precedents={precedents}
practiceArea={practiceArea}
emptyHelperText="עדיין לא צורפה פסיקה לסעיף זה"
/>
</div>
</div>
)}
</article>
);
}

View File

@@ -0,0 +1,208 @@
"use client";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
import { Upload, FileText, CheckCircle2, XCircle, Loader2 } from "lucide-react";
import {
Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { useUploadDocument, useProgress, type ProgressEvent } from "@/lib/api/documents";
/*
* Upload sheet — drag-drop zone + doc-type selector, with live SSE
* progress for the most-recent upload. Intentionally sequential:
* a single file at a time keeps the SSE subscription simple and
* matches how the FastAPI processor handles one task_id per file.
*/
const DOC_TYPES: { value: string; label: string }[] = [
{ value: "auto", label: "זיהוי אוטומטי" },
{ value: "appeal", label: "כתב ערר" },
{ value: "response", label: "כתב תשובה" },
{ value: "protocol", label: "פרוטוקול דיון" },
{ value: "decision", label: "החלטת ועדה מקומית" },
{ value: "plan", label: "תכנית" },
{ value: "reference",label: "חומר רקע" },
];
type UploadRow = {
id: string;
filename: string;
taskId: string | null;
error?: string;
};
function statusLabel(event: ProgressEvent | null): string {
if (!event) return "מתחיל…";
if (event.status === "queued") return "בתור";
if (event.status === "processing")
return event.step ? `בעיבוד · ${event.step}` : "בעיבוד";
if (event.status === "completed") return "הושלם";
if (event.status === "failed") return event.error ?? "נכשל";
return event.status;
}
function progressPercent(event: ProgressEvent | null): number {
if (!event) return 5;
if (event.status === "queued") return 10;
if (event.status === "processing") return 55;
if (event.status === "completed") return 100;
if (event.status === "failed") return 100;
return 25;
}
function UploadRowView({ row }: { row: UploadRow }) {
const progress = useProgress(row.taskId);
const pct = row.error ? 100 : progressPercent(progress);
const failed = row.error || progress?.status === "failed";
const done = progress?.status === "completed";
return (
<li className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2">
<div className="flex items-center gap-2">
{done ? (
<CheckCircle2 className="w-4 h-4 text-success shrink-0" />
) : failed ? (
<XCircle className="w-4 h-4 text-danger shrink-0" />
) : (
<Loader2 className="w-4 h-4 text-gold animate-spin shrink-0" />
)}
<FileText className="w-4 h-4 text-ink-muted shrink-0" />
<span className="text-sm text-ink truncate flex-1" title={row.filename}>
{row.filename}
</span>
<span
className={`text-[0.72rem] tabular-nums shrink-0 ${
done ? "text-success" : failed ? "text-danger" : "text-ink-muted"
}`}
>
{row.error ?? statusLabel(progress)}
</span>
</div>
<Progress
value={pct}
className={failed ? "[&>div]:bg-danger" : done ? "[&>div]:bg-success" : ""}
/>
</li>
);
}
export function UploadSheet({ caseNumber }: { caseNumber: string }) {
const [open, setOpen] = useState(false);
const [docType, setDocType] = useState("auto");
const [rows, setRows] = useState<UploadRow[]>([]);
const mutate = useUploadDocument(caseNumber);
const onDrop = useCallback(
async (files: File[]) => {
for (const file of files) {
const rowId = crypto.randomUUID();
setRows((r) => [
...r,
{ id: rowId, filename: file.name, taskId: null },
]);
try {
const res = await mutate.mutateAsync({ file, docType });
setRows((r) =>
r.map((row) =>
row.id === rowId ? { ...row, taskId: res.task_id } : row,
),
);
} catch (e) {
setRows((r) =>
r.map((row) =>
row.id === rowId
? { ...row, error: e instanceof Error ? e.message : "שגיאה" }
: row,
),
);
}
}
},
[docType, mutate],
);
const dropzone = useDropzone({
onDrop,
accept: {
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"text/plain": [".txt"],
"text/markdown": [".md"],
},
maxSize: 50 * 1024 * 1024,
});
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="sm">
<Upload className="w-4 h-4 me-1" /> העלאת מסמכים
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-full sm:max-w-lg" dir="rtl">
<SheetHeader>
<SheetTitle className="text-navy">העלאת מסמכים לתיק {caseNumber}</SheetTitle>
<SheetDescription className="text-ink-muted">
PDF, DOCX, DOC, TXT, MD עד 50MB לקובץ. הקבצים מעובדים ברקע
והסטטוס מתעדכן בזמן אמת.
</SheetDescription>
</SheetHeader>
<div className="mt-5 space-y-4 px-4 overflow-y-auto flex-1 min-h-0">
<div>
<label className="block text-sm font-medium text-navy mb-1.5">
סיווג
</label>
<Select value={docType} onValueChange={setDocType} dir="rtl">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{DOC_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div
{...dropzone.getRootProps()}
className={`
rounded-lg border-2 border-dashed p-8 text-center cursor-pointer
transition-colors
${
dropzone.isDragActive
? "border-gold bg-gold-wash"
: "border-rule bg-parchment/40 hover:bg-gold-wash/50 hover:border-gold/60"
}
`}
>
<input {...dropzone.getInputProps()} />
<Upload className="w-8 h-8 mx-auto mb-2 text-gold-deep" />
<p className="text-sm text-navy font-medium">
{dropzone.isDragActive
? "שחרר כאן להעלאה"
: "גרור קבצים או לחץ לבחירה"}
</p>
<p className="text-[0.72rem] text-ink-muted mt-1">
ניתן להעלות מספר קבצים בבת אחת
</p>
</div>
{rows.length > 0 && (
<ul className="space-y-2">
{rows.map((row) => (
<UploadRowView key={row.id} row={row} />
))}
</ul>
)}
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,189 @@
"use client";
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import {
useCorpus, useCompare, type CompareSide, type PatternEntry,
} from "@/lib/api/training";
/*
* Compare two decisions from the style corpus side-by-side. Uses the
* training/compare endpoint which already does the heavy lifting (pattern
* extraction, section stats, shared/unique pattern sets). Our job is
* layout: two columns of metadata + section bars, plus a third "shared"
* section listing patterns that appear in both.
*/
function SideColumn({ side }: { side: CompareSide }) {
return (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 space-y-3">
<header>
<h3 className="text-navy text-lg mb-0 tabular-nums">
{side.decision_number || "—"}
</h3>
<p className="text-[0.78rem] text-ink-muted tabular-nums">
{side.decision_date || "—"} · {(side.chars / 1000).toFixed(1)}K תווים
</p>
</header>
{side.subjects.length > 0 && (
<div className="flex flex-wrap gap-1">
{side.subjects.map((s) => (
<Badge
key={s}
variant="outline"
className="text-[0.7rem] bg-gold-wash text-gold-deep border-gold/40"
>
{s}
</Badge>
))}
</div>
)}
{side.sections.length > 0 && (
<div>
<h4 className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-semibold mb-1.5">
חלוקה לחלקים
</h4>
<ul className="space-y-1 text-[0.78rem]">
{side.sections.map((sec) => (
<li key={sec.type} className="flex items-center justify-between gap-2">
<span className="text-ink-soft truncate">{sec.type}</span>
<span className="text-ink-muted tabular-nums shrink-0">
{(sec.chars / 1000).toFixed(1)}K
</span>
</li>
))}
</ul>
</div>
)}
<p className="text-[0.78rem] text-ink-muted">
דפוסי סגנון שנמצאו: <span className="text-navy font-semibold tabular-nums">{side.patterns_count}</span>
</p>
</CardContent>
</Card>
);
}
function PatternList({
title,
items,
tone,
}: {
title: string;
items: PatternEntry[];
tone: "shared" | "a" | "b";
}) {
const toneClass =
tone === "shared"
? "bg-success-bg border-success/40"
: tone === "a"
? "bg-info-bg border-info/40"
: "bg-gold-wash border-gold/40";
const toneHeading =
tone === "shared"
? "text-success"
: tone === "a"
? "text-info"
: "text-gold-deep";
return (
<Card className={`${toneClass} shadow-sm`}>
<CardContent className="px-5 py-4">
<h4 className={`${toneHeading} text-sm font-semibold mb-3 flex items-center gap-2`}>
{title}
<span className="text-[0.7rem] tabular-nums opacity-70">{items.length}</span>
</h4>
{items.length === 0 ? (
<p className="text-ink-muted text-[0.78rem]"></p>
) : (
<ul className="space-y-1.5 text-[0.78rem] max-h-60 overflow-y-auto">
{items.slice(0, 20).map((p) => (
<li key={p.id} className="text-ink leading-relaxed">
{p.text}
</li>
))}
</ul>
)}
</CardContent>
</Card>
);
}
export function ComparePanel() {
const { data: corpus, isPending } = useCorpus();
const [a, setA] = useState<string | null>(null);
const [b, setB] = useState<string | null>(null);
const cmp = useCompare(a, b);
return (
<div className="space-y-6">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4">
<h3 className="text-navy text-base mb-3">בחר שתי החלטות להשוואה</h3>
<div className="grid gap-3 md:grid-cols-2">
{(["a", "b"] as const).map((slot) => (
<div key={slot}>
<label className="block text-[0.78rem] font-medium text-navy mb-1">
{slot === "a" ? "החלטה א" : "החלטה ב"}
</label>
<Select
disabled={isPending}
value={(slot === "a" ? a : b) ?? ""}
onValueChange={(v) => (slot === "a" ? setA(v) : setB(v))}
dir="rtl"
>
<SelectTrigger>
<SelectValue placeholder={isPending ? "טוען…" : "בחר החלטה"} />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{corpus?.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.decision_number || "—"}
{c.decision_date ? ` · ${c.decision_date}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</CardContent>
</Card>
{a && b && a === b && (
<p className="text-ink-muted text-sm text-center">בחר שתי החלטות שונות</p>
)}
{cmp.error && (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-5 text-danger text-center">
{cmp.error.message}
</CardContent>
</Card>
)}
{cmp.isPending && a && b && a !== b && (
<Skeleton className="h-60 w-full" />
)}
{cmp.data && (
<>
<div className="grid gap-4 md:grid-cols-2">
<SideColumn side={cmp.data.a} />
<SideColumn side={cmp.data.b} />
</div>
<div className="grid gap-4 md:grid-cols-3">
<PatternList title="דפוסים משותפים" items={cmp.data.shared} tone="shared" />
<PatternList title="רק בהחלטה א" items={cmp.data.only_a} tone="a" />
<PatternList title="רק בהחלטה ב" items={cmp.data.only_b} tone="b" />
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,140 @@
"use client";
import { Trash2 } from "lucide-react";
import { toast } from "sonner";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useCorpus, useDeleteCorpusEntry, type CorpusDecision } from "@/lib/api/training";
/*
* Corpus tab: table of all decisions currently in the style corpus, with a
* single destructive action (remove from corpus). Uses browser confirm() for
* the confirmation — a full shadcn AlertDialog would be overkill for an
* admin-only destructive action with a server-side safety net.
*/
function formatChars(n: number) {
return `${(n / 1000).toFixed(1)}K`;
}
function formatDate(iso: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL");
} catch {
return iso;
}
}
function Row({ item }: { item: CorpusDecision }) {
const del = useDeleteCorpusEntry();
const onDelete = async () => {
if (!window.confirm(`למחוק את החלטה ${item.decision_number} מהקורפוס?`)) return;
try {
await del.mutateAsync(item.id);
toast.success("נמחק מהקורפוס");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה במחיקה");
}
};
return (
<TableRow className="border-rule hover:bg-gold-wash/30">
<TableCell className="font-semibold text-navy tabular-nums">
{item.decision_number || "—"}
</TableCell>
<TableCell className="text-ink-muted tabular-nums">
{formatDate(item.decision_date)}
</TableCell>
<TableCell>
{item.subject_categories.length === 0 ? (
<span className="text-ink-light"></span>
) : (
<div className="flex flex-wrap gap-1">
{item.subject_categories.map((s) => (
<Badge
key={s}
variant="outline"
className="text-[0.7rem] bg-gold-wash text-gold-deep border-gold/40"
>
{s}
</Badge>
))}
</div>
)}
</TableCell>
<TableCell className="text-ink-soft tabular-nums">
{formatChars(item.chars)}
</TableCell>
<TableCell className="text-ink-muted tabular-nums text-[0.78rem]">
{formatDate(item.created_at)}
</TableCell>
<TableCell className="text-end">
<Button
variant="ghost"
size="sm"
onClick={onDelete}
disabled={del.isPending}
aria-label={`הסר את ${item.decision_number || "החלטה זו"} מהקורפוס`}
className="text-danger hover:text-danger hover:bg-danger-bg"
>
<Trash2 className="w-4 h-4" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
);
}
export function CorpusPanel() {
const { data, isPending, error } = useCorpus();
if (error) {
return (
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
{error.message}
</div>
);
}
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-rule-soft/60">
<TableRow className="border-rule">
<TableHead className="text-navy text-right">מס׳ החלטה</TableHead>
<TableHead className="text-navy text-right">תאריך</TableHead>
<TableHead className="text-navy text-right">נושאים</TableHead>
<TableHead className="text-navy text-right">תווים</TableHead>
<TableHead className="text-navy text-right">נוסף בתאריך</TableHead>
<TableHead className="text-navy" />
</TableRow>
</TableHeader>
<TableBody>
{isPending ? (
[...Array(4)].map((_, i) => (
<TableRow key={i} className="border-rule">
{[...Array(6)].map((_, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))
) : data?.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-ink-muted py-12">
הקורפוס ריק
</TableCell>
</TableRow>
) : (
data?.map((item) => <Row key={item.id} item={item} />)
)}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,195 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SubjectDonut } from "@/components/training/subject-donut";
import { useStyleReport } from "@/lib/api/training";
function KPICard({
label,
value,
caption,
}: {
label: string;
value: string;
caption?: string;
}) {
return (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 flex flex-col gap-0.5">
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
{label}
</span>
<span className="font-display text-[2rem] font-black leading-none text-navy">
{value}
</span>
{caption && (
<span className="text-[0.78rem] text-ink-muted mt-1">{caption}</span>
)}
</CardContent>
</Card>
);
}
export function StyleReportPanel() {
const { data, isPending, error } = useStyleReport();
if (error) {
return (
<Card className="bg-danger-bg border-danger/40">
<CardContent className="px-6 py-5 text-center text-danger">
{error.message}
</CardContent>
</Card>
);
}
if (isPending || !data) {
return (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-40 w-full" />
</div>
);
}
const c = data.corpus;
const dateRange =
c.date_range[0] && c.date_range[1]
? `${c.date_range[0]} ${c.date_range[1]}`
: undefined;
const total = c.decision_count;
const totalSubjects = c.subject_distribution.reduce((a, b) => a + b.count, 0);
return (
<div className="space-y-6">
{/* Headline */}
<Card className="bg-gold-wash border-gold/40 shadow-sm">
<CardContent className="px-6 py-4">
<p className="font-display text-gold-deep text-lg font-semibold leading-snug">
{c.headline}
</p>
</CardContent>
</Card>
{/* KPIs */}
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
<KPICard label="החלטות בקורפוס" value={String(c.decision_count)} />
<KPICard
label="סך תווים"
value={`${(c.total_chars / 1000).toFixed(0)}K`}
/>
<KPICard
label="ממוצע להחלטה"
value={`${(c.avg_chars / 1000).toFixed(1)}K`}
/>
<KPICard
label="דפוסי סגנון"
value={String(data.signature_phrases.items.length)}
caption={`מתוך ${data.contribution.total_patterns} שחולצו`}
/>
</div>
{/* Subjects + anatomy */}
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-lg mb-4">פיזור נושאים</h3>
<SubjectDonut
segments={c.subject_distribution}
total={totalSubjects}
/>
{dateRange && (
<p className="text-[0.72rem] text-ink-muted mt-4">
טווח תאריכים: {dateRange}
</p>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-lg mb-1">אנטומיה של החלטה ממוצעת</h3>
{data.anatomy.headline && (
<p className="text-[0.78rem] text-gold-deep mb-4">
{data.anatomy.headline}
</p>
)}
{data.anatomy.sections.length === 0 ? (
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
) : (
<ul className="space-y-2.5">
{data.anatomy.sections.map((s) => {
const pct = Math.round(s.pct * 100);
return (
<li key={s.type} className="space-y-1">
<div className="flex items-center justify-between text-[0.78rem]">
<span className="text-ink-soft font-medium">
{s.label}
</span>
<span className="text-ink-muted tabular-nums">
{pct}% · {s.avg_chars.toLocaleString()} תווים
</span>
</div>
<div className="h-2 rounded bg-rule-soft overflow-hidden">
<div
className="h-full bg-gradient-to-l from-gold to-gold-deep"
style={{ width: `${pct}%` }}
/>
</div>
</li>
);
})}
</ul>
)}
</CardContent>
</Card>
</div>
{/* Signature phrases */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
{data.signature_phrases.headline && (
<p className="text-[0.78rem] text-gold-deep mb-4">
{data.signature_phrases.headline}
</p>
)}
{data.signature_phrases.items.length === 0 ? (
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
) : (
<ol className="space-y-2">
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
<li
key={`${p.type}-${i}`}
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2"
>
<span className="text-[0.7rem] text-ink-muted tabular-nums shrink-0 mt-0.5">
#{i + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
{p.context && (
<p className="text-[0.7rem] text-ink-muted mt-0.5">
{p.context}
</p>
)}
</div>
<span
className="
shrink-0 text-[0.72rem] rounded-full
bg-gold-wash text-gold-deep border border-gold/40
px-2 py-0.5 tabular-nums
"
>
×{p.frequency}
</span>
</li>
))}
</ol>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
/*
* Corpus subject-distribution donut.
*
* Pure CSS conic-gradient — same recipe as the cases StatusDonut, but
* uses a palette-of-gold instead of a status-tone palette. Ported from
* legal-ai/web/static/index.html `renderHero`.
*/
const DONUT_COLORS = [
"var(--color-navy)",
"var(--color-gold)",
"var(--color-info)",
"var(--color-warn)",
"var(--color-success)",
"var(--color-ink-muted)",
"var(--color-gold-deep)",
];
export function SubjectDonut({
segments,
total,
}: {
segments: Array<{ label: string; count: number }>;
total: number;
}) {
let pct = 0;
const parts = segments.map((s, i) => {
const start = total === 0 ? 0 : (pct / total) * 360;
pct += s.count;
const end = total === 0 ? 360 : (pct / total) * 360;
return { ...s, start, end, color: DONUT_COLORS[i % DONUT_COLORS.length] };
});
const background =
total === 0
? "conic-gradient(var(--color-rule-soft) 0deg 360deg)"
: `conic-gradient(${parts
.map((p) => `${p.color} ${p.start}deg ${p.end}deg`)
.join(", ")})`;
return (
<div className="flex items-center gap-6">
<div
className="relative w-[140px] h-[140px] rounded-full shadow-sm shrink-0"
style={{ background }}
aria-label="פיזור נושאים בקורפוס"
>
<div className="absolute inset-[18px] bg-surface rounded-full flex flex-col items-center justify-center">
<span className="font-display text-2xl font-black text-navy leading-none">
{total}
</span>
<span className="text-[0.7rem] text-ink-muted mt-1">החלטות</span>
</div>
</div>
<ul className="flex flex-col gap-1.5 text-sm min-w-0">
{parts.map((p) => (
<li key={p.label} className="flex items-center gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
style={{ background: p.color }}
/>
<span className="text-ink-soft truncate">{p.label}</span>
<span className="text-ink-muted tabular-nums ms-1">{p.count}</span>
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,168 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 start-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 end-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,269 @@
"use client"
import * as React from "react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
align = "start",
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute end-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:ps-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute end-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:ps-7",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ms-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:ps-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="rtl:rotate-180 ms-auto" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,116 @@
"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
/*
* Tiny markdown renderer for Hebrew prose blocks — paragraphs, lists,
* emphasis, and GFM tables (the main reason this exists). The parsed
* research_md fields and the conclusions field both contain tables
* like "ציר דיוני" that we want to render as real <table>s, RTL, with
* auto-sized columns that line up row-to-row.
*
* Table styling uses `table-auto` + `whitespace-nowrap` on header cells
* so the column widths are dictated by the longest cell in that column,
* and every row's borders align exactly underneath each other. The
* overflow-x-auto wrapper catches extremely wide tables on narrow
* viewports without letting the parent card grow.
*/
export function Markdown({ content }: { content: string }) {
return (
<div className="prose-md text-sm text-ink-soft leading-relaxed" dir="rtl">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ node: _n, ...props }) => (
<p className="mb-2 last:mb-0 text-justify" {...props} />
),
strong: ({ node: _n, ...props }) => (
<strong className="text-navy font-semibold" {...props} />
),
em: ({ node: _n, ...props }) => (
<em className="text-ink" {...props} />
),
a: ({ node: _n, ...props }) => (
<a
className="text-gold-deep hover:text-gold underline underline-offset-2"
target="_blank"
rel="noreferrer"
{...props}
/>
),
ul: ({ node: _n, ...props }) => (
<ul className="list-disc ps-5 mb-2 space-y-1" {...props} />
),
ol: ({ node: _n, ...props }) => (
<ol className="list-decimal ps-5 mb-2 space-y-1" {...props} />
),
li: ({ node: _n, ...props }) => (
<li className="text-ink" {...props} />
),
h1: ({ node: _n, ...props }) => (
<h3 className="text-navy text-base font-semibold mt-3 mb-1" {...props} />
),
h2: ({ node: _n, ...props }) => (
<h4 className="text-navy text-sm font-semibold mt-3 mb-1" {...props} />
),
h3: ({ node: _n, ...props }) => (
<h5 className="text-navy text-sm font-semibold mt-2 mb-1" {...props} />
),
blockquote: ({ node: _n, ...props }) => (
<blockquote
className="border-e-2 border-gold-soft pe-3 text-ink italic my-2"
{...props}
/>
),
code: ({ node: _n, ...props }) => (
<code
className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem] text-ink"
{...props}
/>
),
/* ── Tables ─────────────────────────────────────────────────
Wrapped in an overflow-x-auto so very wide tables don't push
the parent card out of its track. table-auto lets the browser
size columns by their longest cell (that's what keeps borders
aligned row-to-row) and whitespace-nowrap on the headers
ensures the header row sets column widths instead of
breaking mid-word. */
table: ({ node: _n, ...props }) => (
<div className="my-3 -mx-1 overflow-x-auto">
<table
className="w-full table-auto border-collapse border border-rule text-sm text-right"
{...props}
/>
</div>
),
thead: ({ node: _n, ...props }) => (
<thead className="bg-rule-soft/70" {...props} />
),
tbody: ({ node: _n, ...props }) => <tbody {...props} />,
tr: ({ node: _n, ...props }) => (
<tr className="border-b border-rule last:border-b-0" {...props} />
),
th: ({ node: _n, ...props }) => (
<th
className="border border-rule px-3 py-2 text-right text-navy font-semibold whitespace-nowrap align-top"
{...props}
/>
),
td: ({ node: _n, ...props }) => (
<td
className="border border-rule px-3 py-2 text-right text-ink align-top"
{...props}
/>
),
hr: ({ node: _n, ...props }) => (
<hr className="my-3 border-rule" {...props} />
),
}}
>
{content}
</ReactMarkdown>
</div>
);
}

View File

@@ -0,0 +1,89 @@
"use client"
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-0.5 text-sm", className)}
{...props}
/>
)
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-heading font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
export {
Popover,
PopoverAnchor,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="size-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-s data-vertical:border-s-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,192 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pe-2 ps-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 rtl:data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 rtl:data-[side=right]:-translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute end-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@@ -0,0 +1,147 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-e data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-s data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button
variant="ghost"
className="absolute top-3 end-3"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-0.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"font-heading text-base font-medium text-foreground",
className
)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,49 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

Some files were not shown because too many files have changed in this diff Show More