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.
- frontmattering/FRONTMATTER_INDEX_PROVENANCE.json +13 -0
- frontmattering/__init__.py +1 -0
- frontmattering/__main__.py +1263 -0
- frontmattering/ai_instruction_risk.py +51 -0
- frontmattering/authoring.py +320 -0
- frontmattering/body_tether.py +176 -0
- frontmattering/common/__init__.py +3 -0
- frontmattering/common/errors.py +1074 -0
- frontmattering/core.py +147 -0
- frontmattering/corpus_selection.py +436 -0
- frontmattering/frontmatter_index.py +340 -0
- frontmattering/harness_errors.py +555 -0
- frontmattering/inventory.py +83 -0
- frontmattering/markdown/MARKDOWN_PARSE_RESULT_schema.json +870 -0
- frontmattering/markdown/MARKDOWN_PROVENANCE.json +8 -0
- frontmattering/markdown/USER_MANUAL.md +263 -0
- frontmattering/markdown/__init__.py +0 -0
- frontmattering/markdown/api.py +553 -0
- frontmattering/markdown/cli/__init__.py +1 -0
- frontmattering/markdown/cli/__main__.py +10 -0
- frontmattering/markdown/cli/dump_sections.py +100 -0
- frontmattering/markdown/collectors_phase8/__init__.py +15 -0
- frontmattering/markdown/collectors_phase8/blockquotes.py +117 -0
- frontmattering/markdown/collectors_phase8/codeblocks.py +46 -0
- frontmattering/markdown/collectors_phase8/footnotes.py +152 -0
- frontmattering/markdown/collectors_phase8/frontmatter.py +80 -0
- frontmattering/markdown/collectors_phase8/headings.py +85 -0
- frontmattering/markdown/collectors_phase8/html.py +29 -0
- frontmattering/markdown/collectors_phase8/html_collector.py +89 -0
- frontmattering/markdown/collectors_phase8/images.py +68 -0
- frontmattering/markdown/collectors_phase8/inline_code.py +61 -0
- frontmattering/markdown/collectors_phase8/links.py +104 -0
- frontmattering/markdown/collectors_phase8/lists.py +66 -0
- frontmattering/markdown/collectors_phase8/math.py +28 -0
- frontmattering/markdown/collectors_phase8/paragraphs.py +41 -0
- frontmattering/markdown/collectors_phase8/tables.py +65 -0
- frontmattering/markdown/collectors_phase8/tasklists.py +59 -0
- frontmattering/markdown/fetchers/__init__.py +8 -0
- frontmattering/markdown/fetchers/preview.py +123 -0
- frontmattering/markdown/fetchers/url_fetcher.py +123 -0
- frontmattering/markdown/security/__init__.py +6 -0
- frontmattering/markdown/security/scanner.py +436 -0
- frontmattering/markdown/utils/__init__.py +0 -0
- frontmattering/markdown/utils/profiling.py +137 -0
- frontmattering/markdown/utils/section.py +98 -0
- frontmattering/markdown/utils/text_normalization.py +91 -0
- frontmattering/markdown/utils/timeout.py +165 -0
- frontmattering/markdown/utils/token_warehouse.py +1300 -0
- frontmattering/markdown/utils/url_utils.py +179 -0
- frontmattering/promotion.py +170 -0
- frontmattering/resources/AI_INDEX.json +691 -0
- frontmattering/resources/AI_START_HERE.md +378 -0
- frontmattering/resources/DAILY_USE_GUIDE.md +87 -0
- frontmattering/resources/FRONTMATTERING_FRONTMATTER_SPEC_v0_1_5.md +3203 -0
- frontmattering/resources/KERNEL_ACCEPTANCE_REPORT.md +180 -0
- frontmattering/resources/__init__.py +0 -0
- frontmattering/resources/examples/no_frontmatter_demo/00_AI_START_HERE.md +43 -0
- frontmattering/resources/examples/no_frontmatter_demo/10_NORMATIVE_SPEC.md +28 -0
- frontmattering/resources/examples/no_frontmatter_demo/20_IMPLEMENTATION_CONTRACT.md +33 -0
- frontmattering/resources/examples/no_frontmatter_demo/30_TEST_SUFFICIENCY.md +34 -0
- frontmattering/resources/examples/no_frontmatter_demo/40_EVIDENCE_RECORD.md +33 -0
- frontmattering/resources/examples/no_frontmatter_demo/50_RELEASE_DECISION.md +29 -0
- frontmattering/resources/examples/power_demo/00_AI_START_HERE.md +142 -0
- frontmattering/resources/examples/power_demo/10_NORMATIVE_SPEC.md +117 -0
- frontmattering/resources/examples/power_demo/20_IMPLEMENTATION_CONTRACT.md +130 -0
- frontmattering/resources/examples/power_demo/30_TEST_SUFFICIENCY.md +132 -0
- frontmattering/resources/examples/power_demo/40_EVIDENCE_RECORD.md +128 -0
- frontmattering/resources/examples/power_demo/50_RELEASE_DECISION.md +124 -0
- frontmattering/resources/fixtures/invalid/adoption_digest_mismatch_rejected/doc.md +80 -0
- frontmattering/resources/fixtures/invalid/adoption_record_on_wrong_role_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/adoption_record_shape_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/adoption_without_bootstrap_receipt_rejected/doc.md +78 -0
- frontmattering/resources/fixtures/invalid/authored_go_used_as_evidence_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/body_tether_body_digest_mismatch_rejected/doc.md +65 -0
- frontmattering/resources/fixtures/invalid/body_tether_fields_mismatch_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/body_tether_malformed_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/body_tether_missing_under_trust_rejected/.frontmattering/project.yaml +11 -0
- frontmattering/resources/fixtures/invalid/body_tether_missing_under_trust_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/body_tether_scope_selector_rejected/doc.md +68 -0
- frontmattering/resources/fixtures/invalid/body_tether_unknown_canonicalization_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/bootstrap_receipt_shape_rejected/doc.md +80 -0
- frontmattering/resources/fixtures/invalid/bootstrap_without_validator_blocks_adoption/doc.md +71 -0
- frontmattering/resources/fixtures/invalid/claim_boundary_missing_does_not_verify_rejected/doc.md +55 -0
- frontmattering/resources/fixtures/invalid/claimed_state_overreach_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/clause_enforcement_drift_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/container_cardinality_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/deepest_intention_missing_rejected/doc.md +56 -0
- frontmattering/resources/fixtures/invalid/downstream_phase_without_upstream_evidence_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/duplicate_document_id_rejected/a.md +57 -0
- frontmattering/resources/fixtures/invalid/duplicate_document_id_rejected/b.md +57 -0
- frontmattering/resources/fixtures/invalid/duplicate_yaml_key_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/evidence_edge_digest_missing_rejected/doc.md +65 -0
- frontmattering/resources/fixtures/invalid/evidence_reference_used_as_truth_rejected/doc.md +66 -0
- frontmattering/resources/fixtures/invalid/external_edge_locator_missing_rejected/doc.md +64 -0
- frontmattering/resources/fixtures/invalid/external_edge_set_id_missing_rejected/doc.md +64 -0
- frontmattering/resources/fixtures/invalid/extracted_catalog_drift_rejected/doc.md +60 -0
- frontmattering/resources/fixtures/invalid/fixture_reverse_reachability_drift_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/frontmatter_only_view_incomplete_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/frontmatter_registry_conflict_blocks/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/frontmatter_view_body_truth_overclaim_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/generated_state_in_frontmatter_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/green_checks_conflation_rejected/doc.md +76 -0
- frontmattering/resources/fixtures/invalid/historical_digest_used_as_current_evidence_rejected/doc.md +66 -0
- frontmattering/resources/fixtures/invalid/implementation_start_gate_bypass_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/intention_positive_negative_conflation_rejected/doc.md +76 -0
- frontmattering/resources/fixtures/invalid/legacy_string_machine_surface_requires_mapping/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/load_order_inversion_flagged/a.md +63 -0
- frontmattering/resources/fixtures/invalid/load_order_inversion_flagged/b.md +57 -0
- frontmattering/resources/fixtures/invalid/machine_surface_identity_incomplete_rejected/doc.md +62 -0
- frontmattering/resources/fixtures/invalid/migration_field_drop_rejected/doc.md +60 -0
- frontmattering/resources/fixtures/invalid/migration_silent_coercion_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/missing_frontmatter_rejected/doc.md +1 -0
- frontmattering/resources/fixtures/invalid/non_root_exception_or_silent_fallback_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/nonowner_redefines_intention_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/output_format_conflation_rejected/doc.md +76 -0
- frontmattering/resources/fixtures/invalid/parser_byte_ranges_missing_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/parser_partial_model_on_error_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/parser_source_locator_missing_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/projected_field_without_origin_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/projection_rebuild_nondeterminism_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/projection_row_without_source_locator_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/relationship_resolution_scope_missing_rejected/doc.md +62 -0
- frontmattering/resources/fixtures/invalid/relationship_resolution_scope_unknown_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/relationship_semantics_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/relationship_target_unresolved_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/release_surface_digest_missing_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/required_dependency_cycle_rejected/a.md +63 -0
- frontmattering/resources/fixtures/invalid/required_dependency_cycle_rejected/b.md +63 -0
- frontmattering/resources/fixtures/invalid/routing_tag_not_in_task_catalog_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/routing_tag_router_not_in_scope_rejected/doc.md +55 -0
- frontmattering/resources/fixtures/invalid/scope_on_nonsupersedes_rejected/doc.md +65 -0
- frontmattering/resources/fixtures/invalid/stale_bootstrap_receipt_rejected/doc.md +80 -0
- frontmattering/resources/fixtures/invalid/stale_validation_result_rejected/doc.md +60 -0
- frontmattering/resources/fixtures/invalid/supersedes_without_scope_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/target_section_not_found_rejected/a.md +65 -0
- frontmattering/resources/fixtures/invalid/target_section_not_found_rejected/b.md +57 -0
- frontmattering/resources/fixtures/invalid/target_sections_on_external_edge_rejected/doc.md +67 -0
- frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/router.md +59 -0
- frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/selected.md +56 -0
- frontmattering/resources/fixtures/invalid/task_scoped_defects_filtering/skipped.md +56 -0
- frontmattering/resources/fixtures/invalid/template_claims_normative_authority_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/two_documents_own_same_intention_rejected/a.md +76 -0
- frontmattering/resources/fixtures/invalid/two_documents_own_same_intention_rejected/b.md +76 -0
- frontmattering/resources/fixtures/invalid/undefined_conditional_token_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/undefined_type_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/unknown_authority_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/unknown_document_role_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/unknown_field_rejected/doc.md +58 -0
- frontmattering/resources/fixtures/invalid/unknown_root_block_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/unresolved_reference_on_required_edge_rejected/doc.md +63 -0
- frontmattering/resources/fixtures/invalid/unsupported_schema_version_rejected/doc.md +57 -0
- frontmattering/resources/fixtures/invalid/untemplated_frontmatter_bypass_rejected/doc.md +59 -0
- frontmattering/resources/fixtures/invalid/validator_passes_conflation_rejected/doc.md +76 -0
- frontmattering/resources/fixtures/invalid/yaml_1_1_boolean_words_not_coerced/doc.md +57 -0
- frontmattering/resources/fixtures/pilot_security/blockquote.md +3 -0
- frontmattering/resources/fixtures/pilot_security/code_fence.md +5 -0
- frontmattering/resources/fixtures/pilot_security/dangerous_html.md +3 -0
- frontmattering/resources/fixtures/pilot_security/dangerous_uri.md +3 -0
- frontmattering/resources/fixtures/pilot_security/direct_instruction.md +3 -0
- frontmattering/resources/fixtures/pilot_security/generated_resource_limits.yaml +16 -0
- frontmattering/resources/fixtures/pilot_security/indirect_instruction.md +4 -0
- frontmattering/resources/fixtures/pilot_security/legitimate_security_discussion.md +3 -0
- frontmattering/resources/fixtures/pilot_security/link.md +3 -0
- frontmattering/resources/fixtures/pilot_security/table.md +5 -0
- frontmattering/resources/fixtures/valid/adoption_locator_trigger_word_is_data_valid/doc.md +80 -0
- frontmattering/resources/fixtures/valid/body_tether_fresh_valid/doc.md +63 -0
- frontmattering/resources/fixtures/valid/bootstrap_validation_receipt_valid/doc.md +66 -0
- frontmattering/resources/fixtures/valid/claim_prose_substrings_are_not_verdicts_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/companion_and_appendix_edges_valid/a.md +69 -0
- frontmattering/resources/fixtures/valid/companion_and_appendix_edges_valid/b.md +57 -0
- frontmattering/resources/fixtures/valid/document_binding_to_external_intention_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/document_realizing_external_intention_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/document_referencing_external_intention_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/evidence_record_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/evidence_support_substrings_are_not_proof_valid/doc.md +66 -0
- frontmattering/resources/fixtures/valid/external_document_set_edge_valid/doc.md +65 -0
- frontmattering/resources/fixtures/valid/frontmatter_adoption_record_valid/doc.md +80 -0
- frontmattering/resources/fixtures/valid/frontmatter_projection_mapping_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/frontmatter_spec_bootstrap_valid/doc.md +76 -0
- frontmattering/resources/fixtures/valid/implementation_contract_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/normative_spec_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/orientation_router_valid/doc.md +59 -0
- frontmattering/resources/fixtures/valid/release_metadata_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/target_sections_resolved_valid/a.md +65 -0
- frontmattering/resources/fixtures/valid/target_sections_resolved_valid/b.md +57 -0
- frontmattering/resources/fixtures/valid/template_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/unresolved_reference_optional_edge_valid/doc.md +63 -0
- frontmattering/resources/fixtures/valid/untemplated_document_valid/doc.md +57 -0
- frontmattering/resources/fixtures/valid/yaml_1_2_plain_words_valid/doc.md +60 -0
- frontmattering/resources/generated/FRONTMATTERING_FRONTMATTER_SCHEMA.schema.json +2422 -0
- frontmattering/resources/tools/__init__.py +0 -0
- frontmattering/resources/tools/check_frontmatter_sqlite_index.py +40 -0
- frontmattering/resources/tools/check_kernel_cleanliness.py +103 -0
- frontmattering/resources/tools/check_kernel_exception_taxonomy.py +145 -0
- frontmattering/resources/tools/check_markdown_parser_adoption.py +35 -0
- frontmattering/resources/tools/check_pilot_readiness.py +545 -0
- frontmattering/resources/tools/check_projection_consistency.py +35 -0
- frontmattering/resources/tools/check_python_syntax.py +29 -0
- frontmattering/resources/tools/check_report_quality.py +478 -0
- frontmattering/resources/tools/distribution_receipt.py +54 -0
- frontmattering/resources/tools/finding_registry.py +625 -0
- frontmattering/resources/tools/frontmattering_cli.py +18 -0
- frontmattering/resources/tools/generate_ai_index.py +123 -0
- frontmattering/resources/tools/generate_frontmatter_schema.py +70 -0
- frontmattering/resources/tools/kernel_reporting.py +1495 -0
- frontmattering/resources/tools/make_fixtures.py +507 -0
- frontmattering/resources/tools/pack_kernel_distro.py +430 -0
- frontmattering/resources/tools/test_verify_kernel.py +177 -0
- frontmattering/resources/tools/validate_frontmatters.py +100 -0
- frontmattering/resources/tools/verify_kernel.py +197 -0
- frontmattering/resources/workflow-docs-manifest.yaml +199 -0
- frontmattering/schema_projection.py +288 -0
- frontmattering/speccompile.py +81 -0
- frontmattering/validator.py +957 -0
- frontmattering-0.4.2.dist-info/METADATA +91 -0
- frontmattering-0.4.2.dist-info/RECORD +224 -0
- frontmattering-0.4.2.dist-info/WHEEL +5 -0
- frontmattering-0.4.2.dist-info/entry_points.txt +2 -0
- frontmattering-0.4.2.dist-info/licenses/COMMERCIAL_LICENSE_REQUIRED.md +14 -0
- frontmattering-0.4.2.dist-info/licenses/LICENSE.md +39 -0
- frontmattering-0.4.2.dist-info/licenses/NOTICE +10 -0
- frontmattering-0.4.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12 -0
- frontmattering-0.4.2.dist-info/licenses/TRADEMARKS.md +7 -0
- 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
|