Write and revise developer blog posts or run a humanizer / humanization pass on existing drafts using the AI anti-pattern catalog, structural audits, and reusable personal and explicitly selected corporate writing identities.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
"""Shared validation and path contracts for writing-identity tools."""
from __future__ import annotations
import json
import os
import re
import stat
from pathlib import Path
NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
ROLE_RE = NAME_RE
CURRENT_USER_PREFIXES = tuple(
f"~{separator}" for separator in (os.sep, os.altsep) if separator
)
CURRENT_SELECTION_SCHEMA = 2
SELECTION_SCHEMAS = {1, CURRENT_SELECTION_SCHEMA}
class IdentityError(ValueError):
"""Selected identity or project configuration is invalid."""
class ToolError(RuntimeError):
"""An identity tool could not inspect or update the requested files."""
def inside(base: Path, candidate: Path) -> bool:
try:
candidate.relative_to(base)
return True
except ValueError:
return False
def load_json(path: Path) -> dict:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise IdentityError(
f"required identity file is missing: {path}; restore it or select a "
"different identity, then re-run"
) from exc
except (UnicodeError, json.JSONDecodeError) as exc:
raise IdentityError(f"invalid JSON {path}: {exc}") from exc
except OSError as exc:
raise ToolError(f"cannot read {path}: {exc}") from exc
if not isinstance(value, dict):
raise IdentityError(f"expected a JSON object: {path}")
return value
def uses_named_user_path(raw: str) -> bool:
return (
raw.startswith("~") and raw != "~" and not raw.startswith(CURRENT_USER_PREFIXES)
)
def resolve_relative(raw: str, base: Path, *, allow_named_user: bool = False) -> Path:
try:
if uses_named_user_path(raw) and not allow_named_user:
raise IdentityError(
f"invalid path {raw!r}: named-user expansion is not allowed"
)
path = Path(raw).expanduser()
if not path.is_absolute():
path = base / path
return path.resolve()
except IdentityError:
raise
except (RuntimeError, ValueError) as exc:
raise IdentityError(f"invalid path {raw!r}: {exc}") from exc
except OSError as exc:
raise ToolError(f"cannot resolve path {raw!r}: {exc}") from exc
def validate_identity(
raw_path: str,
expected_type: str,
base: Path,
*,
allow_named_user: bool = False,
) -> dict:
root = resolve_relative(raw_path, base, allow_named_user=allow_named_user)
manifest_path = root / "identity.json"
manifest = load_json(manifest_path)
errors: list[str] = []
if manifest.get("schema_version") != 1:
errors.append("schema_version must be 1")
if manifest.get("type") != expected_type:
errors.append(f"type must be {expected_type!r}")
name = manifest.get("name")
if not isinstance(name, str) or not NAME_RE.fullmatch(name):
errors.append("name must be lowercase kebab-case")
if manifest.get("status") not in {"draft", "approved"}:
errors.append("status must be 'draft' or 'approved'")
if manifest.get("entrypoint") != "identity.md":
errors.append("entrypoint must be 'identity.md'")
if manifest.get("sources") != "sources.md":
errors.append("sources must be 'sources.md'")
files: list[dict[str, str]] = []
def add_file(role: str, raw: object) -> None:
if not isinstance(raw, str) or not raw:
errors.append(f"{role} path must be a non-empty string")
return
if "\x00" in raw:
errors.append(f"{role} path must not contain a NUL byte")
return
if raw.startswith("~"):
errors.append(f"{role} path must not start with '~': {raw}")
return
if Path(raw).is_absolute():
errors.append(f"{role} path must be relative: {raw}")
return
candidate = resolve_relative(raw, root)
if not inside(root, candidate):
errors.append(f"{role} path escapes identity directory: {raw}")
elif not candidate.is_file():
errors.append(f"missing {role} file: {candidate}")
else:
files.append({"role": role, "path": str(candidate)})
add_file("entrypoint", manifest.get("entrypoint"))
resources = manifest.get("resources", [])
if not isinstance(resources, list):
errors.append("resources must be a list")
else:
for index, resource in enumerate(resources):
if not isinstance(resource, dict):
errors.append(f"resources[{index}] must be an object")
continue
role = resource.get("role")
if not isinstance(role, str) or not ROLE_RE.fullmatch(role):
errors.append(f"resources[{index}].role must be lowercase kebab-case")
continue
add_file(role, resource.get("path"))
add_file("sources", manifest.get("sources"))
if errors:
raise IdentityError(
f"invalid {expected_type} identity at {root}: " + "; ".join(errors)
)
return {
"mode": "v1",
"root": str(root),
"name": name,
"status": manifest["status"],
"files": files,
}
def legacy_persona(path: Path) -> dict | None:
voice = path / "voice.md"
try:
if not voice.is_file():
return None
if not voice.read_text(encoding="utf-8").strip():
return None
except UnicodeError as exc:
raise IdentityError(f"legacy voice is not UTF-8: {voice}") from exc
except OSError as exc:
raise ToolError(f"cannot read legacy voice {voice}: {exc}") from exc
files: list[dict[str, str]] = []
for role, name in (
("voice", "voice.md"),
("composition", "framework.md"),
("examples", "examples.md"),
("bio", "bio.md"),
("legacy-product-context", "product.md"),
):
candidate = path / name
try:
if candidate.is_file() and candidate.stat().st_size:
files.append({"role": role, "path": str(candidate.resolve())})
except OSError as exc:
raise ToolError(f"cannot inspect legacy file {candidate}: {exc}") from exc
return {
"mode": "legacy",
"root": str(path.resolve()),
"name": "legacy-persona",
"status": "approved",
"files": files,
}
def load_selection_config(path: Path) -> dict:
config = load_json(path)
errors: list[str] = []
schema_version = config.get("schema_version")
if schema_version not in SELECTION_SCHEMAS:
errors.append("schema_version must be 1 or 2")
for key in ("personal", "corporate"):
if key in config and (not isinstance(config[key], str) or not config[key]):
errors.append(f"{key} must be a non-empty path string when present")
elif (
schema_version == CURRENT_SELECTION_SCHEMA
and key in config
and uses_named_user_path(config[key])
):
errors.append(f"{key} must not use named-user expansion")
if errors:
raise IdentityError(f"invalid identity config {path}: " + "; ".join(errors))
return config
def selection_target(config_path: Path, blog_home: Path) -> Path | None:
"""Resolve an existing selection file without crossing the project boundary."""
try:
config_path.lstat()
except FileNotFoundError:
try:
prospective = config_path.resolve(strict=False)
except RuntimeError as exc:
raise IdentityError(
f"identity config path has a symlink loop: {config_path}"
) from exc
except OSError as exc:
raise ToolError(
f"cannot resolve identity config path {config_path}: {exc}"
) from exc
if not inside(blog_home, prospective):
raise IdentityError(
f"identity config path escapes blog home: {prospective}"
)
return None
except OSError as exc:
raise ToolError(f"cannot inspect {config_path}: {exc}") from exc
try:
target = config_path.resolve(strict=True)
except FileNotFoundError as exc:
raise IdentityError(
f"identity config is a dangling symlink: {config_path}"
) from exc
except RuntimeError as exc:
raise IdentityError(
f"identity config has a symlink loop: {config_path}"
) from exc
except OSError as exc:
raise ToolError(f"cannot resolve identity config {config_path}: {exc}") from exc
if not inside(blog_home, target):
raise IdentityError(f"identity config path escapes blog home: {target}")
try:
target_mode = target.stat().st_mode
except OSError as exc:
raise ToolError(f"cannot inspect identity config {target}: {exc}") from exc
if not stat.S_ISREG(target_mode):
raise IdentityError(f"identity config is not a regular file: {target}")
return target
def read_paths(*identities: dict | None) -> list[str]:
ordered: list[str] = []
seen: set[str] = set()
for identity in identities:
if not identity:
continue
for item in identity["files"]:
path = item["path"]
if path not in seen:
ordered.append(path)
seen.add(path)
return ordered.tessl-plugin
skills
blog-writer
cfp-abstract
create-corporate-identity
create-personal-identity
references