(
note_plan: dict[str, Any],
source_manifest: dict[str, Any],
)
| 195 | |
| 196 | |
| 197 | def validate_note_plan( |
| 198 | note_plan: dict[str, Any], |
| 199 | source_manifest: dict[str, Any], |
| 200 | ) -> list[dict[str, Any]]: |
| 201 | issues: list[dict[str, Any]] = [] |
| 202 | for field in NOTE_PLAN_REQUIRED_FIELDS: |
| 203 | if field not in note_plan: |
| 204 | issues.append(issue("note_plan_required_field_missing", field=field)) |
| 205 | for field in NOTE_PLAN_STRING_FIELDS: |
| 206 | if field in note_plan and not normalize_whitespace(str(note_plan.get(field, ""))): |
| 207 | issues.append(issue("note_plan_required_field_empty", field=field)) |
| 208 | for field in NOTE_PLAN_LIST_FIELDS: |
| 209 | value = note_plan.get(field) |
| 210 | if field in note_plan and (not isinstance(value, list) or not value): |
| 211 | issues.append(issue("note_plan_required_field_empty", field=field)) |
| 212 | |
| 213 | paper_type = normalize_whitespace(str(note_plan.get("paper_type", ""))) |
| 214 | if paper_type not in PAPER_TYPE_VALUES: |
| 215 | issues.append(issue("note_plan_paper_type_invalid", paper_type=paper_type)) |
| 216 | |
| 217 | if source_is_truncated(source_manifest) and not accepts_partial_reading(note_plan): |
| 218 | issues.append(issue("source_manifest_truncated_without_partial_acceptance")) |
| 219 | |
| 220 | valid_ids = source_section_ids(source_manifest) |
| 221 | max_page = total_pages(source_manifest) |
| 222 | required_sections = set(WRITING_CONTRACT_RULES["grounding_required_sections"]) |
| 223 | grounded_sections: set[str] = set() |
| 224 | section_plan = note_plan.get("section_plan", []) |
| 225 | if not isinstance(section_plan, list): |
| 226 | issues.append(issue("note_plan_section_plan_invalid")) |
| 227 | return issues |
| 228 | |
| 229 | for item in section_plan: |
| 230 | if not isinstance(item, dict): |
| 231 | issues.append(issue("note_plan_section_plan_item_invalid")) |
| 232 | continue |
| 233 | section_name = normalize_whitespace( |
| 234 | str(item.get("section") or item.get("name") or item.get("heading") or "") |
| 235 | ) |
| 236 | if section_name in required_sections: |
| 237 | grounded_sections.add(section_name) |
| 238 | sources = evidence_sources(item) |
| 239 | if section_name in required_sections and not sources: |
| 240 | issues.append(issue("section_plan_grounding_missing", section=section_name)) |
| 241 | continue |
| 242 | if section_name in required_sections and not focus_is_substantive( |
| 243 | section_focus_text(item) |
| 244 | ): |
| 245 | issues.append(issue("section_plan_focus_too_thin", section=section_name)) |
| 246 | for source in sources: |
| 247 | for code in source_grounding_errors(source, valid_ids, max_page): |
| 248 | issues.append(issue(code, section=section_name, source=source)) |
| 249 | |
| 250 | for section_name in sorted(required_sections - grounded_sections): |
| 251 | issues.append(issue("section_plan_required_section_missing", section=section_name)) |
| 252 | |
| 253 | issues.extend(validate_central_claims(note_plan, valid_ids, max_page)) |
| 254 |
no test coverage detected