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>
This commit is contained in:
2026-04-04 14:27:07 +00:00
parent d5ccf03e4c
commit 911c797eb2
21 changed files with 224 additions and 316 deletions

View File

@@ -0,0 +1,635 @@
#!/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);
});