CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/speaker-toolkit

Six-skill presentation system: ingest talks into a rhetoric vault, run interactive clarification, generate a speaker profile, create presentations that match your documented patterns, produce the deck illustrations + thumbnail visual layer, and publish talk pages to a Jekyll shownotes site. Includes a 111-entry Presentation Patterns taxonomy (81 observable: 62 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns) for scoring, brainstorming, and go-live preparation.

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

scan-shownotes.pyskills/vault-ingress/scripts/

#!/usr/bin/env python3
"""Scan a configured local shownotes collection into tracking-DB proposals.

The default mode is read-only. ``--apply`` atomically adds complete new talks and
fills empty fields on exact-filename matches. Conflicts, incomplete metadata,
case-folded filename collisions, and rejected source identities remain explicit
review proposals. Remote or disabled shownotes sources produce a structured no-op.
"""

from __future__ import annotations

import argparse
import copy
import importlib
import json
import os
import re
import stat
import sys
import unicodedata
from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path
from typing import Any, NoReturn
from urllib.parse import urlparse

import yaml

from catalog_io import load_catalog_yaml
from ingress_contract import (
    GOOGLE_DRIVE_ID_RE,
    TALK_SCHEMA_VERSION,
    YOUTUBE_ID_RE,
    is_youtube_url,
    parse_google_drive_id,
    parse_youtube_id,
    validate_talk_record_schemas,
)
from source_identity_matching import shownotes_titles_agree
from tracking_database import (
    TrackingDatabaseError,
    assess_tracking_database,
    require_current_tracking_database,
)
from tracking_database_io import (
    TrackingDatabaseConflictError,
    TrackingDatabaseIOError,
    TrackingDatabaseSnapshot,
    decode_json_object,
    snapshot_tracking_database,
    unchanged_write_result,
    write_json_object,
)

try:  # Python 3.11+.
    tomllib = importlib.import_module("tomllib")
except ModuleNotFoundError:  # pragma: no cover - exercised on Python 3.10 only
    try:
        tomllib = importlib.import_module("tomli")
    except ModuleNotFoundError:
        tomllib = None


REPORT_SCHEMA_VERSION = 3
LOCAL_SOURCE_TYPES = frozenset(
    {"local_jekyll", "local_hugo", "local_eleventy", "local_astro"}
)
NONLOCAL_SOURCE_TYPES = frozenset({"remote_url", "none"})
REQUIRED_METADATA = ("title", "conference", "date")
TRACKED_FIELDS = (
    "title",
    "conference",
    "date",
    "video_url",
    "slides_url",
    "youtube_id",
    "google_drive_id",
)
METADATA_ALIASES = {
    "title": ("title",),
    "conference": ("conference", "event", "venue"),
    "date": ("date",),
    "video_url": ("video_url", "video", "recording_url", "recording", "youtube"),
    "slides_url": ("slides_url", "slides", "deck_url", "deck"),
    "youtube_id": ("youtube_id",),
    "google_drive_id": ("google_drive_id", "drive_id"),
}
BODY_LABEL_FIELDS = {
    "conference": "conference",
    "event": "conference",
    "venue": "conference",
    "date": "date",
    "video": "video_url",
    "recording": "video_url",
    "slides": "slides_url",
    "deck": "slides_url",
}
LABEL_RE = re.compile(
    r"^\s*(?:\*\*)?(?P<label>conference|event|venue|date|video|recording|slides|deck)"
    r"\s*:(?:\*\*)?\s*(?P<value>.*?)\s*$",
    re.IGNORECASE,
)
H1_RE = re.compile(r"^#\s+(?P<title>\S.*)$")
MARKDOWN_URL_RE = re.compile(r"\[[^\]]*\]\((?P<url>https?://[^)\s]+)\)")
RAW_URL_RE = re.compile(r"https?://[^\s<>]+")
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
FileGeneration = tuple[int, int, int, int, int]


class ShownotesScanError(ValueError):
    """Input or state prevents a deterministic shownotes scan."""


class _ArgumentParser(argparse.ArgumentParser):
    def error(self, message: str) -> NoReturn:
        raise ShownotesScanError(
            f"invalid arguments: {message}; run scan-shownotes.py --help and retry"
        )


@dataclass(frozen=True)
class ShownotesLocation:
    enabled: bool
    source_type: str
    config_origin: str
    root: Path | None
    talks_subdir: str | None
    talks_directory: Path | None


