← Files Portable ResumeARCHIVED FILE

skills/.portable-resume/runtime/portable_resume/diagnostics.py

8.45 KB · Oct 2, 2026 · 00:33 UTC

↓ Download file

"""Bounded machine diagnostics and stable process exit codes."""

from __future__ import annotations

import json
from dataclasses import dataclass
from enum import IntEnum
from typing import Any, NoReturn

from .bounds import DEFAULT_BOUNDS
from .model import SOURCE_KEYS


class ExitCode(IntEnum):
    OK = 0
    INVALID_INPUT = 2
    NO_MATCH = 3
    AMBIGUOUS = 4
    UNSUPPORTED = 5
    UNSAFE_OR_BUSY = 6
    CORRUPT_OR_LIMIT = 7
    INVARIANT = 8


ERROR_EXIT_CODES: dict[str, ExitCode] = {
    "E_INVALID_INPUT": ExitCode.INVALID_INPUT,
    "E_NO_MATCH": ExitCode.NO_MATCH,
    "E_AMBIGUOUS": ExitCode.AMBIGUOUS,
    "E_UNSUPPORTED_FORMAT": ExitCode.UNSUPPORTED,
    "E_CAPABILITY_UNAVAILABLE": ExitCode.UNSUPPORTED,
    "E_UNSAFE_PATH": ExitCode.UNSAFE_OR_BUSY,
    "E_SOURCE_BUSY": ExitCode.UNSAFE_OR_BUSY,
    "E_SQLITE_HOT_JOURNAL": ExitCode.UNSAFE_OR_BUSY,
    "E_SQLITE_LIVE_WAL": ExitCode.UNSAFE_OR_BUSY,
    "E_LIMIT_EXCEEDED": ExitCode.CORRUPT_OR_LIMIT,
    "E_CORRUPT_RECORD": ExitCode.CORRUPT_OR_LIMIT,
    "E_INVARIANT": ExitCode.INVARIANT,
    "E_INSTALL_BUSY": ExitCode.UNSAFE_OR_BUSY,
    "E_INSTALL_CONFLICT": ExitCode.UNSAFE_OR_BUSY,
    "E_INSTALL_SHADOW": ExitCode.UNSAFE_OR_BUSY,
    "E_INSTALL_UNSUPPORTED_PLATFORM": ExitCode.UNSUPPORTED,
    "E_RECOVERY_REQUIRED": ExitCode.UNSAFE_OR_BUSY,
    "E_VERIFY_MISMATCH": ExitCode.CORRUPT_OR_LIMIT,
}

WARNING_CODES = frozenset(
    {
        "W_TRUNCATED",
        "W_PARTIAL_TAIL",
        "W_BROKEN_CHAIN",
        "W_MISSING_BLOB",
        "W_STALE_INDEX",
        "W_CLI_MESSAGES_LANE",
        "W_OPTIONAL_ZSTD_UNAVAILABLE",
        "W_METADATA_REDACTED",
        "W_CONTROLS_REMOVED",
        "W_BINARY_OMITTED",
        "W_UNKNOWN_RECORD_SKIPPED",
        "W_HOST_DISCOVERY_UNPROVEN",
        "W_LIVE_SMOKE_NOT_RUN",
        "W_SKILL_SHADOW",
        "W_SKILL_DUPLICATE",
        "W_RUNTIME_IDENTITY_DRIFT",
        "W_SOURCE_PROVIDER_SKIPPED",
    }
)

_DEFAULT_MESSAGES = {
    "E_INVALID_INPUT": "The request is invalid.",
    "E_NO_MATCH": "No eligible session matched the request.",
    "E_AMBIGUOUS": "More than one eligible session matched the request.",
    "E_UNSUPPORTED_FORMAT": "No supported persisted-session format was detected.",
    "E_CAPABILITY_UNAVAILABLE": "The requested source capability is unavailable.",
    "E_UNSAFE_PATH": "The requested path is outside an approved safe root or is not a regular file.",
    "E_SOURCE_BUSY": "The source changed during bounded stable-read attempts.",
    "E_SQLITE_HOT_JOURNAL": "The SQLite family contains an unproven rollback journal.",
    "E_SQLITE_LIVE_WAL": "The SQLite database has live WAL sidecars that cannot be read safely on this host.",
    "E_LIMIT_EXCEEDED": "A configured resource bound was exceeded.",
    "E_CORRUPT_RECORD": "A persisted record is corrupt or invalid.",
    "E_INVARIANT": "An internal contract invariant failed.",
    "E_INSTALL_BUSY": "Another install operation holds the destination root lock.",
    "E_INSTALL_CONFLICT": "A destination path conflicts with a non-owned or incompatible file.",
    "E_INSTALL_SHADOW": (
        "A higher-precedence discovery root already holds a divergent Portable Resume Skill."
    ),
    "E_INSTALL_UNSUPPORTED_PLATFORM": (
        "Mutating installer operations are not supported on this platform."
    ),
    "E_RECOVERY_REQUIRED": "A durable install journal requires recovery before mutation.",
    "E_VERIFY_MISMATCH": "Installed files do not match the owned manifest.",
}

