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,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
_PATTERNS = (
|
|
6
|
+
"ignore previous instructions",
|
|
7
|
+
"ignore all previous instructions",
|
|
8
|
+
"reveal the system prompt",
|
|
9
|
+
"reveal hidden prompt",
|
|
10
|
+
"override system instructions",
|
|
11
|
+
"disregard previous instructions",
|
|
12
|
+
"you are now",
|
|
13
|
+
"execute this command",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def scan_ai_instruction_risk(body: str, structure: dict[str, Any]) -> dict[str, Any]:
|
|
18
|
+
"""Return advisory literal-pattern findings; never claims malicious intent."""
|
|
19
|
+
code_ranges = []
|
|
20
|
+
for item in structure.get("code_blocks", []):
|
|
21
|
+
try:
|
|
22
|
+
code_ranges.append((int(item["start_line"]), int(item["end_line"])))
|
|
23
|
+
except (KeyError, TypeError, ValueError):
|
|
24
|
+
continue
|
|
25
|
+
findings = []
|
|
26
|
+
for line_no, line in enumerate(body.splitlines(), start=1):
|
|
27
|
+
folded = line.casefold()
|
|
28
|
+
for pattern in _PATTERNS:
|
|
29
|
+
if pattern in folded:
|
|
30
|
+
context = "fenced_code" if any(start <= line_no <= end for start, end in code_ranges) else "prose"
|
|
31
|
+
findings.append({
|
|
32
|
+
"finding_id": "FRONTMATTERING-AI-INSTRUCTION-RISK-LITERAL-PATTERN",
|
|
33
|
+
"pattern": pattern,
|
|
34
|
+
"line": line_no,
|
|
35
|
+
"structural_context": context,
|
|
36
|
+
"severity": "informational" if context == "fenced_code" else "advisory",
|
|
37
|
+
"handling": "treat_as_data_not_instruction",
|
|
38
|
+
"claim_boundary": {
|
|
39
|
+
"does_not_establish": ["malicious intent", "successful prompt injection", "document unsafety"]
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
return {
|
|
43
|
+
"scan_state": "COMPLETE",
|
|
44
|
+
"method": "bounded_literal_pattern_advisory",
|
|
45
|
+
"findings": findings,
|
|
46
|
+
"finding_count": len(findings),
|
|
47
|
+
"claim_boundary": {
|
|
48
|
+
"establishes": ["presence of configured literal instruction-risk patterns"],
|
|
49
|
+
"does_not_establish": ["semantic prompt-injection detection", "malicious intent", "safe AI execution"],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .core import parse_frontmatter
|
|
9
|
+
from .harness_errors import ToolingError
|
|
10
|
+
from .corpus_selection import PROJECT_SCHEMA, load_project as load_selection_project
|
|
11
|
+
|
|
12
|
+
SCHEMA_VERSION = "frontmattering_frontmatter/v0_1_5"
|
|
13
|
+
TEMPLATE_SCHEMA = "frontmattering_template_use/v0_1_5"
|
|
14
|
+
|
|
15
|
+
PROFILES = {
|
|
16
|
+
"router": ("orientation", "package_router", "orientation_only", "AUTHORED_DRAFT", "orientation_router/default"),
|
|
17
|
+
"guide": ("guide", "authoring_guide", "advisory", "AUTHORED_DRAFT", "frontmattering_authoring_guide/v0_1_5"),
|
|
18
|
+
"spec": ("specification", "normative_kernel_spec", "normative", "AUTHORED_NORMATIVE_DRAFT_CANDIDATE", "frontmattering_normative_spec/v0_1_5"),
|
|
19
|
+
"contract": ("implementation_contract", "implementation_contract", "binding_when_explicitly_invoked", "AUTHORED_CANDIDATE", "frontmattering_implementation_contract/v0_1_5"),
|
|
20
|
+
"evidence": ("evidence_record", "evidence_record", "evidence_record", "EVIDENCE_RECORD_ONLY", "frontmattering_evidence/v0_1_5"),
|
|
21
|
+
"release": ("release_record", "release_metadata", "release_metadata", "RELEASE_METADATA_ONLY", "frontmattering_release/v0_1_5"),
|
|
22
|
+
"registry": ("registry_definition", "registry_definition", "advisory", "AUTHORED_DRAFT", "frontmattering_registry/v0_1_5"),
|
|
23
|
+
}
|
|
24
|
+
BUILTIN_SKIP_TAGS = {"unrelated_document_set"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def slug(value: str) -> str:
|
|
28
|
+
return re.sub(r"[^A-Z0-9]+", "-", value.upper()).strip("-") or "DOC"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def project_file(start: Path) -> Path | None:
|
|
32
|
+
cur = start.resolve()
|
|
33
|
+
if cur.is_file():
|
|
34
|
+
cur = cur.parent
|
|
35
|
+
for p in (cur, *cur.parents):
|
|
36
|
+
candidate = p / ".frontmattering" / "project.yaml"
|
|
37
|
+
if candidate.exists():
|
|
38
|
+
return candidate
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_project(start: Path) -> dict[str, Any]:
|
|
43
|
+
p = project_file(start)
|
|
44
|
+
if p is None:
|
|
45
|
+
raise ToolingError(
|
|
46
|
+
"Frontmattering project not initialized",
|
|
47
|
+
str(start),
|
|
48
|
+
"No .frontmattering/project.yaml was found in this directory or its parents.",
|
|
49
|
+
"Run `frontmattering init <directory> --set-id <ID> --title <TITLE>` first.",
|
|
50
|
+
error_id="FRONTMATTERING-AUTHORING-PROJECT-MISSING",
|
|
51
|
+
)
|
|
52
|
+
return load_selection_project(p)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _project_root(project: dict[str, Any]) -> Path:
|
|
56
|
+
root = project.get("_project_root")
|
|
57
|
+
if not root:
|
|
58
|
+
raise ToolingError(
|
|
59
|
+
"Project root was not resolved by the canonical project loader",
|
|
60
|
+
"frontmattering_project/v0_3",
|
|
61
|
+
"Authoring commands require the same closed project model as corpus selection.",
|
|
62
|
+
"Load the project through frontmattering.corpus_selection.load_project.",
|
|
63
|
+
error_id="FRONTMATTERING-AUTHORING-PROJECT-ROOT-MISSING",
|
|
64
|
+
)
|
|
65
|
+
return Path(str(root))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _project_paths(project: dict[str, Any]) -> list[Path]:
|
|
69
|
+
root = _project_root(project)
|
|
70
|
+
selection = project.get("_selection") or {}
|
|
71
|
+
include = selection.get("include") or []
|
|
72
|
+
exclude = selection.get("exclude") or []
|
|
73
|
+
selected: set[Path] = set()
|
|
74
|
+
for pattern in include:
|
|
75
|
+
for candidate in root.glob(str(pattern)):
|
|
76
|
+
if candidate.is_file() and candidate.suffix.lower() == ".md":
|
|
77
|
+
selected.add(candidate.resolve())
|
|
78
|
+
for pattern in exclude:
|
|
79
|
+
for candidate in root.glob(str(pattern)):
|
|
80
|
+
selected.discard(candidate.resolve())
|
|
81
|
+
return sorted(selected, key=lambda item: item.as_posix())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_existing_frontmatter(path: Path) -> dict[str, Any] | None:
|
|
85
|
+
try:
|
|
86
|
+
text = path.read_text(encoding="utf-8")
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
raise ToolingError(
|
|
89
|
+
"Existing Markdown cannot be read during authoring",
|
|
90
|
+
str(path),
|
|
91
|
+
"Router discovery and catalog harvesting must not silently skip unreadable selected documents.",
|
|
92
|
+
"Repair filesystem permissions or remove the unreadable path from the project corpus.",
|
|
93
|
+
{"error": str(exc)},
|
|
94
|
+
error_id="FRONTMATTERING-AUTHORING-READ-FAILED",
|
|
95
|
+
) from exc
|
|
96
|
+
if not text.startswith("---"):
|
|
97
|
+
return None
|
|
98
|
+
fm, _body, findings = parse_frontmatter(text)
|
|
99
|
+
if findings:
|
|
100
|
+
raise ToolingError(
|
|
101
|
+
"Selected governed Markdown has invalid frontmatter",
|
|
102
|
+
str(path),
|
|
103
|
+
"Router discovery and catalog harvesting refuse to hide malformed governed documents.",
|
|
104
|
+
"Validate and repair the selected document before running authoring promotion.",
|
|
105
|
+
{"finding_count": len(findings), "finding_ids": [getattr(f, "id", None) if hasattr(f, "id") else str(f[0]) for f in findings]},
|
|
106
|
+
error_id="FRONTMATTERING-AUTHORING-GOVERNED-DOC-INVALID",
|
|
107
|
+
)
|
|
108
|
+
if fm is None:
|
|
109
|
+
raise ToolingError(
|
|
110
|
+
"Existing Markdown frontmatter parse returned no document and no finding",
|
|
111
|
+
str(path),
|
|
112
|
+
"This is an internal authoring invariant violation.",
|
|
113
|
+
"Report the kernel bug with the path and input bytes.",
|
|
114
|
+
error_id="FRONTMATTERING-AUTHORING-PARSE-INVARIANT",
|
|
115
|
+
)
|
|
116
|
+
return fm if isinstance(fm, dict) else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _router_records(project: dict[str, Any]) -> list[tuple[Path, dict[str, Any]]]:
|
|
120
|
+
records: list[tuple[Path, dict[str, Any]]] = []
|
|
121
|
+
for path in _project_paths(project):
|
|
122
|
+
fm = _parse_existing_frontmatter(path)
|
|
123
|
+
if not isinstance(fm, dict):
|
|
124
|
+
continue
|
|
125
|
+
adc = fm.get("ai_document_contract", {}) if isinstance(fm, dict) else {}
|
|
126
|
+
if adc.get("document_role") == "package_router":
|
|
127
|
+
records.append((path, fm))
|
|
128
|
+
return records
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _canonical_owner_document_id(project: dict[str, Any]) -> str:
|
|
132
|
+
routers = _router_records(project)
|
|
133
|
+
if len(routers) != 1:
|
|
134
|
+
raise ToolingError(
|
|
135
|
+
"Frontmattering project must have exactly one package_router for starter ownership",
|
|
136
|
+
", ".join(path.as_posix() for path, _fm in routers) or "none",
|
|
137
|
+
"New and promoted non-router documents need one canonical owner document id.",
|
|
138
|
+
"Initialize the project or promote exactly one router before adding guide/spec documents.",
|
|
139
|
+
{"router_count": len(routers)},
|
|
140
|
+
error_id="FRONTMATTERING-AUTHORING-ROUTER-COUNT-INVALID",
|
|
141
|
+
)
|
|
142
|
+
adc = routers[0][1].get("ai_document_contract", {})
|
|
143
|
+
did = adc.get("document_id")
|
|
144
|
+
if not isinstance(did, str) or not did:
|
|
145
|
+
raise ToolingError(
|
|
146
|
+
"The package_router has no document_id",
|
|
147
|
+
routers[0][0].as_posix(),
|
|
148
|
+
"Ownership binding requires a stable owner document id.",
|
|
149
|
+
"Repair the router frontmatter.",
|
|
150
|
+
error_id="FRONTMATTERING-AUTHORING-ROUTER-ID-MISSING",
|
|
151
|
+
)
|
|
152
|
+
return did
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _all_declared_task_tags(project: dict[str, Any], extra_frontmatters: Iterable[dict[str, Any]] = ()) -> list[str]:
|
|
156
|
+
tags: set[str] = set()
|
|
157
|
+
default_tag = project.get("default_task_tag")
|
|
158
|
+
if isinstance(default_tag, str) and default_tag:
|
|
159
|
+
tags.add(default_tag)
|
|
160
|
+
for _path in _project_paths(project):
|
|
161
|
+
fm = _parse_existing_frontmatter(_path)
|
|
162
|
+
if not isinstance(fm, dict):
|
|
163
|
+
continue
|
|
164
|
+
routing = fm.get("routing", {}) if isinstance(fm, dict) else {}
|
|
165
|
+
for field in ("read_when_tags", "skip_when_tags", "task_catalog"):
|
|
166
|
+
for tag in routing.get(field) or []:
|
|
167
|
+
if isinstance(tag, str) and tag and tag not in BUILTIN_SKIP_TAGS:
|
|
168
|
+
tags.add(tag)
|
|
169
|
+
for fm in extra_frontmatters:
|
|
170
|
+
routing = fm.get("routing", {}) if isinstance(fm, dict) else {}
|
|
171
|
+
for field in ("read_when_tags", "skip_when_tags", "task_catalog"):
|
|
172
|
+
for tag in routing.get(field) or []:
|
|
173
|
+
if isinstance(tag, str) and tag and tag not in BUILTIN_SKIP_TAGS:
|
|
174
|
+
tags.add(tag)
|
|
175
|
+
return sorted(tags)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _write_frontmatter(path: Path, fm: dict[str, Any], body: str) -> None:
|
|
179
|
+
y = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True, width=1000).rstrip()
|
|
180
|
+
path.write_text(f"---\n{y}\n---\n{body}", encoding="utf-8")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _split_existing_doc(path: Path) -> tuple[dict[str, Any], str]:
|
|
184
|
+
text = path.read_text(encoding="utf-8")
|
|
185
|
+
fm, body, findings = parse_frontmatter(text)
|
|
186
|
+
if fm is None or findings:
|
|
187
|
+
raise ToolingError(
|
|
188
|
+
"Selected Markdown is not a valid Frontmattering document",
|
|
189
|
+
str(path),
|
|
190
|
+
"Catalog harvesting mutates only valid governed frontmatter and refuses guessed repairs.",
|
|
191
|
+
"Validate the document first, then rerun the promotion command.",
|
|
192
|
+
{"finding_count": len(findings)},
|
|
193
|
+
error_id="FRONTMATTERING-AUTHORING-GOVERNED-DOC-INVALID",
|
|
194
|
+
)
|
|
195
|
+
return fm, body
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def refresh_router_task_catalog(project: dict[str, Any], *, extra_frontmatters: Iterable[dict[str, Any]] = ()) -> list[Path]:
|
|
199
|
+
"""Refresh exactly one router's task_catalog from current promoted tags."""
|
|
200
|
+
routers = _router_records(project)
|
|
201
|
+
if len(routers) != 1:
|
|
202
|
+
raise ToolingError(
|
|
203
|
+
"Cannot refresh task catalog without exactly one package_router",
|
|
204
|
+
", ".join(path.as_posix() for path, _fm in routers) or "none",
|
|
205
|
+
"A corpus-local task catalog must have one router owner.",
|
|
206
|
+
"Promote or repair the router so exactly one package_router is selected.",
|
|
207
|
+
{"router_count": len(routers)},
|
|
208
|
+
error_id="FRONTMATTERING-AUTHORING-CATALOG-ROUTER-COUNT-INVALID",
|
|
209
|
+
)
|
|
210
|
+
router_path, _router_fm = routers[0]
|
|
211
|
+
fm, body = _split_existing_doc(router_path)
|
|
212
|
+
routing = fm.setdefault("routing", {})
|
|
213
|
+
catalog = _all_declared_task_tags(project, extra_frontmatters=extra_frontmatters)
|
|
214
|
+
if routing.get("task_catalog") == catalog:
|
|
215
|
+
return []
|
|
216
|
+
routing["task_catalog"] = catalog
|
|
217
|
+
_write_frontmatter(router_path, fm, body)
|
|
218
|
+
return [router_path]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _remove_generated_starter_router(project: dict[str, Any], *, replacement_path: Path) -> list[Path]:
|
|
222
|
+
sid = str(project["document_set_id"])
|
|
223
|
+
starter_id = f"{sid}-START-HERE"
|
|
224
|
+
removed: list[Path] = []
|
|
225
|
+
for path, fm in list(_router_records(project)):
|
|
226
|
+
if path.resolve() == replacement_path.resolve():
|
|
227
|
+
continue
|
|
228
|
+
adc = fm.get("ai_document_contract", {})
|
|
229
|
+
di = fm.get("deepest_intention", {})
|
|
230
|
+
if adc.get("document_id") == starter_id and di.get("canonical_owner_document_id") == starter_id:
|
|
231
|
+
path.unlink()
|
|
232
|
+
removed.append(path)
|
|
233
|
+
return removed
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def dump_doc(frontmatter: dict[str, Any], title: str, sections: list[str]) -> str:
|
|
237
|
+
y = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True, width=1000).rstrip()
|
|
238
|
+
body = [f"# {title}", ""]
|
|
239
|
+
for s in sections:
|
|
240
|
+
body += [f"## {s}", "", "TODO", ""]
|
|
241
|
+
return f"---\n{y}\n---\n\n" + "\n".join(body).rstrip() + "\n"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def init_project(root: Path, set_id: str, title: str, force: bool = False) -> list[Path]:
|
|
245
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
cfg_dir = root / ".frontmattering"; cfg_dir.mkdir(exist_ok=True)
|
|
247
|
+
cfg = cfg_dir / "project.yaml"
|
|
248
|
+
docs = root / "docs"; docs.mkdir(exist_ok=True)
|
|
249
|
+
router = docs / "00_START_HERE.md"
|
|
250
|
+
for p in (cfg, router):
|
|
251
|
+
if p.exists() and not force:
|
|
252
|
+
raise ToolingError("Generated file already exists", str(p), "Authoring scaffolds do not overwrite files by default.", "Choose another path or use --force intentionally.", error_id="FRONTMATTERING-AUTHORING-OVERWRITE-BLOCKED")
|
|
253
|
+
sid = slug(set_id)
|
|
254
|
+
cfg.write_text(yaml.safe_dump({
|
|
255
|
+
"project_schema": PROJECT_SCHEMA,
|
|
256
|
+
"document_set_id": sid,
|
|
257
|
+
"document_set_version": "0.1.0",
|
|
258
|
+
"title": title,
|
|
259
|
+
"default_task_tag": "general",
|
|
260
|
+
"next_load_order": 20,
|
|
261
|
+
"validation_profile": "default",
|
|
262
|
+
"corpus": {"include": ["docs/**/*.md"], "exclude": []},
|
|
263
|
+
}, sort_keys=False), encoding="utf-8")
|
|
264
|
+
fm = {
|
|
265
|
+
"ai_document_contract": {"schema_version": SCHEMA_VERSION, "document_id": f"{sid}-START-HERE", "document_title": f"{title} — start here", "document_version": "0.1.0", "document_set_id": sid, "document_set_version": "0.1.0", "document_role": "package_router", "document_kind": "orientation", "authority": "orientation_only", "lifecycle_state": "draft", "primary_action_expected": "Orient before substantive work on this document set.", "must_not_be_used_for": ["Treating orientation as normative authority."], "audience": {"primary": "dual", "human_reader_role": "maintainer", "human_readability_priority": "high"}, "body_binding": {"clause_index_required": False, "body_required_sections": ["Purpose", "How to use", "Claim boundary"]}},
|
|
266
|
+
"template_contract": {"schema_version": TEMPLATE_SCHEMA, "template_id": "orientation_router/default", "conformance_required": True},
|
|
267
|
+
"routing": {"load_order": 1, "agent_summary": f"Entry point for {title}.", "task_catalog": ["general"], "read_when": ["Beginning substantive work on this document set."], "read_when_tags": ["general"], "skip_when": ["The task is unrelated to this document set."], "skip_when_tags": ["unrelated_document_set"]},
|
|
268
|
+
"deepest_intention": {"intention_id": f"{sid}-DI-ORIENT", "canonical_owner_document_id": f"{sid}-START-HERE", "ownership": "owns", "one_sentence": "Provide bounded orientation before readers spend context on document bodies.", "protected_value": "Readers can identify authority, order, and hazards without reconstructing them from prose.", "failure_mode_prevented": ["Readers infer authority from filenames or prose alone."], "common_wrong_interpretations": ["The router is normative authority."], "achieved_by": ["A validated brief and typed document mesh."], "not_achieved_by": ["Treating a clean validation result as semantic approval."], "validator_gates": ["FRONTMATTERING-KERNEL-VALIDATE"], "evidence_required": ["The document set validates with the pinned kernel."], "not_the_intention": [{"statement": "The router approves the document set.", "category": "authority_laundering", "confusion_tether": "Orientation identifies structure; it does not approve content.", "aliases": ["orientation is approval"]}]},
|
|
269
|
+
"claim_boundary_packet": {"claimed_state": "AUTHORED_DRAFT", "verifies": ["This document declares the entry point and routing intent."], "does_not_verify": ["Semantic adequacy or release readiness of the document set."], "judgment_left_to_agent": ["Whether the documents are fit for the current purpose."]},
|
|
270
|
+
"relationships": [], "evidence_references": []}
|
|
271
|
+
router.write_text(dump_doc(fm, f"{title} — start here", ["Purpose", "How to use", "Claim boundary"]), encoding="utf-8")
|
|
272
|
+
return [cfg, router]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def starter_frontmatter(path: Path, profile: str, title: str, summary: str | None = None) -> tuple[dict[str, Any], list[str]]:
|
|
276
|
+
"""Build the canonical starter profile frontmatter without writing a file."""
|
|
277
|
+
if profile not in PROFILES:
|
|
278
|
+
raise ToolingError("Unknown authoring profile", profile, "The profile is outside the shipped closed starter profile set.", f"Choose one of: {', '.join(sorted(PROFILES))}.", error_id="FRONTMATTERING-AUTHORING-PROFILE-UNKNOWN")
|
|
279
|
+
project = load_project(path.parent)
|
|
280
|
+
sid = project["document_set_id"]; dsv = project.get("document_set_version", "0.1.0")
|
|
281
|
+
did = f"{sid}-{slug(path.stem)}"
|
|
282
|
+
kind, role, authority, claim, template = PROFILES[profile]
|
|
283
|
+
task = project.get("default_task_tag", "general")
|
|
284
|
+
load_order = 1 if profile == "router" else int(project.get("next_load_order", 20))
|
|
285
|
+
sections = ["Purpose", "How to use", "Claim boundary"] if profile == "router" else ["Purpose", "Content", "Claim boundary"]
|
|
286
|
+
owner_id = did if profile == "router" else _canonical_owner_document_id(project)
|
|
287
|
+
if profile == "router":
|
|
288
|
+
intention = {"intention_id": f"{sid}-DI-ORIENT", "ownership": "owns", "canonical_owner_document_id": did, "one_sentence": "Provide bounded orientation before readers spend context on document bodies.", "protected_value": "Readers can identify authority, order, and hazards without reconstructing them from prose.", "failure_mode_prevented": ["Readers infer authority from filenames or prose alone."], "common_wrong_interpretations": ["The router is normative authority."], "achieved_by": ["A validated brief and typed document mesh."], "not_achieved_by": ["Treating a clean validation result as semantic approval."], "validator_gates": ["FRONTMATTERING-KERNEL-VALIDATE"], "evidence_required": ["The document set validates with the pinned kernel."], "not_the_intention": [{"statement": "The router approves the document set.", "category": "authority_laundering", "confusion_tether": "Orientation identifies structure; it does not approve content.", "aliases": ["orientation is approval"]}]}
|
|
289
|
+
primary_action = "Orient before substantive work on this document set."
|
|
290
|
+
must_not = ["Treating orientation as normative authority."]
|
|
291
|
+
read_when = ["Beginning substantive work on this document set."]
|
|
292
|
+
catalog = _all_declared_task_tags(project, extra_frontmatters=()) or [str(task)]
|
|
293
|
+
else:
|
|
294
|
+
intention = {"intention_id": f"{sid}-DI-ORIENT", "ownership": "binds_to", "canonical_owner_document_id": owner_id}
|
|
295
|
+
primary_action = f"Use this document for its declared {profile} purpose."
|
|
296
|
+
must_not = ["Claims beyond the declared claim boundary."]
|
|
297
|
+
read_when = [f"The task requires this {profile}."]
|
|
298
|
+
catalog = None
|
|
299
|
+
routing = {"agent_summary": summary or title, "read_when": read_when, "read_when_tags": [task], "skip_when": ["The task does not require this document."], "skip_when_tags": ["unrelated_document_set"], "load_order": load_order}
|
|
300
|
+
if catalog is not None:
|
|
301
|
+
routing["task_catalog"] = catalog
|
|
302
|
+
fm = {
|
|
303
|
+
"ai_document_contract": {"schema_version": SCHEMA_VERSION, "document_id": did, "document_set_id": sid, "document_version": "0.1.0", "document_set_version": dsv, "document_title": title, "document_kind": kind, "document_role": role, "lifecycle_state": "draft" if claim=="AUTHORED_DRAFT" else "candidate", "audience": {"primary": "dual", "human_reader_role": "maintainer", "human_readability_priority": "high"}, "authority": authority, "primary_action_expected": primary_action, "body_binding": {"body_required_sections": sections, "clause_index_required": False}, "clause_prefixes": [slug(sid)[:12]], "must_not_be_used_for": must_not},
|
|
304
|
+
"routing": routing,
|
|
305
|
+
"deepest_intention": intention,
|
|
306
|
+
"claim_boundary_packet": {"claimed_state": claim, "verifies": [f"This document records authored {profile} content."], "does_not_verify": ["Independent correctness, adoption, or release readiness."], "judgment_left_to_agent": ["Whether the body is semantically adequate for the task."]},
|
|
307
|
+
"relationships": [], "evidence_references": [],
|
|
308
|
+
"template_contract": {"schema_version": TEMPLATE_SCHEMA, "template_id": template, "conformance_required": profile == "router"}}
|
|
309
|
+
return fm, sections
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def new_document(path: Path, profile: str, title: str, summary: str | None = None, force: bool = False) -> Path:
|
|
313
|
+
if path.exists() and not force:
|
|
314
|
+
raise ToolingError("Generated file already exists", str(path), "Authoring scaffolds do not overwrite files by default.", "Choose another path or use --force intentionally.", error_id="FRONTMATTERING-AUTHORING-OVERWRITE-BLOCKED")
|
|
315
|
+
fm, sections = starter_frontmatter(path, profile, title, summary=summary)
|
|
316
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
317
|
+
path.write_text(dump_doc(fm, title, sections), encoding="utf-8")
|
|
318
|
+
if profile != "router":
|
|
319
|
+
refresh_router_task_catalog(load_project(path.parent), extra_frontmatters=[fm])
|
|
320
|
+
return path
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Body/frontmatter freshness tether utilities for Frontmattering frontmatter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Sequence
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from .core import Y12Core, split_frontmatter
|
|
13
|
+
from .harness_errors import ToolingError
|
|
14
|
+
|
|
15
|
+
BODY_TETHER_SCHEMA_VERSION = "frontmattering_body_tether/v1"
|
|
16
|
+
BODY_TETHER_ALGORITHM = "sha256"
|
|
17
|
+
BODY_TETHER_CANONICALIZATION = "frontmattering_markdown_body_bytes_v1"
|
|
18
|
+
_FM_BYTES_RE = re.compile(rb"\A---(?:\n|\r\n).*?(?:\n|\r\n)---(?:\n|\r\n|$)", re.DOTALL)
|
|
19
|
+
_SHA256_RE = re.compile(r"[0-9a-f]{64}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def canonical_body_bytes(raw: bytes) -> bytes:
|
|
23
|
+
"""frontmattering_markdown_body_bytes_v1: raw Markdown bytes minus first frontmatter block."""
|
|
24
|
+
return _FM_BYTES_RE.sub(b"", raw, count=1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def body_sha256_for_bytes(raw: bytes) -> str:
|
|
28
|
+
return hashlib.sha256(canonical_body_bytes(raw)).hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _canonical_fields(canonical_fields: Sequence[str]) -> list[str]:
|
|
32
|
+
if not isinstance(canonical_fields, Sequence) or isinstance(canonical_fields, (str, bytes)):
|
|
33
|
+
raise ToolingError(
|
|
34
|
+
"Body tether canonical field set is unavailable",
|
|
35
|
+
"body_tether_canonical_trust_fields",
|
|
36
|
+
"The validator was invoked without the spec-declared canonical trust-field list.",
|
|
37
|
+
"Restore the body_tether_canonical_trust_fields machine block in the Frontmattering spec.",
|
|
38
|
+
error_id="FRONTMATTERING-BODY-TETHER-CANONICAL-FIELDS-MISSING",
|
|
39
|
+
)
|
|
40
|
+
fields = [str(item) for item in canonical_fields]
|
|
41
|
+
if not fields or any(not item.strip() for item in fields) or len(set(fields)) != len(fields):
|
|
42
|
+
raise ToolingError(
|
|
43
|
+
"Body tether canonical field set is invalid",
|
|
44
|
+
"body_tether_canonical_trust_fields",
|
|
45
|
+
"The spec-declared canonical trust-field list must be non-empty and duplicate-free.",
|
|
46
|
+
"Repair the body_tether_canonical_trust_fields machine block in the Frontmattering spec.",
|
|
47
|
+
{"canonical_fields": fields},
|
|
48
|
+
error_id="FRONTMATTERING-BODY-TETHER-CANONICAL-FIELDS-INVALID",
|
|
49
|
+
)
|
|
50
|
+
return fields
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _dig(fm: dict[str, Any], dotted: str) -> Any:
|
|
54
|
+
cur: Any = fm
|
|
55
|
+
for part in dotted.split("."):
|
|
56
|
+
if isinstance(cur, dict) and part in cur:
|
|
57
|
+
cur = cur[part]
|
|
58
|
+
else:
|
|
59
|
+
return None
|
|
60
|
+
return cur
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tethered_fields_sha256(fm: dict[str, Any], canonical_fields: Sequence[str]) -> str:
|
|
64
|
+
"""Digest the complete spec-declared canonical trust-bearing field set."""
|
|
65
|
+
fields = _canonical_fields(canonical_fields)
|
|
66
|
+
payload = [[path, _dig(fm, path)] for path in fields]
|
|
67
|
+
blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
68
|
+
return hashlib.sha256(blob).hexdigest()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def classify_tether(raw: bytes, fm: dict[str, Any], canonical_fields: Sequence[str]) -> dict[str, Any]:
|
|
72
|
+
"""Return FRESH/STALE/UNTETHERED status under canonical-scope semantics."""
|
|
73
|
+
fields = _canonical_fields(canonical_fields)
|
|
74
|
+
tether = fm.get("body_tether") if isinstance(fm, dict) else None
|
|
75
|
+
if tether is None:
|
|
76
|
+
return {"freshness": "UNTETHERED", "summary_trust": "orientation_only", "reasons": ["body_tether absent"], "body_sha256_current": body_sha256_for_bytes(raw), "canonical_tethered_fields": fields}
|
|
77
|
+
if not isinstance(tether, dict):
|
|
78
|
+
return {"freshness": "STALE", "summary_trust": "do_not_rely_without_reading_body", "reasons": ["body_tether malformed"], "body_sha256_current": body_sha256_for_bytes(raw), "canonical_tethered_fields": fields}
|
|
79
|
+
current_body_sha = body_sha256_for_bytes(raw)
|
|
80
|
+
current_fields_sha = tethered_fields_sha256(fm, fields)
|
|
81
|
+
reasons: list[str] = []
|
|
82
|
+
if "tethered_fields" in tether:
|
|
83
|
+
reasons.append("body_tether.tethered_fields forbidden")
|
|
84
|
+
if tether.get("schema_version") != BODY_TETHER_SCHEMA_VERSION:
|
|
85
|
+
reasons.append("schema_version mismatch")
|
|
86
|
+
if tether.get("body_algorithm") != BODY_TETHER_ALGORITHM:
|
|
87
|
+
reasons.append("body_algorithm mismatch")
|
|
88
|
+
if tether.get("body_canonicalization") != BODY_TETHER_CANONICALIZATION:
|
|
89
|
+
reasons.append("body_canonicalization unknown")
|
|
90
|
+
if tether.get("body_sha256") != current_body_sha:
|
|
91
|
+
reasons.append("body_sha256 mismatch")
|
|
92
|
+
if tether.get("tethered_fields_sha256") != current_fields_sha:
|
|
93
|
+
reasons.append("tethered_fields_sha256 mismatch")
|
|
94
|
+
return {
|
|
95
|
+
"freshness": "FRESH" if not reasons else "STALE",
|
|
96
|
+
"summary_trust": "fresh_canonical_tether_declared" if not reasons else "do_not_rely_without_reading_body",
|
|
97
|
+
"reasons": reasons,
|
|
98
|
+
"body_sha256_current": current_body_sha,
|
|
99
|
+
"body_sha256_declared": tether.get("body_sha256"),
|
|
100
|
+
"tethered_fields_sha256_current": current_fields_sha,
|
|
101
|
+
"tethered_fields_sha256_declared": tether.get("tethered_fields_sha256"),
|
|
102
|
+
"canonical_tethered_fields": fields,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sha256_like(value: Any) -> bool:
|
|
107
|
+
return isinstance(value, str) and _SHA256_RE.fullmatch(value) is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def findings_for_tether(raw: bytes, fm: dict[str, Any], canonical_fields: Sequence[str]) -> list[tuple[str, str]]:
|
|
111
|
+
_canonical_fields(canonical_fields)
|
|
112
|
+
tether = fm.get("body_tether") if isinstance(fm, dict) else None
|
|
113
|
+
if tether is None:
|
|
114
|
+
return []
|
|
115
|
+
if not isinstance(tether, dict):
|
|
116
|
+
return [("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether")]
|
|
117
|
+
out: list[tuple[str, str]] = []
|
|
118
|
+
required = ["schema_version", "body_algorithm", "body_canonicalization", "body_sha256", "tethered_fields_sha256"]
|
|
119
|
+
missing = [name for name in required if name not in tether]
|
|
120
|
+
if missing:
|
|
121
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", ",".join(missing)))
|
|
122
|
+
return out
|
|
123
|
+
if "tethered_fields" in tether:
|
|
124
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether.tethered_fields"))
|
|
125
|
+
if tether.get("schema_version") != BODY_TETHER_SCHEMA_VERSION:
|
|
126
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether.schema_version"))
|
|
127
|
+
if tether.get("body_algorithm") != BODY_TETHER_ALGORITHM:
|
|
128
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether.body_algorithm"))
|
|
129
|
+
if tether.get("body_canonicalization") != BODY_TETHER_CANONICALIZATION:
|
|
130
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-UNKNOWN-CANONICALIZATION", "body_tether.body_canonicalization"))
|
|
131
|
+
if not _sha256_like(tether.get("body_sha256")):
|
|
132
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether.body_sha256"))
|
|
133
|
+
elif tether.get("body_sha256") != body_sha256_for_bytes(raw):
|
|
134
|
+
out.append(("FRONTMATTERING-FM-BODY-DIGEST-MISMATCH", "body_tether.body_sha256"))
|
|
135
|
+
if not _sha256_like(tether.get("tethered_fields_sha256")):
|
|
136
|
+
out.append(("FRONTMATTERING-FM-BODY-TETHER-MALFORMED", "body_tether.tethered_fields_sha256"))
|
|
137
|
+
elif tether.get("tethered_fields_sha256") != tethered_fields_sha256(fm, canonical_fields):
|
|
138
|
+
out.append(("FRONTMATTERING-FM-TETHERED-FIELDS-MISMATCH", "body_tether.tethered_fields_sha256"))
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def read_frontmatter_and_body(path: Path) -> tuple[dict[str, Any], str]:
|
|
143
|
+
text = path.read_text(encoding="utf-8")
|
|
144
|
+
fm_text, body, findings = split_frontmatter(text)
|
|
145
|
+
if findings or fm_text is None:
|
|
146
|
+
raise ToolingError("Body tether update cannot parse frontmatter", str(path), "The file does not contain parseable Frontmattering frontmatter.", "Add a valid byte-zero Frontmattering YAML frontmatter block before updating the tether.")
|
|
147
|
+
fm = yaml.load(fm_text, Loader=Y12Core)
|
|
148
|
+
if not isinstance(fm, dict):
|
|
149
|
+
raise ToolingError("Body tether update cannot parse frontmatter", str(path), "The frontmatter root is not a mapping.", "Rewrite the frontmatter as a YAML mapping before updating the tether.")
|
|
150
|
+
return fm, body
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def write_frontmatter_and_body(path: Path, fm: dict[str, Any], body: str) -> None:
|
|
154
|
+
rendered = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True, width=1000).rstrip()
|
|
155
|
+
path.write_text(f"---\n{rendered}\n---\n{body}", encoding="utf-8")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def update_tether(path: Path, *, add: bool = False, canonical_fields: Sequence[str]) -> bool:
|
|
159
|
+
"""Refresh body_tether for one file. Return True when a block was written."""
|
|
160
|
+
fields = _canonical_fields(canonical_fields)
|
|
161
|
+
fm, body = read_frontmatter_and_body(path)
|
|
162
|
+
if "body_tether" not in fm and not add:
|
|
163
|
+
return False
|
|
164
|
+
fm["body_tether"] = {
|
|
165
|
+
"schema_version": BODY_TETHER_SCHEMA_VERSION,
|
|
166
|
+
"body_algorithm": BODY_TETHER_ALGORITHM,
|
|
167
|
+
"body_canonicalization": BODY_TETHER_CANONICALIZATION,
|
|
168
|
+
"body_sha256": "0" * 64,
|
|
169
|
+
"tethered_fields_sha256": "0" * 64,
|
|
170
|
+
}
|
|
171
|
+
write_frontmatter_and_body(path, fm, body)
|
|
172
|
+
fm, body = read_frontmatter_and_body(path)
|
|
173
|
+
fm["body_tether"]["body_sha256"] = body_sha256_for_bytes(path.read_bytes())
|
|
174
|
+
fm["body_tether"]["tethered_fields_sha256"] = tethered_fields_sha256(fm, fields)
|
|
175
|
+
write_frontmatter_and_body(path, fm, body)
|
|
176
|
+
return True
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"""Structured errors used by Frontmattering."""
|
|
2
|
+
from .errors import (FrontmatteringError, ParsingError, ValidationError, SchemaValidationError, ReentrancyError, CollectorTimeoutError)
|
|
3
|
+
__all__ = ["FrontmatteringError", "ParsingError", "ValidationError", "SchemaValidationError", "ReentrancyError", "CollectorTimeoutError"]
|