frontmattering 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. frontmattering/FRONTMATTER_INDEX_PROVENANCE.json +13 -0
  2. frontmattering/__init__.py +1 -0
  3. frontmattering/__main__.py +1263 -0
  4. frontmattering/ai_instruction_risk.py +51 -0
  5. frontmattering/authoring.py +320 -0
  6. frontmattering/body_tether.py +176 -0
  7. frontmattering/common/__init__.py +3 -0
  8. frontmattering/common/errors.py +1074 -0
  9. frontmattering/core.py +147 -0
  10. frontmattering/corpus_selection.py +436 -0
  11. frontmattering/frontmatter_index.py +340 -0
  12. frontmattering/harness_errors.py +555 -0
  13. frontmattering/inventory.py +83 -0
  14. frontmattering/markdown/MARKDOWN_PARSE_RESULT_schema.json +870 -0
  15. frontmattering/markdown/MARKDOWN_PROVENANCE.json +8 -0
  16. frontmattering/markdown/USER_MANUAL.md +263 -0
  17. frontmattering/markdown/__init__.py +0 -0
  18. frontmattering/markdown/api.py +553 -0
  19. frontmattering/markdown/cli/__init__.py +1 -0
  20. frontmattering/markdown/cli/__main__.py +10 -0
  21. frontmattering/markdown/cli/dump_sections.py +100 -0
  22. frontmattering/markdown/collectors_phase8/__init__.py +15 -0
  23. frontmattering/markdown/collectors_phase8/blockquotes.py +117 -0
  24. frontmattering/markdown/collectors_phase8/codeblocks.py +46 -0
  25. frontmattering/markdown/collectors_phase8/footnotes.py +152 -0
  26. frontmattering/markdown/collectors_phase8/frontmatter.py +80 -0
  27. frontmattering/markdown/collectors_phase8/headings.py +85 -0
  28. frontmattering/markdown/collectors_phase8/html.py +29 -0
  29. frontmattering/markdown/collectors_phase8/html_collector.py +89 -0
  30. frontmattering/markdown/collectors_phase8/images.py +68 -0
  31. frontmattering/markdown/collectors_phase8/inline_code.py +61 -0
  32. frontmattering/markdown/collectors_phase8/links.py +104 -0
  33. frontmattering/markdown/collectors_phase8/lists.py +66 -0
  34. frontmattering/markdown/collectors_phase8/math.py +28 -0
  35. frontmattering/markdown/collectors_phase8/paragraphs.py +41 -0
  36. frontmattering/markdown/collectors_phase8/tables.py +65 -0
  37. frontmattering/markdown/collectors_phase8/tasklists.py +59 -0
  38. frontmattering/markdown/fetchers/__init__.py +8 -0
  39. frontmattering/markdown/fetchers/preview.py +123 -0
  40. frontmattering/markdown/fetchers/url_fetcher.py +123 -0
  41. frontmattering/markdown/security/__init__.py +6 -0
  42. frontmattering/markdown/security/scanner.py +436 -0
  43. frontmattering/markdown/utils/__init__.py +0 -0
  44. frontmattering/markdown/utils/profiling.py +137 -0
  45. frontmattering/markdown/utils/section.py +98 -0
  46. frontmattering/markdown/utils/text_normalization.py +91 -0
  47. frontmattering/markdown/utils/timeout.py +165 -0
  48. frontmattering/markdown/utils/token_warehouse.py +1300 -0
  49. frontmattering/markdown/utils/url_utils.py +179 -0
  50. frontmattering/promotion.py +170 -0
  51. frontmattering/resources/AI_INDEX.json +691 -0
  52. frontmattering/resources/AI_START_HERE.md +378 -0
  53. frontmattering/resources/DAILY_USE_GUIDE.md +87 -0
  54. frontmattering/resources/FRONTMATTERING_FRONTMATTER_SPEC_v0_1_5.md +3203 -0
  55. frontmattering/resources/KERNEL_ACCEPTANCE_REPORT.md +180 -0
  56. frontmattering/resources/__init__.py +0 -0
  57. frontmattering/resources/examples/no_frontmatter_demo/00_AI_START_HERE.md +43 -0
  58. frontmattering/resources/examples/no_frontmatter_demo/10_NORMATIVE_SPEC.md +28 -0
  59. frontmattering/resources/examples/no_frontmatter_demo/20_IMPLEMENTATION_CONTRACT.md +33 -0
  60. frontmattering/resources/examples/no_frontmatter_demo/30_TEST_SUFFICIENCY.md +34 -0
  61. frontmattering/resources/examples/no_frontmatter_demo/40_EVIDENCE_RECORD.md +33 -0
  62. frontmattering/resources/examples/no_frontmatter_demo/50_RELEASE_DECISION.md +29 -0
  63. frontmattering/resources/examples/power_demo/00_AI_START_HERE.md +142 -0
  64. frontmattering/resources/examples/power_demo/10_NORMATIVE_SPEC.md +117 -0
  65. frontmattering/resources/examples/power_demo/20_IMPLEMENTATION_CONTRACT.md +130 -0
  66. frontmattering/resources/examples/power_demo/30_TEST_SUFFICIENCY.md +132 -0
  67. frontmattering/resources/examples/power_demo/40_EVIDENCE_RECORD.md +128 -0
  68. frontmattering/resources/examples/power_demo/50_RELEASE_DECISION.md +124 -0
  69. frontmattering/resources/fixtures/invalid/adoption_digest_mismatch_rejected/doc.md +80 -0
  70. frontmattering/resources/fixtures/invalid/adoption_record_on_wrong_role_rejected/doc.md +59 -0
  71. frontmattering/resources/fixtures/invalid/adoption_record_shape_rejected/doc.md +58 -0
  72. frontmattering/resources/fixtures/invalid/adoption_without_bootstrap_receipt_rejected/doc.md +78 -0
  73. frontmattering/resources/fixtures/invalid/authored_go_used_as_evidence_rejected/doc.md +57 -0
  74. frontmattering/resources/fixtures/invalid/body_tether_body_digest_mismatch_rejected/doc.md +65 -0
  75. frontmattering/resources/fixtures/invalid/body_tether_fields_mismatch_rejected/doc.md +63 -0
  76. frontmattering/resources/fixtures/invalid/body_tether_malformed_rejected/doc.md +58 -0
  77. frontmattering/resources/fixtures/invalid/body_tether_missing_under_trust_rejected/.frontmattering/project.yaml +11 -0
  78. frontmattering/resources/fixtures/invalid/body_tether_missing_under_trust_rejected/doc.md +57 -0
  79. frontmattering/resources/fixtures/invalid/body_tether_scope_selector_rejected/doc.md +68 -0
  80. frontmattering/resources/fixtures/invalid/body_tether_unknown_canonicalization_rejected/doc.md +63 -0
  81. frontmattering/resources/fixtures/invalid/bootstrap_receipt_shape_rejected/doc.md +80 -0
  82. frontmattering/resources/fixtures/invalid/bootstrap_without_validator_blocks_adoption/doc.md +71 -0
  83. frontmattering/resources/fixtures/invalid/claim_boundary_missing_does_not_verify_rejected/doc.md +55 -0
  84. frontmattering/resources/fixtures/invalid/claimed_state_overreach_rejected/doc.md +57 -0
  85. frontmattering/resources/fixtures/invalid/clause_enforcement_drift_rejected/doc.md +59 -0
  86. frontmattering/resources/fixtures/invalid/container_cardinality_rejected/doc.md +57 -0
  87. frontmattering/resources/fixtures/invalid/deepest_intention_missing_rejected/doc.md +56 -0
  88. frontmattering/resources/fixtures/invalid/downstream_phase_without_upstream_evidence_rejected/doc.md +59 -0
  89. frontmattering/resources/fixtures/invalid/duplicate_document_id_rejected/a.md +57 -0
  90. frontmattering/resources/fixtures/invalid/duplicate_document_id_rejected/b.md +57 -0
  91. frontmattering/resources/fixtures/invalid/duplicate_yaml_key_rejected/doc.md +58 -0
  92. frontmattering/resources/fixtures/invalid/evidence_edge_digest_missing_rejected/doc.md +65 -0
  93. frontmattering/resources/fixtures/invalid/evidence_reference_used_as_truth_rejected/doc.md +66 -0
  94. frontmattering/resources/fixtures/invalid/external_edge_locator_missing_rejected/doc.md +64 -0
  95. frontmattering/resources/fixtures/invalid/external_edge_set_id_missing_rejected/doc.md +64 -0
  96. frontmattering/resources/fixtures/invalid/extracted_catalog_drift_rejected/doc.md +60 -0
  97. frontmattering/resources/fixtures/invalid/fixture_reverse_reachability_drift_rejected/doc.md +59 -0
  98. frontmattering/resources/fixtures/invalid/frontmatter_only_view_incomplete_rejected/doc.md +59 -0
  99. frontmattering/resources/fixtures/invalid/frontmatter_registry_conflict_blocks/doc.md +59 -0
  100. frontmattering/resources/fixtures/invalid/frontmatter_view_body_truth_overclaim_rejected/doc.md +59 -0
  101. frontmattering/resources/fixtures/invalid/generated_state_in_frontmatter_rejected/doc.md +58 -0
  102. frontmattering/resources/fixtures/invalid/green_checks_conflation_rejected/doc.md +76 -0
  103. frontmattering/resources/fixtures/invalid/historical_digest_used_as_current_evidence_rejected/doc.md +66 -0
  104. frontmattering/resources/fixtures/invalid/implementation_start_gate_bypass_rejected/doc.md +59 -0
  105. frontmattering/resources/fixtures/invalid/intention_positive_negative_conflation_rejected/doc.md +76 -0
  106. frontmattering/resources/fixtures/invalid/legacy_string_machine_surface_requires_mapping/doc.md +59 -0
  107. frontmattering/resources/fixtures/invalid/load_order_inversion_flagged/a.md +63 -0
  108. frontmattering/resources/fixtures/invalid/load_order_inversion_flagged/b.md +57 -0
  109. frontmattering/resources/fixtures/invalid/machine_surface_identity_incomplete_rejected/doc.md +62 -0
  110. frontmattering/resources/fixtures/invalid/migration_field_drop_rejected/doc.md +60 -0
  111. frontmattering/resources/fixtures/invalid/migration_silent_coercion_rejected/doc.md +59 -0
  112. frontmattering/resources/fixtures/invalid/missing_frontmatter_rejected/doc.md +1 -0
  113. frontmattering/resources/fixtures/invalid/non_root_exception_or_silent_fallback_rejected/doc.md +59 -0
  114. frontmattering/resources/fixtures/invalid/nonowner_redefines_intention_rejected/doc.md +58 -0
  115. frontmattering/resources/fixtures/invalid/output_format_conflation_rejected/doc.md +76 -0
  116. frontmattering/resources/fixtures/invalid/parser_byte_ranges_missing_rejected/doc.md +59 -0
  117. frontmattering/resources/fixtures/invalid/parser_partial_model_on_error_rejected/doc.md +59 -0
  118. frontmattering/resources/fixtures/invalid/parser_source_locator_missing_rejected/doc.md +59 -0
  119. frontmattering/resources/fixtures/invalid/projected_field_without_origin_rejected/doc.md +59 -0
  120. frontmattering/resources/fixtures/invalid/projection_rebuild_nondeterminism_rejected/doc.md +59 -0
  121. frontmattering/resources/fixtures/invalid/projection_row_without_source_locator_rejected/doc.md +59 -0
  122. frontmattering/resources/fixtures/invalid/relationship_resolution_scope_missing_rejected/doc.md +62 -0
  123. frontmattering/resources/fixtures/invalid/relationship_resolution_scope_unknown_rejected/doc.md +63 -0
  124. frontmattering/resources/fixtures/invalid/relationship_semantics_rejected/doc.md +63 -0
  125. frontmattering/resources/fixtures/invalid/relationship_target_unresolved_rejected/doc.md +63 -0
  126. frontmattering/resources/fixtures/invalid/release_surface_digest_missing_rejected/doc.md +63 -0
  127. frontmattering/resources/fixtures/invalid/required_dependency_cycle_rejected/a.md +63 -0
  128. frontmattering/resources/fixtures/invalid/required_dependency_cycle_rejected/b.md +63 -0
  129. frontmattering/resources/fixtures/invalid/routing_tag_not_in_task_catalog_rejected/doc.md +59 -0
  130. frontmattering/resources/fixtures/invalid/routing_tag_router_not_in_scope_rejected/doc.md +55 -0
  131. frontmattering/resources/fixtures/invalid/scope_on_nonsupersedes_rejected/doc.md +65 -0
  132. frontmattering/resources/fixtures/invalid/stale_bootstrap_receipt_rejected/doc.md +80 -0
  133. frontmattering/resources/fixtures/invalid/stale_validation_result_rejected/doc.md +60 -0
  134. frontmattering/resources/fixtures/invalid/supersedes_without_scope_rejected/doc.md +63 -0
  135. frontmattering/resources/fixtures/invalid/target_section_not_found_rejected/a.md +65 -0
  136. frontmattering/resources/fixtures/invalid/target_section_not_found_rejected/b.md +57 -0
  137. frontmattering/resources/fixtures/invalid/target_sections_on_external_edge_rejected/doc.md +67 -0
  138. frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/router.md +59 -0
  139. frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/selected.md +56 -0
  140. frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/skipped.md +56 -0
  141. frontmattering/resources/fixtures/invalid/template_claims_normative_authority_rejected/doc.md +57 -0
  142. frontmattering/resources/fixtures/invalid/two_documents_own_same_intention_rejected/a.md +76 -0
  143. frontmattering/resources/fixtures/invalid/two_documents_own_same_intention_rejected/b.md +76 -0
  144. frontmattering/resources/fixtures/invalid/undefined_conditional_token_rejected/doc.md +59 -0
  145. frontmattering/resources/fixtures/invalid/undefined_type_rejected/doc.md +59 -0
  146. frontmattering/resources/fixtures/invalid/unknown_authority_rejected/doc.md +57 -0
  147. frontmattering/resources/fixtures/invalid/unknown_document_role_rejected/doc.md +57 -0
  148. frontmattering/resources/fixtures/invalid/unknown_field_rejected/doc.md +58 -0
  149. frontmattering/resources/fixtures/invalid/unknown_root_block_rejected/doc.md +59 -0
  150. frontmattering/resources/fixtures/invalid/unresolved_reference_on_required_edge_rejected/doc.md +63 -0
  151. frontmattering/resources/fixtures/invalid/unsupported_schema_version_rejected/doc.md +57 -0
  152. frontmattering/resources/fixtures/invalid/untemplated_frontmatter_bypass_rejected/doc.md +59 -0
  153. frontmattering/resources/fixtures/invalid/validator_passes_conflation_rejected/doc.md +76 -0
  154. frontmattering/resources/fixtures/invalid/yaml_1_1_boolean_words_not_coerced/doc.md +57 -0
  155. frontmattering/resources/fixtures/pilot_security/blockquote.md +3 -0
  156. frontmattering/resources/fixtures/pilot_security/code_fence.md +5 -0
  157. frontmattering/resources/fixtures/pilot_security/dangerous_html.md +3 -0
  158. frontmattering/resources/fixtures/pilot_security/dangerous_uri.md +3 -0
  159. frontmattering/resources/fixtures/pilot_security/direct_instruction.md +3 -0
  160. frontmattering/resources/fixtures/pilot_security/generated_resource_limits.yaml +16 -0
  161. frontmattering/resources/fixtures/pilot_security/indirect_instruction.md +4 -0
  162. frontmattering/resources/fixtures/pilot_security/legitimate_security_discussion.md +3 -0
  163. frontmattering/resources/fixtures/pilot_security/link.md +3 -0
  164. frontmattering/resources/fixtures/pilot_security/table.md +5 -0
  165. frontmattering/resources/fixtures/valid/adoption_locator_trigger_word_is_data_valid/doc.md +80 -0
  166. frontmattering/resources/fixtures/valid/body_tether_fresh_valid/doc.md +63 -0
  167. frontmattering/resources/fixtures/valid/bootstrap_validation_receipt_valid/doc.md +66 -0
  168. frontmattering/resources/fixtures/valid/claim_prose_substrings_are_not_verdicts_valid/doc.md +57 -0
  169. frontmattering/resources/fixtures/valid/companion_and_appendix_edges_valid/a.md +69 -0
  170. frontmattering/resources/fixtures/valid/companion_and_appendix_edges_valid/b.md +57 -0
  171. frontmattering/resources/fixtures/valid/document_binding_to_external_intention_valid/doc.md +57 -0
  172. frontmattering/resources/fixtures/valid/document_realizing_external_intention_valid/doc.md +57 -0
  173. frontmattering/resources/fixtures/valid/document_referencing_external_intention_valid/doc.md +57 -0
  174. frontmattering/resources/fixtures/valid/evidence_record_valid/doc.md +57 -0
  175. frontmattering/resources/fixtures/valid/evidence_support_substrings_are_not_proof_valid/doc.md +66 -0
  176. frontmattering/resources/fixtures/valid/external_document_set_edge_valid/doc.md +65 -0
  177. frontmattering/resources/fixtures/valid/frontmatter_adoption_record_valid/doc.md +80 -0
  178. frontmattering/resources/fixtures/valid/frontmatter_projection_mapping_valid/doc.md +57 -0
  179. frontmattering/resources/fixtures/valid/frontmatter_spec_bootstrap_valid/doc.md +76 -0
  180. frontmattering/resources/fixtures/valid/implementation_contract_valid/doc.md +57 -0
  181. frontmattering/resources/fixtures/valid/normative_spec_valid/doc.md +57 -0
  182. frontmattering/resources/fixtures/valid/orientation_router_valid/doc.md +59 -0
  183. frontmattering/resources/fixtures/valid/release_metadata_valid/doc.md +57 -0
  184. frontmattering/resources/fixtures/valid/target_sections_resolved_valid/a.md +65 -0
  185. frontmattering/resources/fixtures/valid/target_sections_resolved_valid/b.md +57 -0
  186. frontmattering/resources/fixtures/valid/template_valid/doc.md +57 -0
  187. frontmattering/resources/fixtures/valid/unresolved_reference_optional_edge_valid/doc.md +63 -0
  188. frontmattering/resources/fixtures/valid/untemplated_document_valid/doc.md +57 -0
  189. frontmattering/resources/fixtures/valid/yaml_1_2_plain_words_valid/doc.md +60 -0
  190. frontmattering/resources/generated/FRONTMATTERING_FRONTMATTER_SCHEMA.schema.json +2422 -0
  191. frontmattering/resources/tools/__init__.py +0 -0
  192. frontmattering/resources/tools/check_frontmatter_sqlite_index.py +40 -0
  193. frontmattering/resources/tools/check_kernel_cleanliness.py +103 -0
  194. frontmattering/resources/tools/check_kernel_exception_taxonomy.py +145 -0
  195. frontmattering/resources/tools/check_markdown_parser_adoption.py +35 -0
  196. frontmattering/resources/tools/check_pilot_readiness.py +545 -0
  197. frontmattering/resources/tools/check_projection_consistency.py +35 -0
  198. frontmattering/resources/tools/check_python_syntax.py +29 -0
  199. frontmattering/resources/tools/check_report_quality.py +478 -0
  200. frontmattering/resources/tools/distribution_receipt.py +54 -0
  201. frontmattering/resources/tools/finding_registry.py +625 -0
  202. frontmattering/resources/tools/frontmattering_cli.py +18 -0
  203. frontmattering/resources/tools/generate_ai_index.py +123 -0
  204. frontmattering/resources/tools/generate_frontmatter_schema.py +70 -0
  205. frontmattering/resources/tools/kernel_reporting.py +1495 -0
  206. frontmattering/resources/tools/make_fixtures.py +507 -0
  207. frontmattering/resources/tools/pack_kernel_distro.py +430 -0
  208. frontmattering/resources/tools/test_verify_kernel.py +177 -0
  209. frontmattering/resources/tools/validate_frontmatters.py +100 -0
  210. frontmattering/resources/tools/verify_kernel.py +197 -0
  211. frontmattering/resources/workflow-docs-manifest.yaml +199 -0
  212. frontmattering/schema_projection.py +288 -0
  213. frontmattering/speccompile.py +81 -0
  214. frontmattering/validator.py +957 -0
  215. frontmattering-0.4.2.dist-info/METADATA +91 -0
  216. frontmattering-0.4.2.dist-info/RECORD +224 -0
  217. frontmattering-0.4.2.dist-info/WHEEL +5 -0
  218. frontmattering-0.4.2.dist-info/entry_points.txt +2 -0
  219. frontmattering-0.4.2.dist-info/licenses/COMMERCIAL_LICENSE_REQUIRED.md +14 -0
  220. frontmattering-0.4.2.dist-info/licenses/LICENSE.md +39 -0
  221. frontmattering-0.4.2.dist-info/licenses/NOTICE +10 -0
  222. frontmattering-0.4.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12 -0
  223. frontmattering-0.4.2.dist-info/licenses/TRADEMARKS.md +7 -0
  224. frontmattering-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1074 @@