# Static, content-free remediation hints (plan 031). Callers cannot inject
# paths or user data; only codes listed here emit a hint.
_DEFAULT_HINTS: dict[str, str] = {
    "E_UNSAFE_PATH": (
        "Do not use an unsafe symlink/junction spelling or other non-regular path. For "
        "installer roots, select the physical Skill directory with --root, then retry. "
        "See https://github.com/ImL1s/resume-skills/blob/main/docs/install-hosts.md"
        "#windows-user-install-and-shared-skill-roots."
    ),
    "E_SQLITE_LIVE_WAL": (
        "Close or quiesce all processes and database connections using this source, then retry once. "
        "If this diagnostic persists after SQLite removes live sidecars, the database remains in "
        "persistent WAL mode and this reader cannot open it safely on this host; use an eligible "
        "persisted file or export fallback. Do not delete or checkpoint WAL manually."
    ),
    "E_INSTALL_SHADOW": (
        "Run 'install-resume-skills audit-host --host <host> --scope <scope>' "
        "to locate the conflicting root; then uninstall the stale claim or "
        "re-run install with --project/--root."
    ),
    "E_VERIFY_MISMATCH": (
        "Ownership state or installed payload verification failed. Inspect or repair "
        "invalid state first. If this is specifically a missing shared-root claim, "
        "re-install every intended host claim together, then "
        "verify each host separately. See https://github.com/ImL1s/resume-skills/blob/main/"
        "docs/install-hosts.md#windows-user-install-and-shared-skill-roots."
    ),
}


@dataclass(slots=True)
class DiagnosticError(Exception):
    """Expected, content-free failure crossing the CLI boundary."""

    code: str
    message: str | None = None
    source: str | None = None
    provider: str | None = None
    attempts: int | None = None
    family: tuple[str, ...] = ()
    hint: str | None = None

    def __post_init__(self) -> None:
        if self.code not in ERROR_EXIT_CODES:
            self.code = "E_INVARIANT"
        # English prose is intentionally fixed by code.  Adapter-supplied or
        # recovered text can therefore never leak through an exception message.
        self.message = _bounded_message(_DEFAULT_MESSAGES[self.code])
        self.family = tuple(_safe_family_name(name) for name in self.family[: DEFAULT_BOUNDS.family_members])
        # Hints are map-only (never caller-supplied).
        raw_hint = _DEFAULT_HINTS.get(self.code)
        self.hint = _bounded_message(raw_hint) if raw_hint is not None else None
        Exception.__init__(self, self.message)

    @property
    def exit_code(self) -> ExitCode:
        return ERROR_EXIT_CODES[self.code]

    def to_dict(self) -> dict[str, Any]:
        return {
            "schema_version": "portable-resume/diagnostic-v1",
            "code": self.code,
            "message": self.message,
            "exit_code": int(self.exit_code),
            "source": self.source if self.source in SOURCE_KEYS else None,
            "provider": _bounded_identifier(self.provider),
            "attempts": self.attempts if isinstance(self.attempts, int) and self.attempts >= 0 else None,
            "family": list(self.family),
            # Always present so validate_diagnostic keeps a closed key set.
            "hint": self.hint,
        }

    def to_json(self) -> str:
        return json.dumps(self.to_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))

    @classmethod
    def invalid(cls, message: str | None = None, *, source: str | None = None) -> "DiagnosticError":
        return cls("E_INVALID_INPUT", message, source=source)

    @classmethod
    def unsafe_path(cls) -> "DiagnosticError":
        return cls("E_UNSAFE_PATH")

    @classmethod
    def source_busy(
        cls, *, attempts: int | None = None, family: tuple[str, ...] = (), provider: str | None = None
    ) -> "DiagnosticError":
        return cls("E_SOURCE_BUSY", attempts=attempts, family=family, provider=provider)

    @classmethod
    def limit_exceeded(cls) -> "DiagnosticError":
        return cls("E_LIMIT_EXCEEDED")


def _bounded_message(message: object) -> str:
    text = str(message).replace("\r", " ").replace("\n", " ")
    text = "".join(ch for ch in text if ch == "\t" or ord(ch) >= 0x20)
    return text[: DEFAULT_BOUNDS.diagnostic_chars]


def _bounded_identifier(value: object) -> str | None:
    if not isinstance(value, str):
        return None
    safe = "".join(ch for ch in value if ch.isalnum() or ch in "-_.")
    return safe[:128] or None


def _safe_family_name(value: object) -> str:
    # Diagnostics expose only a bounded basename-like token, never recovered text or paths.
    text = str(value).replace("\\", "/").rsplit("/", 1)[-1]
    safe = "".join(ch for ch in text if ch.isalnum() or ch in "-_.")
    return (safe or "member")[:128]


def emit_diagnostic(error: DiagnosticError, *, stream: Any) -> int:
    stream.write(error.to_json() + "\n")
    return int(error.exit_code)


def fail(code: str, message: str | None = None, **kwargs: Any) -> NoReturn:
    raise DiagnosticError(code, message, **kwargs)

SHA-256: 02c60fce91eabcde0b06507f47b3de094121aa57a8ed16e0d6f97a0b1f3a2da1