artifact.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Freeze artifact parsing and canonical digest bytes (§K5.4 / §K5.7).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import hashlib |
| 6 | import re |
| 7 | from dataclasses import dataclass |
| 8 | from pathlib import Path |
| 9 | |
| 10 | import yaml |
| 11 | |
| 12 | from tools.freeze_reviewer.serializer import dump_freeze_mapping, parse_freeze_mapping |
| 13 | from tools.freeze_reviewer.types import ArtifactKind, DeclarationStatus |
| 14 | |
| 15 | UTF8_BOM = b"\xef\xbb\xbf" |
| 16 | FENCE_RE = re.compile( |
| 17 | r"```[ \t]*(yaml|yml)[ \t]*\n(.*?)```", |
| 18 | re.DOTALL | re.IGNORECASE, |
| 19 | ) |
| 20 | STAMP_MARKER = "<!-- overseer:review-stamp -->" |
| 21 | |
| 22 | |
| 23 | @dataclass(frozen=True) |
| 24 | class ParsedArtifact: |
| 25 | """Parsed freeze artifact metadata.""" |
| 26 | |
| 27 | text: str |
| 28 | canonical_text: str |
| 29 | declaration: DeclarationStatus |
| 30 | kind: ArtifactKind |
| 31 | freeze_mapping: dict | None |
| 32 | fence_match: re.Match[str] | None |
| 33 | rel_path: str |
| 34 | |
| 35 | |
| 36 | def strip_bom_and_normalize_newlines(data: bytes) -> str: |
| 37 | """Apply §K4.7 canonical byte rules items 1 and 2.""" |
| 38 | if data.startswith(UTF8_BOM): |
| 39 | data = data[UTF8_BOM.__len__() :] |
| 40 | try: |
| 41 | text = data.decode("utf-8") |
| 42 | except UnicodeDecodeError as exc: |
| 43 | raise ValueError("not-utf8") from exc |
| 44 | return text.replace("\r\n", "\n").replace("\r", "\n") |
| 45 | |
| 46 | |
| 47 | def read_artifact_bytes(path: Path) -> str: |
| 48 | """Read artifact bytes as canonical text or raise ``not-utf8``.""" |
| 49 | return strip_bom_and_normalize_newlines(path.read_bytes()) |
| 50 | |
| 51 | |
| 52 | def _is_valid_declaration(mapping: dict) -> bool: |
| 53 | if not isinstance(mapping.get("phase"), str): |
| 54 | return False |
| 55 | outputs = mapping.get("outputs") |
| 56 | if not isinstance(outputs, list) or not outputs: |
| 57 | return False |
| 58 | for item in outputs: |
| 59 | if not isinstance(item, dict): |
| 60 | return False |
| 61 | if item.get("frozen") is True: |
| 62 | return True |
| 63 | return False |
| 64 | |
| 65 | |
| 66 | def _detect_kind(path: Path, text: str) -> tuple[ArtifactKind, DeclarationStatus, dict | None, re.Match | None]: |
| 67 | suffix = path.suffix.lower() |
| 68 | if suffix in {".yaml", ".yml"}: |
| 69 | try: |
| 70 | mapping = parse_freeze_mapping(text) |
| 71 | except ValueError: |
| 72 | mapping = None |
| 73 | if mapping and _is_valid_declaration(mapping): |
| 74 | return "yaml_whole", "present", mapping, None |
| 75 | return "operator_forced_yaml", "absent", mapping if isinstance(mapping, dict) else None, None |
| 76 | |
| 77 | match = FENCE_RE.search(text) |
| 78 | if match is not None: |
| 79 | body = match.group(2) |
| 80 | try: |
| 81 | mapping = parse_freeze_mapping(body) |
| 82 | except ValueError: |
| 83 | mapping = None |
| 84 | if mapping and _is_valid_declaration(mapping): |
| 85 | return "markdown_fence", "present", mapping, match |
| 86 | return "operator_forced_md", "absent", None, match |
| 87 | |
| 88 | |
| 89 | def parse_artifact(path: Path, *, rel_path: str) -> ParsedArtifact: |
| 90 | """Parse artifact text and detect declaration status.""" |
| 91 | text = read_artifact_bytes(path) |
| 92 | kind, declaration, mapping, fence_match = _detect_kind(path, text) |
| 93 | return ParsedArtifact( |
| 94 | text=text, |
| 95 | canonical_text=text, |
| 96 | declaration=declaration, |
| 97 | kind=kind, |
| 98 | freeze_mapping=mapping, |
| 99 | fence_match=fence_match, |
| 100 | rel_path=rel_path, |
| 101 | ) |
| 102 | |
| 103 | |
| 104 | def _operator_forced_md_prefix(text: str) -> str: |
| 105 | """Recover pre-stamp prose bytes for operator-forced Markdown.""" |
| 106 | marker_index = text.rfind(STAMP_MARKER) |
| 107 | if marker_index == -1: |
| 108 | return text |
| 109 | prefix = text[:marker_index] |
| 110 | if prefix.endswith("\n\n"): |
| 111 | return prefix[:-1] |
| 112 | return prefix |
| 113 | |
| 114 | |
| 115 | def _remove_operator_forced_md_suffix(text: str) -> str: |
| 116 | return _operator_forced_md_prefix(text) |
| 117 | |
| 118 | |
| 119 | def pre_stamp_canonical_bytes(parsed: ParsedArtifact) -> bytes: |
| 120 | """Compute pre-stamp canonical form bytes for digest (§K5.7).""" |
| 121 | if parsed.kind == "markdown_fence" and parsed.freeze_mapping is not None and parsed.fence_match: |
| 122 | mapping = dict(parsed.freeze_mapping) |
| 123 | mapping.pop("review_stamp", None) |
| 124 | serialized = dump_freeze_mapping(mapping) |
| 125 | start, end = parsed.fence_match.span() |
| 126 | fence_lang = parsed.fence_match.group(1) |
| 127 | before = parsed.text[:start] |
| 128 | after = parsed.text[end:] |
| 129 | body = f"```{fence_lang}\n{serialized}```" |
| 130 | return (before + body + after).encode("utf-8") |
| 131 | |
| 132 | if parsed.kind == "yaml_whole" and parsed.freeze_mapping is not None: |
| 133 | mapping = dict(parsed.freeze_mapping) |
| 134 | mapping.pop("review_stamp", None) |
| 135 | return dump_freeze_mapping(mapping).encode("utf-8") |
| 136 | |
| 137 | if parsed.kind == "operator_forced_md": |
| 138 | return _remove_operator_forced_md_suffix(parsed.text).encode("utf-8") |
| 139 | |
| 140 | if parsed.kind == "operator_forced_yaml": |
| 141 | if parsed.freeze_mapping is not None: |
| 142 | mapping = dict(parsed.freeze_mapping) |
| 143 | mapping.pop("review_stamp", None) |
| 144 | return dump_freeze_mapping(mapping).encode("utf-8") |
| 145 | return parsed.text.encode("utf-8") |
| 146 | |
| 147 | return parsed.text.encode("utf-8") |
| 148 | |
| 149 | |
| 150 | def artifact_digest(parsed: ParsedArtifact) -> str: |
| 151 | """Return sha256 digest over pre-stamp canonical form.""" |
| 152 | payload = pre_stamp_canonical_bytes(parsed) |
| 153 | digest_hex = hashlib.sha256(payload).hexdigest() |
| 154 | return f"sha256:{digest_hex}" |
| 155 | |
| 156 | |
| 157 | def extract_existing_stamp(parsed: ParsedArtifact) -> dict | None: |
| 158 | """Return existing review_stamp mapping if present.""" |
| 159 | if parsed.freeze_mapping and isinstance(parsed.freeze_mapping.get("review_stamp"), dict): |
| 160 | return parsed.freeze_mapping["review_stamp"] |
| 161 | if parsed.kind == "operator_forced_md": |
| 162 | marker_index = parsed.text.rfind(STAMP_MARKER) |
| 163 | if marker_index == -1: |
| 164 | return None |
| 165 | tail = parsed.text[marker_index:] |
| 166 | match = FENCE_RE.search(tail) |
| 167 | if not match: |
| 168 | return None |
| 169 | try: |
| 170 | mapping = parse_freeze_mapping(match.group(2)) |
| 171 | except ValueError: |
| 172 | return None |
| 173 | stamp = mapping.get("review_stamp") |
| 174 | return stamp if isinstance(stamp, dict) else None |
| 175 | return None |
File History
2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439
feat: K1-P1 complete — agent provenance, build-verification…
Sonnet 4.6
patch
53 days ago