From 2f6bbe55d81575ee9458bbc5ff629649c199b533 Mon Sep 17 00:00:00 2001 From: Francesco Vadicamo Date: Mon, 13 Jul 2026 18:58:04 +0000 Subject: [PATCH] test(eval): add the textbook collection (TECH-005 collection 1) Second evaluation corpus: one long Italian legal textbook (CC BY-SA 4.0), 449 child chunks, whose prose depends on the surrounding section. The existing corpora are short self-contained legal articles, which understate anything context-dependent (parent expansion in particular) and, in the excerpt case, saturate the hit rate. - tests/eval/dataset-textbook.jsonl: 60 Italian questions (24 factual, 15 reasoning, 12 multi-chunk, 9 adversarial), authored from full chapters, keywords verified unique to their section. - scripts/eval_clean_textbook.py: reproducible PDF cleaning (font-size classification, footnote and header removal, hyphenation repair, structure preserved as headings). - tests/eval/README.md: collection matrix, corpus provenance with the licensing rule (ND clauses forbid a chunked derivative), the ground-truth methodology including the closed-book control, and an explicit validation-status table. First baseline (MiniLM, dual, top_k=5): hit 78.4%, MRR 0.5873, against 82.6% / 0.7029 on eval-full. The textbook is the harder bench, which is the point. The ground truth is machine-validated and passed the closed-book control (0 keywords leaked, 53/60 answered NON SO), but the human spot-check is still open, so this stays in draft. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/eval_clean_textbook.py | 536 ++++++++++++++++++++++++++++++ tests/eval/README.md | 89 ++++- tests/eval/dataset-textbook.jsonl | 60 ++++ 3 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 scripts/eval_clean_textbook.py create mode 100644 tests/eval/dataset-textbook.jsonl diff --git a/scripts/eval_clean_textbook.py b/scripts/eval_clean_textbook.py new file mode 100644 index 00000000..da4b88ec --- /dev/null +++ b/scripts/eval_clean_textbook.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python3 +""" +Clean-extract "Diritto penale del lavoro" (Carlo Smuraglia, Milano University +Press 2025, CC BY-SA 4.0) from its PDF text layer into a structured markdown +corpus for RAG evaluation. + +Run with: + uv run --with pdfplumber python scripts/eval_clean_textbook.py + +Source PDF (CC BY-SA 4.0, free download): + https://libri.unimi.it/index.php/milanoup/catalog/download/236/841/2256?inline=1 + sha256 ef43d9cf11f52d8f7f17c97c24ee4bbb9cdcb2836de12ebce94340cb15fb3ec5 + +Method (measured, not guessed -- see the printed report): + - Font-size based classification of every text line on every page: + body prose ~= 11.0pt (dominant size, ~604k chars) + footnotes ~= 9.0pt (~304k chars) -> dropped entirely + running header~= 10.0pt, first line on the page, top < 60pt -> dropped + headings >= 13pt (14pt = numbered sub-section, 18pt = chapter/ + front-matter section) -> converted to markdown headings + superscript footnote markers ~6.3-6.5pt embedded mid-body -> stripped + small-caps continuation letters ~7.3-7.4pt embedded mid-body (Italian + legal-citation convention, e.g. "Mazzoni" typeset as a + normal "M" + reduced-size "azzoni") -> KEPT, merged + into the surrounding word (dropping them would corrupt + names inline in body prose). + - Paragraph reconstruction uses the first-line indent (x0 ~= 87.9pt vs the + flush margin x0 ~= 76.5pt) as the paragraph-break signal. + - End-of-line hyphenation is repaired using a self-referential heuristic: + any "word-word" hyphen pair that appears INLINE (not at a line break) + anywhere in the body is treated as a genuine compound and its hyphen is + preserved; a hyphen at a line break that never appears inline elsewhere + is treated as a soft (typesetting) break and is removed on join. + - Front matter (cover, colophon, licence, Piano dell'opera, Comitato + Scientifico/Editoriale, book-level Sommario/TOC) is dropped by skipping + everything before the "Presentazione" heading. + - The per-chapter in-body "Sommario" paragraph (an outline of the coming + sub-sections, printed right after each chapter title) is dropped: it + duplicates the sub-section headings and is not prose. + - The back-matter "Bibliografia essenziale" (a ~13-page author-sorted + citation list, no prose) is dropped: extraction stops at that heading, + treated the same as footnotes (dense citation apparatus). +""" + +import re +import sys +from collections import Counter + +import pdfplumber + +PDF_PATH = sys.argv[1] if len(sys.argv) > 1 else "smuraglia-diritto-penale-lavoro.pdf" +OUTPUT_MD = ( + sys.argv[2] if len(sys.argv) > 2 else "smuraglia-diritto-penale-lavoro-clean.md" +) + +BOOK_TITLE = "Diritto penale del lavoro" + +ROMAN_MAP = { + "I": 1, + "II": 2, + "III": 3, + "IV": 4, + "V": 5, + "VI": 6, + "VII": 7, + "VIII": 8, + "IX": 9, + "X": 10, + "XI": 11, + "XII": 12, + "XIII": 13, + "XIV": 14, + "XV": 15, +} + +BODY_SIZE_LO, BODY_SIZE_HI = 10.9, 11.1 # body prose ~11.0pt +FOOTNOTE_SIZE_LO, FOOTNOTE_SIZE_HI = 8.9, 9.1 # footnotes ~9.0pt +HEADER_SIZES = (10.0, 10.5) # running header line +HEADER_TOP_MAX = 60.0 # header always at top of page +HEADING_MIN_SIZE = 13.0 # 14pt / 18pt headings +SMALLCAPS_MIN_SIZE = 7.0 # keep >= this within body lines +INDENT_X0_THRESHOLD = 82.0 # between flush(76.5) and indent(87.9) + +FRONT_MATTER_HEADING = "Presentazione" +BIBLIOGRAPHY_HEADING_PREFIX = "Bibliografia essenziale" + +TOP_LEVEL_SECTIONS = {"Presentazione", "Premessa al VI volume", "Introduzione"} + + +# --------------------------------------------------------------------------- +# Low-level PDF line grouping +# --------------------------------------------------------------------------- + + +def group_lines(chars, tol=4): + """Cluster chars into visual lines by proximity of their 'top' coordinate. + + Uses interval-merge clustering (not fixed rounding) so that a single + visual line whose glyphs have slightly different baselines (e.g. mixed + font sizes on one line, observed on the Capitolo IV heading) is still + grouped as one line, while distinct lines (13pt+ apart) stay separate. + """ + chars = sorted(chars, key=lambda c: c["top"]) + clusters = [] + cur, cur_max = [], None + for c in chars: + if cur and c["top"] > cur_max + tol: + clusters.append(cur) + cur, cur_max = [], None + cur.append(c) + cur_max = c["top"] if cur_max is None else max(cur_max, c["top"]) + if cur: + clusters.append(cur) + clusters.sort(key=lambda lc: min(c["top"] for c in lc)) + return clusters + + +def line_text(chars, min_size=0.0): + """Concatenate chars left-to-right, dropping any char smaller than min_size.""" + kept = [c for c in chars if c["size"] >= min_size] + return "".join(c["text"] for c in sorted(kept, key=lambda c: c["x0"])) + + +def has_size_near(chars, lo, hi): + return any(lo <= c["size"] <= hi for c in chars) + + +def dominant_size(chars): + sizes = Counter(round(c["size"], 1) for c in chars) + return sizes.most_common(1)[0][0] + + +# --------------------------------------------------------------------------- +# Pass 1: find genuine inline hyphenated compounds (self-referential lexicon) +# --------------------------------------------------------------------------- + +WORD_RE = r"[A-Za-zÀ-ÖØ-öø-ÿ]+" +INLINE_COMPOUND_RE = re.compile(rf"({WORD_RE})-({WORD_RE})") +TRAILING_HYPHEN_WORD_RE = re.compile(rf"({WORD_RE})-\s*$") +LEADING_WORD_RE = re.compile(rf"^({WORD_RE})") + + +def find_known_compounds(pdf, start_idx, end_idx): + """Scan body-size lines for '-' that is NOT the last character of the + line (i.e. not a line-wrap artifact): those are unambiguous genuine + hyphenated compounds. Build a lowercase set of 'word-word' pairs. + """ + known = set() + for pno in range(start_idx, end_idx): + page = pdf.pages[pno] + if not page.chars: + continue + for lc in group_lines(page.chars): + if not has_size_near(lc, BODY_SIZE_LO, BODY_SIZE_HI): + continue + text = line_text(lc, min_size=SMALLCAPS_MIN_SIZE) + stripped = text.rstrip() + if stripped.endswith("-"): + # this hyphen IS at line end -> not usable for pass 1 + body_wo_trailing = stripped[:-1] + else: + body_wo_trailing = stripped + for m in INLINE_COMPOUND_RE.finditer(body_wo_trailing): + known.add(f"{m.group(1).lower()}-{m.group(2).lower()}") + return known + + +# --------------------------------------------------------------------------- +# Pass 2: locate structural boundaries (front matter end / bibliography start) +# --------------------------------------------------------------------------- + + +def find_heading_blocks(pdf, start_idx, end_idx): + """Yield (page_idx, text) for every consecutive run of heading-size + (>=13pt) lines on pages [start_idx, end_idx).""" + for pno in range(start_idx, end_idx): + page = pdf.pages[pno] + if not page.chars: + continue + lines = group_lines(page.chars) + block = [] + for lc in lines: + if ( + has_size_near(lc, 13.0, 100.0) + or max(c["size"] for c in lc) >= HEADING_MIN_SIZE + ): + block.append(lc) + else: + if block: + text = " ".join(line_text(ln, min_size=10.0) for ln in block) + yield pno, re.sub(r"\s+", " ", text).strip() + block = [] + if block: + text = " ".join(line_text(ln, min_size=10.0) for ln in block) + yield pno, re.sub(r"\s+", " ", text).strip() + + +def locate_boundaries(pdf): + n = len(pdf.pages) + body_start = None + body_end = None + for pno, text in find_heading_blocks(pdf, 0, n): + if body_start is None and text.startswith(FRONT_MATTER_HEADING): + body_start = pno + if body_start is not None and text.startswith(BIBLIOGRAPHY_HEADING_PREFIX): + body_end = pno + break + if body_start is None: + raise RuntimeError( + f"Could not find front-matter boundary heading {FRONT_MATTER_HEADING!r}" + ) + if body_end is None: + raise RuntimeError( + f"Could not find back-matter boundary heading {BIBLIOGRAPHY_HEADING_PREFIX!r}" + ) + return body_start, body_end + + +# --------------------------------------------------------------------------- +# Pass 3: main extraction +# --------------------------------------------------------------------------- + + +class Stats: + def __init__(self): + self.pages_processed = 0 + self.headers_removed = 0 + self.footnote_blocks_removed = 0 + self.footnote_lines_removed = 0 + self.footnote_words_removed = 0 + self.stray_marker_lines_dropped = 0 + self.sommario_paragraphs_dropped = 0 + self.sommario_words_dropped = 0 + self.headings = [] # (level, text) + self.body_words_kept = 0 + self.hyphen_joins_soft = 0 + self.hyphen_joins_compound = 0 + self.unclassified_lines = [] + + +def roman_to_title(text): + m = re.match(r"^Capitolo\s+([IVXLC]+)\.?\s*(.*)$", text, re.IGNORECASE) + if not m: + return None, None + roman, title = m.group(1), m.group(2).strip() + title = re.sub( + r"\s*\*\s*$", "", title + ) # drop trailing footnote-marker asterisk if any survived + return roman, title + + +def repair_hyphen_join(prev_text, next_text, known_compounds, stats): + """Join prev_text (ends with '-') and next_text, deciding whether the + hyphen is a genuine compound (kept) or a soft line-break (removed).""" + m = TRAILING_HYPHEN_WORD_RE.search(prev_text) + m2 = LEADING_WORD_RE.match(next_text) + if not m or not m2: + # can't identify a clean word pair; default to keeping the hyphen as-is + return prev_text.rstrip() + next_text.lstrip() + word_before, word_after = m.group(1), m2.group(1) + key = f"{word_before.lower()}-{word_after.lower()}" + if key in known_compounds: + stats.hyphen_joins_compound += 1 + return prev_text.rstrip() + next_text.lstrip() + else: + stats.hyphen_joins_soft += 1 + return prev_text.rstrip()[:-1] + next_text.lstrip() + + +def extract_body(pdf, body_start, body_end, known_compounds, stats): + """Returns a list of markdown blocks (strings), in reading order.""" + + blocks = [] # finished output: ("h2"/"h3"/"p", text) + para_lines = [] # accumulator for the current paragraph + heading_lines = [] # accumulator for the current heading block + in_heading = False + current_chapter_num = None # arabic chapter number for ### N.M numbering + just_started_chapter = False # true right after emitting a ## Capitolo heading + + def flush_paragraph(): + nonlocal para_lines + if not para_lines: + return + text = para_lines[0] + for nxt in para_lines[1:]: + if text.rstrip().endswith("-"): + text = repair_hyphen_join(text, nxt, known_compounds, stats) + else: + text = text.rstrip() + " " + nxt.lstrip() + text = re.sub(r"[ \t]+", " ", text).strip() + para_lines = [] + return text + + def emit_paragraph(): + nonlocal just_started_chapter + text = flush_paragraph() + if not text: + return + if just_started_chapter and re.match(r"^Sommario\b", text): + stats.sommario_paragraphs_dropped += 1 + stats.sommario_words_dropped += len(text.split()) + just_started_chapter = False + return + just_started_chapter = False + blocks.append(("p", text)) + stats.body_words_kept += len(text.split()) + + def emit_heading(): + nonlocal heading_lines, current_chapter_num, just_started_chapter + if not heading_lines: + return + text = heading_lines[0] + for nxt in heading_lines[1:]: + if text.rstrip().endswith("-"): + text = repair_hyphen_join(text, nxt, known_compounds, stats) + else: + text = text.rstrip() + " " + nxt.lstrip() + text = re.sub(r"\s+", " ", text).strip() + text = re.sub(r"\s*\*\s*$", "", text) + heading_lines = [] + + roman, title = roman_to_title(text) + if roman: + arabic = ROMAN_MAP.get(roman.upper()) + current_chapter_num = arabic + heading_text = f"Capitolo {roman} - {title}" + blocks.append(("h2", heading_text)) + stats.headings.append(("h2", heading_text)) + just_started_chapter = True + return + + if any(text.startswith(s) for s in TOP_LEVEL_SECTIONS): + blocks.append(("h2", text)) + stats.headings.append(("h2", text)) + current_chapter_num = None + just_started_chapter = False + return + + m = re.match(r"^(\d+)\.\s*(.*)$", text) + if m: + num, title = int(m.group(1)), m.group(2).strip() + if current_chapter_num is not None: + heading_text = f"{current_chapter_num}.{num} {title}" + else: + heading_text = f"{num} {title}" + blocks.append(("h3", heading_text)) + stats.headings.append(("h3", heading_text)) + return + + stats.unclassified_lines.append(text) + blocks.append(("h2", text)) + stats.headings.append(("h2 (UNCLASSIFIED)", text)) + + for pno in range(body_start, body_end): + page = pdf.pages[pno] + if not page.chars: + continue + stats.pages_processed += 1 + lines = group_lines(page.chars) + + idx = 0 + # drop running header: first line on page, small size, near top + if lines: + first = lines[0] + if ( + min(c["top"] for c in first) < HEADER_TOP_MAX + and round(dominant_size(first), 1) in HEADER_SIZES + ): + idx = 1 + stats.headers_removed += 1 + + page_lines = lines[idx:] + for li, lc in enumerate(page_lines): + if has_size_near(lc, BODY_SIZE_LO, BODY_SIZE_HI): + # BODY line (possibly with small-caps decoration / stray markers) + if in_heading: + emit_heading() + in_heading = False + text = line_text(lc, min_size=SMALLCAPS_MIN_SIZE) + if not text.strip(): + continue + x0 = min(c["x0"] for c in lc if c["size"] >= SMALLCAPS_MIN_SIZE) + # An indented line only starts a genuine new paragraph if the + # text accumulated so far actually ended a sentence. This + # guards against hanging-indent list continuations (e.g. a + # lettered sub-list "a. ... \n di controllo ...") whose + # wrapped second line is indented further than the flush + # margin but is NOT a new paragraph -- confirmed by measurement + # (page idx 162: "a. norme che tendono a rendere possibile ed + # effettiva l'attività di vigilanza e" / "di controllo (e + # quindi ..." wrongly split without this guard). + prev_complete = (not para_lines) or para_lines[-1].rstrip().endswith( + (".", "?", "!", ":", ";", "»", "”", ")") + ) + is_new_paragraph = x0 > INDENT_X0_THRESHOLD and prev_complete + if is_new_paragraph and para_lines: + emit_paragraph() + para_lines.append(text) + elif max(c["size"] for c in lc) >= HEADING_MIN_SIZE: + # HEADING line + if para_lines: + emit_paragraph() + in_heading = True + heading_lines.append(line_text(lc, min_size=10.0)) + elif has_size_near(lc, FOOTNOTE_SIZE_LO, FOOTNOTE_SIZE_HI): + # footnote block begins: drop this line and everything else on the page + # (confirmed by measurement: footnotes never carry over to the + # next page and no page starts mid-footnote, so the remainder + # of the page's lines are all footnote content too). + # NOTE: do NOT flush para_lines here. The footnote apparatus sits + # at the bottom of the page, visually separate from body flow; the + # body paragraph in progress may legitimately continue on the next + # page's first line. Flushing here would wrongly split a sentence + # into two markdown paragraphs (found via manual page comparison, + # see e.g. "...dell'autorità" / "amministrativa, ..." on pages + # idx 294-295). + if in_heading: + emit_heading() + in_heading = False + stats.footnote_blocks_removed += 1 + remainder = page_lines[li:] + stats.footnote_lines_removed += len(remainder) + for rlc in remainder: + stats.footnote_words_removed += len(line_text(rlc).split()) + break + else: + # tiny stray content only (superscript footnote marker clustered alone) + stats.stray_marker_lines_dropped += 1 + continue + + if in_heading: + emit_heading() + if para_lines: + emit_paragraph() + + return blocks + + +# --------------------------------------------------------------------------- +# Rendering +# --------------------------------------------------------------------------- + + +def render_markdown(blocks): + out = [f"# {BOOK_TITLE}", ""] + for kind, text in blocks: + if kind == "h2": + out.append(f"## {text}") + out.append("") + elif kind == "h3": + out.append(f"### {text}") + out.append("") + else: + out.append(text) + out.append("") + return "\n".join(out).rstrip() + "\n" + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + stats = Stats() + with pdfplumber.open(PDF_PATH) as pdf: + n_pages = len(pdf.pages) + print(f"Total PDF pages: {n_pages}") + + body_start, body_end = locate_boundaries(pdf) + print( + f"Front matter dropped: pages 0-{body_start - 1} (0-indexed), " + f"{body_start} pages" + ) + print( + f"Body range: pages {body_start}-{body_end - 1} (0-indexed), " + f"{body_end - body_start} pages" + ) + print( + f"Back matter dropped from page {body_end} onward " + f"({n_pages - body_end} pages: Bibliografia essenziale + colophon)" + ) + + print( + "\nBuilding self-referential hyphenated-compound lexicon " + "from inline (non-line-break) hyphens..." + ) + known_compounds = find_known_compounds(pdf, body_start, body_end) + print(f"Known inline compounds found: {len(known_compounds)}") + + print("\nExtracting body...") + blocks = extract_body(pdf, body_start, body_end, known_compounds, stats) + + md = render_markdown(blocks) + with open(OUTPUT_MD, "w", encoding="utf-8") as f: + f.write(md) + + total_words = sum(len(t.split()) for k, t in blocks if k == "p") + + print("\n=== EXTRACTION REPORT ===") + print(f"Pages processed (body): {stats.pages_processed}") + print(f"Running headers removed: {stats.headers_removed}") + print( + f"Footnote blocks removed: {stats.footnote_blocks_removed} " + f"({stats.footnote_lines_removed} lines, " + f"~{stats.footnote_words_removed} words)" + ) + print(f"Stray superscript-marker lines dropped: {stats.stray_marker_lines_dropped}") + print( + f"Per-chapter 'Sommario' paragraphs dropped: {stats.sommario_paragraphs_dropped} " + f"(~{stats.sommario_words_dropped} words)" + ) + print( + f"Hyphen line-breaks joined as SOFT (hyphen removed): {stats.hyphen_joins_soft}" + ) + print(f"Hyphen line-breaks kept as GENUINE COMPOUND: {stats.hyphen_joins_compound}") + print(f"Headings detected: {len(stats.headings)}") + for level, text in stats.headings: + print(f" {level:>18}: {text}") + if stats.unclassified_lines: + print( + f"UNCLASSIFIED heading-size blocks (needs manual review): " + f"{len(stats.unclassified_lines)}" + ) + for t in stats.unclassified_lines: + print(f" ??? {t}") + print(f"\nFinal body word count: {total_words}") + print( + f"Estimated chunks at ~350 words/chunk (~500 tokens IT): " + f"{round(total_words / 350)}" + ) + print(f"\nOutput written to: {OUTPUT_MD}") + + +if __name__ == "__main__": + main() diff --git a/tests/eval/README.md b/tests/eval/README.md index e09afe41..3c4ddc4d 100644 --- a/tests/eval/README.md +++ b/tests/eval/README.md @@ -19,13 +19,30 @@ record aggregates in the active `.s2s/plans/` file and in vektra-internal). |------|-----------|--------| | `dataset.jsonl` | `default` | Excerpt corpus, 12 chunks: `costituzione_italiana.md` v2 (6), `udhr_excerpts.md` v2 (4), `sample.pdf` (2). Hit rate saturates here; useful for smoke/regression, not for tuning. | | `dataset-full.jsonl` | `eval-full` | Full-document corpus, 78 chunks: clean full Italian Constitution (72) + official UDHR English PDF (6). Same 55 questions, discriminative metrics. | +| `dataset-textbook.jsonl` | `eval-textbook` | Textbook corpus (TECH-005 collection 1), 449 child chunks + 113 parents: one long Italian legal textbook. 60 questions, Italian. **Ground truth pending human spot-check: do not gate a release on it yet** (see "Validation status" below). | -Both files share the same 55 questions (21 factual, 15 reasoning, 10 multi-chunk, -9 adversarial without ground truth; 37 IT / 18 EN). Entry shape: -`{id, question, expected_keywords, namespace, category, language}`. Relevance is +`dataset.jsonl` and `dataset-full.jsonl` share the same 55 questions (21 factual, +15 reasoning, 10 multi-chunk, 9 adversarial without ground truth; 37 IT / 18 EN). +Entry shape: `{id, question, expected_keywords, namespace, category, language}` +(`dataset-textbook.jsonl` adds `source_section`, which the harness ignores and which +exists so a human can audit where each question came from). Relevance is keyword-based (`expected_keywords`, diacritic-insensitive substring match), so it is chunking-independent and survives reingestion. +### Which one to use + +The excerpt corpus saturates: its hit rate is near 100% regardless of configuration, +because 20 retrieved candidates cover most of a 12-chunk corpus. Use it as a smoke +test, never to compare configurations. `eval-full` discriminates but its documents are +short, self-contained legal articles, so it understates anything that depends on +surrounding context (parent chunk expansion in particular). `eval-textbook` is the hard +bench: long argumentative prose whose chunks are ambiguous on their own. + +First measured baseline on `eval-textbook` (MiniLM, dual chunking, top_k=5): hit rate +78.4%, MRR 0.5873 (factual 79%, multi-chunk 92%, reasoning 67%). Compare with +`eval-full` under the same configuration: 82.6% / 0.7029. The textbook is harder, which +is the point. + ## Corpus provenance (eval-full) - `costituzione-full-clean.md`: full text of the Italian Constitution converted @@ -44,6 +61,72 @@ is chunking-independent and survives reingestion. To rebuild the corpus: download the two sources, then `scripts/ingest.sh eval-full` for each. +## Corpus provenance (eval-textbook) + +Carlo Smuraglia, *Diritto penale del lavoro*, Milano University Press 2025 (reissue of +the 1980 Cedam text), DOI `10.54103/milanoup.236`. **Licence CC BY-SA 4.0**, so the text +may be redistributed and modified with attribution under the same licence. + +```bash +curl -L -o textbook.pdf \ + "https://libri.unimi.it/index.php/milanoup/catalog/download/236/841/2256?inline=1" +# sha256 ef43d9cf11f52d8f7f17c97c24ee4bbb9cdcb2836de12ebce94340cb15fb3ec5 +uv run --with pdfplumber python scripts/eval_clean_textbook.py textbook.pdf textbook-clean.md +scripts/ingest.sh textbook-clean.md eval-textbook +``` + +Why this book: 13 chapters, numbered sections, prose with dense implicit +cross-references ("le considerazioni suesposte", "come si e' detto"), no formulas and no +tables, so the corpus stresses retrieval rather than PDF extraction. The cleaning script +classifies text by font size (body 11pt, footnotes 9pt, running headers 10pt, headings +14/18pt), drops footnotes and headers, repairs end-of-line hyphenation, and preserves the +chapter/section structure as markdown headings, which is what the dual chunker turns into +parent chunks. It removes the citation apparatus on purpose: footnotes in a legal treatise +are dense reference lists that would pollute the chunks. + +Licensing note for anyone adding a corpus: prefer CC BY or CC BY-SA. Sources carrying an +**ND** (NoDerivatives) clause cannot be used, because a cleaned and chunked corpus is a +derivative work. Most Italian university lecture notes are CC BY-NC-ND for this reason, and +OpenStax advertises CC BY while its live per-book metadata says CC BY-NC-SA. Check the +licence on the actual artefact, not the marketing page. + +## Ground truth methodology + +Applies to any new dataset. Keyword ground truth is only as good as its construction, and a +bad dataset fails silently: it produces plausible numbers that mean nothing. + +1. **Author questions from full documents, not from chunks.** Reading a chunk and writing a + question about it guarantees the question is answerable from that chunk, which biases the + whole dataset towards trivially retrievable passages. +2. **Every keyword must be individually discriminative.** The harness scores a chunk as a hit + if *any one* keyword appears in it (case- and diacritic-insensitive substring). A generic + term therefore marks wrong chunks as hits. Use verbatim multi-word phrases lifted from the + source passage, and verify each one occurs in the intended section and nowhere else. +3. **Adversarial questions must be verified non-answerable**, and not just by grep: the corpus + may cover the topic elsewhere in different words. An adversarial question the corpus can + actually answer punishes the system for being right. +4. **Run the closed-book control.** Put every question to the answering LLM with no context and + an instruction to answer only if certain. Any question it answers correctly from parametric + memory does not measure retrieval: drop it or rewrite it to depend on the source (for + example by anchoring it to the author's own argument). On `eval-textbook`: 0 expected + keywords leaked, 53/60 answered "NON SO". +5. **Spot-check a stratified sample by hand.** Machine checks cannot see a question that a + single section already answers although it is labelled multi-chunk, nor an adversarial + question the corpus covers under a synonym. + +## Validation status + +| Dataset | Machine-validated | Closed-book control | Human spot-check | +|---------|-------------------|---------------------|------------------| +| `dataset.jsonl` | partial | not run | no | +| `dataset-full.jsonl` | partial | not run | no | +| `dataset-textbook.jsonl` | yes | yes (0 leaks) | **pending** | + +`dataset-textbook.jsonl` is published so the evaluation can be reproduced and reviewed, but +its ground truth has not been reviewed by a human yet. Until it has, treat its numbers as +indicative and do not gate a release on them. The closed-book control should also be run +retroactively on the two older datasets. + ## Adding questions Append JSONL entries with a unique `id`, the target `namespace`, and 1-3 diff --git a/tests/eval/dataset-textbook.jsonl b/tests/eval/dataset-textbook.jsonl new file mode 100644 index 00000000..aee21fc0 --- /dev/null +++ b/tests/eval/dataset-textbook.jsonl @@ -0,0 +1,60 @@ +{"id": "TB-F-01", "question": "Quale legge, citata dall'autore come primo esempio di disciplina «rafforzata» nel settore della tutela penale del lavoro, prevedeva la sospensione dall'esercizio dell'industria in caso di recidiva e un provvedimento del Prefetto in caso di ripetuta oblazione?", "expected_keywords": ["ripetuta oblazione", "sospensione dall'esercizio dell'industria"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "1.6 Esperienze legislative di altri Paesi e del nostro"} +{"id": "TB-F-02", "question": "Quale principio tradizionale, di origine latina, nega la possibilità che le società, in quanto tali, possano essere ritenute penalmente responsabili?", "expected_keywords": ["societas delinquere non potest"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "1.8 Il problema della responsabilità penale delle persone giuridiche"} +{"id": "TB-F-03", "question": "Quale criterio deve prevalere, secondo l'autore, per individuare la reale figura del datore di lavoro nei casi concreti dubbi?", "expected_keywords": ["prevalenza della situazione reale su quella apparente"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "2.1 Il «datore di lavoro»"} +{"id": "TB-F-04", "question": "Quale articolo del D.P.R. 27 aprile 1955 n. 547 pone specifici obblighi di sicurezza a carico di chi costruisce, vende o noleggia macchine, attrezzature e impianti?", "expected_keywords": ["costruiscono, vendono, noleggiano"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "2.3 Altri soggetti (dirigenti, preposti, costruttori e venditori, lavoratori)"} +{"id": "TB-F-05", "question": "Per quale motivo, secondo l'autore, il principio dell'ordine del superiore (art. 51 cod. pen.) non può essere esteso ai rapporti di lavoro privatistici?", "expected_keywords": ["sconosciuto al diritto privato"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "3.2 Segue: b) l'ordine del superiore"} +{"id": "TB-F-06", "question": "Quale decisione giurisprudenziale, relativa a un caso di occupazione d'azienda per ragioni sindacali, viene citata dall'autore a proposito dell'attenuante dei motivi di particolare valore sociale?", "expected_keywords": ["fizialetti"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "3.4 L'attenuante dei motivi di particolare valore sociale"} +{"id": "TB-F-07", "question": "Quale reato commette il collocatore, in qualità di pubblico ufficiale, se si appropria di denaro della pubblica amministrazione di cui abbia il possesso per ragioni di ufficio?", "expected_keywords": ["reato di peculato"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "4.2 Il collocamento ordinario: a) Il collocamento come servizio pubblico"} +{"id": "TB-F-08", "question": "Con quale decisione la Corte costituzionale dichiarò infondata la questione di legittimità costituzionale sollevata contro i minimi edittali elevati previsti per le violazioni della legge sul lavoro dei fanciulli e degli adolescenti?", "expected_keywords": ["23 marzo 1970 n. 45"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "5.1 La protezione di alcune particolari categorie (fanciulli e adolescenti, donne, apprendisti)"} +{"id": "TB-R-01", "question": "Quali sono le due categorie di cause individuate dall'autore per spiegare l'inefficacia del sistema sanzionatorio penale in materia di lavoro, e cosa comprende ciascuna?", "expected_keywords": ["cause strutturali e funzionali", "disfunzioni dell'apparato di controllo"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "1.4 L'efficacia del sistema e le sue lacune"} +{"id": "TB-R-02", "question": "Quali condizioni devono sussistere, secondo l'autore, perché la ripartizione o delega di compiti all'interno di un'impresa di notevoli dimensioni abbia efficacia liberatoria per il titolare rispetto alla responsabilità penale?", "expected_keywords": ["efficacia liberatoria", "salti qualitativi nell'organizzazione aziendale"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "2.2 La ripartizione di compiti e di attribuzioni: limiti"} +{"id": "TB-R-03", "question": "Perché l'autore ritiene inaccettabile la tesi secondo cui l'errore del datore di lavoro sulla qualificazione del rapporto come subordinato costituirebbe un errore sul fatto, anziché un errore di diritto non scusabile?", "expected_keywords": ["errore sulla qualificazione del rapporto", "sbarramento costituito dal principio di non scusabilita'"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "3.1 Le cause di giustificazione: a) l'errore sui presupposti"} +{"id": "TB-R-04", "question": "Secondo l'autore, qual è la vera alternativa da affrontare in tema di riforma del collocamento, in luogo della contrapposizione tra rigidità del sistema e sua liberalizzazione?", "expected_keywords": ["politica attiva del lavoro", "rigidita' del sistema e la sua liberalizzazione"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "4.1 La disciplina giuridica del collocamento e i suoi limiti"} +{"id": "TB-R-05", "question": "Perché, secondo l'autore, l'apparato sanzionatorio previsto dallo Statuto dei lavoratori (art. 38 e norme collegate) non può essere considerato un vero e proprio «sistema» sanzionatorio unitario?", "expected_keywords": ["reale omogeneita'", "tutela della personalita' del lavoratore"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "5.2 Tutela della libertà e dignità dei lavoratori"} +{"id": "TB-MC-01", "question": "Quali soluzioni alternative propone l'autore per superare i limiti della responsabilità penale individuale nelle imprese di grandi dimensioni: sia sul piano della responsabilità delle persone giuridiche come tali, sia sul piano interno della ripartizione dei compiti tra i soggetti dell'impresa?", "expected_keywords": ["amministratori, fiduciari, designati dal giudice", "efficacia liberatoria"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "1.8 Il problema della responsabilità penale delle persone giuridiche + 2.2 La ripartizione di compiti e di attribuzioni: limiti"} +{"id": "TB-MC-02", "question": "L'art. 51 del codice penale viene escluso dall'autore come esimente per l'osservanza dell'ordine del datore di lavoro, ma può invece operare come esimente per l'esercizio del diritto di sciopero: quale differenza tra le due situazioni giustifica questa diversa conclusione?", "expected_keywords": ["rapporto di dipendenza gerarchica", "coincidenza tra condotta concreta e condotta lecita"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "3.2 Segue: b) l'ordine del superiore + 3.3 Segue: c) l'esercizio del diritto (anche sotto il profilo putativo) nelle azioni sindacali"} +{"id": "TB-MC-03", "question": "Individua tutti i casi, tra i capitoli IV e V, in cui l'autore qualifica un reato come «permanente» anziché istantaneo, indicando per quale condotta.", "expected_keywords": ["omessa comunicazione all'ufficio della cessazione del rapporto di lavoro", "la consumazione si protrae", "reato omissivo permanente"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "4.2 Il collocamento ordinario: f) Istantaneità e permanenza dei reati + 4.4 Il collocamento come monopolio e il divieto di intermediazione + 5.1 La protezione di alcune particolari categorie (fanciulli e adolescenti, donne, apprendisti)"} +{"id": "TB-MC-04", "question": "In che modo si combinano, nella ricostruzione dell'autore, l'obbligo di sicurezza «a monte» imposto ai costruttori e venditori di macchine e la responsabilità del datore di lavoro che impiega un minore o un apprendista su una macchina pericolosa, ai fini della responsabilità per l'infortunio?", "expected_keywords": ["costruiscono, vendono, noleggiano", "adibire un minore o un apprendista ad una macchina pericolosa"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "2.3 Altri soggetti (dirigenti, preposti, costruttori e venditori, lavoratori) + 5.1 La protezione di alcune particolari categorie (fanciulli e adolescenti, donne, apprendisti)"} +{"id": "TB-ADV-01", "question": "Quali sanzioni penali prevede il testo per l'omessa redazione del documento di valutazione dei rischi (DVR) previsto dal D.Lgs. 81/2008?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": ""} +{"id": "TB-ADV-02", "question": "Come disciplina il testo le sanzioni penali per la violazione delle norme sul lavoro agile (smart working), introdotte dalla legge 22 maggio 2017 n. 81?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": ""} +{"id": "TB-ADV-03", "question": "Quale trattazione dedica il testo al mobbing come autonoma fattispecie di reato contro la salute psichica del lavoratore?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": ""} +{"id": "TB-F-09", "question": "Quale disposizione dello Statuto dei lavoratori viene definita dall'autore come la fonte che ha rappresentato una vera svolta nella concezione normativa della tutela della sicurezza del lavoro?", "expected_keywords": ["concezione normativa del problema"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "6.1 Il dovere di sicurezza: aspetti generali"} +{"id": "TB-F-10", "question": "Negli appalti vietati di pura manodopera, chi assume il dovere di sicurezza nei confronti dei lavoratori secondo l'autore, e in base a quale titolo di responsabilità?", "expected_keywords": ["falso committente non può che assumere"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "6.5 L'intreccio di rapporti: appalti e subappalti"} +{"id": "TB-F-11", "question": "Secondo la più recente decisione della Corte Suprema richiamata dall'autore, l'infortunio previsto dall'art. 437 cod. pen. deve necessariamente assumere le proporzioni di un disastro per la pubblica incolumità?", "expected_keywords": ["infortunio si distingue dal disastro"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "6.7 L'ipotesi dolosa di cui all'art. 437 cod. pen."} +{"id": "TB-F-12", "question": "Qual era la misura dell'ammenda per ogni lavoratore interessato, nella versione finale approvata dal Parlamento dell'art. 8 della legge 741/1959, dopo la discussione sulla proposta iniziale?", "expected_keywords": ["5.000 a 100.000"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "Cap. VII - La tutela rafforzata del contratto collettivo"} +{"id": "TB-F-13", "question": "Quale attività viene indicata dall'autore come uno dei rari esempi, in materia previdenziale, di previsione della pena detentiva (ammenda e arresto) anziché della sola sanzione pecuniaria?", "expected_keywords": ["libri paga e matricola"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "8.1 Aspetti generali: i reati previsti dalle leggi speciali e dalle norme di diritto penale comune; la tecnica sanzionatoria; i soggetti; l'elemento soggettivo"} +{"id": "TB-F-14", "question": "Quale evento fa cessare la permanenza del reato di omissione contributiva nel caso in cui il datore di lavoro fallisca, secondo l'opinione prevalente riportata dall'autore?", "expected_keywords": ["sentenza dichiarativa"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "8.3 Il reato di omissione contributiva"} +{"id": "TB-F-15", "question": "Quali due caratteristiche tipiche dei reati perseguibili a querela di parte non si applicano al reato di inottemperanza al decreto del Pretore ex art. 28, comma 4, dello Statuto, proprio perché la norma rinvia (quoad poenam) all'art. 650 cod. pen.?", "expected_keywords": ["procedibilità su querela di parte", "possibilità di oblazione"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "9.1 Il reato di cui all'art. 28, comma 4, dello Statuto"} +{"id": "TB-F-16", "question": "Quale elemento soggettivo specifico (dolo specifico) richiede la lettera dell'art. 508, comma 1, cod. pen. per la configurabilità del reato di occupazione d'azienda?", "expected_keywords": ["col solo scopo di impedire o turbare"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "10.2 L'occupazione delle aziende e il reato di cui all'art. 508 cod. pen."} +{"id": "TB-R-06", "question": "Perché, secondo l'autore, l'eventuale inerzia degli organi ispettivi non può funzionare come esimente per il datore di lavoro che non abbia osservato il dovere di sicurezza?", "expected_keywords": ["natura funzionale ed autonoma"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "6.3 Il contenuto del dovere di sicurezza e degli obblighi penalmente sanzionati"} +{"id": "TB-R-07", "question": "Perché una semplice negligenza o disattenzione del lavoratore infortunato non basta, secondo la giurisprudenza riportata dall'autore, ad escludere la responsabilità del datore di lavoro? Che tipo di comportamento è invece richiesto?", "expected_keywords": ["esorbitante dal procedimento di lavoro"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "6.4 Il comportamento del lavoratore e la sua incidenza sulla serie causale"} +{"id": "TB-R-08", "question": "Per quale ragione la Corte costituzionale ha escluso che l'art. 8 della legge 741/1959, punendo solo il datore di lavoro e non anche il lavoratore, violi il principio di uguaglianza di cui all'art. 3 Cost.?", "expected_keywords": ["riequilibrare una situazione di palese disparità"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "Cap. VII - La tutela rafforzata del contratto collettivo"} +{"id": "TB-R-09", "question": "Perché, secondo l'autore, non è configurabile il reato di invasione di terreni o edifici (art. 633 cod. pen.) nei confronti di lavoratori che si trovino già all'interno dell'azienda per un motivo legittimo, ad esempio in stato di sciopero?", "expected_keywords": ["invadere solo dal di fuori"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "10.3 Segue: In relazione al reato di cui all'art. 633 cod. pen."} +{"id": "TB-R-10", "question": "Perché l'esercizio del diritto di sciopero non può, secondo l'autore, funzionare come causa di giustificazione per l'occupazione d'azienda, pur trattandosi spesso di un'azione sindacale reattiva a comportamenti illegittimi dell'imprenditore?", "expected_keywords": ["esimente per l'occupazione delle aziende"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "10.6 Rapporti tra i reati in tema di occupazione d'aziende ed esercizio di attività sindacali"} +{"id": "TB-MC-05", "question": "Che differenza sostanziale esiste tra l'elemento soggettivo richiesto dalla forma omissiva dell'art. 437 cod. pen. (sez. 6.7) e la fattispecie colposa dell'art. 451 cod. pen. (sez. 6.9), tale da escludere che la seconda sia semplicemente 'l'ipotesi colposa' della prima?", "expected_keywords": ["riferibile in concreto solo al datore di lavoro", "profonde differenze esistono tra i due"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "6.7 L'ipotesi dolosa di cui all'art. 437 cod. pen. + 6.9 L'ipotesi colposa di cui all'art. 451 cod. pen."} +{"id": "TB-MC-06", "question": "In che modo cambia la struttura della condotta tipica, e in particolare la collocazione dell'elemento dell'occupazione, passando dall'art. 508 cod. pen. (sez. 10.2) all'art. 633 cod. pen. (sez. 10.3)?", "expected_keywords": ["invade od occupa", "trasferita nell'ambito del fine"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "10.2 L'occupazione delle aziende e il reato di cui all'art. 508 cod. pen. + 10.3 Segue: In relazione al reato di cui all'art. 633 cod. pen."} +{"id": "TB-MC-07", "question": "In un appalto lecito, come si combinano la responsabilità del preposto, descritta a proposito dei soggetti responsabili (sez. 6.6), e l'eventuale colpa concorrente del committente in tema di appalti e subappalti (sez. 6.5), tenuto conto delle rispettive sfere di attribuzioni?", "expected_keywords": ["ingerenza nella esecuzione del lavoro", "vigila sull'esecuzione del lavoro"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "6.5 L'intreccio di rapporti: appalti e subappalti + 6.6 I soggetti responsabili"} +{"id": "TB-MC-08", "question": "In che modo si atteggia diversamente il rapporto tra giudizio penale e giudizio civile nel caso dell'omissione contributiva (sez. 8.4) rispetto al caso dell'inottemperanza al decreto del Pretore ex art. 28 dello Statuto (sez. 9.1)?", "expected_keywords": ["giudicato penale fa sicuramente stato", "rapporto di pregiudizialità"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "8.4 Rapporti tra giudizio penale e giudizio civile + 9.1 Il reato di cui all'art. 28, comma 4, dello Statuto"} +{"id": "TB-ADV-04", "question": "Cosa prevede il d. lgs. n. 81/2008 riguardo all'obbligo del datore di lavoro di redigere il documento di valutazione dei rischi (DVR)?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "N/A - argomento non trattato nel corpus (il d. lgs. 81/2008 e' successivo all'opera)"} +{"id": "TB-ADV-05", "question": "Qual è l'importo della sanzione amministrativa pecuniaria prevista in seguito alla depenalizzazione dell'art. 509 cod. pen. operata dall'art. 13 del d. lgs. n. 758/1994?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "N/A - il decreto e' solo citato tra parentesi (Cap. VII) senza indicazione dell'importo"} +{"id": "TB-ADV-06", "question": "Come qualifica l'autore, sotto il profilo del diritto penale del lavoro, il fenomeno del mobbing (le vessazioni psicologiche reiterate sul luogo di lavoro)?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "N/A - argomento non trattato nel corpus"} +{"id": "TB-F-17", "question": "Quale reato ravvisa la giurisprudenza quando gli scioperanti, durante un picchettaggio, impediscono fisicamente ai lavoratori dissenzienti l'accesso in azienda?", "expected_keywords": ["impediscono fisicamente l'accesso in azienda"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "11.2 La tutela della libertà di lavoro, il picchettaggio e i reati di violenza privata (art. 610 cod. pen.) e sequestro di persona (art. 605 cod. pen.)"} +{"id": "TB-F-18", "question": "Da quale legge del periodo corporativo deriva strettamente, secondo l'autore, l'art. 330 cod. pen. che punisce l'abbandono collettivo di pubblici uffici, servizi o impieghi?", "expected_keywords": ["abbandono collettivo di pubblici uffici"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "11.3 Lo sciopero dei pubblici dipendenti e il reato di abbandono di pubblici uffici (art. 330 cod. pen.)"} +{"id": "TB-F-19", "question": "Quale reato del codice della navigazione può configurarsi quando i marittimi scioperano durante il corso del viaggio, secondo l'art. 1105 n. 1 cod. navig.?", "expected_keywords": ["ammutinamento"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "11.4 Situazioni particolari: lo sciopero nei trasporti pubblici e lo sciopero dei vigili urbani (in relazione all'art. 330 cod. pen.); lo sciopero dei marittimi (in relazione all'art. 1105 cod. navig.); lo sciopero degli addetti ai passaggi a livello (e l'art. 432 cod. pen.); lo sciopero degli addetti ad ospedali psichiatrici (e l'art. 591 cod. pen.)"} +{"id": "TB-F-20", "question": "Con quale sentenza la Corte costituzionale ha dichiarato illegittimo l'art. 503 cod. pen. nella parte in cui puniva anche lo sciopero politico non diretto a sovvertire l'ordinamento costituzionale?", "expected_keywords": ["sovvertire l'ordinamento costituzionale"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "11.6 Sciopero politico, di solidarietà e di protesta"} +{"id": "TB-F-21", "question": "Quale specifico requisito soggettivo richiede, secondo la sua stessa formulazione testuale, la norma sul blocco stradale (D. Legisl. 22 gennaio 1948 n. 66)?", "expected_keywords": ["impedire od ostacolare la libera circolazione"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "11.7 La tutela della circolazione e il reato di blocco stradale (D. Legisl. 22 gennaio 1948 n. 66); la tutela delle occupazioni e del riposo delle persone (art. 659 cod. pen.)"} +{"id": "TB-F-22", "question": "Da quale combinato disposto normativo si ricava, secondo l'autore, chi sia il titolare dell'azione civile nel processo penale?", "expected_keywords": ["combinato disposto dell'art. 185 cod. pen. e dell'art. 22 cod. proc. pen."], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "12.2 Il «danno risarcibile» in sede penale"} +{"id": "TB-F-23", "question": "A quale organo la legge di riforma sanitaria n. 833/1978 devolve i compiti di igiene e medicina del lavoro e di prevenzione degli infortuni sul lavoro prima svolti dall'Ispettorato del lavoro?", "expected_keywords": ["Unità sanitarie locali"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "13.1 Il «sistema» di vigilanza, controllo e prevenzione, alla luce della normativa vigente e della recente riforma sanitaria"} +{"id": "TB-F-24", "question": "Entro quanti giorni deve essere iniziata l'inchiesta infortunistica prevista dal T.U. 30 giugno 1965 n. 1124?", "expected_keywords": ["quattro giorni", "inchiesta infortunistica"], "namespace": "eval-textbook", "category": "factual", "language": "it", "source_section": "13.5 L'inchiesta infortunistica e i suoi rapporti col procedimento penale"} +{"id": "TB-R-11", "question": "Perché l'autore sostiene che si debba parlare di 'reciprocità di limiti' tra l'art. 40 Cost. e le norme penali, anziché configurare queste ultime come un semplice limite esterno al diritto di sciopero?", "expected_keywords": ["reciprocità di limiti"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "11.1 L'esercizio del diritto di sciopero e la tutela di altri beni giuridicamente rilevanti: il problema del bilanciamento degli interessi"} +{"id": "TB-R-12", "question": "Per quale motivo l'autore ritiene che l'art. 330 cod. pen. debba considerarsi tacitamente abrogato dall'art. 40 Cost., piuttosto che reso inapplicabile caso per caso tramite l'esimente dell'art. 51 cod. pen.?", "expected_keywords": ["tacitamente abrogata"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "11.3 Lo sciopero dei pubblici dipendenti e il reato di abbandono di pubblici uffici (art. 330 cod. pen.)"} +{"id": "TB-R-13", "question": "Perché l'autore ritiene scorretto mutuare dall'art. 1223 cod. civ. il criterio del danno 'immediato e diretto' per delimitare il danno risarcibile in sede penale?", "expected_keywords": ["neminem laedere"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "12.2 Il «danno risarcibile» in sede penale"} +{"id": "TB-R-14", "question": "Su quali basi l'autore critica la decisione della Cassazione che nega la legittimazione del sindacato a costituirsi parte civile nel processo per l'omicidio colposo plurimo verificatosi nella cosiddetta 'fabbrica del cancro'?", "expected_keywords": ["fabbrica del cancro"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "12.5 Una recente decisione della Corte di cassazione e le sue implicazioni; conclusioni"} +{"id": "TB-R-15", "question": "Perché l'autore ritiene inaccettabile la tesi secondo cui il potere di diffida attribuito agli Ispettori del lavoro dall'art. 9 del D.P.R. 520/1955 costituirebbe una deroga implicita all'obbligo di rapporto ex art. 2 cod. proc. pen.?", "expected_keywords": ["potere di diffida"], "namespace": "eval-textbook", "category": "reasoning", "language": "it", "source_section": "13.2 Poteri e funzioni dell'Ispettorato del lavoro: distinzione tra attività amministrativa e funzioni di polizia giudiziaria"} +{"id": "TB-MC-09", "question": "Che differenza c'è tra le condotte degli scioperanti che la giurisprudenza qualifica come violenza privata (sez. 11.2) e quelle che integrano invece il reato di interruzione o turbamento di pubblico servizio ex art. 340 cod. pen. (sez. 11.5)?", "expected_keywords": ["impediscono fisicamente l'accesso in azienda", "tolgono la corrente"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "11.2 La tutela della libertà di lavoro, il picchettaggio e i reati di violenza privata (art. 610 cod. pen.) e sequestro di persona (art. 605 cod. pen.) + 11.5 Reati a causa di sciopero ed in occasione di sciopero, in relazione all'art. 328 ed all'art. 340 cod. pen."} +{"id": "TB-MC-10", "question": "Che differenza c'è tra l'orientamento giurisprudenziale che ammette la costituzione di parte civile del sindacato per la lesione all'interesse collettivo alla sicurezza del lavoro (sez. 12.4) e la posizione restrittiva della Cassazione, criticata dall'autore, secondo cui il danno risarcibile richiederebbe la 'normale coincidenza' tra soggetto passivo e danneggiato (sez. 12.5)?", "expected_keywords": ["interesse collettivo alla sicurezza", "normale coincidenza tra soggetto passivo e danneggiato"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "12.4 Il sindacato e la tutela dell'interesse collettivo + 12.5 Una recente decisione della Corte di cassazione e le sue implicazioni; conclusioni"} +{"id": "TB-MC-11", "question": "In che modo l'istituto dell'inchiesta infortunistica (sez. 13.5) può collegarsi ai provvedimenti previsti dall'art. 219 cod. proc. pen. (sez. 13.7), e quale rimedio processuale resta comunque esperibile contro tali provvedimenti secondo l'autore?", "expected_keywords": ["inchiesta infortunistica", "incidente di esecuzione"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "13.5 L'inchiesta infortunistica e i suoi rapporti col procedimento penale + 13.7 La funzione di prevenzione e l'art. 219 cod. proc. pen."} +{"id": "TB-MC-12", "question": "Quali sono i due diversi meccanismi giuridici attraverso cui, secondo l'autore, il diritto penale ha cessato di sanzionare rispettivamente l'abbandono collettivo di pubblici uffici (sez. 11.3, art. 330 cod. pen.) e lo sciopero politico in senso stretto (sez. 11.6, art. 503 cod. pen.)?", "expected_keywords": ["abbandono collettivo di pubblici uffici", "sovvertire l'ordinamento costituzionale"], "namespace": "eval-textbook", "category": "multi-chunk", "language": "it", "source_section": "11.3 Lo sciopero dei pubblici dipendenti e il reato di abbandono di pubblici uffici (art. 330 cod. pen.) + 11.6 Sciopero politico, di solidarietà e di protesta"} +{"id": "TB-ADV-07", "question": "Che disciplina specifica riserva il libro allo sciopero dei dipendenti degli istituti di credito (settore bancario), in relazione alla loro eventuale essenzialità come servizio pubblico?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "n/a - not covered"} +{"id": "TB-ADV-08", "question": "Qual è il numero minimo di manifestanti che, secondo la giurisprudenza citata nel libro, fa scattare automaticamente la 'minaccia implicita' idonea a configurare il reato di violenza privata durante un picchettaggio?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "n/a - not covered"} +{"id": "TB-ADV-09", "question": "Qual è l'importo della sanzione pecuniaria prevista dall'art. 506 cod. pen. per la serrata di piccoli esercenti privi di dipendenti?", "expected_keywords": [], "namespace": "eval-textbook", "category": "adversarial", "language": "it", "source_section": "n/a - not covered"}