def _nonempty(value: object) -> str | None:
    return value.strip() if isinstance(value, str) and value.strip() else None


def _normalized_filename(value: str) -> str:
    return unicodedata.normalize("NFC", value).casefold()


def _normalized_conference_for_comparison(value: str) -> str:
    """Normalize conference Unicode and case only; whitespace stays significant."""
    return unicodedata.normalize("NFC", value).casefold()


def _catalog_values_agree(
    field: str,
    left: object,
    right: object,
    *,
    conference: object,
    talk_date: object,
) -> bool:
    """Return whether stored and shownotes metadata agree without rewriting."""
    if not isinstance(left, str) or not isinstance(right, str):
        return False
    if field == "title":
        return shownotes_titles_agree(
            left,
            right,
            conference=conference,
            talk_date=talk_date,
        )
    if field == "conference":
        return _normalized_conference_for_comparison(
            left
        ) == _normalized_conference_for_comparison(right)
    return False


def _reject_symlink_components(path: Path, *, subject: str) -> None:
    if not path.is_absolute():
        raise ShownotesScanError(f"{subject} path {path} must be absolute")
    current = Path(path.anchor)
    for part in path.parts[1:]:
        current /= part
        try:
            metadata = current.lstat()
        except FileNotFoundError as exc:
            raise ShownotesScanError(
                f"{subject} path component {current} is missing; repair config and retry"
            ) from exc
        except OSError as exc:
            raise ShownotesScanError(
                f"cannot inspect {subject} path component {current}: {exc}; "
                "repair access and retry"
            ) from exc
        if stat.S_ISLNK(metadata.st_mode):
            raise ShownotesScanError(
                f"{subject} path component {current} is a symbolic link; configure "
                "the canonical non-symlink path and retry"
            )


def _file_generation(metadata: os.stat_result) -> FileGeneration:
    return (
        metadata.st_dev,
        metadata.st_ino,
        metadata.st_size,
        metadata.st_mtime_ns,
        metadata.st_ctime_ns,
    )


def _load_database(
    path: Path,
) -> tuple[dict[str, Any], TrackingDatabaseSnapshot]:
    try:
        snapshot = snapshot_tracking_database(path)
        payload = decode_json_object(snapshot)
    except TrackingDatabaseIOError as exc:
        raise ShownotesScanError(str(exc)) from exc
    try:
        assessment = assess_tracking_database(payload)
    except TrackingDatabaseError as exc:
        raise ShownotesScanError(str(exc)) from exc
    if not assessment.usable:
        raise ShownotesScanError(
            "tracking database is not usable by this reader: "
            + ", ".join(assessment.reason_codes)
        )
    try:
        validate_talk_record_schemas(payload.get("talks"))
    except ValueError as exc:
        raise ShownotesScanError(
            f"tracking database talk records are invalid: {exc}; repair them and retry"
        ) from exc
    if not isinstance(payload.get("config"), dict):
        raise ShownotesScanError(
            "tracking database config must be a JSON object; run vault-ingress Step 1"
        )
    return payload, snapshot


def _safe_local_location(
    source_type: str,
    root_value: object,
    subdir_value: object,
    *,
    config_origin: str,
) -> ShownotesLocation:
    root_text = _nonempty(root_value)
    subdir_text = _nonempty(subdir_value)
    if root_text is None or subdir_text is None:
        raise ShownotesScanError(
            "local shownotes config requires non-empty path_or_url and talks_subdir; "
            "run vault-ingress Step 1 to complete it"
        )
    root_candidate = Path(root_text).expanduser()
    if not root_candidate.is_absolute():
        raise ShownotesScanError(
            f"shownotes root {root_text!r} is relative; configure an absolute path"
        )
    subdir = Path(subdir_text)
    if subdir.is_absolute() or any(part in {"", ".", ".."} for part in subdir.parts):
        raise ShownotesScanError(
            f"shownotes talks_subdir {subdir_text!r} must be a safe relative path"
        )
    talks_candidate = root_candidate / subdir
    _reject_symlink_components(root_candidate, subject="shownotes root")
    _reject_symlink_components(talks_candidate, subject="shownotes talks directory")
    try:
        root = root_candidate.resolve(strict=True)
        talks_directory = talks_candidate.resolve(strict=True)
        talks_directory.relative_to(root)
    except (FileNotFoundError, OSError, ValueError) as exc:
        raise ShownotesScanError(
            "cannot resolve the configured shownotes talks directory inside its root: "
            f"{exc}; repair path_or_url/talks_subdir and retry"
        ) from exc
    if not root.is_dir() or not talks_directory.is_dir():
        raise ShownotesScanError(
            f"shownotes talks path {talks_directory} is not a directory; repair config"
        )
    return ShownotesLocation(
        enabled=True,
        source_type=source_type,
        config_origin=config_origin,
        root=root,
        talks_subdir=subdir.as_posix(),
        talks_directory=talks_directory,
    )


