Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .agents/skills/review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: review
description: Review or re-review a Docling pull request with reproducible findings and explicit validation results. Use for code, tests, dependencies, documentation, or agent guidance.
---

# Review a Docling pull request

Use these steps for every review. Human reviewers can use the same steps.
Explain the affected contract so that a reviewer need not know Docling internals.
Read [AGENTS.md](../../../AGENTS.md) and load other applicable
[task routes](../skill-router.json). For document conversion without repository
changes, use the [package usage skill](../../../docling/.agents/skills/docling/SKILL.md).

## 1. Fix the review scope

Read the live PR description, diff, reviews, and CI results. Record the base and
head commit IDs. Use an isolated worktree; preserve other changes and staging.
For a re-review, read each open finding and the author's response.

**Exit:** The changed files and affected contracts are known. The reviewed head
is exact, and prior findings are listed for verification.

## 2. Check behavior and tests

Follow input through the affected code to the user-visible result.

- For conversion changes, check source content, `DoclingDocument` ownership and
order, and the affected exports. Check nested and non-text content where relevant.
Text presence or item counts alone do not prove correct structure.
- For a bug fix, run a small case on base and head when practical. The regression
test must fail for the original defect and pass with the fix. State any limit.
- Check public types, Python 3.10 support, defaults, error paths, and optional
dependencies where affected. Prefer explicit contracts to attribute probing.
- Inspect test assertions and changed reference data. Confirm that they detect
the defect rather than accept the new output without a reason.
- For loops over document content, check how work grows with input size. Compare
base and head on the same input when needed. Do not use noisy timing thresholds
as a correctness gate.

**Exit:** Each affected contract has evidence, or an explicit unchecked limit.
Every prior finding is confirmed fixed or still reproducible on the new head.

## 3. Run the applicable checks

Use the commands in `AGENTS.md`. For changes you make, run `make validate`, inspect
hook edits, and repeat until it passes. For a read-only review, use `make check`
and targeted tests. Do not regenerate reference data during a read-only review.
Inspect CI on the exact reviewed head and explain failures that affect the verdict.

`python3 .github/scripts/check_skill_routes.py` checks contributor skill routes
and Codex/Claude links. Existing Ruff, ty, Tach, and lock checks check code and
dependency rules. These checks do not prove conversion quality, test adequacy,
or full ASD-STE100 compliance. Those decisions require review.

**Exit:** Record commands, results, reviewed commit, and checks not run. Never
describe skipped, pending, or failed checks as passed.

## 4. Give a precise verdict

Use one finding per defect. Include its location, trigger, actual and expected
behavior, user impact, and reproduction or test evidence. Link the affected code.
Use these severity terms consistently:

| Severity | Meaning |
| --- | --- |
| Blocker | Security defect, data loss, incorrect supported behavior, or a required check failure. Request changes. |
| Suggestion | A useful improvement with no demonstrated contract failure. Do not block on preference. |
| Question | Evidence is missing. Ask for the specific evidence; do not assert a defect. |

Approve only when no blocker remains and relevant validation is complete.
If required evidence is unavailable, state the limit and withhold approval.
Post reviews or comments only when the user has authorized that action. Before
posting, confirm that the PR head still matches the reviewed commit. If it changed,
check the new diff and repeat affected validation. Avoid duplicate findings.

**Exit:** The verdict identifies the reviewed head, blockers, validation results,
and limits. Follow the communication rule in `AGENTS.md`.
5 changes: 5 additions & 0 deletions .agents/skills/skill-router.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"review": "Review or re-review a pull request. Start here, then load other applicable skills.",
"dignified-python": "Write, review, or change Python code. Repository rules take priority.",
"building-pydantic-ai-agents": "Build or review code that uses Pydantic AI."
}
1 change: 1 addition & 0 deletions .claude/skills/review
1 change: 1 addition & 0 deletions .codex/skills/review
6 changes: 6 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ Resolves #
- [ ] Documentation has been updated, if necessary.
- [ ] Examples have been added, if necessary.
- [ ] Tests have been added, if necessary.

**Review evidence:**

<!-- State the behavior before and after this change. For conversion changes,
describe source, document-tree, and export checks. Explain reference-data
changes. List test/check commands, results, and any checks not run. -->
64 changes: 64 additions & 0 deletions .github/scripts/check_skill_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# SPDX-FileCopyrightText: The Docling Contributors
# SPDX-License-Identifier: MIT

