{"record":{"id":"5ffe4702940ddc65","repo":"opendataloader-project/opendataloader-pdf","slug":"f-e","errorCode":null,"errorMessage":"{f}: {e}","messagePattern":"\\{f\\}: \\{e\\}","errorType":"exception","errorClass":"ConfigError","httpStatus":null,"severity":"error","filePath":"skills/odl-pdf-maintenance/sync-skill-refs.py","lineNumber":100,"sourceCode":"    distinct from exit 1 (a coupling/structural violation in the authored prose).\"\"\"\n\n\ndef _scanned_files(skill_dir: Path):\n    \"\"\"Agent-facing prose: SKILL.md + references/*.md. ConfigError if the dir does\n    not look like a skill bundle (no SKILL.md) — an empty/wrong --skill-dir must not\n    silently scan 0 files and report a false PASS.\"\"\"\n    skill_md = skill_dir / \"SKILL.md\"\n    if not skill_md.is_file():\n        raise ConfigError(f\"no SKILL.md found under {skill_dir} (nothing to lint)\")\n    files = [skill_md] + sorted((skill_dir / \"references\").glob(\"*.md\"))\n    return [f for f in files if f.is_file()]\n\n\ndef _read(f: Path) -> str:\n    try:\n        return f.read_text(encoding=\"utf-8\")\n    except OSError as e:\n        raise ConfigError(f\"{f}: {e}\") from e\n\n\ndef _is_ipv4(run: str) -> bool:\n    \"\"\"A dotted run that is a valid dotted-quad IPv4 (4 octets, each 0-255).\"\"\"\n    parts = run.split(\".\")\n    return len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)\n\n\ndef check_versions(rel: str, text: str, violations: list):\n    \"\"\"Check 1 — baked semver, excluding dotted-quad IPv4.\"\"\"\n    for i, line in enumerate(text.splitlines(), 1):\n        for m in _DOTTED_RE.finditer(line):\n            run = m.group(0)\n            if _is_ipv4(run):\n                continue                       # IPv4 safety advice, not a version\n            if run.count(\".\") >= 2:            # 3+ components -> semver-like\n                violations.append(\n                    f\"{rel}:{i}  baked version '{run}'  \"","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/opendataloader-project/opendataloader-pdf/blob/a7789b8e77dd05e2b8659eb3ea12fc458f80bfb8/skills/odl-pdf-maintenance/sync-skill-refs.py#L82-L118","documentation":"ConfigError (exit code 2) raised by _read when read_text(encoding='utf-8') on a scanned prose file raises OSError — permission denied, I/O error, or an unreadable file. The message echoes the file path and the underlying OSError. Like error 96 this is an input/config problem (exit 2), reported distinctly from a coupling violation (exit 1), so CI can tell a broken environment from a real lint failure.","triggerScenarios":"The lint scans SKILL.md or a references/*.md file that exists but cannot be read — permission denied (mode 000), an I/O error on a failing disk/NFS mount, or a broken symlink that lstat'd as a file but cannot be opened.","commonSituations":"File permissions prevent the CI user from reading the file (chmod 600 owned by another user). A broken symlink inside references/. An NFS mount hiccup. A file locked/exclusive by another process.","solutions":["Fix permissions: chmod/chown the file so the lint process can read it (e.g. chmod 644).","Replace broken symlinks with real files or valid symlinks.","Re-run; if the OSError is a transient mount/IO error, remount or retry after the FS recovers.","Confirm the file is not a special file/device that read_text cannot handle."],"exampleFix":"# before: unreadable file\n$ ls -l references/eval-metrics.md\n-rw------- 1 root root ... references/eval-metrics.md  # CI user cannot read -> ConfigError\n# after: world-readable\n$ chmod 644 references/eval-metrics.md\n$ ./sync-skill-refs.py --skill-dir skills/odl-pdf","handlingStrategy":"validation","validationCode":"from pathlib import Path\ndef all_prose_readable(skill_dir: str) -> bool:\n    files = [Path(skill_dir) / \"SKILL.md\"] + list((Path(skill_dir) / \"references\").glob(\"*.md\"))\n    return all(f.is_file() and os.access(f, os.R_OK) for f in files if f.is_file())\n","typeGuard":"def is_unreadable_file_error(exc: Exception) -> bool:\n    return exc.__class__.__name__ == \"ConfigError\" and \": \" in str(exc) and \"SKILL.md found\" not in str(exc)","tryCatchPattern":"import subprocess, sys\nrc = subprocess.run([sys.executable, \"sync-skill-refs.py\", \"--skill-dir\", d]).returncode\nif rc == 2:\n    # could be error 96 or 97; re-run verbosely to see which file\n    print(\"Config error (exit 2): check --skill-dir exists and all prose files are readable (chmod 644)\")","preventionTips":["Ensure all scanned prose files are readable by the lint user (chmod 644).","Replace broken symlinks in references/ with real files.","Distinguish exit 2 (config/IO) from exit 1 (real violation) in CI scripting."],"tags":["lint","skill","config","permissions","configerror","ioerror","python"],"backgroundTag":null,"analyzedSha":"a7789b8e77dd05e2b8659eb3ea12fc458f80bfb8","analyzedAt":"2026-08-14T05:22:03.953Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}