def resolve_shownotes_location(config: dict[str, Any]) -> ShownotesLocation:
    """Resolve nested shownotes config or its documented legacy path."""
    if config.get("shownotes") is not None:
        shownotes = config.get("shownotes")
        if not isinstance(shownotes, dict):
            raise ShownotesScanError(
                "config.shownotes must be an object; run vault-ingress Step 1 to repair it"
            )
        enabled = shownotes.get("enabled", True)
        if not isinstance(enabled, bool):
            raise ShownotesScanError(
                "config.shownotes.enabled must be a boolean; correct it and retry"
            )
        source = shownotes.get("source")
        if not isinstance(source, dict):
            raise ShownotesScanError(
                "config.shownotes.source must be an object; run vault-ingress Step 1"
            )
        source_type = _nonempty(source.get("type"))
        if source_type is None:
            raise ShownotesScanError(
                "config.shownotes.source.type is missing; run vault-ingress Step 1"
            )
        if not enabled:
            return ShownotesLocation(False, source_type, "shownotes", None, None, None)
        if source_type in NONLOCAL_SOURCE_TYPES:
            return ShownotesLocation(True, source_type, "shownotes", None, None, None)
        if source_type not in LOCAL_SOURCE_TYPES:
            raise ShownotesScanError(
                f"unsupported shownotes source type {source_type!r}; choose one of "
                f"{sorted(LOCAL_SOURCE_TYPES | NONLOCAL_SOURCE_TYPES)}"
            )
        return _safe_local_location(
            source_type,
            source.get("path_or_url"),
            source.get("talks_subdir"),
            config_origin="shownotes",
        )

    legacy = _nonempty(config.get("talks_source_dir"))
    if legacy is None:
        raise ShownotesScanError(
            "shownotes config is missing; run vault-ingress Step 1 before scanning"
        )
    directory = Path(legacy).expanduser()
    if not directory.is_absolute():
        raise ShownotesScanError(
            f"legacy talks_source_dir {legacy!r} is relative; configure an absolute path"
        )
    return _safe_local_location(
        "legacy_local",
        str(directory.parent),
        directory.name,
        config_origin="talks_source_dir",
    )


def _parse_frontmatter(text: str) -> tuple[dict[str, Any], str, list[dict[str, Any]]]:
    lines = text.splitlines()
    if not lines:
        return {}, "", []
    marker = lines[0].strip()
    if marker in {"---", "+++"}:
        try:
            end = next(
                index
                for index in range(1, len(lines))
                if lines[index].strip() == marker
            )
        except StopIteration:
            return (
                {},
                text,
                [
                    {
                        "code": "frontmatter_unterminated",
                        "message": f"{marker} frontmatter has no closing delimiter; repair the file",
                    }
                ],
            )
        frontmatter_text = "\n".join(lines[1:end])
        body = "\n".join(lines[end + 1 :])
        try:
            if marker == "---":
                parsed = (
                    load_catalog_yaml(frontmatter_text)
                    if frontmatter_text.strip()
                    else {}
                )
            elif tomllib is not None:
                parsed = tomllib.loads(frontmatter_text)
            else:
                return (
                    {},
                    body,
                    [
                        {
                            "code": "toml_parser_unavailable",
                            "message": (
                                "TOML frontmatter needs Python 3.11+ or tomli; configure that "
                                "runtime and rescan"
                            ),
                        }
                    ],
                )
        except (ValueError, yaml.YAMLError) as exc:
            return (
                {},
                body,
                [
                    {
                        "code": "frontmatter_invalid",
                        "message": f"frontmatter is invalid: {exc}; repair the file and rescan",
                    }
                ],
            )
        if not isinstance(parsed, dict):
            return (
                {},
                body,
                [
                    {
                        "code": "frontmatter_not_object",
                        "message": "frontmatter must be an object; repair the file and rescan",
                    }
                ],
            )
        return parsed, body, []

    if text.lstrip().startswith("{"):
        leading = len(text) - len(text.lstrip())
        try:
            parsed, end = json.JSONDecoder().raw_decode(text, leading)
        except json.JSONDecodeError as exc:
            return (
                {},
                text,
                [
                    {
                        "code": "frontmatter_invalid_json",
                        "message": f"JSON frontmatter is invalid: {exc}; repair the file and rescan",
                    }
                ],
            )
        if not isinstance(parsed, dict):
            return (
                {},
                text[end:],
                [
                    {
                        "code": "frontmatter_not_object",
                        "message": "JSON frontmatter must be an object; repair the file and rescan",
                    }
                ],
            )
        return parsed, text[end:].lstrip("\r\n"), []
    return {}, text, []


