Files
legal-ai/skills/decision/scripts/create-decision-structure.cjs
Chaim 911c797eb2 Reorganize: skills/ directory + move memory to docs/
skill-legal-decision/ → skills/decision/
skill-legal-assistant/ → skills/assistant/
skill-legal-docx/ → skills/docx/
memory/*.md → docs/

Also removed: TASKS.md (use TaskMaster), classifier.py (replaced by local_classifier.py)
Updated all references in CLAUDE.md, scripts, PRDs, docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:27:07 +00:00

636 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* create-decision-structure.js — טיוטת מבנה החלטת ועדת ערר
*
* מייצר קובץ DOCX מעוצב עם כל חלקי ההחלטה (בלוקים א-יב).
* בלוקים א-ט ממולאים בתוכן, בלוק י (דיון) ויא (סיכום) = placeholders.
*
* שימוש:
* node create-decision-structure.js <input.json> [output.docx]
*
* מבוסס על create-legal-doc.js מתוך legal-docx skill.
* כללי RTL: START/END (לא LEFT/RIGHT), bidi+bidirectional+rightToLeft בכל רמה.
*
* v1.0
*/
const fs = require('fs');
const path = require('path');
const {
Document, Packer, Paragraph, TextRun, Header, Footer,
AlignmentType, HeadingLevel, PageNumber, LevelFormat,
Table, TableRow, TableCell, WidthType, BorderStyle,
ShadingType, UnderlineType
} = require(path.join(process.cwd(), 'node_modules', 'docx'));
// ═══════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════
const FONT = "David";
const FONT_SIZE = 24; // 12pt
const HEADING1_SIZE = 32; // 16pt — "החלטה"
const HEADING2_SIZE = 28; // 14pt — כותרות פרקים
const MARGIN_CM = 2.5;
const MARGIN_DXA = Math.round(MARGIN_CM / 2.54 * 1440); // 1417
const PAGE_WIDTH = 11906; // A4
const PAGE_HEIGHT = 16838;
const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_DXA * 2; // 9072
const noBorders = {
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }
};
// ═══════════════════════════════════════════════
// RTL HELPERS — מבוסס על create-legal-doc.js
// ═══════════════════════════════════════════════
const rtlRun = (text, opts = {}) => new TextRun({
text,
font: opts.font || FONT,
size: opts.size || FONT_SIZE,
bold: opts.bold || false,
underline: opts.underline ? { type: UnderlineType.SINGLE } : undefined,
rightToLeft: true,
});
const rtlPara = (children, opts = {}) => new Paragraph({
bidirectional: true,
alignment: opts.alignment || AlignmentType.BOTH,
spacing: opts.spacing || { after: 120, line: 276 }, // 1.15 line spacing = 276 twips
indent: opts.indent,
children: Array.isArray(children) ? children : [children],
...(opts.heading ? { heading: opts.heading } : {}),
});
// כותרת ראשית — "החלטה"
const mainTitle = (text) => rtlPara(
rtlRun(text, { bold: true, size: HEADING1_SIZE }),
{ heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, spacing: { before: 240, after: 240 } }
);
// כותרת פרק — "תמצית טענות הצדדים", "דיון והכרעה"
const sectionTitle = (text) => rtlPara(
rtlRun(text, { bold: true, size: HEADING2_SIZE, underline: true }),
{ heading: HeadingLevel.HEADING_2, alignment: AlignmentType.CENTER, spacing: { before: 360, after: 240 } }
);
// כותרת משנה — "טענות העוררים"
const subTitle = (text) => rtlPara(
rtlRun(text, { bold: true, size: FONT_SIZE }),
{ alignment: AlignmentType.CENTER, spacing: { before: 240, after: 160 } }
);
// סעיף ממוספר — מספר bold + טקסט רגיל
const numberedPara = (num, text) => rtlPara([
rtlRun(`${num}. `, { bold: true }),
rtlRun(text),
], { spacing: { after: 120, line: 276 } });
// ציטוט (blockquote) — הזחה משני הצדדים
const blockquote = (text) => rtlPara(
rtlRun(text),
{ indent: { left: 567, right: 567 }, spacing: { before: 120, after: 120, line: 276 } }
);
// תיבת תמונה — מסגרת אפורה עם הנחיה
const imageBox = (description) => new Paragraph({
bidirectional: true,
alignment: AlignmentType.CENTER,
spacing: { before: 200, after: 200 },
shading: { type: ShadingType.CLEAR, fill: "F0F0F0" },
children: [
new TextRun({
text: `📷 תמונה: ${description}`,
font: FONT,
size: FONT_SIZE,
rightToLeft: true,
bold: true,
}),
],
});
// Placeholder — טקסט אפור שמסמן מקום
const placeholder = (text) => rtlPara(
new TextRun({
text: `[${text}]`,
font: FONT,
size: FONT_SIZE,
rightToLeft: true,
italics: true,
color: "808080",
}),
{ alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 } }
);
// רווח
const spacer = (after = 200) => rtlPara(rtlRun(""), { spacing: { after, before: 0 } });
// תא בטבלה
const rtlCell = (children, width, opts = {}) => new TableCell({
borders: noBorders,
width: { size: width, type: WidthType.DXA },
children: Array.isArray(children) ? children : [children],
...(opts.verticalAlign ? { verticalAlign: opts.verticalAlign } : {}),
});
// ═══════════════════════════════════════════════
// BLOCK BUILDERS — בוני הבלוקים
// ═══════════════════════════════════════════════
// בלוק א — כותרת מוסדית (טבלה 2 טורים)
function buildInstitutionalHeader(data) {
const leftCol = CONTENT_WIDTH * 0.5;
const rightCol = CONTENT_WIDTH * 0.5;
// צד ימין — מוסד
const rightCellContent = [
rtlPara(rtlRun("מדינת ישראל", { bold: true }), { alignment: AlignmentType.START, spacing: { after: 0 } }),
rtlPara(rtlRun("ועדת ערר לתכנון ובניה"), { alignment: AlignmentType.START, spacing: { after: 0 } }),
rtlPara(rtlRun("מחוז ירושלים"), { alignment: AlignmentType.START, spacing: { after: 80 } }),
];
// צד שמאל — מספרי תיק
const leftLines = [];
if (data.case_numbers) {
data.case_numbers.forEach(cn => {
leftLines.push(rtlPara([
rtlRun("מס' תיק: "),
rtlRun(cn, { bold: true }),
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
});
}
if (data.plan_number) {
leftLines.push(rtlPara([
rtlRun("מס' תכנית: "),
rtlRun(data.plan_number, { bold: true }),
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
}
if (data.request_number) {
leftLines.push(rtlPara([
rtlRun("מס' בקשה: "),
rtlRun(data.request_number, { bold: true }),
], { alignment: AlignmentType.START, spacing: { after: 0 } }));
}
return new Table({
visuallyRightToLeft: true,
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: [rightCol, leftCol],
rows: [
new TableRow({
children: [
rtlCell(rightCellContent, rightCol),
rtlCell(leftLines.length ? leftLines : [rtlPara(rtlRun(""))], leftCol),
]
})
]
});
}
// בלוק ב — הרכב הוועדה
function buildPanel(data) {
const lines = [];
lines.push(spacer(120));
lines.push(rtlPara([
rtlRun("בפני:", { bold: true }),
], { spacing: { after: 40 } }));
lines.push(rtlPara([
rtlRun("יו\"ר הוועדה: ", { bold: true }),
rtlRun(data.panel?.chair || "עו\"ד דפנה תמיר"),
], { spacing: { after: 40 } }));
if (data.panel?.members) {
lines.push(rtlPara([
rtlRun("חברי הוועדה: ", { bold: true }),
rtlRun(data.panel.members[0] || ""),
], { spacing: { after: 40 } }));
for (let i = 1; i < data.panel.members.length; i++) {
lines.push(rtlPara(rtlRun(data.panel.members[i]), { spacing: { after: 40 } }));
}
}
return lines;
}
// בלוק ג — צדדים
function buildParties(data) {
const lines = [];
lines.push(spacer(120));
// עוררים
if (data.appellants) {
const label = data.appellants.length > 1 ? "העוררים:" : "העורר:";
lines.push(rtlPara(rtlRun(label, { bold: true }), { spacing: { after: 40 } }));
data.appellants.forEach(a => {
lines.push(rtlPara(rtlRun(a.name), { spacing: { after: 20 } }));
if (a.representative) {
lines.push(rtlPara(rtlRun(`ע"י ב"כ ${a.representative}`), { spacing: { after: 20 } }));
}
});
}
// נגד
lines.push(spacer(80));
lines.push(rtlPara(rtlRun("נגד", { bold: true }), {
alignment: AlignmentType.CENTER,
spacing: { before: 80, after: 80 }
}));
// משיבים
if (data.respondents) {
lines.push(rtlPara(rtlRun("המשיבים:", { bold: true }), { spacing: { after: 40 } }));
data.respondents.forEach((r, i) => {
lines.push(rtlPara(rtlRun(`${i + 1}. ${r.name}`), { spacing: { after: 20 } }));
if (r.representative) {
lines.push(rtlPara(rtlRun(`ע"י ב"כ ${r.representative}`), { spacing: { after: 20 } }));
}
});
}
return lines;
}
// בלוק ה — פתיחה
function buildOpening(data) {
const paras = [];
if (data.opening_paragraphs) {
data.opening_paragraphs.forEach((text, i) => {
paras.push(numberedPara(i + 1, text));
});
} else {
paras.push(numberedPara(1, `לפנינו ${data.appeal_description || "[תיאור הערר]"}.`));
}
return paras;
}
// בלוק ו — רקע עובדתי
function buildBackground(data) {
const paras = [];
let num = (data.opening_paragraphs?.length || 1) + 1;
if (data.use_petach_davar !== false) {
paras.push(sectionTitle("פתח דבר"));
}
// מקרקעין
if (data.property_description) {
paras.push(numberedPara(num++, data.property_description));
} else {
paras.push(numberedPara(num++, "[תיאור המקרקעין — מיקום, שטח, שכונה, ייעוד, מאפיינים ייחודיים]"));
}
// היסטוריה תכנונית
if (data.planning_history) {
data.planning_history.forEach(text => {
paras.push(numberedPara(num++, text));
});
} else {
paras.push(numberedPara(num++, "[היסטוריה תכנונית — תכניות קודמות, החלטות קודמות, היתרים]"));
}
// תמונה — מיקום
paras.push(imageBox("תשריט מיקום המגרש מתוך מערכת GIS — לסמן את המגרש"));
// מהות הבקשה
if (data.request_essence) {
if (Array.isArray(data.request_essence)) {
data.request_essence.forEach(text => {
paras.push(numberedPara(num++, text));
});
} else {
paras.push(numberedPara(num++, data.request_essence));
}
} else {
paras.push(numberedPara(num++, "[מהות הבקשה — פירוט מלא של מה שהתבקש]"));
}
// תמונה — תשריט
paras.push(imageBox("תשריט הבקשה / נספח בינוי / תכנית מוצעת"));
// ציטוט מפרוטוקול
if (data.committee_protocol_quote) {
paras.push(numberedPara(num++, "להלן מתוך פרוטוקול הדיון בוועדה המקומית:"));
paras.push(blockquote(data.committee_protocol_quote));
} else {
paras.push(numberedPara(num++, "[ציטוט מלא מפרוטוקול הוועדה המקומית]"));
}
// החלטת הוועדה + תנאים
if (data.committee_decision) {
paras.push(numberedPara(num++, data.committee_decision));
} else {
paras.push(numberedPara(num++, "[החלטת הוועדה המקומית — מה הוחלט, אילו תנאים נקבעו]"));
}
// תמונה אופציונלית — סביבה
if (data.include_aerial_photo !== false) {
paras.push(imageBox("צילום אוויר / מבט על הסביבה עם סימון המגרש"));
}
// סביבת המקרקעין
if (data.surroundings) {
paras.push(numberedPara(num++, data.surroundings));
}
// הגשת הערר
if (data.appeal_filing) {
paras.push(numberedPara(num++, data.appeal_filing));
} else {
paras.push(numberedPara(num++, "[הגשת הערר — תאריך, מי הגיש, הצטרפויות]"));
}
return { paras, nextNum: num };
}
// בלוק ז — טענות הצדדים
function buildClaims(data, startNum) {
const paras = [];
let num = startNum;
paras.push(sectionTitle("תמצית טענות הצדדים"));
// טענות העוררים
if (data.appellant_claims_sections) {
// מספר עוררים עם כותרות נפרדות
data.appellant_claims_sections.forEach(section => {
paras.push(subTitle(section.title));
section.claims.forEach(text => {
paras.push(numberedPara(num++, text));
});
});
} else {
paras.push(subTitle("טענות העוררים"));
if (data.appellant_claims) {
data.appellant_claims.forEach(text => {
paras.push(numberedPara(num++, text));
});
} else {
paras.push(numberedPara(num++, "[טענות העוררים]"));
}
}
// עמדת הוועדה המקומית
paras.push(subTitle("עמדת הוועדה המקומית"));
if (data.committee_position) {
data.committee_position.forEach(text => {
paras.push(numberedPara(num++, text));
});
} else {
paras.push(numberedPara(num++, "[עמדת הוועדה המקומית]"));
}
// עמדת מבקשי ההיתר / מגישי התכנית
const applicantTitle = data.type === "plan"
? "עמדת מגישי התכנית"
: "עמדת מבקשי ההיתר";
paras.push(subTitle(applicantTitle));
if (data.applicant_position) {
data.applicant_position.forEach(text => {
paras.push(numberedPara(num++, text));
});
} else {
paras.push(numberedPara(num++, `[${applicantTitle}]`));
}
return { paras, nextNum: num };
}
// בלוק ח — ההליכים בפני ועדת הערר
function buildProceedings(data, startNum) {
const paras = [];
let num = startNum;
paras.push(sectionTitle("ההליכים בפני ועדת הערר"));
if (data.proceedings) {
data.proceedings.forEach(item => {
if (item.type === "image") {
paras.push(imageBox(item.description));
} else if (item.type === "quote") {
paras.push(numberedPara(num++, item.intro || ""));
paras.push(blockquote(item.text));
} else {
paras.push(numberedPara(num++, item.text));
}
});
} else {
paras.push(numberedPara(num++, "[דיון — תאריך, נוכחים, עיקרי הדברים]"));
paras.push(numberedPara(num++, "[סיור (אם היה) — תאריך, תיאור]"));
paras.push(imageBox("צילומים מהסיור"));
paras.push(numberedPara(num++, "[החלטות ביניים]"));
paras.push(numberedPara(num++, "[השלמות טיעון — כרונולוגי]"));
paras.push(numberedPara(num++, "[חוו\"ד מקצועיות שהתקבלו]"));
paras.push(imageBox("הדמיות / חתכי בינוי מהשלמות טיעון (אם יש)"));
}
return { paras, nextNum: num };
}
// בלוק ט — תכניות חלות (אופציונלי)
function buildPlans(data, startNum) {
if (data.skip_plans_section) return { paras: [], nextNum: startNum };
const paras = [];
let num = startNum;
paras.push(sectionTitle("התכניות החלות על המקרקעין"));
if (data.applicable_plans) {
data.applicable_plans.forEach(item => {
if (item.type === "quote") {
paras.push(numberedPara(num++, item.intro || ""));
paras.push(blockquote(item.text));
} else {
paras.push(numberedPara(num++, item.text));
}
});
} else {
paras.push(numberedPara(num++, "[פירוט התכניות הרלוונטיות עם ציטוט מהוראותיהן]"));
}
return { paras, nextNum: num };
}
// בלוק יב — חתימות
function buildSignatures(data) {
const chairName = data.panel?.chair || "עו\"ד דפנה תמיר";
const secretaryName = data.secretary || "";
const halfWidth = Math.floor(CONTENT_WIDTH / 2);
return [
spacer(400),
rtlPara(rtlRun("ניתנה פה אחד, היום ______________, ______________."), {
spacing: { after: 400 }
}),
new Table({
visuallyRightToLeft: true,
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: [halfWidth, halfWidth],
rows: [
new TableRow({
children: [
rtlCell([
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }),
rtlPara(rtlRun(chairName, { bold: true }), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
rtlPara(rtlRun("יו\"ר ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
], halfWidth),
rtlCell([
rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }),
rtlPara(rtlRun(secretaryName || ""), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
rtlPara(rtlRun("מזכירת ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }),
], halfWidth),
]
})
]
}),
];
}
// ═══════════════════════════════════════════════
// MAIN — הרכבת המסמך
// ═══════════════════════════════════════════════
function buildDocument(data) {
const content = [];
// בלוק א — כותרת מוסדית
content.push(buildInstitutionalHeader(data));
content.push(spacer(160));
// בלוק ב — הרכב
content.push(...buildPanel(data));
content.push(spacer(80));
// בלוק ג — צדדים
content.push(...buildParties(data));
content.push(spacer(160));
// בלוק ד — כותרת "החלטה"
content.push(mainTitle("החלטה"));
// בלוק ה — פתיחה
content.push(...buildOpening(data));
// בלוק ו — רקע
const bg = buildBackground(data);
content.push(...bg.paras);
// בלוק ז — טענות
const claims = buildClaims(data, bg.nextNum);
content.push(...claims.paras);
// בלוק ח — הליכים
const proc = buildProceedings(data, claims.nextNum);
content.push(...proc.paras);
// בלוק ט — תכניות (אופציונלי)
const plans = buildPlans(data, proc.nextNum);
content.push(...plans.paras);
// בלוק י — דיון והכרעה (placeholder)
content.push(sectionTitle("דיון והכרעה"));
content.push(placeholder("כאן מתחיל פרק הדיון וההכרעה — ייכתב בשלב הבא"));
// בלוק יא — סיכום (placeholder)
content.push(sectionTitle("סיכום"));
content.push(placeholder("ייכתב לאחר השלמת פרק הדיון"));
// בלוק יב — חתימות
content.push(...buildSignatures(data));
return new Document({
styles: {
default: {
document: {
run: { font: FONT, size: FONT_SIZE, rightToLeft: true },
paragraph: { bidirectional: true, alignment: AlignmentType.BOTH }
}
},
paragraphStyles: [
{
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal",
quickFormat: true,
run: { size: HEADING1_SIZE, bold: true, font: FONT, rightToLeft: true },
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0,
bidirectional: true, alignment: AlignmentType.CENTER }
},
{
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal",
quickFormat: true,
run: { size: HEADING2_SIZE, bold: true, font: FONT, rightToLeft: true },
paragraph: { spacing: { before: 200, after: 200 }, outlineLevel: 1,
bidirectional: true, alignment: AlignmentType.CENTER }
},
]
},
sections: [{
properties: {
page: {
size: { width: PAGE_WIDTH, height: PAGE_HEIGHT },
margin: { top: MARGIN_DXA, right: MARGIN_DXA, bottom: MARGIN_DXA, left: MARGIN_DXA }
},
bidi: true,
},
footers: {
default: new Footer({
children: [new Paragraph({
bidirectional: true,
alignment: AlignmentType.CENTER,
children: [
rtlRun("עמוד ", { size: 18 }),
new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18 }),
rtlRun(" מתוך ", { size: 18 }),
new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18 }),
]
})]
})
},
children: content
}]
});
}
// ═══════════════════════════════════════════════
// CLI
// ═══════════════════════════════════════════════
async function main() {
const inputFile = process.argv[2];
if (!inputFile) {
console.error('שימוש: node create-decision-structure.js <input.json> [output.docx]');
console.error('');
console.error('קובץ ה-JSON צריך לכלול:');
console.error(' case_numbers, panel, appellants, respondents,');
console.error(' opening_paragraphs, property_description, planning_history,');
console.error(' request_essence, committee_protocol_quote, committee_decision,');
console.error(' appellant_claims, committee_position, applicant_position,');
console.error(' proceedings, applicable_plans');
console.error('');
console.error('ראה: .claude/skills/legal-decision/references/decision-template.json');
process.exit(1);
}
const data = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
const outputFile = process.argv[3] || `החלטה-ערר-${(data.case_numbers?.[0] || 'draft').replace(/\//g, '-')}-מבנה.docx`;
const doc = buildDocument(data);
const buffer = await Packer.toBuffer(doc);
fs.writeFileSync(outputFile, buffer);
console.log(`${outputFile}`);
console.log(` פונט: ${FONT} ${FONT_SIZE / 2}pt`);
console.log(` שוליים: ${MARGIN_CM} ס`);
console.log(` RTL: bidi + bidirectional + rightToLeft ✓`);
console.log(` Alignment: START/END ✓`);
console.log(` גודל: ${(buffer.length / 1024).toFixed(1)} KB`);
}
main().catch(err => {
console.error('❌ שגיאה:', err.message);
process.exit(1);
});