50 lines
1.6 KiB
Python
50 lines
1.6 KiB
Python
import re
|
|
from collections.abc import Mapping
|
|
from pathlib import PurePosixPath
|
|
from types import MappingProxyType
|
|
from typing import ClassVar
|
|
from typing import Final
|
|
|
|
from pydantic import BaseModel
|
|
from pydantic import ConfigDict
|
|
from pydantic import field_validator
|
|
|
|
SKILL_ID_RE: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
SEMVER_RE: Final[re.Pattern[str]] = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
|
|
|
|
|
|
class StrictFrozenModel(BaseModel):
|
|
"""Immutable base model with strict field validation rules."""
|
|
|
|
model_config: ClassVar[ConfigDict] = ConfigDict(
|
|
extra="forbid",
|
|
frozen=True,
|
|
validate_by_alias=True,
|
|
validate_by_name=True,
|
|
str_strip_whitespace=True,
|
|
)
|
|
|
|
|
|
def frozen_mapping[K, V](value: Mapping[K, V] | None = None) -> Mapping[K, V]:
|
|
return MappingProxyType(dict(value) if value is not None else {})
|
|
|
|
|
|
class ReferenceEntry(StrictFrozenModel):
|
|
"""Reference metadata for a markdown file within a skill."""
|
|
|
|
path: str
|
|
mime_type: str = "text/markdown"
|
|
title: str | None = None
|
|
|
|
@field_validator("path")
|
|
@classmethod
|
|
def validate_reference_path(cls, value: str) -> str:
|
|
path = PurePosixPath(value)
|
|
if path.is_absolute() or ".." in path.parts:
|
|
raise ValueError("reference path must be a relative in-skill path")
|
|
if not str(path).startswith("references/"):
|
|
raise ValueError("reference path must stay under references/")
|
|
if path.suffix.lower() != ".md":
|
|
raise ValueError("reference path must target a markdown file")
|
|
return path.as_posix()
|