def _metadata_values(metadata: dict[str, Any], field: str) -> list[object]:
    containers = [metadata]
    params = metadata.get("params")
    if isinstance(params, dict):
        containers.append(params)
    values: list[object] = []
    for container in containers:
        for alias in METADATA_ALIASES[field]:
            if alias in container and container[alias] is not None:
                values.append(container[alias])
    return values


def _extract_url(value: object) -> str | None:
    text = _nonempty(value)
    if text is None:
        return None
    markdown = MARKDOWN_URL_RE.search(text)
    if markdown:
        return markdown.group("url")
    raw = RAW_URL_RE.search(text)
    if raw:
        return raw.group(0).rstrip(".,;]")
    return None


def _normalize_date(value: object) -> str | None:
    if isinstance(value, datetime):
        return value.date().isoformat()
    if isinstance(value, date):
        return value.isoformat()
    text = _nonempty(value)
    if text is None or not DATE_RE.fullmatch(text):
        return None
    try:
        return date.fromisoformat(text).isoformat()
    except ValueError:
        return None


def _url_identity(field: str, value: str) -> tuple[str, str]:
    parsed_id = (
        parse_youtube_id(value)
        if field == "video_url"
        else parse_google_drive_id(value)
    )
    return (field, parsed_id) if parsed_id is not None else (field, value)


def _choose_value(
    filename: str,
    field: str,
    values: list[object],
    issues: list[dict[str, Any]],
) -> object | None:
    normalized: list[object] = []
    for value in values:
        if field in {"video_url", "slides_url"}:
            candidate = _extract_url(value)
        elif field == "date":
            candidate = _normalize_date(value)
        else:
            candidate = _nonempty(value)
        if candidate is None:
            issues.append(
                {
                    "code": f"{field}_invalid",
                    "field": field,
                    "message": f"{filename} has an invalid {field}; correct it and rescan",
                }
            )
            continue
        if candidate not in normalized:
            normalized.append(candidate)
    if not normalized:
        return None
    if len(normalized) > 1:
        if field in {"video_url", "slides_url"}:
            identities = {_url_identity(field, str(value)) for value in normalized}
            if len(identities) == 1:
                return normalized[0]
        issues.append(
            {
                "code": f"{field}_conflict",
                "field": field,
                "message": f"{filename} declares conflicting {field} values; choose one",
            }
        )
    return normalized[0]