1
+ """Structured error hierarchy for Frontmattering.
2
+
3
+ Design principles (from GOVERNANCE_RULES):
4
+ - LOUD: Exceptions must be raised, not silently logged
5
+ - INFORMATIVE: Every exception answers WHAT/WHERE/WHY
6
+ - CONTEXTUAL: Include relevant input values (truncated if large)
7
+
8
+ Hierarchy:
9
+ FrontmatteringError (base)
10
+ ├── CliArgError
11
+ ├── ParsingError
12
+ │ ├── DocumentTooLargeError
13
+ │ └── MalformedContentError
14
+ ├── ValidationError
15
+ │ ├── SchemaValidationError
16
+ │ └── ComplianceError
17
+ ├── StorageError
18
+ │ ├── NotFoundError
19
+ │ └── AlreadyExistsError
20
+ ├── StalenessError
21
+ │ ├── StaleFactsError
22
+ │ └── StaleDocumentError
23
+ ├── EntityError
24
+ │ ├── EntityNotFoundError
25
+ │ └── EntityAmbiguousError
26
+ └── ExternalToolError
27
+ ├── ToolTimeoutError
28
+ └── ToolResponseError
29
+
30
+ Usage:
31
+ from Frontmattering.common import ValidationError, EntityNotFoundError
32
+
33
+ raise ValidationError(
34
+ what="Schema validation failed",
35
+ where="schema/extractor.py:extract_plan_manifest",
36
+ why="Missing required field 'steps'",
37
+ context={"file": plan_file, "fields_found": list(data.keys())}
38
+ )
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from collections.abc import Callable
44
+ from typing import Any, TypeVar
45
+
46
+ T = TypeVar("T")
47
+
48
+
49
+ def capture_exception(
50
+ fn: Callable[..., T], *args: Any, **kwargs: Any
51
+ ) -> tuple[T | None, Exception | None]:
52
+ """Run a callable and capture any raised exception as a value."""
53
+ try:
54
+ return fn(*args, **kwargs), None
55
+ except Exception as exc:
56
+ return None, exc
57
+
58
+
59
+ class FrontmatteringError(Exception):
60
+ """Base exception for all Frontmattering errors.
61
+
62
+ All Frontmattering exceptions follow WHAT/WHERE/WHY/FIX pattern.
63
+
64
+ Args:
65
+ what: Clear description of what failed
66
+ where: Context about location (function, file, entity)
67
+ why: The cause or original exception message
68
+ fix: User-actionable guidance on how to resolve the error
69
+ context: Additional values for debugging (auto-truncated)
70
+
71
+ Display methods:
72
+ str(error) → Single-line format for logs
73
+ error.display() → Multi-line rich format for terminal
74
+ error.to_dict() → Structured format for JSON output
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ what: str,
80
+ where: str | None = None,
81
+ why: str | None = None,
82
+ fix: str | None = None,
83
+ context: dict[str, Any] | None = None,
84
+ ) -> None:
85
+ self.what = what
86
+ self.where = where
87
+ self.why = why
88
+ self.fix = fix
89
+ self.context = context or {}
90
+
91
+ # Build message following GOVERNANCE_RULES template
92
+ parts = [f"WHAT: {what}"]
93
+ if where:
94
+ parts.append(f"WHERE: {where}")
95
+ if why:
96
+ parts.append(f"WHY: {why}")
97
+ if fix:
98
+ parts.append(f"FIX: {fix}")
99
+ if context:
100
+ # Truncate large context values
101
+ truncated = {k: _truncate(v) for k, v in context.items()}
102
+ parts.append(f"CONTEXT: {truncated}")
103
+
104
+ message = " | ".join(parts)
105
+ super().__init__(message)
106
+
107
+ def display(self, use_rich: bool = True) -> str:
108
+ """Format error for terminal display.
109
+
110
+ Args:
111
+ use_rich: If True, include ANSI color codes
112
+
113
+ Returns:
114
+ Multi-line formatted error message
115
+ """
116
+ lines = []
117
+
118
+ # Header with error type
119
+ error_type = self.__class__.__name__
120
+ if use_rich:
121
+ lines.append(f"\033[1;31m✗ {error_type}\033[0m") # Bold red
122
+ else:
123
+ lines.append(f"✗ {error_type}")
124
+
125
+ # WHAT (always present)
126
+ if use_rich:
127
+ lines.append(f" \033[1mWhat:\033[0m {self.what}")
128
+ else:
129
+ lines.append(f" What: {self.what}")
130
+
131
+ # WHERE (if present)
132
+ if self.where:
133
+ if use_rich:
134
+ lines.append(f" \033[1mWhere:\033[0m {self.where}")
135
+ else:
136
+ lines.append(f" Where: {self.where}")
137
+
138
+ # WHY (if present)
139
+ if self.why:
140
+ if use_rich:
141
+ lines.append(f" \033[1mWhy:\033[0m {self.why}")
142
+ else:
143
+ lines.append(f" Why: {self.why}")
144
+
145
+ # FIX (if present - user-actionable guidance)
146
+ if self.fix:
147
+ if use_rich:
148
+ lines.append(f" \033[1;32mFix:\033[0m {self.fix}") # Bold green
149
+ else:
150
+ lines.append(f" Fix: {self.fix}")
151
+
152
+ # CONTEXT (if present, formatted nicely)
153
+ if self.context:
154
+ if use_rich:
155
+ lines.append(" \033[1mContext:\033[0m")
156
+ else:
157
+ lines.append(" Context:")
158
+ for key, value in self.context.items():
159
+ truncated = _truncate(value, 80)
160
+ lines.append(f" {key}: {truncated}")
161
+
162
+ return "\n".join(lines)
163
+
164
+ def to_dict(self) -> dict[str, Any]:
165
+ """Convert error to dictionary for JSON serialization."""
166
+ return {
167
+ "error_type": self.__class__.__name__,
168
+ "what": self.what,
169
+ "where": self.where,
170
+ "why": self.why,
171
+ "fix": self.fix,
172
+ "context": self.context,
173
+ }
174
+
175
+
176
+ class CliArgError(FrontmatteringError):
177
+ """CLI argument parsing/shape errors with actionable fixes."""
178
+
179
+
180
+ def _truncate(value: Any, max_len: int = 100) -> str:
181
+ """Truncate value for error context display."""
182
+ s = repr(value)
183
+ if len(s) > max_len:
184
+ return s[: max_len - 3] + "..."
185
+ return s
186
+
187
+
188
+ # =============================================================================
189
+ # ERROR DISPLAY UTILITIES
190
+ # =============================================================================
191
+
192
+
193
+ def format_error(error: Exception, use_rich: bool = True) -> str:
194
+ """Format any exception for terminal display.
195
+
196
+ Args:
197
+ error: Exception to format
198
+ use_rich: If True, include ANSI color codes
199
+
200
+ Returns:
201
+ Formatted error string
202
+
203
+ Usage:
204
+ try:
205
+ risky_operation()
206
+ except Exception as e:
207
+ print(format_error(e))
208
+ """
209
+ if isinstance(error, FrontmatteringError):
210
+ return error.display(use_rich=use_rich)
211
+
212
+ # Format stdlib exceptions nicely too
213
+ error_type = error.__class__.__name__
214
+ message = str(error)
215
+
216
+ if use_rich:
217
+ return f"\033[1;31m✗ {error_type}\033[0m\n {message}"
218
+ return f"✗ {error_type}\n {message}"
219
+
220
+
221
+ def print_error(error: Exception, use_rich: bool = True) -> None:
222
+ """Print formatted error to stderr.
223
+
224
+ Args:
225
+ error: Exception to print
226
+ use_rich: If True, include ANSI color codes
227
+ """
228
+ import sys
229
+
230
+ print(format_error(error, use_rich=use_rich), file=sys.stderr)
231
+
232
+
233
+ # =============================================================================
234
+ # BATCH ERROR HANDLING (Governance-compliant deferred raising)
235
+ # =============================================================================
236
+
237
+
238
+ class BatchProcessingError(FrontmatteringError):
239
+ """Multiple items failed during batch processing.
240
+
241
+ Per GOVERNANCE_RULES: batch operations should accumulate errors
242
+ and raise a summary at the end (still LOUD, not silent).
243
+
244
+ Usage:
245
+ collector = ErrorCollector("Processing items")
246
+ for i, item in enumerate(items):
247
+ try:
248
+ process(item)
249
+ except SpecificException as e:
250
+ collector.add(i, str(e))
251
+ collector.raise_if_errors() # Raises BatchProcessingError if any
252
+ """
253
+
254
+ def __init__(
255
+ self,
256
+ operation: str,
257
+ errors: list[tuple[int | str, str]],
258
+ where: str | None = None,
259
+ ) -> None:
260
+ self.operation = operation
261
+ self.errors = errors
262
+ self.error_count = len(errors)
263
+
264
+ # Show first 3 errors in summary
265
+ preview = errors[:3]
266
+ preview_str = "; ".join(f"[{idx}] {msg}" for idx, msg in preview)
267
+ if len(errors) > 3:
268
+ preview_str += f"; ... and {len(errors) - 3} more"
269
+
270
+ super().__init__(
271
+ what=f"{operation}: {len(errors)} item(s) failed",
272
+ where=where,
273
+ why=preview_str,
274
+ context={"total_errors": len(errors), "first_errors": preview},
275
+ )
276
+
277
+
278
+ class ErrorCollector:
279
+ """Collect errors during batch processing for deferred raising.
280
+
281
+ Governance-compliant pattern: errors are accumulated but still LOUD
282
+ at the end of the batch.
283
+
284
+ Usage:
285
+ collector = ErrorCollector("Validating entities")
286
+ for entity in entities:
287
+ try:
288
+ validate(entity)
289
+ except ValidationError as e:
290
+ collector.add(entity.name, str(e))
291
+ collector.raise_if_errors() # LOUD at the end
292
+ """
293
+
294
+ def __init__(self, operation: str, where: str | None = None) -> None:
295
+ self.operation = operation
296
+ self.where = where
297
+ self.errors: list[tuple[int | str, str]] = []
298
+
299
+ def add(self, item_id: int | str, message: str) -> None:
300
+ """Record an error for an item."""
301
+ self.errors.append((item_id, message))
302
+
303
+ def has_errors(self) -> bool:
304
+ """Check if any errors were collected."""
305
+ return len(self.errors) > 0
306
+
307
+ def raise_if_errors(self) -> None:
308
+ """Raise BatchProcessingError if any errors were collected.
309
+
310
+ This is the LOUD part - caller knows something failed.
311
+ """
312
+ if self.errors:
313
+ raise BatchProcessingError(
314
+ operation=self.operation,
315
+ errors=self.errors,
316
+ where=self.where,
317
+ )
318
+
319
+ def __len__(self) -> int:
320
+ return len(self.errors)
321
+
322
+
323
+ # =============================================================================
324
+ # PARSING ERRORS (markdown/, mdqt/)
325
+ # =============================================================================
326
+
327
+
328
+ class ParsingError(FrontmatteringError):
329
+ """Base error for parsing failures."""
330
+
331
+
332
+ class YAMLReadError(ParsingError):
333
+ """YAML source file could not be read from disk.
334
+
335
+ Used by: yqt/
336
+ """
337
+
338
+ def __init__(
339
+ self,
340
+ file_path: str,
341
+ *,
342
+ where: str | None = None,
343
+ why: str | None = None,
344
+ fix: str | None = None,
345
+ context: dict[str, Any] | None = None,
346
+ ) -> None:
347
+ ctx = {"file_path": file_path}
348
+ if context:
349
+ ctx.update(context)
350
+ super().__init__(
351
+ what="Failed to read YAML source file",
352
+ where=where or "yqt.spec.extract_record_from_file",
353
+ why=why,
354
+ fix=fix or "Verify file permissions and file system accessibility.",
355
+ context=ctx,
356
+ )
357
+ self.file_path = file_path
358
+
359
+
360
+ class FileReadError(ParsingError):
361
+ """Non-YAML file could not be read from disk.
362
+
363
+ Used by: yqt/ for JSON and markdown propagation checks.
364
+ """
365
+
366
+ def __init__(
367
+ self,
368
+ file_path: str,
369
+ *,
370
+ content_kind: str = "file",
371
+ where: str | None = None,
372
+ why: str | None = None,
373
+ fix: str | None = None,
374
+ context: dict[str, Any] | None = None,
375
+ ) -> None:
376
+ ctx = {"file_path": file_path, "content_kind": content_kind}
377
+ if context:
378
+ ctx.update(context)
379
+ super().__init__(
380
+ what=f"Failed to read {content_kind} file",
381
+ where=where or "yqt.cli._run_propagation_checks",
382
+ why=why,
383
+ fix=fix or "Verify file path and read permissions.",
384
+ context=ctx,
385
+ )
386
+ self.file_path = file_path
387
+ self.content_kind = content_kind
388
+
389
+
390
+ class DocumentTooLargeError(ParsingError):
391
+ """Document exceeds size limits.
392
+
393
+ Used by: markdown/, mdqt/
394
+ """
395
+
396
+ def __init__(
397
+ self,
398
+ file_path: str,
399
+ size_bytes: int,
400
+ max_bytes: int,
401
+ where: str | None = None,
402
+ ) -> None:
403
+ super().__init__(
404
+ what=f"Document too large: {size_bytes:,} bytes",
405
+ where=where or "parser",
406
+ why=f"Exceeds limit of {max_bytes:,} bytes",
407
+ context={"file": file_path, "size": size_bytes, "max": max_bytes},
408
+ )
409
+ self.file_path = file_path
410
+ self.size_bytes = size_bytes
411
+ self.max_bytes = max_bytes
412
+
413
+
414
+ class MalformedContentError(ParsingError):
415
+ """Content structure is invalid.
416
+
417
+ Used by: markdown/, mdqt/, schema/
418
+ """
419
+
420
+ def __init__(
421
+ self,
422
+ what: str,
423
+ where: str | None = None,
424
+ why: str | None = None,
425
+ fix: str | None = None,
426
+ line: int | None = None,
427
+ context: dict[str, Any] | None = None,
428
+ ) -> None:
429
+ ctx = context or {}
430
+ if line is not None:
431
+ ctx["line"] = line
432
+ super().__init__(what=what, where=where, why=why, fix=fix, context=ctx)
433
+ self.line = line
434
+
435
+
436
+ class CollectorTimeoutError(ParsingError, TimeoutError):
437
+ """Collector exceeded timeout during parsing.
438
+
439
+ Used by: markdown/utils/timeout.py
440
+
441
+ Inherits from TimeoutError for backward compatibility with existing
442
+ except TimeoutError handlers.
443
+ """
444
+
445
+ def __init__(
446
+ self,
447
+ collector_name: str | None = None,
448
+ timeout_seconds: float = 0.0,
449
+ where: str | None = None,
450
+ ) -> None:
451
+ super().__init__(
452
+ what=f"Collector timeout after {timeout_seconds}s",
453
+ where=where or "markdown.collectors",
454
+ why=f"Collector '{collector_name or 'unknown'}' exceeded time limit",
455
+ fix="Simplify document structure or increase timeout limit",
456
+ context={"timeout": timeout_seconds, "collector": collector_name},
457
+ )
458
+ self.collector_name = collector_name
459
+ self.timeout_seconds = timeout_seconds
460
+
461
+
462
+ class ReentrancyError(ParsingError):
463
+ """Reentrancy violation detected (nested call corrupts state).
464
+
465
+ Used by: markdown/utils/token_warehouse.py
466
+
467
+ This is a programming error - calling a non-reentrant method while
468
+ it's already executing will corrupt internal state.
469
+ """
470
+
471
+ def __init__(
472
+ self,
473
+ method: str,
474
+ where: str | None = None,
475
+ fix: str | None = None,
476
+ ) -> None:
477
+ super().__init__(
478
+ what=f"Reentrancy violation in {method}()",
479
+ where=where,
480
+ why=f"{method}() called while already executing - state corruption risk",
481
+ fix=fix or f"Check collectors for reentrant calls to {method}()",
482
+ context={"method": method},
483
+ )
484
+ self.method = method
485
+
486
+
487
+ # =============================================================================
488
+ # VALIDATION ERRORS (schema/, fdp/)
489
+ # =============================================================================
490
+
491
+
492
+ class ValidationError(FrontmatteringError, ValueError):
493
+ """Base error for validation failures."""
494
+
495
+
496
+ class QueryNoMatchError(FrontmatteringError):
497
+ """A targeted query expected at least one row but matched none.
498
+
499
+ Used by: yqt/
500
+ """
501
+
502
+ def __init__(
503
+ self,
504
+ *,
505
+ query_label: str,
506
+ where: str | None = None,
507
+ why: str | None = None,
508
+ fix: str | None = None,
509
+ context: dict[str, Any] | None = None,
510
+ ) -> None:
511
+ super().__init__(
512
+ what=f"{query_label} query matched zero rows for a targeted extraction",
513
+ where=where,
514
+ why=why or "The supplied selector combination resolved to no indexed rows.",
515
+ fix=fix
516
+ or "Verify the selector values and rerun the query against indexed files.",
517
+ context=context,
518
+ )
519
+
520
+
521
+ class DependencyMissingError(ValidationError):
522
+ """Required (optional) dependency is not installed or cannot be imported.
523
+
524
+ Used by: qt_base/, rag/
525
+ """
526
+
527
+ def __init__(
528
+ self,
529
+ dependency: str,
530
+ *,
531
+ where: str | None = None,
532
+ why: str | None = None,
533
+ fix: str | None = None,
534
+ context: dict[str, Any] | None = None,
535
+ ) -> None:
536
+ ctx = {"dependency": dependency}
537
+ if context:
538
+ ctx.update(context)
539
+ super().__init__(
540
+ what=f"Dependency not available: {dependency}",
541
+ where=where,
542
+ why=why,
543
+ fix=fix or f"Install with: uv add {dependency}",
544
+ context=ctx,
545
+ )
546
+ self.dependency = dependency
547
+
548
+
549
+ class SchemaValidationError(ValidationError):
550
+ """Schema validation failed.
551
+
552
+ Used by: schema/
553
+ """
554
+
555
+ def __init__(
556
+ self,
557
+ what: str,
558
+ field: str | None = None,
559
+ expected: str | None = None,
560
+ got: Any = None,
561
+ where: str | None = None,
562
+ fix: str | None = None,
563
+ ) -> None:
564
+ why_parts = []
565
+ if expected:
566
+ why_parts.append(f"expected {expected}")
567
+ if got is not None:
568
+ why_parts.append(f"got {_truncate(got, 50)}")
569
+ why = ", ".join(why_parts) if why_parts else None
570
+
571
+ ctx: dict[str, Any] = {}
572
+ if field:
573
+ ctx["field"] = field
574
+
575
+ super().__init__(what=what, where=where, why=why, fix=fix, context=ctx)
576
+ self.field = field
577
+ self.expected = expected
578
+ self.got = got
579
+
580
+
581
+ class ComplianceError(ValidationError):
582
+ """FDP compliance check failed.
583
+
584
+ Used by: fdp/
585
+ """
586
+
587
+ def __init__(
588
+ self,
589
+ rule: str,
590
+ violation: str,
591
+ where: str | None = None,
592
+ context: dict[str, Any] | None = None,
593
+ ) -> None:
594
+ super().__init__(
595
+ what=f"Compliance violation: {rule}",
596
+ where=where,
597
+ why=violation,
598
+ context=context,
599
+ )
600
+ self.rule = rule
601
+ self.violation = violation
602
+
603
+
604
+ class Q6CompileError(FrontmatteringError):
605
+ """Base error for q6 compiler failures with explicit blocker routing."""
606
+
607
+ def __init__(
608
+ self,
609
+ *,
610
+ blocker_code: str,
611
+ what: str,
612
+ where: str | None = None,
613
+ why: str | None = None,
614
+ fix: str | None = None,
615
+ context: dict[str, Any] | None = None,
616
+ ) -> None:
617
+ merged_context = {"blocker_code": blocker_code}
618
+ if context:
619
+ merged_context.update(context)
620
+ super().__init__(
621
+ what=what,
622
+ where=where,
623
+ why=why,
624
+ fix=fix,
625
+ context=merged_context,
626
+ )
627
+ self.blocker_code = blocker_code
628
+
629
+
630
+ class Q6IntakeContractError(Q6CompileError):
631
+ """Authored intake violates the q6 SSOT contract."""
632
+
633
+
634
+ class Q6MdparseAcquisitionError(Q6CompileError):
635
+ """Compiler could not acquire structured intake data from mdparse."""
636
+
637
+
638
+ class Q6MdparseContractError(Q6CompileError):
639
+ """mdparse returned a payload that violates compiler expectations."""
640
+
641
+
642
+ class Q6TopologyError(Q6CompileError):
643
+ """Authoritative q6 topology cannot be resolved exactly."""
644
+
645
+
646
+ class Q6CompilerAuthorityError(Q6CompileError):
647
+ """Compiler authority/provenance binding is missing or inconsistent."""
648
+
649
+
650
+ class Q6EmissionError(Q6CompileError):
651
+ """Compiler could not safely emit the required q6 artifact."""
652
+
653
+
654
+ class Q6InternalCompilerError(Q6CompileError):
655
+ """Unexpected internal compiler failure escaped typed q6 checks."""
656
+
657
+
658
+ class ColumnNotFoundError(ValidationError):
659
+ """Requested column(s) not found in parquet schema.
660
+
661
+ Used by shared query layers.
662
+ """
663
+
664
+ def __init__(
665
+ self,
666
+ unknown_columns: list[str],
667
+ available_columns: list[str],
668
+ suggestions: dict[str, list[str]] | None = None,
669
+ where: str | None = None,
670
+ parquet_name: str = "parquet",
671
+ ) -> None:
672
+ # Build suggestion text
673
+ suggestion_lines = []
674
+ if suggestions:
675
+ for col, hints in suggestions.items():
676
+ if hints:
677
+ suggestion_lines.append(f"'{col}' → did you mean: {', '.join(hints)}?")
678
+
679
+ super().__init__(
680
+ what=f"Unknown column(s): {', '.join(sorted(unknown_columns))}",
681
+ where=where,
682
+ why="\n".join(suggestion_lines) if suggestion_lines else None,
683
+ fix=f"Available columns in {parquet_name}: {', '.join(sorted(available_columns))}",
684
+ context={"unknown": unknown_columns, "available": available_columns},
685
+ )
686
+ self.unknown_columns = unknown_columns
687
+ self.available_columns = available_columns
688
+ self.suggestions = suggestions
689
+
690
+
691
+ # =============================================================================
692
+ # STORAGE ERRORS (state/, rag/, fact_archive/)
693
+ # =============================================================================
694
+
695
+
696
+ class StorageError(FrontmatteringError):
697
+ """Base error for storage operations."""
698
+
699
+
700
+ class NotFoundError(StorageError):
701
+ """Requested item not found in storage.
702
+
703
+ Used by: state/, rag/, crossref/
704
+ """
705
+
706
+ def __init__(
707
+ self,
708
+ item_type: str,
709
+ item_id: str,
710
+ where: str | None = None,
711
+ fix: str | None = None,
712
+ ) -> None:
713
+ super().__init__(
714
+ what=f"{item_type} not found",
715
+ where=where,
716
+ why=f"No {item_type} with id '{item_id}'",
717
+ fix=fix,
718
+ context={"type": item_type, "id": item_id},
719
+ )
720
+ self.item_type = item_type
721
+ self.item_id = item_id
722
+
723
+
724
+ class AlreadyExistsError(StorageError):
725
+ """Item already exists in storage.
726
+
727
+ Used by: state/, rag/
728
+ """
729
+
730
+ def __init__(
731
+ self,
732
+ item_type: str,
733
+ item_id: str,
734
+ where: str | None = None,
735
+ ) -> None:
736
+ super().__init__(
737
+ what=f"{item_type} already exists",
738
+ where=where,
739
+ why=f"{item_type} with id '{item_id}' already present",
740
+ context={"type": item_type, "id": item_id},
741
+ )
742
+ self.item_type = item_type
743
+ self.item_id = item_id
744
+
745
+
746
+ class LockTimeoutError(StorageError):
747
+ """Failed to acquire a parquet lock within the timeout window."""
748
+
749
+
750
+ # =============================================================================
751
+ # STALENESS ERRORS (crossref/, fdp/, mdqt/)
752
+ # =============================================================================
753
+
754
+
755
+ class StalenessError(FrontmatteringError):
756
+ """Base error for stale data detection."""
757
+
758
+
759
+ class StaleFactsError(StalenessError):
760
+ """FACTS.parquet is stale relative to source code.
761
+
762
+ Used by: crossref/, fdp/, intelligence/
763
+
764
+ TODO(human): Implement staleness detection logic
765
+ """
766
+
767
+ def __init__(
768
+ self,
769
+ facts_file: str,
770
+ facts_mtime: str,
771
+ source_mtime: str,
772
+ where: str | None = None,
773
+ ) -> None:
774
+ super().__init__(
775
+ what="FACTS.parquet is stale",
776
+ where=where,
777
+ why=f"Facts from {facts_mtime}, source modified {source_mtime}",
778
+ context={"facts_file": facts_file},
779
+ )
780
+ self.facts_file = facts_file
781
+ self.facts_mtime = facts_mtime
782
+ self.source_mtime = source_mtime
783
+
784
+
785
+ class StaleDocumentError(StalenessError):
786
+ """MD.parquet is stale relative to markdown files.
787
+
788
+ Used by: mdqt/, crossref/
789
+ """
790
+
791
+ def __init__(
792
+ self,
793
+ md_file: str,
794
+ parquet_mtime: str,
795
+ source_mtime: str,
796
+ where: str | None = None,
797
+ ) -> None:
798
+ super().__init__(
799
+ what="MD.parquet is stale",
800
+ where=where,
801
+ why=f"Parquet from {parquet_mtime}, source modified {source_mtime}",
802
+ context={"md_file": md_file},
803
+ )
804
+ self.md_file = md_file
805
+ self.parquet_mtime = parquet_mtime
806
+ self.source_mtime = source_mtime
807
+
808
+
809
+ class StaleIndexError(StalenessError):
810
+ """RAG index is stale relative to source parquet.
811
+
812
+ Used by domain RAG implementations.
813
+
814
+ Raised when a FAISS index was built from a different parquet snapshot
815
+ than the current one. Fail-closed behavior to prevent stale RAG answers.
816
+ """
817
+
818
+ def __init__(
819
+ self,
820
+ index_path: str,
821
+ index_fingerprint: str,
822
+ current_fingerprint: str,
823
+ source_path: str | None = None,
824
+ where: str | None = None,
825
+ ) -> None:
826
+ fix_cmd = "Rebuild the RAG index from the current parquet source."
827
+ super().__init__(
828
+ what="RAG index is stale",
829
+ where=where,
830
+ why=f"Index fingerprint {index_fingerprint[:12]}... != current {current_fingerprint[:12]}...",
831
+ fix=fix_cmd,
832
+ context={
833
+ "index_path": index_path,
834
+ "index_fingerprint": index_fingerprint,
835
+ "current_fingerprint": current_fingerprint,
836
+ "source_path": source_path,
837
+ },
838
+ )
839
+ self.index_path = index_path
840
+ self.index_fingerprint = index_fingerprint
841
+ self.current_fingerprint = current_fingerprint
842
+ self.source_path = source_path
843
+
844
+
845
+ # =============================================================================
846
+ # PARQUET SAFETY ERRORS (qt_base/)
847
+ # =============================================================================
848
+
849
+
850
+ class UnsafeParquetAccessError(FrontmatteringError):
851
+ """Unsafe parquet read blocked (bypassed freshness/lock enforcement)."""
852
+
853
+ def __init__(
854
+ self,
855
+ api: str,
856
+ *,
857
+ caller_module: str | None = None,
858
+ caller_function: str | None = None,
859
+ where: str | None = None,
860
+ ) -> None:
861
+ ctx: dict[str, Any] = {"api": api}
862
+ if caller_module:
863
+ ctx["caller_module"] = caller_module
864
+ if caller_function:
865
+ ctx["caller_function"] = caller_function
866
+
867
+ super().__init__(
868
+ what="Unsafe direct parquet read blocked",
869
+ where=where,
870
+ why=f"{api} was called without passing through qt_base.index_guard",
871
+ fix="Use Frontmattering.qt_base.index_guard.ensure_index_fresh (or a guard-protected Query class) before reading parquet.",
872
+ context=ctx,
873
+ )
874
+ self.api = api
875
+ self.caller_module = caller_module
876
+ self.caller_function = caller_function
877
+
878
+
879
+ # =============================================================================
880
+ # ENTITY ERRORS (schema/, intelligence/)
881
+ # =============================================================================
882
+
883
+
884
+ class EntityError(FrontmatteringError):
885
+ """Base error for entity-related issues."""
886
+
887
+
888
+ class EntityNotFoundError(EntityError):
889
+ """Referenced entity does not exist in codebase.
890
+
891
+ Used by: schema/, intelligence/, crossref/
892
+ """
893
+
894
+ def __init__(
895
+ self,
896
+ entity_type: str,
897
+ entity_name: str,
898
+ searched_in: str | None = None,
899
+ where: str | None = None,
900
+ ) -> None:
901
+ ctx: dict[str, Any] = {"type": entity_type, "name": entity_name}
902
+ if searched_in:
903
+ ctx["searched_in"] = searched_in
904
+
905
+ super().__init__(
906
+ what=f"{entity_type} '{entity_name}' not found",
907
+ where=where,
908
+ why=f"No matching {entity_type} in codebase",
909
+ context=ctx,
910
+ )
911
+ self.entity_type = entity_type
912
+ self.entity_name = entity_name
913
+ self.searched_in = searched_in
914
+
915
+
916
+ class EntityAmbiguousError(EntityError):
917
+ """Entity reference matches multiple definitions.
918
+
919
+ Used by: schema/, crossref/
920
+ """
921
+
922
+ def __init__(
923
+ self,
924
+ entity_name: str,
925
+ matches: list[str],
926
+ where: str | None = None,
927
+ ) -> None:
928
+ super().__init__(
929
+ what=f"Ambiguous entity reference '{entity_name}'",
930
+ where=where,
931
+ why=f"Matches {len(matches)} definitions",
932
+ context={"matches": matches[:5]}, # Limit to 5
933
+ )
934
+ self.entity_name = entity_name
935
+ self.matches = matches
936
+
937
+
938
+ # =============================================================================
939
+ # EXTERNAL TOOL ERRORS (subprocess, FFQT, git)
940
+ # =============================================================================
941
+
942
+
943
+ class ExternalToolError(FrontmatteringError):
944
+ """Base error for external tool/subprocess failures.
945
+
946
+ Used by: intelligence/, schema/verifier, mdqt/staleness
947
+
948
+ Covers failures from:
949
+ - FFQT queries (subprocess calls to fact-file-query-tool)
950
+ - Git operations (subprocess calls to git)
951
+ - Other external CLI tools
952
+
953
+ Usage:
954
+ raise ExternalToolError(
955
+ tool="ffqt",
956
+ command="query --file validator.py --entity function.validate",
957
+ why="Process timed out after 30s",
958
+ fix="Increase FFQT_TIMEOUT or check if FACTS.parquet exists"
959
+ )
960
+ """
961
+
962
+ def __init__(
963
+ self,
964
+ tool: str,
965
+ command: str | None = None,
966
+ why: str | None = None,
967
+ fix: str | None = None,
968
+ where: str | None = None,
969
+ stdout: str | None = None,
970
+ stderr: str | None = None,
971
+ exit_code: int | None = None,
972
+ ) -> None:
973
+ self.tool = tool
974
+ self.command = command
975
+ self.stdout = stdout
976
+ self.stderr = stderr
977
+ self.exit_code = exit_code
978
+
979
+ ctx: dict[str, Any] = {"tool": tool}
980
+ if command:
981
+ ctx["command"] = command[:200] + "..." if len(command) > 200 else command
982
+ if exit_code is not None:
983
+ ctx["exit_code"] = exit_code
984
+ if stderr:
985
+ ctx["stderr"] = stderr[:200] + "..." if len(stderr) > 200 else stderr
986
+
987
+ super().__init__(
988
+ what=f"External tool '{tool}' failed",
989
+ where=where,
990
+ why=why,
991
+ fix=fix or f"Verify '{tool}' is installed and accessible",
992
+ context=ctx,
993
+ )
994
+
995
+
996
+ class ToolTimeoutError(ExternalToolError):
997
+ """External tool exceeded timeout.
998
+
999
+ Used by: schema/verifier
1000
+ """
1001
+
1002
+ def __init__(
1003
+ self,
1004
+ tool: str,
1005
+ timeout_seconds: float,
1006
+ command: str | None = None,
1007
+ where: str | None = None,
1008
+ ) -> None:
1009
+ self.timeout_seconds = timeout_seconds
1010
+ super().__init__(
1011
+ tool=tool,
1012
+ command=command,
1013
+ why=f"Process timed out after {timeout_seconds}s",
1014
+ fix="Increase timeout or simplify the query",
1015
+ where=where,
1016
+ )
1017
+
1018
+
1019
+ class ToolResponseError(ExternalToolError):
1020
+ """External tool returned invalid/unparseable response.
1021
+
1022
+ Used by: intelligence/, schema/verifier (JSON decode failures)
1023
+ """
1024
+
1025
+ def __init__(
1026
+ self,
1027
+ tool: str,
1028
+ expected_format: str = "JSON",
1029
+ parse_error: str | None = None,
1030
+ command: str | None = None,
1031
+ where: str | None = None,
1032
+ stdout: str | None = None,
1033
+ ) -> None:
1034
+ self.expected_format = expected_format
1035
+ self.parse_error = parse_error
1036
+ super().__init__(
1037
+ tool=tool,
1038
+ command=command,
1039
+ why=f"Expected {expected_format}, got unparseable response: {parse_error}",
1040
+ fix=f"Check '{tool}' output format or regenerate FACTS.parquet",
1041
+ where=where,
1042
+ stdout=stdout,
1043
+ )
1044
+
1045
+
1046
+ # =============================================================================
1047
+ # MODULE ERRORS (Python protocol compliance)
1048
+ # =============================================================================
1049
+
1050
+
1051
+ class ModuleAttributeError(FrontmatteringError, AttributeError):
1052
+ """Module attribute not found.
1053
+
1054
+ Used by: __init__.py __getattr__ for lazy loading
1055
+
1056
+ Inherits from AttributeError for Python protocol compliance
1057
+ (hasattr, getattr expect AttributeError for missing attributes).
1058
+ """
1059
+
1060
+ def __init__(
1061
+ self,
1062
+ module: str,
1063
+ attribute: str,
1064
+ where: str | None = None,
1065
+ ) -> None:
1066
+ super().__init__(
1067
+ what=f"Module '{module}' has no attribute '{attribute}'",
1068
+ where=where or f"{module}.__getattr__",
1069
+ why="Attribute not found in module exports",
1070
+ fix=f"Check available exports with: dir({module})",
1071
+ context={"module": module, "attribute": attribute},
1072
+ )
1073
+ self.module = module
1074
+ self.attribute = attribute