from __future__ import annotations

import json
import re
import sys
from pathlib import Path


def check_routes(repo_root: Path) -> list[str]:
skills_root = repo_root / ".agents/skills"
router = skills_root / "skill-router.json"
try:
routes = json.loads(router.read_text(encoding="utf-8"))
except (OSError, ValueError) as error:
return [f"Cannot read {router}: {error}"]

if not isinstance(routes, dict) or not routes:
return ["Skill routes must be a non-empty object of skill names and tasks."]
if any(
not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name)
or not isinstance(task, str)
or not task.strip()
for name, task in routes.items()
):
return ["Each route needs a skill directory name and a non-empty task."]

errors: list[str] = []
discovered = {path.name for path in skills_root.iterdir() if path.is_dir()}
for name in sorted(discovered - routes.keys()):
errors.append(f"Contributor skill has no task route: {name}")
for name in sorted(routes):
skill = skills_root / name
if not (skill / "SKILL.md").is_file():
errors.append(f"Route has no SKILL.md: {name}")
for agent in ("codex", "claude"):
link = repo_root / f".{agent}/skills" / name
if not link.is_symlink() or link.resolve() != skill.resolve():
errors.append(f".{agent}/skills/{name} must link to {skill}")

for agent in ("codex", "claude"):
links = repo_root / f".{agent}/skills"
if links.is_dir():
for link in sorted(links.iterdir()):
if link.name not in routes:
errors.append(
f"Agent skill has no task route: .{agent}/skills/{link.name}"
)
return errors


def main() -> int:
errors = check_routes(Path(__file__).resolve().parents[2])
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
print("Contributor skill routes and agent links are valid.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ jobs:
with:
filters: |
run_ci:
- "AGENTS.md"
- ".agents/skills/**"
- ".codex/skills/**"
- ".claude/skills/**"
- ".github/workflows/**"
- ".github/scripts/**"
- ".github/actions/**"
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ repos:
files: '^(docling|tests|docs/examples|perfs).*\.(py|ipynb)$'
- repo: local
hooks:
- id: skill-routes
name: Skill routing and link integrity
entry: python3 .github/scripts/check_skill_routes.py
pass_filenames: false
language: system
files: '^(AGENTS\.md|\.agents/skills/|\.(codex|claude)/skills/|\.github/scripts/check_skill_routes\.py|tests/test_check_skill_routes\.py)'
- id: ty
name: ty
entry: uv run --no-sync ty check
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ scripts/ # project maintenance scripts
- **Development skills** (for contributors working *on* Docling) live in
[`.agents/skills/`](.agents/skills/) at the repo root, e.g. `dignified-python`
and `building-pydantic-ai-agents`.
Select the applicable skills from the
[task routes](.agents/skills/skill-router.json). For PR reviews and re-reviews,
use [review](.agents/skills/review/SKILL.md) before you assess the changes.
- **Usage skills** (for agents *using* Docling to convert documents) are shipped
inside the package at
[`docling/.agents/skills/docling/`](docling/.agents/skills/docling/SKILL.md).
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ check-all: ## Run all read-only local checks.
uv run --no-sync tach check
python3 scripts/check_tach_module_coverage.py
python3 scripts/check_max_lines.py
python3 .github/scripts/check_skill_routes.py
uv run --no-sync dprint check --config .github/dprint.json --config-discovery=false
uv lock --locked

Expand Down
4 changes: 3 additions & 1 deletion docling/backend/afp_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
DeclarativeDocumentBackend,
PaginatedDocumentBackend,
)
from docling.datamodel.backend_options import BackendOptions
from docling.datamodel.base_models import InputFormat
from docling.datamodel.document import InputDocument
from docling.exceptions import DocumentLoadError
Expand Down Expand Up @@ -216,8 +217,9 @@ def __init__(
self,
in_doc: InputDocument,
path_or_stream: BytesIO | Path,
options: BackendOptions | None = None,
) -> None:
super().__init__(in_doc, path_or_stream)
super().__init__(in_doc, path_or_stream, options)
self.page_range = in_doc.limits.page_range
try:
self.content = (
Expand Down
Loading
Loading