def _read_markdown_text(path: Path) -> str:
    _reject_symlink_components(path.parent, subject="shownotes talks directory")
    try:
        path_metadata = path.lstat()
    except OSError as exc:
        raise ShownotesScanError(
            f"cannot inspect shownotes file {path.name}: {exc}; repair it and rescan"
        ) from exc
    if stat.S_ISLNK(path_metadata.st_mode):
        raise ShownotesScanError(
            f"{path.name} must be a regular non-symlink Markdown file"
        )
    if not stat.S_ISREG(path_metadata.st_mode):
        raise ShownotesScanError(f"{path.name} must be a regular Markdown file")

    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
    try:
        descriptor = os.open(path, flags)
        with os.fdopen(descriptor, "rb") as handle:
            opened_metadata = os.fstat(handle.fileno())
            if not stat.S_ISREG(opened_metadata.st_mode) or _file_generation(
                opened_metadata
            ) != _file_generation(path_metadata):
                raise ShownotesScanError(
                    f"{path.name} changed while it was opened; repair it and rescan"
                )
            raw = handle.read()
            read_metadata = os.fstat(handle.fileno())
    except OSError as exc:
        raise ShownotesScanError(
            f"cannot read {path.name} without following symlinks: {exc}; "
            "repair it and rescan"
        ) from exc
    if _file_generation(read_metadata) != _file_generation(opened_metadata):
        raise ShownotesScanError(
            f"{path.name} changed while it was read; repair it and rescan"
        )
    try:
        final_metadata = path.lstat()
    except OSError as exc:
        raise ShownotesScanError(
            f"cannot recheck {path.name}: {exc}; repair it and rescan"
        ) from exc
    if stat.S_ISLNK(final_metadata.st_mode) or _file_generation(
        final_metadata
    ) != _file_generation(read_metadata):
        raise ShownotesScanError(
            f"{path.name} path changed while it was read; repair it and rescan"
        )
    try:
        return raw.decode("utf-8-sig")
    except UnicodeDecodeError as exc:
        raise ShownotesScanError(
            f"cannot read {path.name} as UTF-8: {exc}; repair it and rescan"
        ) from exc


