Initial commit

This commit is contained in:
2026-09-04 14:58:42 +08:00
commit 439cad87d9
4601 changed files with 29440 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+480
View File
@@ -0,0 +1,480 @@
"""Directory compiler orchestration for model-profile Skill adaptation."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import shutil
import tempfile
from typing import Any, Callable
from .annotator import (
AnnotationError,
OpenCodeAnnotator,
SemanticPlanner,
plan_once,
)
from .document import (
DocumentError,
parse_document,
resolve_annotation_conflicts,
skill_name,
static_annotations,
)
from .guard import run_semantic_guard
from .format_policy import apply_format_style, reduce_format_policy
from .models import CompileResult, SemanticPlanResult, Signal
from .profile import (
ProfileError,
load_profile,
selected_passes,
target_model_id,
)
from .rewriter import RewriteError, rewrite_document
from .semantic_plan import apply_semantic_plan, semantic_plan_needed
class ModelCompilerError(RuntimeError):
"""A model preference compilation failed."""
ProgressCallback = Callable[[int, str], None]
def _notify(
progress: ProgressCallback | None,
percent: int,
message: str,
) -> None:
if progress is not None:
progress(percent, message)
def _sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _slug(value: str) -> str:
result = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
return result or "model"
def _validate_source_tree(source: Path) -> None:
source_resolved = source.resolve()
for path in source.rglob("*"):
if not path.is_symlink():
continue
try:
target = path.resolve(strict=True)
target.relative_to(source_resolved)
except (OSError, ValueError) as exc:
raise ModelCompilerError(
f"symlink escapes or is broken in Skill source: {path}"
) from exc
def _is_within(path: Path, parent: Path) -> bool:
try:
path.resolve().relative_to(parent.resolve())
return True
except ValueError:
return False
def _retained_diagnostics(profile: dict[str, Any]) -> dict[str, Any]:
dimensions = profile.get("behavioral_profile", {}).get("numeric_dimensions", [])
retained_ids = {"causal_chain", "abstract_reasoning"}
retained = [
dimension
for dimension in dimensions
if isinstance(dimension, dict) and dimension.get("id") in retained_ids
]
style = profile.get("behavioral_profile", {}).get("style_profile")
return {"numeric_dimensions": retained, "style_profile": style}
def _base_report(
source: Path,
source_bytes: bytes,
profile: dict[str, Any],
profile_hash: str,
signals: dict[str, Signal],
passes: list[str],
) -> dict[str, Any]:
return {
"schema_version": "1.0",
"status": "unchanged",
"source": {
"path": str(source),
"sha256": _sha256(source_bytes),
},
"target_model": {
"id": target_model_id(profile),
"profile_sha256": profile_hash,
},
"signals": {
name: signal.to_dict() for name, signal in sorted(signals.items())
},
"selected_passes": passes,
"retained_diagnostics": _retained_diagnostics(profile),
"semantic_plan": SemanticPlanResult().to_dict(),
"operations": [],
"semantic_guard": {},
"warnings": [],
}
def _write_output(
source_dir: Path,
destination: Path,
skill_content: str,
report: dict[str, Any],
*,
force: bool,
) -> None:
if destination.exists() and not force:
raise ModelCompilerError(
f"output already exists (use --force to replace it): {destination}"
)
destination.parent.mkdir(parents=True, exist_ok=True)
staging = Path(
tempfile.mkdtemp(prefix=f".{destination.name}.tmp-", dir=destination.parent)
)
try:
shutil.rmtree(staging)
shutil.copytree(source_dir, staging, symlinks=True)
(staging / "SKILL.md").write_text(
skill_content, encoding="utf-8", newline=""
)
(staging / "rewrite-report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if destination.exists():
shutil.rmtree(destination)
staging.replace(destination)
finally:
if staging.exists():
shutil.rmtree(staging)
def _copy_pack_scaffolding(
pack_dir: Path,
destination: Path,
skill_dirs: list[Path],
*,
force: bool,
) -> None:
"""Copy files owned by a Skill pack rather than by one of its Skills.
Each Skill is copied by ``compile_skill`` so its SKILL.md can be replaced.
This preserves pack-level manifests, shared assets, and intermediate
directories without copying an old SKILL.md over a rewritten one.
"""
if destination.exists() and not force:
return
destination.mkdir(parents=True, exist_ok=True)
for path in sorted(pack_dir.rglob("*"), key=lambda item: item.as_posix()):
if any(path == skill_dir or skill_dir in path.parents for skill_dir in skill_dirs):
continue
target = destination / path.relative_to(pack_dir)
if path.is_dir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target, follow_symlinks=False)
def compile_skill(
input_dir: Path,
profile_path: Path,
out_root: Path,
*,
mode: str = "deterministic",
annotator_model: str | None = None,
allow_deterministic_fallback: bool = False,
dry_run: bool = False,
force: bool = False,
annotator: SemanticPlanner | None = None,
output_group: str | None = None,
output_relative_path: Path | None = None,
progress: ProgressCallback | None = None,
) -> CompileResult:
_notify(progress, 3, f"{input_dir.name}: reading Skill and profile")
if mode not in {"deterministic", "hybrid"}:
raise ModelCompilerError(f"unsupported mode: {mode}")
source_dir = input_dir.resolve()
skill_path = source_dir / "SKILL.md"
if not skill_path.is_file():
raise ModelCompilerError(f"source Skill directory requires SKILL.md: {input_dir}")
_validate_source_tree(source_dir)
try:
source_bytes = skill_path.read_bytes()
source_text = source_bytes.decode("utf-8")
document = parse_document(source_text)
name = skill_name(document)
profile, signals, profile_hash = load_profile(profile_path.resolve())
except (OSError, UnicodeDecodeError, DocumentError, ProfileError) as exc:
raise ModelCompilerError(str(exc)) from exc
passes = selected_passes(signals)
_notify(progress, 15, f"{name}: profile reduced; {len(passes)} pass(es) selected")
report = _base_report(
skill_path, source_bytes, profile, profile_hash, signals, passes
)
format_policy = reduce_format_policy(profile)
selected_format_styles = list(format_policy.styles) if format_policy.enabled else []
selected_format_style = selected_format_styles[-1] if selected_format_styles else None
report["format_policy"] = format_policy.to_dict()
report["selected_format_styles"] = [
style.to_dict() for style in selected_format_styles
]
report["selected_format_style"] = (
selected_format_style.to_dict() if selected_format_style else None
)
static = static_annotations(document)
_notify(progress, 25, f"{name}: Markdown analyzed; protected blocks identified")
needs_llm = mode == "hybrid" and semantic_plan_needed(document)
report["dry_run"] = dry_run
report["expected_llm_call"] = needs_llm
if dry_run:
_notify(progress, 100, f"{name}: dry run complete")
return CompileResult(None, report, name)
plan_result = SemanticPlanResult()
if needs_llm:
_notify(progress, 30, f"{name}: requesting source-grounded semantic plan")
try:
active_planner = annotator
if active_planner is None:
if not annotator_model:
raise AnnotationError(
"hybrid semantic planning requires a provider-qualified model"
)
active_planner = OpenCodeAnnotator(
annotator_model,
progress=progress,
)
plan_result = plan_once(
active_planner,
document,
signals,
passes,
)
except AnnotationError as exc:
if not allow_deterministic_fallback:
raise ModelCompilerError(str(exc)) from exc
plan_result = SemanticPlanResult(
used=True,
model=(annotator.model_id if annotator is not None else annotator_model),
error=str(exc),
)
report["warnings"].append(
f"semantic planning failed; deterministic fallback used: {exc}"
)
if plan_result.repair_error is not None:
report["warnings"].append(
"semantic repair failed; valid units from the initial plan were retained: "
f"{plan_result.repair_error}"
)
_notify(
progress,
52,
f"{name}: semantic plan ready "
f"({plan_result.accepted} accepted, {plan_result.rejected} rejected)",
)
report["semantic_plan"] = plan_result.to_dict()
reserved_block_ids = {
unit.source_refs[0].block_id
for unit in plan_result.units
if unit.kind == "replace_block"
}
annotations = resolve_annotation_conflicts(
[item for item in static if item.block_id not in reserved_block_ids]
)
try:
_notify(progress, 62, f"{name}: applying deterministic behavioral passes")
rewritten, operations = rewrite_document(
document,
signals,
annotations,
reserved_block_ids=reserved_block_ids,
)
if plan_result.units:
_notify(progress, 72, f"{name}: applying validated semantic rewrites")
rewritten, semantic_operations, skipped = apply_semantic_plan(
rewritten, document, plan_result.units
)
operations.extend(semantic_operations)
plan_result.applied = len(semantic_operations)
plan_result.skipped = len(skipped)
plan_result.skip_reasons = skipped
report["semantic_plan"] = plan_result.to_dict()
if selected_format_styles:
for index, format_style in enumerate(selected_format_styles, start=1):
_notify(
progress,
80 + min(10, index),
f"{name}: applying model format preference {index}/{len(selected_format_styles)}",
)
rewritten, format_operations = apply_format_style(
rewritten, format_style
)
operations.extend(format_operations)
except RewriteError as exc:
raise ModelCompilerError(str(exc)) from exc
_notify(progress, 90, f"{name}: running semantic guard")
guard = run_semantic_guard(source_text, rewritten, operations)
report["operations"] = [operation.to_dict() for operation in operations]
report["semantic_guard"] = guard.to_dict()
if not guard.passed:
output_content = source_text
report["status"] = "rolled_back"
report["warnings"].append(
"semantic guard failed; output SKILL.md was rolled back to source"
)
elif plan_result.error is not None:
output_content = rewritten
report["status"] = "deterministic_fallback"
elif rewritten == source_text:
output_content = source_text
report["status"] = "unchanged"
else:
output_content = rewritten
report["status"] = "adapted"
model_root = out_root.resolve() / _slug(target_model_id(profile))
if output_group is not None and output_relative_path is not None:
raise ModelCompilerError(
"output_group and output_relative_path cannot be used together"
)
if output_relative_path is not None:
if output_relative_path.is_absolute() or any(
part in {"", ".", ".."} for part in output_relative_path.parts
):
raise ModelCompilerError(
f"invalid relative output path: {output_relative_path}"
)
destination = model_root / output_relative_path
elif output_group is not None:
if (
not output_group
or output_group in {".", ".."}
or Path(output_group).name != output_group
):
raise ModelCompilerError(
f"invalid output collection directory name: {output_group!r}"
)
model_root = model_root / output_group
destination = model_root / name
else:
destination = model_root / name
if _is_within(destination, source_dir):
raise ModelCompilerError("output directory must not be inside the source Skill")
_notify(progress, 96, f"{name}: writing compiled Skill and report")
_write_output(
source_dir,
destination,
output_content,
report,
force=force,
)
if skill_path.read_bytes() != source_bytes:
raise ModelCompilerError("source SKILL.md changed during compilation")
_notify(progress, 100, f"{name}: compilation complete ({report['status']})")
return CompileResult(destination, report, name)
def compile_input(
input_dir: Path,
profile_path: Path,
out_root: Path,
**kwargs: Any,
) -> tuple[CompileResult, ...]:
progress = kwargs.pop("progress", None)
source = input_dir.resolve()
if not source.is_dir():
raise ModelCompilerError(f"input directory not found: {input_dir}")
if (source / "SKILL.md").is_file():
return (
compile_skill(
source,
profile_path,
out_root,
progress=progress,
**kwargs,
),
)
_validate_source_tree(source)
skill_dirs = sorted(
(path.parent for path in source.rglob("SKILL.md") if path.is_file()),
key=lambda child: child.relative_to(source).as_posix(),
)
if not skill_dirs:
raise ModelCompilerError(
f"input requires a Skill directory or a Skill pack containing SKILL.md files: "
f"{input_dir}"
)
try:
profile, _, _ = load_profile(profile_path.resolve())
except ProfileError as exc:
raise ModelCompilerError(str(exc)) from exc
pack_destination = (
out_root.resolve() / _slug(target_model_id(profile)) / source.name
)
if _is_within(pack_destination, source):
raise ModelCompilerError("output directory must not be inside the source Skill pack")
if not kwargs.get("dry_run", False):
_copy_pack_scaffolding(
source,
pack_destination,
skill_dirs,
force=bool(kwargs.get("force", False)),
)
# A pack is a batch boundary, not a transaction. Compile Skills
# Skills sequentially in a stable order and isolate an expected failure to
# the current Skill. This preserves the strict single-Skill behavior while
# ensuring one provider/validation/output error cannot skip later Skills.
results: list[CompileResult] = []
total = len(skill_dirs)
for index, skill_dir in enumerate(skill_dirs):
child_progress: ProgressCallback | None = None
if progress is not None:
def child_progress(
percent: int,
message: str,
*,
_index: int = index,
) -> None:
overall = int(((_index + percent / 100) / total) * 100)
progress(overall, f"[{_index + 1}/{total}] {message}")
try:
result = compile_skill(
skill_dir,
profile_path,
out_root,
# A pack mirrors each Skill's path below the pack root. Using the
# directory path rather than frontmatter name also avoids collisions
# when separate subdirectories contain Skills with the same name.
output_relative_path=Path(source.name) / skill_dir.relative_to(source),
progress=child_progress,
**kwargs,
)
except ModelCompilerError as exc:
result = CompileResult(
output_dir=None,
skill_name=skill_dir.name,
report={
"schema_version": "1.0",
"status": "failed",
"source": {
"path": str((skill_dir / "SKILL.md").resolve()),
},
"error": str(exc),
"warnings": [f"Skill compilation failed: {exc}"],
},
)
results.append(result)
return tuple(results)
+424
View File
@@ -0,0 +1,424 @@
"""Source-preserving Markdown block analysis and static annotations."""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
import re
from typing import Iterable
import yaml
from markdown_it import MarkdownIt
from .models import Annotation, BodyBlock
class DocumentError(RuntimeError):
"""The Skill Markdown cannot be parsed safely."""
HEADING_RE = re.compile(r"^(#{1,6})[ \t]+(.+?)[ \t]*$")
FENCE_RE = re.compile(r"^[ \t]*(```+|~~~+)")
LIST_RE = re.compile(r"^([ \t]*)(?:[-+*]|\d+[.)])[ \t]+")
TABLE_RE = re.compile(r"^[ \t]*\|.*\|[ \t]*(?:\r?\n)?$")
INLINE_PROTECTED_RE = re.compile(
r"`[^`\n]+`|https?://[^\s)>]+|(?<![A-Za-z0-9_])(?:\./|\.\./|/)"
r"[A-Za-z0-9_./{}$@%:+-]*[A-Za-z0-9_/{}$@%:+-]"
r"|\b[A-Za-z_][A-Za-z0-9_]*\.(?:json|ya?ml|toml|md|py|sh|js|ts|csv|xml)\b"
r"|\b\d+(?:\.\d+)*%?\b"
)
SECTION_ALIASES = {
"definitions": {"definitions", "definition", "术语", "术语定义", "定义"},
"critical_rules": {
"critical rules",
"critical rule",
"rules",
"constraints",
"关键规则",
"规则",
"约束",
},
"evidence_priority": {"evidence priority", "证据优先级"},
"scope": {"scope", "范围"},
"decision_criteria": {"decision criteria", "criteria", "判断标准", "决策标准"},
"uncertainty_rule": {"uncertainty rule", "uncertainty", "不确定性规则"},
"completion_criterion": {
"completion criterion",
"completion criteria",
"completion",
"完成条件",
},
"task": {"task", "workflow", "instructions", "任务", "工作流", "步骤", "执行"},
"inputs": {"input", "inputs", "输入"},
"output": {"output", "outputs", "输出"},
"validation": {"validation", "validate", "checks", "验证", "检查"},
}
CANONICAL_HEADINGS = {
"en": {
"definitions": "Definitions",
"critical_rules": "Critical Rules",
"evidence_priority": "Evidence Priority",
"scope": "Scope",
"decision_criteria": "Decision Criteria",
"uncertainty_rule": "Uncertainty Rule",
"completion_criterion": "Completion Criterion",
"task": "Task",
"inputs": "Inputs",
"output": "Output",
"validation": "Validation",
},
"zh": {
"definitions": "术语定义",
"critical_rules": "关键规则",
"evidence_priority": "证据优先级",
"scope": "范围",
"decision_criteria": "判断标准",
"uncertainty_rule": "不确定性规则",
"completion_criterion": "完成条件",
"task": "任务",
"inputs": "输入",
"output": "输出",
"validation": "验证",
},
}
ANNOTATION_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
(
"completion_criterion",
re.compile(
r"只有.+才(?:算|可以|可|能).*(?:完成|结束)|完成条件\s*[::]|"
r"only\s+.+\s+(?:counts?\s+as|is)\s+(?:complete|done)",
re.I,
),
),
(
"evidence_priority_rule",
re.compile(
r"以.+为准|.+优先于.+|(?:冲突|不一致)时.+(?:为准|优先)|"
r"\b.+takes?\s+precedence\s+over\b.+|\bprefer\s+.+\s+over\b",
re.I,
),
),
(
"uncertainty_rule",
re.compile(
r"无法确定|证据不足|不得猜测|不要猜测|不应推断|"
r"\bdo\s+not\s+guess\b|\binsufficient\s+evidence\b|\buncertain\b",
re.I,
),
),
(
"scope_rule",
re.compile(
r"仅指|不包括|范围为|范围包括|\bscope\s*[::]|\bdoes\s+not\s+include\b",
re.I,
),
),
(
"decision_criterion",
re.compile(
r"按.+(?:排序|判断)|根据.+判断|判断标准\s*[::]|\bcriteria\s*[::]",
re.I,
),
),
(
"definition",
re.compile(
r"(?:此处|这里|本任务中).+?(?:是指|指的是|定义为)|"
r"^[A-Za-z][A-Za-z0-9 _-]{0,40}\s+(?:means|refers to|is defined as)\b",
re.I,
),
),
(
"critical_rule",
re.compile(
r"\bMUST(?:\s+NOT)?\b|\b(?:IMPORTANT|CRITICAL)\s*[::]|"
r"必须|不得|禁止|仅可|只能|不能",
re.I,
),
),
]
VIEWPOINT_A_RE = re.compile(
r"^(?:[-+*]\s*)?(?:支持|赞成|优点|收益|采用|in favor|advantages?|benefits?)\s*[::]",
re.I,
)
VIEWPOINT_B_RE = re.compile(
r"^(?:[-+*]\s*)?(?:反对|缺点|风险|不采用|against|disadvantages?|risks?)\s*[::]",
re.I,
)
@dataclass
class SkillDocument:
original: str
frontmatter: str
body: str
blocks: list[BodyBlock]
newline: str
@property
def block_index(self) -> dict[str, BodyBlock]:
return {block.id: block for block in self.blocks}
@property
def language(self) -> str:
nonspace = [char for char in self.body if not char.isspace()]
if not nonspace:
return "en"
cjk = sum("\u4e00" <= char <= "\u9fff" for char in nonspace)
return "zh" if cjk / len(nonspace) >= 0.30 else "en"
def split_frontmatter(content: str) -> tuple[str, str]:
if not content.startswith("---"):
raise DocumentError("SKILL.md requires YAML frontmatter")
match = re.search(r"\A---[ \t]*\r?\n.*?\r?\n---[ \t]*(?:\r?\n|\Z)", content, re.S)
if not match:
raise DocumentError("unterminated YAML frontmatter")
frontmatter = match.group(0)
yaml_text = re.sub(r"\A---[ \t]*\r?\n|\r?\n---[ \t]*(?:\r?\n)?\Z", "", frontmatter)
try:
loaded = yaml.safe_load(yaml_text)
except yaml.YAMLError as exc:
raise DocumentError(f"invalid YAML frontmatter: {exc}") from exc
if not isinstance(loaded, dict):
raise DocumentError("YAML frontmatter must be a mapping")
return frontmatter, content[match.end() :]
def _protected_spans(text: str, *, whole_block: bool = False) -> list[tuple[int, int]]:
if whole_block:
return [(0, len(text))]
return [(match.start(), match.end()) for match in INLINE_PROTECTED_RE.finditer(text)]
def _looks_like_code(text: str) -> bool:
lines = [line for line in text.splitlines() if line.strip()]
if len(lines) < 2:
return False
code_line = re.compile(
r"^[ \t]{2,}(?:def |class |if |elif |else:|for |while |return |"
r"print\(|raise |try:|except |[A-Za-z_][A-Za-z0-9_]*\s*=|[}\]])"
)
signals = sum(bool(code_line.match(line)) for line in lines)
return signals >= 2 and signals >= len(lines) / 2
def _line_offsets(body: str) -> tuple[list[str], list[int]]:
lines = body.splitlines(keepends=True)
if body and not lines:
lines = [body]
offsets: list[int] = []
position = 0
for line in lines:
offsets.append(position)
position += len(line)
return lines, offsets
def parse_document(content: str) -> SkillDocument:
frontmatter, body = split_frontmatter(content)
newline = "\r\n" if "\r\n" in content else "\n"
MarkdownIt("commonmark", {"html": True}).parse(body)
lines, offsets = _line_offsets(body)
blocks: list[BodyBlock] = []
index = 0
parent_heading: str | None = None
block_number = 0
def add_block(start: int, end: int, kind: str, heading_level: int | None = None) -> None:
nonlocal block_number, parent_heading
raw = "".join(lines[start:end]).rstrip("\r\n")
if not raw:
return
if kind in {"paragraph", "list_item"} and _looks_like_code(raw):
kind = "code_like"
block_number += 1
block_id = f"B{block_number:03d}"
start_offset = offsets[start]
end_offset = start_offset + len("".join(lines[start:end]))
list_match = LIST_RE.match(raw)
block = BodyBlock(
id=block_id,
kind=kind,
text=raw,
start_line=start + 1,
end_line=end,
start_offset=start_offset,
end_offset=end_offset,
parent_heading=parent_heading,
heading_level=heading_level,
list_depth=(len(list_match.group(1).replace("\t", " ")) // 2 if list_match else 0),
protected_spans=_protected_spans(
raw, whole_block=kind in {"code", "code_like", "html", "table"}
),
)
blocks.append(block)
if kind == "heading":
heading = HEADING_RE.match(raw)
parent_heading = heading.group(2).strip() if heading else raw
while index < len(lines):
stripped = lines[index].strip()
if not stripped:
index += 1
continue
fence = FENCE_RE.match(lines[index])
if fence:
marker = fence.group(1)[0]
end = index + 1
while end < len(lines) and not re.match(rf"^[ \t]*{re.escape(marker)}{{3,}}", lines[end]):
end += 1
end = min(end + 1, len(lines))
add_block(index, end, "code")
index = end
continue
heading = HEADING_RE.match(lines[index].rstrip("\r\n"))
if heading:
add_block(index, index + 1, "heading", len(heading.group(1)))
index += 1
continue
if lines[index].lstrip().startswith("<"):
add_block(index, index + 1, "html")
index += 1
continue
if TABLE_RE.match(lines[index]):
end = index + 1
while end < len(lines) and TABLE_RE.match(lines[end]):
end += 1
add_block(index, end, "table")
index = end
continue
if LIST_RE.match(lines[index]):
end = index + 1
while (
end < len(lines)
and lines[end].strip()
and not HEADING_RE.match(lines[end].rstrip("\r\n"))
and not LIST_RE.match(lines[end])
and not FENCE_RE.match(lines[end])
):
end += 1
add_block(index, end, "list_item")
index = end
continue
end = index + 1
while (
end < len(lines)
and lines[end].strip()
and not HEADING_RE.match(lines[end].rstrip("\r\n"))
and not LIST_RE.match(lines[end])
and not TABLE_RE.match(lines[end])
and not FENCE_RE.match(lines[end])
):
end += 1
add_block(index, end, "paragraph")
index = end
return SkillDocument(content, frontmatter, body, blocks, newline)
def section_key(title: str | None) -> str | None:
if not title:
return None
normalized = " ".join(title.lower().strip().rstrip("::-–—").split())
for key, aliases in SECTION_ALIASES.items():
if normalized in aliases:
return key
return None
def _sentences(text: str) -> Iterable[str]:
prefix = ""
list_match = LIST_RE.match(text)
content = text
if list_match:
prefix = text[: list_match.end()]
content = text[list_match.end() :]
parts = re.split(r"(?<=[。!?.!?;;])(?:[ \t]+|\r?\n+)", content)
for index, part in enumerate(parts):
clean = part.strip()
if clean:
yield (prefix if index == 0 else "") + clean
def static_annotations(document: SkillDocument) -> list[Annotation]:
annotations: list[Annotation] = []
for block in document.blocks:
if block.kind in {"code", "code_like", "html", "heading", "table"}:
continue
parent_key = section_key(block.parent_heading)
candidates = list(_sentences(block.text))
for quote in candidates:
found: list[str] = []
if parent_key == "definitions":
found.append("definition")
elif parent_key == "completion_criterion":
found.append("completion_criterion")
elif parent_key == "evidence_priority":
found.append("evidence_priority_rule")
elif parent_key == "scope":
found.append("scope_rule")
elif parent_key == "decision_criteria":
found.append("decision_criterion")
elif parent_key == "uncertainty_rule":
found.append("uncertainty_rule")
elif parent_key == "critical_rules":
found.append("critical_rule")
for annotation_type, pattern in ANNOTATION_PATTERNS:
if pattern.search(quote):
found.append(annotation_type)
if VIEWPOINT_A_RE.search(quote):
found.append("viewpoint_side_a")
if VIEWPOINT_B_RE.search(quote):
found.append("viewpoint_side_b")
for annotation_type in dict.fromkeys(found):
annotations.append(
Annotation(annotation_type, block.id, quote, 1.0, "static")
)
return resolve_annotation_conflicts(annotations)
ANNOTATION_PRIORITY = {
"completion_criterion": 100,
"evidence_priority_rule": 90,
"uncertainty_rule": 80,
"scope_rule": 70,
"decision_criterion": 60,
"definition": 50,
"viewpoint_side_a": 40,
"viewpoint_side_b": 40,
"critical_rule": 10,
"coreference": 5,
}
def resolve_annotation_conflicts(annotations: list[Annotation]) -> list[Annotation]:
grouped: dict[tuple[str, str], list[Annotation]] = defaultdict(list)
for annotation in annotations:
grouped[(annotation.block_id, annotation.quote)].append(annotation)
resolved: list[Annotation] = []
for values in grouped.values():
values.sort(
key=lambda item: (
ANNOTATION_PRIORITY.get(item.type, 0),
item.confidence,
item.source == "static",
),
reverse=True,
)
resolved.append(values[0])
return sorted(resolved, key=lambda item: (item.block_id, item.quote))
def skill_name(document: SkillDocument) -> str:
yaml_text = re.sub(
r"\A---[ \t]*\r?\n|\r?\n---[ \t]*(?:\r?\n)?\Z", "", document.frontmatter
)
loaded = yaml.safe_load(yaml_text)
name = loaded.get("name") if isinstance(loaded, dict) else None
if not isinstance(name, str) or not name.strip():
raise DocumentError("frontmatter requires non-empty name")
return name.strip()
@@ -0,0 +1,215 @@
"""Reduce task-specific format measurements into a conservative Skill policy."""
from __future__ import annotations
from dataclasses import asdict, dataclass
import re
from typing import Any
from .document import CANONICAL_HEADINGS, HEADING_RE, parse_document, section_key
from .models import Operation
@dataclass(frozen=True)
class FormatStyle:
id: str
source_format: str | None
strict_accuracy: float | None
prior_rank: int
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class FormatPolicy:
enabled: bool
classification: str
strict_accuracy_spread: float | None
styles: tuple[FormatStyle, ...]
avoid_patterns: tuple[str, ...]
cautions: tuple[str, ...]
def to_dict(self) -> dict[str, Any]:
return {
"enabled": self.enabled,
"classification": self.classification,
"strict_accuracy_spread": self.strict_accuracy_spread,
"styles": [style.to_dict() for style in self.styles],
"avoid_patterns": list(self.avoid_patterns),
"cautions": list(self.cautions),
}
def _number(value: Any) -> float | None:
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
result = float(value)
return result if 0.0 <= result <= 1.0 else None
def _infer_style(prompt_format: str) -> str | None:
"""Map a short-template result to a heading-label hypothesis.
The format benchmark does not test complete Skills. The mapping therefore
deliberately captures only casing and the label delimiter; Markdown heading
structure is retained by the renderer.
"""
labels = re.findall(r"([A-Za-z]+)\s*([:-])\s*\{\}", prompt_format)
if not labels:
labels = re.findall(r"([A-Za-z]+)\s+([-])\s+\{\}", prompt_format)
if not labels:
return None
words = [word for word, _ in labels]
delimiters = {delimiter for _, delimiter in labels}
if len(delimiters) != 1:
return None
delimiter = next(iter(delimiters))
casing = "uppercase" if all(word.isupper() for word in words) else "title"
suffix = "hyphen" if delimiter == "-" else "colon"
return f"{casing}-{suffix}-labels"
def _avoid_patterns(formats: Any) -> tuple[str, ...]:
if not isinstance(formats, list):
return ()
patterns: list[str] = []
for item in formats:
if not isinstance(item, dict) or not isinstance(item.get("prompt_format"), str):
continue
value = item["prompt_format"]
if "<sep>" in value and "synthetic-separator-token" not in patterns:
patterns.append("synthetic-separator-token")
if re.search(r"\n[ \t]+[A-Za-z]", value) and "indented-label" not in patterns:
patterns.append("indented-label")
labels = re.findall(r"\b([A-Za-z]+)\s*[:-]", value)
if labels and len({word.isupper() for word in labels}) > 1:
if "mixed-label-casing" not in patterns:
patterns.append("mixed-label-casing")
if " " in value and "inconsistent-spacing" not in patterns:
patterns.append("inconsistent-spacing")
return tuple(patterns)
def reduce_format_policy(profile: dict[str, Any]) -> FormatPolicy:
raw = profile.get("format_preference")
if not isinstance(raw, dict):
return FormatPolicy(
enabled=False,
classification="unavailable",
strict_accuracy_spread=None,
styles=(),
avoid_patterns=(),
cautions=("No format_preference object is present in the profile.",),
)
classification = str(raw.get("classification", "unknown"))
spread = _number(raw.get("strict_accuracy_spread"))
enabled = classification == "format_sensitive" and spread is not None and spread >= 0.10
styles: list[FormatStyle] = []
best = raw.get("best_formats")
if isinstance(best, list):
for index, item in enumerate(best):
if not isinstance(item, dict) or not isinstance(item.get("prompt_format"), str):
continue
style_id = _infer_style(item["prompt_format"])
if style_id is None:
continue
styles.append(
FormatStyle(
id=style_id,
source_format=item["prompt_format"],
strict_accuracy=_number(item.get("strict_accuracy")),
prior_rank=index + 1,
)
)
return FormatPolicy(
enabled=enabled and bool(styles),
classification=classification,
strict_accuracy_spread=spread,
styles=tuple(styles) if enabled else (),
avoid_patterns=_avoid_patterns(raw.get("worst_formats")),
cautions=(
"Format scores are priors from short templates, not proof of whole-Skill quality.",
"The compiler applies all ranked safe surface styles in order; the final style wins.",
),
)
def _style_heading(title: str, language: str, style_id: str) -> str:
key = section_key(title)
canonical = (
CANONICAL_HEADINGS[language][key]
if key is not None
else title.strip()
)
delimiter = "-" if style_id.endswith("-hyphen-labels") else ":"
if canonical.endswith(delimiter):
return (
canonical.upper()
if language == "en" and style_id.startswith("uppercase-")
else canonical
)
canonical = canonical.rstrip("::-–—")
labelled = re.match(r"^([^::]+)[::]\s*(.+)$", canonical)
if labelled is None and delimiter == "-":
existing_hyphen = re.match(r"^([^-]+)-\s+(.+)$", canonical)
if existing_hyphen and existing_hyphen.group(1).isupper():
labelled = existing_hyphen
if labelled:
label, payload = labelled.groups()
if language == "en" and style_id.startswith("uppercase-"):
label, payload = label.upper(), payload.upper()
return f"{label}{delimiter} {payload}"
if language == "en" and style_id.startswith("uppercase-"):
canonical = canonical.upper()
return canonical + delimiter
def apply_format_style(
content: str, style: FormatStyle
) -> tuple[str, list[Operation]]:
"""Apply one profile-selected style to safe H2 section-label surfaces."""
document = parse_document(content)
patches: list[tuple[int, int, str]] = []
operations: list[Operation] = []
for block in document.blocks:
# Keep the Skill title and step-level prose unchanged. Inline literals
# in headings are protected because they may be paths or identifiers.
if (
block.kind != "heading"
or block.heading_level != 2
or block.protected_spans
):
continue
match = HEADING_RE.match(block.text)
if match is None:
continue
replacement = (
f"{match.group(1)} "
f"{_style_heading(match.group(2), document.language, style.id)}"
)
if replacement == block.text:
continue
patches.append(
(block.start_offset, block.start_offset + len(block.text), replacement)
)
operations.append(
Operation(
type="FORMAT_HEADING_LABEL",
signal="format_preference",
block_id=block.id,
quote=block.text,
replacement=replacement,
target_section=section_key(match.group(2)),
)
)
if not patches:
return content, []
body = document.body
for start, end, replacement in sorted(patches, reverse=True):
body = body[:start] + replacement + body[end:]
return document.frontmatter + body, operations
+209
View File
@@ -0,0 +1,209 @@
"""Independent semantic-preservation checks for rewritten Skill Markdown."""
from __future__ import annotations
from collections import Counter
import re
from markdown_it import MarkdownIt
from .annotator import annotation_semantically_valid
from .document import DocumentError, FENCE_RE, INLINE_PROTECTED_RE, parse_document
from .models import GuardResult, Operation
CODE_FENCE_RE = re.compile(r"^[ \t]*(```+|~~~+).*?^[ \t]*\1[ \t]*$", re.M | re.S)
HEADING_LINE_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+.*$", re.M)
MARKDOWN_MARKER_RE = re.compile(
r"^[ \t]*(?:[-+*]|\d+[.)])[ \t]+|[*_~>#`]+", re.M
)
PROTECTED_LITERAL_RE = re.compile(
INLINE_PROTECTED_RE.pattern
+ r"|\b(?:MUST(?:\s+NOT)?|不得|必须|禁止|仅可|只能|不能)\b",
re.I,
)
def _code_blocks(body: str) -> list[str]:
return CODE_FENCE_RE.findall(body)
def _full_code_blocks(body: str) -> list[str]:
blocks: list[str] = []
lines = body.splitlines(keepends=True)
index = 0
while index < len(lines):
fence = FENCE_RE.match(lines[index])
if not fence:
index += 1
continue
marker_char = fence.group(1)[0]
start = index
index += 1
while index < len(lines) and not re.match(
rf"^[ \t]*{re.escape(marker_char)}{{3,}}", lines[index]
):
index += 1
index = min(index + 1, len(lines))
blocks.append("".join(lines[start:index]))
return blocks
def _payload_counter(body: str) -> Counter[str]:
without_code = body
for code in _full_code_blocks(body):
without_code = without_code.replace(code, "", 1)
without_headings = HEADING_LINE_RE.sub("", without_code)
without_markers = MARKDOWN_MARKER_RE.sub("", without_headings)
return Counter(char.lower() for char in without_markers if char.isalnum())
def _adjust_expected_payload(payload: Counter[str], operations: list[Operation]) -> Counter[str]:
adjusted = payload.copy()
for operation in operations:
if operation.type == "DUPLICATE_EXACT" and operation.quote is not None:
adjusted.update(_payload_counter(operation.quote))
elif (
operation.type == "REPLACE_COREFERENCE_WITH_SOURCE_QUOTE"
and operation.quote is not None
and operation.replacement is not None
):
adjusted.subtract(
char.lower() for char in operation.quote if char.isalnum()
)
adjusted.update(
char.lower() for char in operation.replacement if char.isalnum()
)
elif operation.type == "SEMANTIC_REWRITE_BLOCK" and operation.quote is not None:
adjusted.subtract(_payload_counter(operation.quote))
adjusted.update(_payload_counter(operation.replacement or ""))
elif operation.type == "ADD_GROUNDED_SUMMARY":
adjusted.update(_payload_counter(operation.replacement or ""))
return +adjusted
def _protected_literals(body: str) -> Counter[str]:
return Counter(match.group(0) for match in PROTECTED_LITERAL_RE.finditer(body))
def _adjust_expected_protected(
literals: Counter[str], operations: list[Operation]
) -> Counter[str]:
adjusted = literals.copy()
for operation in operations:
if operation.type == "DUPLICATE_EXACT" and operation.quote is not None:
adjusted.update(
_protected_literals(operation.replacement or operation.quote)
)
elif (
operation.type == "REPLACE_COREFERENCE_WITH_SOURCE_QUOTE"
and operation.quote is not None
and operation.replacement is not None
):
adjusted.subtract(_protected_literals(operation.quote))
adjusted.update(_protected_literals(operation.replacement))
elif operation.type == "SEMANTIC_REWRITE_BLOCK" and operation.quote is not None:
adjusted.subtract(_protected_literals(operation.quote))
adjusted.update(_protected_literals(operation.replacement or ""))
elif operation.type == "ADD_GROUNDED_SUMMARY":
adjusted.update(_protected_literals(operation.replacement or ""))
elif (
operation.type == "FORMAT_HEADING_LABEL"
and operation.quote is not None
and operation.replacement is not None
):
# Heading-format passes may change casing or punctuation (for example,
# ``Must Follow`` to ``MUST FOLLOW:``). The operation records the
# exact source and replacement, so account for that deliberate,
# surface-only change rather than treating it as an untracked loss of
# a protected literal.
adjusted.subtract(_protected_literals(operation.quote))
adjusted.update(_protected_literals(operation.replacement))
return +adjusted
def run_semantic_guard(
original: str,
rewritten: str,
operations: list[Operation],
) -> GuardResult:
checks: dict[str, bool] = {}
failures: list[str] = []
try:
source = parse_document(original)
target = parse_document(rewritten)
checks["markdown_parseable"] = True
except DocumentError as exc:
return GuardResult(False, {"markdown_parseable": False}, [str(exc)])
try:
MarkdownIt("commonmark", {"html": True}).parse(target.body)
checks["commonmark_parseable"] = True
except Exception as exc: # pragma: no cover - markdown-it is intentionally permissive
checks["commonmark_parseable"] = False
failures.append(f"CommonMark parse failed: {exc}")
checks["frontmatter_exact"] = source.frontmatter == target.frontmatter
checks["code_blocks_exact"] = [
block.text for block in source.blocks if block.kind == "code"
] == [block.text for block in target.blocks if block.kind == "code"]
checks["protected_literals_preserved"] = _adjust_expected_protected(
_protected_literals(source.body), operations
) == _protected_literals(target.body)
expected_payload = _adjust_expected_payload(
_payload_counter(source.body), operations
)
checks["body_payload_preserved"] = expected_payload == _payload_counter(target.body)
duplicated_payloads = [
operation.replacement or operation.quote
for operation in operations
if operation.type == "DUPLICATE_EXACT" and operation.quote
]
checks["duplicated_spans_present"] = all(
payload is not None and payload in target.body
for payload in duplicated_payloads
)
semantic_operations = [
operation
for operation in operations
if operation.annotation_type is not None
and operation.block_id is not None
and operation.quote is not None
]
checks["operation_annotation_types_valid"] = all(
operation.block_id in source.block_index
and annotation_semantically_valid(
operation.annotation_type,
source.block_index[operation.block_id],
operation.quote,
)
for operation in semantic_operations
)
checks["emphasis_not_nested"] = all(
operation.type != "EMPHASIZE_IN_PLACE"
or operation.quote is None
or not re.search(r"\*\*|__", operation.quote)
for operation in operations
)
checks["semantic_rewrites_source_backed"] = all(
(
operation.type not in {"SEMANTIC_REWRITE_BLOCK", "ADD_GROUNDED_SUMMARY"}
or (
bool(operation.replacement)
and bool(operation.source_quotes)
and all(quote in source.body for quote in operation.source_quotes)
and (
operation.type != "SEMANTIC_REWRITE_BLOCK"
or (
operation.block_id in source.block_index
and operation.quote
== source.block_index[operation.block_id].text
)
)
)
)
for operation in operations
)
for name, passed in checks.items():
if not passed:
failures.append(name)
return GuardResult(not failures, checks, failures)
+190
View File
@@ -0,0 +1,190 @@
"""Shared data structures for the model preference compiler."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class Signal:
name: str
prompt_ids: tuple[str, ...]
normalized_score: float | None
level: str
confidence: str
raw_scores: dict[str, float]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class BodyBlock:
id: str
kind: str
text: str
start_line: int
end_line: int
start_offset: int
end_offset: int
parent_heading: str | None = None
heading_level: int | None = None
list_depth: int = 0
protected_spans: list[tuple[int, int]] = field(default_factory=list)
@dataclass(frozen=True)
class Annotation:
type: str
block_id: str
quote: str
confidence: float
source: str
antecedent_quote: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class Operation:
type: str
signal: str
annotation_type: str | None = None
block_id: str | None = None
quote: str | None = None
target_section: str | None = None
replacement: str | None = None
source_quotes: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class AnnotationResult:
annotations: list[Annotation] = field(default_factory=list)
used: bool = False
model: str | None = None
accepted: int = 0
rejected: int = 0
error: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"used": self.used,
"model": self.model,
"accepted": self.accepted,
"rejected": self.rejected,
"error": self.error,
}
@dataclass(frozen=True)
class SourceRef:
block_id: str
quote: str
def to_dict(self) -> dict[str, str]:
return asdict(self)
@dataclass(frozen=True)
class SemanticRewriteUnit:
kind: str
target_section: str | None
source_refs: tuple[SourceRef, ...]
replacement: str
confidence: float
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.kind,
"target_section": self.target_section,
"source_refs": [item.to_dict() for item in self.source_refs],
"replacement": self.replacement,
"confidence": self.confidence,
}
@dataclass
class SemanticPlanResult:
units: list[SemanticRewriteUnit] = field(default_factory=list)
used: bool = False
model: str | None = None
transport_attempts: int = 0
request_variant: str | None = None
accepted: int = 0
rejected: int = 0
rejection_reasons: list[str] = field(default_factory=list)
semantic_rounds: int = 0
provider_request_count: int = 0
initial_proposed: int = 0
initial_accepted: int = 0
initial_rejected: int = 0
initial_rejection_reasons: list[str] = field(default_factory=list)
repair_attempted: bool = False
repair_proposed: int = 0
repair_accepted: int = 0
repair_rejected: int = 0
repair_rejection_reasons: list[str] = field(default_factory=list)
repair_transport_attempts: int = 0
repair_request_variant: str | None = None
repair_error: str | None = None
applied: int = 0
skipped: int = 0
skip_reasons: list[str] = field(default_factory=list)
error: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"used": self.used,
"model": self.model,
"transport_attempts": self.transport_attempts,
"request_variant": self.request_variant,
"accepted": self.accepted,
"rejected": self.rejected,
"rejection_reasons": list(self.rejection_reasons),
"semantic_rounds": self.semantic_rounds,
"provider_request_count": self.provider_request_count,
"initial": {
"proposed": self.initial_proposed,
"accepted": self.initial_accepted,
"rejected": self.initial_rejected,
"rejection_reasons": list(self.initial_rejection_reasons),
},
"repair": {
"attempted": self.repair_attempted,
"proposed": self.repair_proposed,
"accepted": self.repair_accepted,
"rejected": self.repair_rejected,
"rejection_reasons": list(self.repair_rejection_reasons),
"transport_attempts": self.repair_transport_attempts,
"request_variant": self.repair_request_variant,
"error": self.repair_error,
},
"applied": self.applied,
"skipped": self.skipped,
"skip_reasons": list(self.skip_reasons),
"error": self.error,
"units": [unit.to_dict() for unit in self.units],
}
@dataclass
class GuardResult:
passed: bool
checks: dict[str, bool]
failures: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class CompileResult:
output_dir: Path | None
report: dict[str, Any]
skill_name: str
+156
View File
@@ -0,0 +1,156 @@
"""Load behavioral profiles and derive rewrite signals from prompt-level scores."""
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
from typing import Any
from .models import Signal
class ProfileError(RuntimeError):
"""The behavioral profile cannot be used safely."""
SIGNAL_SPECS: dict[str, tuple[tuple[tuple[str, float], ...], str]] = {
"contextual_rule_adherence": (
(("1.1.1", 3.0), ("1.1.2", 3.0), ("1.1.3", 3.0)),
"contextual_rule_adherence",
),
"semantic_robustness": (
(("4.1.1", 2.0), ("4.1.2", 2.0)),
"semantic_robustness",
),
"uncertainty_calibration": ((("2.2.1", 3.0),), "uncertainty_calibration"),
"ambiguity_handling": ((("2.2.2", 2.0),), "ambiguity_handling"),
"evidence_priority": (
(("3.1.1", 2.0), ("3.1.2", 2.0)),
"evidence_priority",
),
"balanced_presentation": ((("3.2.1", 2.0),), "balanced_presentation"),
}
def _level(score: float | None) -> str:
if score is None:
return "unknown"
if score < 0.5:
return "low"
if score < 0.8:
return "medium"
return "high"
def _numeric_score(value: Any, maximum: float) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)):
score = float(value)
elif isinstance(value, str):
try:
score = float(value)
except ValueError:
return None
else:
return None
if score < 0 or score > maximum:
return None
return score
def _score_index(profile: dict[str, Any]) -> dict[str, Any]:
dimensions = (
profile.get("behavioral_profile", {}).get("numeric_dimensions", [])
)
if not isinstance(dimensions, list):
raise ProfileError("behavioral_profile.numeric_dimensions must be a list")
scores: dict[str, Any] = {}
for dimension in dimensions:
if not isinstance(dimension, dict):
continue
raw_scores = dimension.get("raw_scores", {})
if isinstance(raw_scores, dict):
scores.update(raw_scores)
return scores
def derive_signals(profile: dict[str, Any]) -> dict[str, Signal]:
indexed = _score_index(profile)
signals: dict[str, Signal] = {}
for name, (prompt_specs, _) in SIGNAL_SPECS.items():
raw_scores: dict[str, float] = {}
normalized_items: list[float] = []
item_levels: list[str] = []
for prompt_id, maximum in prompt_specs:
score = _numeric_score(indexed.get(prompt_id), maximum)
if score is None:
continue
raw_scores[prompt_id] = score
normalized = score / maximum
normalized_items.append(normalized)
item_levels.append(_level(normalized))
normalized_score = (
round(sum(normalized_items) / len(normalized_items), 4)
if normalized_items
else None
)
signal_level = _level(normalized_score)
expected = len(prompt_specs)
if expected == 1:
confidence = "low"
elif len(normalized_items) != expected:
confidence = "low"
elif len(set(item_levels)) == 1:
confidence = "high"
else:
counts = Counter(item_levels)
top_count = counts.most_common(1)[0][1]
confidence = "medium" if top_count > expected / 2 else "low"
signals[name] = Signal(
name=name,
prompt_ids=tuple(prompt_id for prompt_id, _ in prompt_specs),
normalized_score=normalized_score,
level=signal_level,
confidence=confidence,
raw_scores=raw_scores,
)
return signals
def selected_passes(signals: dict[str, Signal]) -> list[str]:
order = (
"contextual_rule_adherence",
"evidence_priority",
"ambiguity_handling",
"uncertainty_calibration",
"balanced_presentation",
"semantic_robustness",
)
return [
name
for name in order
if signals[name].level in {"low", "medium"}
]
def load_profile(path: Path) -> tuple[dict[str, Any], dict[str, Signal], str]:
try:
raw = path.read_bytes()
profile = json.loads(raw)
except (OSError, json.JSONDecodeError) as exc:
raise ProfileError(f"could not read profile {path}: {exc}") from exc
if not isinstance(profile, dict):
raise ProfileError("profile root must be an object")
model = profile.get("model")
if not isinstance(model, dict) or not isinstance(model.get("id"), str):
raise ProfileError("profile requires model.id")
return profile, derive_signals(profile), hashlib.sha256(raw).hexdigest()
def target_model_id(profile: dict[str, Any]) -> str:
return str(profile["model"]["id"])
+455
View File
@@ -0,0 +1,455 @@
"""Deterministic, source-preserving rewrite planning and application."""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, replace
import re
from .document import CANONICAL_HEADINGS, HEADING_RE, SkillDocument, section_key
from .models import Annotation, BodyBlock, Operation, Signal
ANNOTATION_SIGNAL = {
"critical_rule": "contextual_rule_adherence",
"definition": "contextual_rule_adherence",
"completion_criterion": "contextual_rule_adherence",
"evidence_priority_rule": "evidence_priority",
"scope_rule": "ambiguity_handling",
"decision_criterion": "ambiguity_handling",
"uncertainty_rule": "uncertainty_calibration",
"viewpoint_side_a": "balanced_presentation",
"viewpoint_side_b": "balanced_presentation",
"coreference": "semantic_robustness",
}
ANNOTATION_TARGET = {
"critical_rule": "critical_rules",
"definition": "definitions",
"completion_criterion": "completion_criterion",
"evidence_priority_rule": "evidence_priority",
"scope_rule": "scope",
"decision_criterion": "decision_criteria",
"uncertainty_rule": "uncertainty_rule",
"viewpoint_side_a": "viewpoint_side_a",
"viewpoint_side_b": "viewpoint_side_b",
}
PRE_SECTION_ORDER = (
"definitions",
"critical_rules",
"evidence_priority",
"scope",
"decision_criteria",
"uncertainty_rule",
"viewpoint_side_a",
"viewpoint_side_b",
)
LOCAL_EMPHASIS_TYPES = {
"critical_rule",
"completion_criterion",
"evidence_priority_rule",
}
PROMINENT_RULE_HEADING_RE = re.compile(
r"\b(?:critical|rules?|constraints?|requirements?|best\s+practices?|"
r"steps?|workflow|procedures?|strategy|priority|verification|validation|"
r"completion)\b|关键|规则|约束|要求|最佳实践|步骤|流程|策略|优先级|验证|完成",
re.I,
)
PROMINENT_RULE_LABEL_RE = re.compile(
r"^(?:(?:[-+*]|\d+[.)])\s+)?"
r"\*\*(?:important|critical|best\s+practice|requirement|rule|"
r"注意|重要|关键|规则|要求)\b",
re.I,
)
class RewriteError(RuntimeError):
"""A deterministic rewrite could not preserve its source span."""
@dataclass(frozen=True)
class _Patch:
start: int
end: int
replacement: str
def _enabled(signal: Signal, annotation: Annotation) -> bool:
return signal.level in {"low", "medium"}
def _heading_text(block: BodyBlock) -> str | None:
match = HEADING_RE.match(block.text)
return match.group(2).strip() if match else None
def _format_payload(quote: str) -> str:
stripped = quote.strip()
ordered = re.match(r"^\d+[.)]\s+(.+)$", stripped, re.S)
if ordered:
return f"- {ordered.group(1).strip()}"
if re.match(r"^[-+*]\s+", stripped):
return stripped
return f"- {stripped}"
def _section_heading(key: str, language: str) -> str:
if key == "viewpoint_side_a":
return "支持方" if language == "zh" else "Supporting View"
if key == "viewpoint_side_b":
return "反对方" if language == "zh" else "Opposing View"
return CANONICAL_HEADINGS[language][key]
def _normalize_known_heading(
block: BodyBlock, language: str
) -> tuple[str | None, Operation | None]:
title = _heading_text(block)
if title is None:
return None, None
key = section_key(title)
if key is None:
return None, None
canonical = CANONICAL_HEADINGS[language][key]
match = HEADING_RE.match(block.text)
assert match is not None
replacement = f"{match.group(1)} {canonical}"
if replacement == block.text:
return None, None
return (
replacement,
Operation(
type="RENAME_HEADING",
signal="semantic_robustness",
block_id=block.id,
quote=block.text,
replacement=replacement,
target_section=key,
),
)
def _split_explicit_requirements(
block: BodyBlock,
) -> tuple[str | None, Operation | None]:
if block.kind not in {"paragraph", "list_item"} or not re.search(
r"[;;]", block.text
):
return None, None
if not re.search(
r"\bMUST(?:\s+NOT)?\b|必须|不得|禁止|仅可|只能|不能", block.text, re.I
):
return None, None
parts = [part.strip() for part in re.split(r"[;;]", block.text) if part.strip()]
if len(parts) < 2:
return None, None
replacement = "\n".join(_format_payload(part) for part in parts)
return (
replacement,
Operation(
type="SPLIT_AT_EXISTING_DELIMITER",
signal="semantic_robustness",
block_id=block.id,
quote=block.text,
replacement=replacement,
),
)
def _is_standalone_rule(block: BodyBlock, quote: str) -> bool:
clean = quote.strip()
if block.kind not in {"paragraph", "list_item"}:
return False
if clean != block.text.strip():
return False
if clean.endswith((":", ":", "-", "—")):
return False
content = re.sub(r"^(?:[-+*]|\d+[.)])\s+", "", clean)
return len(content) >= 6
def _already_emphasized(block_text: str, quote_offset: int, quote: str) -> bool:
stripped = quote.strip()
stripped = re.sub(r"^(?:[-+*]|\d+[.)])\s+", "", stripped)
if stripped.startswith(("**", "__")) and stripped.endswith(("**", "__")):
return True
quote_end = quote_offset + len(quote)
for delimiter in ("**", "__"):
if (
block_text[max(0, quote_offset - len(delimiter)) : quote_offset]
== delimiter
and block_text[quote_end : quote_end + len(delimiter)] == delimiter
):
return True
return False
def _already_structurally_prominent(block: BodyBlock, quote: str) -> bool:
if PROMINENT_RULE_HEADING_RE.search(block.parent_heading or ""):
return True
return bool(PROMINENT_RULE_LABEL_RE.search(quote.strip()))
def _safe_to_emphasize(quote: str) -> bool:
# Wrapping a span that already contains emphasis creates ambiguous nested
# Markdown such as **use a **priority cascade**:**.
return not re.search(r"\*\*|__", quote)
def _emphasize(quote: str) -> str:
match = re.match(r"^((?:[-+*]|\d+[.)])\s+)(.+)$", quote.strip(), re.S)
if match:
return f"{match.group(1)}**{match.group(2)}**"
return f"**{quote.strip()}**"
def _apply_patches(body: str, patches: list[_Patch]) -> str:
ordered = sorted(patches, key=lambda item: (item.start, item.end), reverse=True)
last_start = len(body) + 1
result = body
for patch in ordered:
if patch.start < 0 or patch.end < patch.start or patch.end > len(body):
raise RewriteError("rewrite patch is outside the Markdown body")
if patch.end > last_start:
raise RewriteError("rewrite patches overlap")
result = result[: patch.start] + patch.replacement + result[patch.end :]
last_start = patch.start
return result
def _create_section_signal(key: str) -> str:
annotation_type = next(
annotation_type
for annotation_type, target in ANNOTATION_TARGET.items()
if target == key
)
return ANNOTATION_SIGNAL[annotation_type]
def rewrite_document(
document: SkillDocument,
signals: dict[str, Signal],
annotations: list[Annotation],
*,
reserved_block_ids: set[str] | None = None,
) -> tuple[str, list[Operation]]:
blocks = [replace(block) for block in document.blocks]
by_id = {block.id: block for block in blocks}
operations: list[Operation] = []
patches: list[_Patch] = []
reserved = reserved_block_ids or set()
patched_blocks: set[str] = set(reserved)
payloads: dict[str, list[str]] = defaultdict(list)
robustness = signals["semantic_robustness"]
if robustness.level in {"low", "medium"}:
for block in blocks:
if block.kind != "heading":
continue
replacement, operation = _normalize_known_heading(
block, document.language
)
if replacement is not None and operation is not None:
patches.append(
_Patch(
block.start_offset,
block.start_offset + len(block.text),
replacement,
)
)
patched_blocks.add(block.id)
operations.append(operation)
for annotation in annotations:
signal_name = ANNOTATION_SIGNAL.get(annotation.type)
if signal_name is None or not _enabled(signals[signal_name], annotation):
continue
block = by_id.get(annotation.block_id)
if block is None or annotation.quote not in block.text:
continue
if block.id in reserved:
continue
quote_offset = block.text.find(annotation.quote)
absolute_start = block.start_offset + quote_offset
absolute_end = absolute_start + len(annotation.quote)
if annotation.type == "coreference":
if (
robustness.level != "low"
or annotation.antecedent_quote is None
or block.id in patched_blocks
):
continue
patches.append(
_Patch(absolute_start, absolute_end, annotation.antecedent_quote)
)
patched_blocks.add(block.id)
operations.append(
Operation(
type="REPLACE_COREFERENCE_WITH_SOURCE_QUOTE",
signal=signal_name,
annotation_type=annotation.type,
block_id=block.id,
quote=annotation.quote,
replacement=annotation.antecedent_quote,
)
)
continue
target = ANNOTATION_TARGET[annotation.type]
if section_key(block.parent_heading) == target:
continue
if _already_structurally_prominent(block, annotation.quote):
continue
# If the exact rule already occurs more than once, a prior compilation
# has already added a summary copy. This makes compilation idempotent.
if document.body.count(annotation.quote) > 1:
continue
if _is_standalone_rule(block, annotation.quote):
formatted = _format_payload(annotation.quote)
if formatted not in payloads[target]:
payloads[target].append(formatted)
operations.append(
Operation(
type="DUPLICATE_EXACT",
signal=signal_name,
annotation_type=annotation.type,
block_id=block.id,
quote=annotation.quote,
replacement=formatted,
target_section=target,
)
)
continue
# Context-dependent fragments stay where they are. Critical directives
# get local Markdown emphasis; other non-standalone semantic fragments
# are left untouched.
if (
annotation.type in LOCAL_EMPHASIS_TYPES
and not _already_emphasized(
block.text, quote_offset, annotation.quote
)
and _safe_to_emphasize(annotation.quote)
and block.id not in patched_blocks
):
replacement = _emphasize(annotation.quote)
patches.append(_Patch(absolute_start, absolute_end, replacement))
patched_blocks.add(block.id)
operations.append(
Operation(
type="EMPHASIZE_IN_PLACE",
signal=signal_name,
annotation_type=annotation.type,
block_id=block.id,
quote=annotation.quote,
replacement=replacement,
target_section=target,
)
)
if robustness.level == "low":
for block in blocks:
if block.id in patched_blocks:
continue
replacement, operation = _split_explicit_requirements(block)
if replacement is not None and operation is not None:
patches.append(
_Patch(
block.start_offset,
block.start_offset + len(block.text),
replacement,
)
)
patched_blocks.add(block.id)
operations.append(operation)
existing_sections: dict[str, BodyBlock] = {}
for block in blocks:
if block.kind == "heading":
key = section_key(_heading_text(block))
if key is not None:
existing_sections[key] = block
insertions: dict[int, list[str]] = defaultdict(list)
new_pre_sections: list[str] = []
completion_section: str | None = None
for key in PRE_SECTION_ORDER + ("completion_criterion",):
values = payloads.get(key, [])
if not values:
continue
if key in existing_sections:
heading = existing_sections[key]
insertions[heading.end_offset].append(
document.newline + document.newline.join(values) + document.newline
)
continue
section = (
f"## {_section_heading(key, document.language)}"
f"{document.newline}{document.newline}"
+ document.newline.join(values)
)
operations.append(
Operation(
type="CREATE_SECTION",
signal=_create_section_signal(key),
target_section=key,
)
)
if key == "completion_criterion":
completion_section = section
else:
new_pre_sections.append(section)
if new_pre_sections:
first_h2 = next(
(
block
for block in blocks
if block.kind == "heading" and (block.heading_level or 0) >= 2
),
None,
)
if first_h2 is not None:
position = first_h2.start_offset
text = (
(document.newline * 2).join(new_pre_sections)
+ document.newline
+ document.newline
)
else:
first_h1 = next(
(
block
for block in blocks
if block.kind == "heading" and block.heading_level == 1
),
None,
)
position = first_h1.end_offset if first_h1 is not None else 0
text = (
document.newline
+ (document.newline * 2).join(new_pre_sections)
+ document.newline
+ document.newline
)
insertions[position].append(text)
if completion_section is not None:
prefix = "" if document.body.endswith(document.newline * 2) else document.newline
insertions[len(document.body)].append(
prefix + completion_section + document.newline
)
for position, values in insertions.items():
patches.append(_Patch(position, position, "".join(values)))
if not operations:
return document.original, []
body = _apply_patches(document.body, patches)
return document.frontmatter + body, operations
@@ -0,0 +1,600 @@
"""Validate and apply source-grounded semantic rewrite plans."""
from __future__ import annotations
from collections import Counter, defaultdict
import re
from typing import Any
from .document import (
CANONICAL_HEADINGS,
HEADING_RE,
INLINE_PROTECTED_RE,
LIST_RE,
SkillDocument,
parse_document,
section_key,
)
from .models import Operation, SemanticRewriteUnit, SourceRef
PLAN_SCHEMA_VERSION = "2.0"
ALLOWED_KINDS = {"add_summary", "replace_block"}
# Tables and code samples may contain explicit operational constraints. They
# are read-only evidence: usable for summaries, never as replacement targets.
SUMMARY_SOURCE_KINDS = frozenset({"paragraph", "list_item", "table", "code"})
REPLACE_SOURCE_KINDS = frozenset({"paragraph", "list_item"})
ALLOWED_SECTIONS = {
"definitions",
"critical_rules",
"evidence_priority",
"scope",
"decision_criteria",
"uncertainty_rule",
"task",
"inputs",
"output",
"validation",
"completion_criterion",
}
SECTION_ORDER = (
"definitions",
"critical_rules",
"evidence_priority",
"scope",
"decision_criteria",
"uncertainty_rule",
"inputs",
"task",
"output",
"validation",
"completion_criterion",
)
HARD_PROTECTED_RE = re.compile(
INLINE_PROTECTED_RE.pattern
+ r"|\b\d+(?:\.\d+)*%?\b"
+ r"|\b(?:MUST(?:\s+NOT)?|SHALL(?:\s+NOT)?|SHOULD(?:\s+NOT)?|"
+ r"NEVER|ALWAYS|DO\s+NOT|ONLY|"
+ r"IF|WHEN|UNLESS|BEFORE|AFTER|EXCEPT|WITHOUT)\b"
+ r"|不得|必须|禁止|仅可|只能|不能|不要|始终|如果|当|除非|之前|之后|除外|不得不",
re.I,
)
SOFT_MODAL_RE = re.compile(r"\b(?:MAY(?:\s+NOT)?|CAN(?:\s+NOT)?)\b", re.I)
PROTECTED_RE = re.compile(
HARD_PROTECTED_RE.pattern + r"|" + SOFT_MODAL_RE.pattern,
re.I,
)
LIST_PREFIX_RE = re.compile(r"^([ \t]*(?:[-+*]|\d+[.)])[ \t]+)")
ORDERED_LIST_PREFIX_RE = re.compile(r"^[ \t]*\d+[.)][ \t]+", re.M)
EXPLICIT_RULE_RE = re.compile(
r"\b(?:MUST(?:\s+NOT)?|SHALL(?:\s+NOT)?|SHOULD(?:\s+NOT)?|"
r"MAY(?:\s+NOT)?|NEVER|ALWAYS|DO\s+NOT|ONLY|REQUIRED|PROHIBITED|"
r"IF|WHEN|UNLESS|BEFORE|AFTER|EXCEPT|WITHOUT)\b"
r"|\b(?:is|are|was|were|do|does|can|will|they(?:'re|\s+are))\s+not\b"
r"|(?:^|\n|\s-\s)(?:keep|remove|use|read|write|recurse|work|check|"
r"validate|ensure|avoid|preserve|process|return|run|call|set|include|"
r"exclude)\b(?!\s+\d+\s*:)"
r"|不得|必须|禁止|仅可|只能|不能|不要|始终|如果|当|除非|之前|之后|"
r"应当|需要|务必|确保|保留|删除|移除|使用|读取|写入|检查|验证",
re.I,
)
RESOURCE_CONFIG_ASSIGNMENT_RE = re.compile(
r"^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*(?:PATH|DIR|CACHE|ROOT|HOME|"
r"CONFIG|ENDPOINT|HOST|PORT|MODE|OFFLINE|DATABASE|DB)[A-Za-z0-9_]*\s*=\s*"
r"(?:[rRuUbBfF]{0,2})?['\"][^'\"]+['\"]\s*$",
re.I,
)
RESOURCE_PARAMETER_RE = re.compile(
r"`--[A-Za-z0-9][A-Za-z0-9-]*\s+<[^>]+>`.*"
r"\b(?:path|directory|dir|cache|database|db|location|file)\b",
re.I,
)
NARROW_TASK_HEADING_RE = re.compile(
r"\b(?:conditional|condition|branch|pattern|example|implementation|detail|"
r"substep|edge case)s?\b|条件|分支|模式|示例|实现细节|子步骤|边界情况",
re.I,
)
REJECTION_INDEX_RE = re.compile(r"^rewrite\[(\d+)]\s*:")
REPAIRABLE_REJECTION_MARKERS = (
"replacement is not text",
"replacement is empty",
"replacement may not inject headings or fenced code",
"replacement is disproportionately longer than its sources",
"changed a protected literal, number, or modality",
"block rewrite is too short",
"block rewrite changed paragraph/list structure",
"block rewrite changed the list marker",
"summary changed a hard protected source literal",
)
def semantic_plan_needed(document: SkillDocument) -> bool:
return any(
block.kind in SUMMARY_SOURCE_KINDS and block.text.strip()
for block in document.blocks
)
def rejection_index(reason: str) -> int | None:
match = REJECTION_INDEX_RE.match(reason)
return int(match.group(1)) if match else None
def repairable_rewrite_indices(reasons: list[str]) -> list[int]:
indices = []
for reason in reasons:
index = rejection_index(reason)
if index is not None and any(
marker in reason for marker in REPAIRABLE_REJECTION_MARKERS
):
indices.append(index)
return indices
def _protected(
text: str,
*,
ignore_list_ordinals: bool = False,
hard_only: bool = False,
) -> Counter[str]:
if ignore_list_ordinals:
# Ordered-list markers describe Markdown structure, not task semantics.
# Remove only line-leading markers; numbers in the item body remain
# protected (for example, "Retry 3 times" or "use version 2.1").
text = ORDERED_LIST_PREFIX_RE.sub("", text)
pattern = HARD_PROTECTED_RE if hard_only else PROTECTED_RE
return Counter(match.group(0).casefold() for match in pattern.finditer(text))
def _soft_modal_change(source: str, replacement: str) -> str | None:
source_values = Counter(
match.group(0).casefold() for match in SOFT_MODAL_RE.finditer(source)
)
replacement_values = Counter(
match.group(0).casefold() for match in SOFT_MODAL_RE.finditer(replacement)
)
if set(source_values) == set(replacement_values):
return None
return _protected_change(source_values, replacement_values, compare_counts=False)
def _protected_change(
source: Counter[str], replacement: Counter[str], *, compare_counts: bool
) -> str:
if compare_counts:
missing = sorted((source - replacement).elements())
added = sorted((replacement - source).elements())
else:
missing = sorted(set(source) - set(replacement))
added = sorted(set(replacement) - set(source))
details = []
if missing:
details.append(f"missing={missing!r}")
if added:
details.append(f"added={added!r}")
return ", ".join(details) or "no difference"
def _source_material(refs: tuple[SourceRef, ...]) -> str:
return "\n".join(ref.quote for ref in refs)
def _literal_map(
text: str,
*,
ignore_list_ordinals: bool = False,
hard_only: bool = False,
soft_only: bool = False,
) -> dict[str, str]:
if ignore_list_ordinals:
text = ORDERED_LIST_PREFIX_RE.sub("", text)
if soft_only:
pattern = SOFT_MODAL_RE
elif hard_only:
pattern = HARD_PROTECTED_RE
else:
pattern = PROTECTED_RE
values: dict[str, str] = {}
for match in pattern.finditer(text):
values.setdefault(match.group(0).casefold(), match.group(0))
return values
def replacement_literal_delta(
raw: dict[str, Any], document: SkillDocument
) -> dict[str, list[str]]:
"""Return structured literal edits for a locked replacement repair."""
raw_refs = raw.get("source_refs")
replacement = raw.get("replacement")
kind = raw.get("kind")
if not isinstance(raw_refs, list) or not isinstance(replacement, str):
return {
"restore_verbatim": [],
"remove_verbatim": [],
"soft_modal_missing": [],
"soft_modal_added": [],
}
quotes = [
ref.get("quote")
for ref in raw_refs
if isinstance(ref, dict) and isinstance(ref.get("quote"), str)
]
if len(quotes) != len(raw_refs):
return {
"restore_verbatim": [],
"remove_verbatim": [],
"soft_modal_missing": [],
"soft_modal_added": [],
}
source = "\n".join(quotes)
summary = kind == "add_summary"
source_hard = _literal_map(
source,
ignore_list_ordinals=summary,
hard_only=summary,
)
replacement_hard = _literal_map(
replacement,
ignore_list_ordinals=summary,
hard_only=summary,
)
source_soft = _literal_map(source, soft_only=True)
replacement_soft = _literal_map(replacement, soft_only=True)
return {
"restore_verbatim": [
source_hard[key] for key in sorted(set(source_hard) - set(replacement_hard))
],
"remove_verbatim": [
replacement_hard[key]
for key in sorted(set(replacement_hard) - set(source_hard))
],
"soft_modal_missing": [
source_soft[key] for key in sorted(set(source_soft) - set(replacement_soft))
],
"soft_modal_added": [
replacement_soft[key]
for key in sorted(set(replacement_soft) - set(source_soft))
],
}
def _summary_target_error(
target: str, source: str, refs: list[SourceRef], blocks: dict[str, Any]
) -> str | None:
resource_parameter_binding = (
any(
blocks[ref.block_id].kind == "code"
and RESOURCE_CONFIG_ASSIGNMENT_RE.match(ref.quote)
for ref in refs
)
and any(RESOURCE_PARAMETER_RE.search(ref.quote) for ref in refs)
)
if (
target == "critical_rules"
and not EXPLICIT_RULE_RE.search(source)
and not resource_parameter_binding
):
return (
"critical_rules summary source is descriptive rather than an explicit "
"directive, prohibition, condition, or required invariant"
)
if target == "task":
headings = [blocks[ref.block_id].parent_heading for ref in refs]
if headings and all(
heading and NARROW_TASK_HEADING_RE.search(heading)
for heading in headings
):
return "task summary may not promote a narrow subsection into the global task"
return None
def _validate_replacement_shape(
kind: str, source: str, replacement: str
) -> str | None:
if not replacement.strip():
return "replacement is empty"
if "```" in replacement or "~~~" in replacement or re.search(
r"^#{1,6}[ \t]+", replacement, re.M
):
return "replacement may not inject headings or fenced code"
if len(replacement) > max(400, int(len(source) * 1.75)):
return "replacement is disproportionately longer than its sources"
if kind == "replace_block":
source_protected = _protected(source)
replacement_protected = _protected(replacement)
if source_protected != replacement_protected:
change = _protected_change(
source_protected, replacement_protected, compare_counts=True
)
return (
"block rewrite changed a protected literal, number, or modality "
f"({change})"
)
ratio = len(replacement.strip()) / max(1, len(source.strip()))
if ratio < 0.55:
return "block rewrite is too short to preserve all source content"
source_prefix = LIST_PREFIX_RE.match(source)
replacement_prefix = LIST_PREFIX_RE.match(replacement)
if bool(source_prefix) != bool(replacement_prefix):
return "block rewrite changed paragraph/list structure"
if source_prefix and replacement_prefix:
if source_prefix.group(1) != replacement_prefix.group(1):
return "block rewrite changed the list marker"
else:
source_protected = _protected(
source, ignore_list_ordinals=True, hard_only=True
)
replacement_protected = _protected(
replacement, ignore_list_ordinals=True, hard_only=True
)
if set(source_protected) != set(replacement_protected):
change = _protected_change(
source_protected, replacement_protected, compare_counts=False
)
soft_change = _soft_modal_change(source, replacement)
if soft_change:
change += f"; soft_modal_change=({soft_change})"
return (
"summary changed a hard protected source literal "
f"({change})"
)
return None
def validate_semantic_plan(
payload: dict[str, Any], document: SkillDocument
) -> tuple[list[SemanticRewriteUnit], list[str]]:
if payload.get("schema_version") != PLAN_SCHEMA_VERSION:
raise ValueError("semantic plan has unsupported schema_version")
raw_units = payload.get("rewrites")
if not isinstance(raw_units, list):
raise ValueError("semantic plan requires rewrites list")
if len(raw_units) > 24:
raise ValueError("semantic plan exceeds 24 rewrite units")
blocks = document.block_index
accepted: list[SemanticRewriteUnit] = []
rejected: list[str] = []
replaced_blocks: set[str] = set()
for index, raw in enumerate(raw_units):
prefix = f"rewrite[{index}]"
if not isinstance(raw, dict):
rejected.append(f"{prefix}: item is not an object")
continue
kind = raw.get("kind")
target = raw.get("target_section")
replacement = raw.get("replacement")
confidence = raw.get("confidence")
raw_refs = raw.get("source_refs")
if kind not in ALLOWED_KINDS:
rejected.append(f"{prefix}: unsupported kind")
continue
if kind == "add_summary" and target not in ALLOWED_SECTIONS:
rejected.append(f"{prefix}: unsupported target_section")
continue
if kind == "replace_block":
target = None
if not isinstance(replacement, str):
rejected.append(f"{prefix}: replacement is not text")
continue
if (
isinstance(confidence, bool)
or not isinstance(confidence, (int, float))
or float(confidence) < (0.90 if kind == "replace_block" else 0.85)
):
rejected.append(f"{prefix}: invalid confidence")
continue
if not isinstance(raw_refs, list) or not 1 <= len(raw_refs) <= 8:
rejected.append(f"{prefix}: invalid source_refs")
continue
refs: list[SourceRef] = []
invalid_ref = False
non_prose_replacement_ref = False
for raw_ref in raw_refs:
if not isinstance(raw_ref, dict):
invalid_ref = True
break
block_id = raw_ref.get("block_id")
quote = raw_ref.get("quote")
block = blocks.get(block_id) if isinstance(block_id, str) else None
if (
block is None
or block.kind not in SUMMARY_SOURCE_KINDS
or not isinstance(quote, str)
or not quote
or quote not in block.text
):
invalid_ref = True
break
if kind == "replace_block" and block.kind not in REPLACE_SOURCE_KINDS:
non_prose_replacement_ref = True
break
refs.append(SourceRef(block_id, quote))
if invalid_ref:
rejected.append(f"{prefix}: source_refs are not exact prose spans")
continue
if non_prose_replacement_ref:
rejected.append(
f"{prefix}: replace_block may only cite paragraph or list_item sources"
)
continue
if kind == "replace_block":
if len(refs) != 1 or refs[0].quote != blocks[refs[0].block_id].text:
rejected.append(f"{prefix}: replace_block must cite one complete block")
continue
if refs[0].block_id in replaced_blocks:
rejected.append(f"{prefix}: block already has an accepted replacement")
continue
source = _source_material(tuple(refs))
if kind == "add_summary":
target_error = _summary_target_error(target, source, refs, blocks)
if target_error:
rejected.append(f"{prefix}: {target_error}")
continue
shape_error = _validate_replacement_shape(kind, source, replacement)
if shape_error:
rejected.append(f"{prefix}: {shape_error}")
continue
unit = SemanticRewriteUnit(
kind=kind,
target_section=target,
source_refs=tuple(refs),
replacement=replacement.strip(),
confidence=float(confidence),
)
accepted.append(unit)
if kind == "replace_block":
replaced_blocks.add(refs[0].block_id)
return accepted, rejected
def _heading_for(key: str, language: str) -> str:
return CANONICAL_HEADINGS[language][key]
def _section_text(document: SkillDocument, heading: Any) -> str:
"""Return only the content governed by a recognized heading."""
end = len(document.body)
level = heading.heading_level or 6
for block in document.blocks:
if (
block.kind == "heading"
and block.start_offset > heading.start_offset
and (block.heading_level or 6) <= level
):
end = block.start_offset
break
return document.body[heading.end_offset:end]
def apply_semantic_plan(
content: str,
source_document: SkillDocument,
units: list[SemanticRewriteUnit],
) -> tuple[str, list[Operation], list[str]]:
"""Apply valid units independently; return skip reasons for local fallback."""
body = parse_document(content).body
frontmatter = parse_document(content).frontmatter
operations: list[Operation] = []
skipped: list[str] = []
patches: list[tuple[int, int, str]] = []
summary_values: dict[str, list[tuple[SemanticRewriteUnit, str]]] = defaultdict(list)
for index, unit in enumerate(units):
if unit.kind == "add_summary":
assert unit.target_section is not None
value = unit.replacement
if not re.match(r"^[-+*][ \t]+", value):
value = f"- {value}"
summary_values[unit.target_section].append((unit, value))
continue
ref = unit.source_refs[0]
source_block = source_document.block_index[ref.block_id]
matches = list(re.finditer(re.escape(source_block.text), body))
if len(matches) != 1:
skipped.append(
f"rewrite[{index}]: source block changed before semantic replacement"
)
continue
match = matches[0]
patches.append((match.start(), match.end(), unit.replacement))
operations.append(
Operation(
type="SEMANTIC_REWRITE_BLOCK",
signal="semantic_plan",
block_id=ref.block_id,
quote=source_block.text,
replacement=unit.replacement,
source_quotes=[item.quote for item in unit.source_refs],
)
)
for start, end, replacement in sorted(patches, reverse=True):
body = body[:start] + replacement + body[end:]
if summary_values:
current = parse_document(frontmatter + body)
existing: dict[str, Any] = {}
for block in current.blocks:
if block.kind == "heading":
match = HEADING_RE.match(block.text)
key = section_key(match.group(2)) if match else None
if key:
existing[key] = block
insertions: dict[int, list[str]] = defaultdict(list)
new_sections: list[str] = []
for key in SECTION_ORDER:
values = summary_values.get(key, [])
if not values:
continue
retained: list[tuple[SemanticRewriteUnit, str]] = []
seen: set[str] = set()
existing_text = _section_text(current, existing[key]) if key in existing else ""
for unit, value in values:
if (
value in seen
or value in existing_text
or unit.replacement in existing_text
):
skipped.append(
f"add_summary[{key}]: equivalent summary already exists"
)
continue
seen.add(value)
retained.append((unit, value))
values = retained
if not values:
continue
payload = current.newline.join(value for _, value in values)
if key in existing:
insertions[existing[key].end_offset].append(
current.newline + payload + current.newline
)
else:
new_sections.append(
f"## {_heading_for(key, current.language)}"
f"{current.newline}{current.newline}{payload}"
)
for unit, value in values:
operations.append(
Operation(
type="ADD_GROUNDED_SUMMARY",
signal="semantic_plan",
target_section=key,
replacement=value,
source_quotes=[item.quote for item in unit.source_refs],
)
)
if new_sections:
first_h2 = next(
(
block
for block in current.blocks
if block.kind == "heading" and (block.heading_level or 0) >= 2
),
None,
)
if first_h2:
position = first_h2.start_offset
prefix = ""
else:
first_h1 = next(
(
block
for block in current.blocks
if block.kind == "heading" and block.heading_level == 1
),
None,
)
position = first_h1.end_offset if first_h1 else 0
prefix = current.newline
insertions[position].append(
prefix
+ (current.newline * 2).join(new_sections)
+ current.newline * 2
)
for position, values in sorted(insertions.items(), reverse=True):
body = body[:position] + "".join(values) + body[position:]
return frontmatter + body, operations, skipped