def parse_shownotes_file(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    """Return a normalized talk proposal and review issues for one Markdown file."""
    issues: list[dict[str, Any]] = []
    try:
        text = _read_markdown_text(path)
    except ShownotesScanError as exc:
        return {"filename": path.name}, [
            {
                "code": "shownotes_file_unreadable",
                "message": str(exc),
            }
        ]
    metadata, body, frontmatter_issues = _parse_frontmatter(text)
    issues.extend(frontmatter_issues)

    body_values: dict[str, list[object]] = {field: [] for field in METADATA_ALIASES}
    h1_titles: list[str] = []
    for line in body.splitlines():
        h1 = H1_RE.match(line)
        if h1:
            h1_titles.append(h1.group("title").strip())
        label = LABEL_RE.match(line)
        if label:
            field = BODY_LABEL_FIELDS[label.group("label").casefold()]
            body_values[field].append(label.group("value"))

    proposal: dict[str, Any] = {"filename": path.name}
    for field in ("title", "conference", "date", "video_url", "slides_url"):
        values = _metadata_values(metadata, field)
        if field == "title":
            values.extend(h1_titles)
        else:
            values.extend(body_values[field])
        selected = _choose_value(path.name, field, values, issues)
        if selected is not None:
            proposal[field] = selected

    video_url = proposal.get("video_url")
    if isinstance(video_url, str):
        parsed = urlparse(video_url)
        if parsed.scheme.casefold() not in {"http", "https"} or not parsed.hostname:
            issues.append(
                {
                    "code": "video_url_invalid",
                    "field": "video_url",
                    "message": f"{path.name} video_url must be HTTP(S); correct it",
                }
            )
        elif is_youtube_url(video_url):
            youtube_id = parse_youtube_id(video_url)
            if youtube_id is None:
                issues.append(
                    {
                        "code": "youtube_url_invalid",
                        "field": "video_url",
                        "message": f"{path.name} has an unsupported YouTube URL; correct it",
                    }
                )
            else:
                proposal["youtube_id"] = youtube_id

    slides_url = proposal.get("slides_url")
    if isinstance(slides_url, str):
        parsed = urlparse(slides_url)
        if parsed.scheme.casefold() not in {"http", "https"} or not parsed.hostname:
            issues.append(
                {
                    "code": "slides_url_invalid",
                    "field": "slides_url",
                    "message": f"{path.name} slides_url must be HTTP(S); correct it",
                }
            )
        elif (parsed.hostname or "").casefold().rstrip(".") in {
            "drive.google.com",
            "docs.google.com",
        }:
            drive_id = parse_google_drive_id(slides_url)
            if drive_id is None:
                issues.append(
                    {
                        "code": "drive_url_invalid",
                        "field": "slides_url",
                        "message": f"{path.name} has an unsupported Google Drive URL; correct it",
                    }
                )
            else:
                proposal["google_drive_id"] = drive_id

    for id_field, regex in (
        ("youtube_id", YOUTUBE_ID_RE),
        ("google_drive_id", GOOGLE_DRIVE_ID_RE),
    ):
        declared_values = _metadata_values(metadata, id_field)
        if not declared_values:
            continue
        declared = _choose_value(path.name, id_field, declared_values, issues)
        if not isinstance(declared, str) or regex.fullmatch(declared) is None:
            issues.append(
                {
                    "code": f"{id_field}_invalid",
                    "field": id_field,
                    "message": f"{path.name} has an invalid {id_field}; correct it",
                }
            )
            continue
        derived = proposal.get(id_field)
        if derived is not None and derived != declared:
            issues.append(
                {
                    "code": f"{id_field}_mismatch",
                    "field": id_field,
                    "message": (
                        f"{path.name} declares {id_field} {declared!r}, but its URL "
                        f"resolves to {derived!r}; choose the correct identity"
                    ),
                }
            )
        elif derived is None:
            proposal[id_field] = declared

    for field in REQUIRED_METADATA:
        if field not in proposal:
            issues.append(
                {
                    "code": f"{field}_missing",
                    "field": field,
                    "message": f"{path.name} is missing {field}; add it before import",
                }
            )
    return proposal, issues


def _source_id(field: str, url: str) -> str | None:
    parser = parse_youtube_id if field == "video_url" else parse_google_drive_id
    return parser(url)


def _same_source(field: str, left: str, right: str) -> bool:
    if left == right:
        return True
    left_id = _source_id(field, left)
    return left_id is not None and _source_id(field, right) == left_id


def _rejected_source_issue(
    talk: dict[str, Any], field: str, candidate: str
) -> dict[str, Any] | None:
    """The blocking review item for a candidate that a ledger entry rejects.

    A match carries the exact ledger record that produced it, so the reviewer
    decides from the report instead of reopening the database. Only the matched
    record is included — unrelated rejections stay private to the talk.
    """
    if "source_rejections" not in talk:
        return None
    rejections = talk.get("source_rejections")
    if not isinstance(rejections, list):
        return {
            "code": "source_rejections_invalid",
            "field": "source_rejections",
            "message": "source_rejections must be an array; repair it before import",
        }
    expected_type = "video" if field == "video_url" else "slides"
    for index, rejection in enumerate(rejections):
        if not isinstance(rejection, dict):
            return {
                "code": "source_rejections_invalid",
                "field": "source_rejections",
                "message": (
                    f"source_rejections[{index}] must be an object; repair it before import"
                ),
            }
        rejected_url = _nonempty(rejection.get("url"))
        if rejection.get("source_type") != expected_type or rejected_url is None:
            continue
        if not _same_source(field, candidate, rejected_url):
            continue
        # reason / evidence / verified_at are reported verbatim as review
        # evidence. They are safe to read unchecked because _load_database
        # rejects the whole scan on a malformed ledger record before reaching
        # here — see tracking_database._validate_source_rejection for the exact
        # required shape. Re-checking here would be unreachable.
        candidate_id = _source_id(field, candidate)
        rejected_id = _source_id(field, rejected_url)
        return {
            "code": "rejected_source_reappeared",
            "field": field,
            "message": (
                f"shownotes proposes a known-bad {field}; keep it inactive until "
                "human review supplies replacement evidence"
            ),
            "matched_rejection": {
                "source_type": expected_type,
                "url": rejected_url,
                "provider_id": rejected_id,
                "reason": rejection["reason"],
                "evidence": rejection["evidence"],
                "verified_at": rejection["verified_at"],
            },
            "match": {
                "method": "exact_url" if candidate == rejected_url else "provider_id",
                "candidate_url": candidate,
                "candidate_provider_id": candidate_id,
            },
        }
    return None


def _existing_entry(
    talk: dict[str, Any],
    proposal: dict[str, Any],
    parse_issues: list[dict[str, Any]],
) -> tuple[str, dict[str, Any], list[dict[str, Any]]]:
    issues = list(parse_issues)
    patch: dict[str, Any] = {}
    stored_conference = talk.get("conference")
    stored_date = talk.get("date")
    for field in TRACKED_FIELDS:
        if field not in proposal:
            continue
        candidate = proposal[field]
        if field in {"video_url", "slides_url"}:
            rejection = _rejected_source_issue(talk, field, str(candidate))
            if rejection is not None:
                issues.append(rejection)
                continue
        current = talk.get(field)
        if current is None or current == "":
            patch[field] = candidate
            continue
        if current == candidate:
            continue
        if _catalog_values_agree(
            field,
            current,
            candidate,
            conference=stored_conference,
            talk_date=stored_date,
        ):
            continue
        if (
            field in {"video_url", "slides_url"}
            and isinstance(current, str)
            and _same_source(field, current, str(candidate))
        ):
            continue
        issues.append(
            {
                "code": f"existing_{field}_conflict",
                "field": field,
                "message": (
                    f"tracking DB {field} {current!r} conflicts with shownotes "
                    f"{candidate!r}; choose the authoritative value"
                ),
            }
        )
    if issues:
        return "review_required", {}, issues
    return ("update", patch, []) if patch else ("unchanged", {}, [])


def build_scan_report(
    database_path: Path,
    database: dict[str, Any],
    location: ShownotesLocation,
    *,
    apply_requested: bool,
) -> tuple[dict[str, Any], dict[str, Any]]:
    """Build a stable report and the candidate post-scan database."""
    talks = database["talks"]
    exact: dict[str, tuple[int, dict[str, Any]]] = {}
    folded: dict[str, list[str]] = {}
    for index, talk in enumerate(talks):
        filename = _nonempty(talk.get("filename"))
        if filename is None:
            raise ShownotesScanError(
                f"talks[{index}].filename is missing; repair it before shownotes scan"
            )
        if filename in exact:
            raise ShownotesScanError(
                f"tracking database repeats filename {filename!r}; deduplicate it first"
            )
        exact[filename] = (index, talk)
        folded.setdefault(_normalized_filename(filename), []).append(filename)
    collisions = {key: names for key, names in folded.items() if len(names) > 1}
    if collisions:
        raise ShownotesScanError(
            f"tracking database has normalized filename collisions {collisions}; "
            "resolve them before scanning"
        )

    report: dict[str, Any] = {
        "schema_version": REPORT_SCHEMA_VERSION,
        "ok": True,
        "mode": "apply" if apply_requested else "dry-run",
        "database": str(database_path.resolve(strict=False)),
        "shownotes": {
            "enabled": location.enabled,
            "source_type": location.source_type,
            "config_origin": location.config_origin,
            "root": str(location.root) if location.root else None,
            "talks_subdir": location.talks_subdir,
            "talks_directory": (
                str(location.talks_directory) if location.talks_directory else None
            ),
        },
        "operation": "scan",
        "apply_requested": apply_requested,
        "database_written": False,
        "mutation_count": 0,
        "scanned_file_count": 0,
        "existing_talk_count": len(talks),
        "counts": {
            "add": 0,
            "update": 0,
            "unchanged": 0,
            "review_required": 0,
        },
        "entries": [],
    }
    candidate_database = copy.deepcopy(database)
    if not location.enabled:
        report["operation"] = "skipped_disabled"
        return report, candidate_database
    if location.source_type in NONLOCAL_SOURCE_TYPES:
        report["operation"] = "skipped_nonlocal"
        return report, candidate_database
    if location.talks_directory is None:
        raise ShownotesScanError("local shownotes location has no talks directory")

    _reject_symlink_components(
        location.talks_directory,
        subject="shownotes talks directory",
    )
    paths = sorted(location.talks_directory.glob("*.md"), key=lambda item: item.name)
    scanned_folded: dict[str, list[str]] = {}
    for path in paths:
        scanned_folded.setdefault(_normalized_filename(path.name), []).append(path.name)
    report["scanned_file_count"] = len(paths)
    additions: list[dict[str, Any]] = []
    updates: list[tuple[int, dict[str, Any]]] = []
    entries: list[dict[str, Any]] = []
    for path in paths:
        proposal, parse_issues = parse_shownotes_file(path)
        filename = path.name
        patch: dict[str, Any] = {}
        if filename in exact:
            index, talk = exact[filename]
            disposition, patch, issues = _existing_entry(talk, proposal, parse_issues)
            if disposition == "update":
                updates.append((index, patch))
        else:
            normalized = _normalized_filename(filename)
            casefold_matches = sorted(
                {
                    *folded.get(normalized, []),
                    *(
                        scanned_name
                        for scanned_name in scanned_folded.get(normalized, [])
                        if scanned_name != filename
                    ),
                }
            )
            issues = list(parse_issues)
            if casefold_matches:
                issues.append(
                    {
                        "code": "filename_identity_ambiguous",
                        "field": "filename",
                        "message": (
                            f"{filename!r} collides with {casefold_matches}; resolve exact "
                            "filename identity before import"
                        ),
                    }
                )
            if issues:
                disposition = "review_required"
            else:
                disposition = "add"
                record = {
                    key: value
                    for key, value in proposal.items()
                    if key == "filename" or key in TRACKED_FIELDS
                }
                record.update(
                    {
                        "schema_version": TALK_SCHEMA_VERSION,
                        "status": "pending",
                    }
                )
                additions.append(record)
                patch = record
        report["counts"][disposition] += 1
        entries.append(
            {
                "filename": filename,
                "disposition": disposition,
                "proposal": proposal,
                "changes": patch,
                "issues": issues,
                "applied": False,
            }
        )

    if apply_requested:
        for index, patch in updates:
            candidate_database["talks"][index].update(copy.deepcopy(patch))
        candidate_database["talks"].extend(copy.deepcopy(additions))
        for entry in entries:
            if entry["disposition"] in {"add", "update"}:
                entry["applied"] = True
    report["entries"] = entries
    report["mutation_count"] = len(additions) + len(updates)
    return report, candidate_database


def _atomic_write_database(
    path: Path,
    database: dict[str, Any],
    *,
    expected_snapshot: TrackingDatabaseSnapshot,
):
    """Commit against the exact generation captured before shownotes scanning."""
    try:
        return write_json_object(expected_snapshot, database)
    except TrackingDatabaseConflictError as exc:
        raise ShownotesScanError(
            "tracking database content or generation changed after the scan; "
            "rerun before applying"
        ) from exc
    except TrackingDatabaseIOError as exc:
        raise ShownotesScanError(
            f"cannot atomically update tracking database {path}: {exc}"
        ) from exc


def execute(database_path: Path, *, apply_requested: bool) -> dict[str, Any]:
    database, database_snapshot = _load_database(database_path)
    if apply_requested:
        try:
            require_current_tracking_database(database)
        except TrackingDatabaseError as exc:
            raise ShownotesScanError(str(exc)) from exc
    location = resolve_shownotes_location(database["config"])
    report, candidate = build_scan_report(
        database_path,
        database,
        location,
        apply_requested=apply_requested,
    )
    if apply_requested:
        if report["mutation_count"]:
            write_result = _atomic_write_database(
                database_path,
                candidate,
                expected_snapshot=database_snapshot,
            )
        else:
            write_result = unchanged_write_result(database_snapshot)
        write_fields = {
            "input_sha256": write_result.input_sha256,
            "output_sha256": write_result.output_sha256,
            "database_written": write_result.installed,
            "durability_state": write_result.durability_state,
            "warnings": list(write_result.warnings),
        }
    else:
        write_fields = {
            "input_sha256": database_snapshot.sha256,
            "output_sha256": database_snapshot.sha256,
            "database_written": False,
            "durability_state": "dry_run",
            "warnings": [],
        }
    report.update(write_fields)
    return report


def _error_payload(message: str) -> dict[str, Any]:
    return {
        "schema_version": REPORT_SCHEMA_VERSION,
        "ok": False,
        "error": message,
    }


def main(argv: list[str] | None = None) -> int:
    parser = _ArgumentParser(description=(__doc__ or "").split("\n")[0])
    parser.add_argument("database", type=Path, help="canonical tracking-database.json")
    parser.add_argument(
        "--apply",
        action="store_true",
        help="atomically apply only deterministic add/update entries",
    )
    try:
        args = parser.parse_args(argv)
        report = execute(args.database, apply_requested=args.apply)
    except (ShownotesScanError, OSError) as exc:
        message = str(exc)
        print(json.dumps(_error_payload(message), sort_keys=True))
        print(f"shownotes scan failed: {message}", file=sys.stderr)
        return 2
    print(json.dumps(report, indent=2, sort_keys=True, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    # outer-boundary-process-contract: skill callers treat missing JSON as a
    # silent scan failure; emit one error object and recovery action before exit.
    except Exception as exc:  # noqa: BLE001
        message = (
            f"unexpected shownotes scan failure: {type(exc).__name__}: {exc}; "
            "inspect the input/configuration, repair the named fault, and rerun"
        )
        print(json.dumps(_error_payload(message), sort_keys=True))
        print(message, file=sys.stderr)
        raise SystemExit(2) from exc

skills

README.md

tile.json