diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4a37d12..e271237 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
- url: https://github.com/raintree-ai/docpull#readme
+ url: https://github.com/raintree-technology/docpull#readme
about: Check the README for usage instructions and examples
- name: Discussions
- url: https://github.com/raintree-ai/docpull/discussions
+ url: https://github.com/raintree-technology/docpull/discussions
about: Ask questions and share ideas
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index bde8701..bbd17f8 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -18,6 +18,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
benchmark:
if: github.event_name != 'schedule' || github.event.schedule == '17 4 * * 3'
@@ -29,21 +33,22 @@ jobs:
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --extra dev
- name: Run 10k-page benchmark
env:
DOCPULL_BENCHMARK_10K: "1"
run: |
- PYTHONPATH=src python -m pytest -v -s tests/benchmarks/test_10k_pages.py \
+ uv run --locked python -m pytest -v -s tests/benchmarks/test_10k_pages.py \
| tee benchmark.log
- name: Surface report in summary
@@ -74,19 +79,20 @@ jobs:
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --extra dev
- name: Run public contract smoke tests
run: |
- PYTHONPATH=src python -m pytest -q \
+ uv run --locked python -m pytest -q \
tests/test_surface_contract.py \
tests/test_public_feature_smoke.py \
tests/test_output_contract.py \
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0d1b0dc..58484f4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,61 +14,76 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
metrics-fresh:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
+ with:
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
+
- name: Check metrics freshness
env:
METRICS_MAX_AGE_HOURS: "168"
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
- python .github/scripts/check_metrics_fresh.py || {
+ uv run --locked python .github/scripts/check_metrics_fresh.py || {
echo "::warning::Metrics are stale; run the scheduled Update metrics workflow after merge."
}
else
- python .github/scripts/check_metrics_fresh.py
+ uv run --locked python .github/scripts/check_metrics_fresh.py
fi
test:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ env:
+ MISE_PYTHON_VERSION: ${{ matrix.python-version }}
strategy:
fail-fast: false
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.10.20", "3.11.15", "3.12.13", "3.13.14", "3.14.6"]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: ${{ matrix.python-version }}
- cache: pip
- cache-dependency-path: pyproject.toml
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[all,dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --all-extras
- name: Run tests
- if: matrix.python-version != '3.11'
- run: python -m pytest -q
+ if: matrix.python-version != '3.11.15'
+ run: uv run --locked --all-extras python -m pytest -q
- name: Run tests with coverage
- if: matrix.python-version == '3.11'
- run: python -m pytest --cov=docpull --cov-report=xml --cov-report=term -q
+ if: matrix.python-version == '3.11.15'
+ run: uv run --locked --all-extras python -m pytest --cov=docpull --cov-report=xml --cov-report=term -q
- name: Upload coverage XML
- if: matrix.python-version == '3.11'
+ if: matrix.python-version == '3.11.15'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: coverage-xml
@@ -76,89 +91,88 @@ jobs:
lint:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
- cache: pip
- cache-dependency-path: pyproject.toml
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[all,dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --all-extras
- name: Check generated release metadata
- run: python scripts/sync_release_metadata.py --check
+ run: uv run --locked python scripts/sync_release_metadata.py --check
+
+ - name: Check generated contract schemas
+ run: uv run --locked python scripts/generate_contract_schemas.py --check
- name: Run ruff
- run: python -m ruff check .
+ run: uv run --locked python -m ruff check .
- name: Check formatting
- run: python -m ruff format --check .
+ run: uv run --locked python -m ruff format --check .
- name: Run pre-commit
- run: python -m pre_commit run --all-files --show-diff-on-failure
+ run: uv run --locked python -m pre_commit run --all-files --show-diff-on-failure
typecheck:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
- cache: pip
- cache-dependency-path: pyproject.toml
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[all,dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --all-extras
- name: Run mypy
- run: python -m mypy src
+ run: uv run --locked python -m mypy src
package:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
- cache: pip
- cache-dependency-path: |
- pyproject.toml
- requirements-release.txt
-
- - name: Install pinned release tooling
- run: python -m pip install -r requirements-release.txt
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- name: Build wheel and sdist
- run: python scripts/build_release.py --verify-reproducible
+ run: uv run --locked --with-requirements requirements-release.txt python scripts/build_release.py --verify-reproducible
- name: Verify wheel and sdist metadata
- run: python -m twine check dist/*
+ run: uv run --locked --with-requirements requirements-release.txt python -m twine check dist/*
- name: Smoke install built wheel
run: |
- python -m venv .pkg-smoke
- .pkg-smoke/bin/python -m pip install --upgrade pip
- .pkg-smoke/bin/python -m pip install dist/*.whl
+ uv venv .pkg-smoke
+ uv pip install --python .pkg-smoke/bin/python dist/*.whl
.pkg-smoke/bin/docpull --version
.pkg-smoke/bin/python -c "import docpull; print(docpull.__version__)"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 4d8c191..e4a352d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,5 +1,8 @@
name: CodeQL
+env:
+ MISE_LOCKED: "1"
+
on:
workflow_dispatch:
pull_request:
@@ -22,6 +25,7 @@ jobs:
analyze:
name: analyze (${{ matrix.language }})
runs-on: ubuntu-24.04
+ timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -36,6 +40,11 @@ jobs:
with:
persist-credentials: false
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
+ with:
+ version: 2026.7.1
+ install: true
+ cache: true
- name: Initialize CodeQL
uses: github/codeql-action/init@b22c66273205240d86582638b860f9b25772b4d3
with:
diff --git a/.github/workflows/live-typed-packs.yml b/.github/workflows/live-typed-packs.yml
index 55816a3..2ff77ff 100644
--- a/.github/workflows/live-typed-packs.yml
+++ b/.github/workflows/live-typed-packs.yml
@@ -12,6 +12,10 @@ concurrency:
group: live-typed-packs
cancel-in-progress: false
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
live-typed-packs:
runs-on: ubuntu-24.04
@@ -22,17 +26,16 @@ jobs:
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
- cache: pip
- cache-dependency-path: pyproject.toml
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --extra dev
- name: Run free live typed-pack smokes
env:
@@ -40,7 +43,7 @@ jobs:
run: |
set +e
mkdir -p live-reports
- python -m pytest tests/test_typed_knowledge_packs.py::test_live_typed_pack_smokes -q \
+ uv run --locked python -m pytest tests/test_typed_knowledge_packs.py::test_live_typed_pack_smokes -q \
| tee live-reports/live-typed-packs.log
status=${PIPESTATUS[0]}
python - "$status" <<'PY'
diff --git a/.github/workflows/live-web-smoke.yml b/.github/workflows/live-web-smoke.yml
index 57ac2a3..86a46f9 100644
--- a/.github/workflows/live-web-smoke.yml
+++ b/.github/workflows/live-web-smoke.yml
@@ -12,6 +12,10 @@ concurrency:
group: live-web-smoke
cancel-in-progress: false
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
live-web-smoke:
runs-on: ubuntu-24.04
@@ -22,17 +26,16 @@ jobs:
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
- cache: pip
- cache-dependency-path: pyproject.toml
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
+ - name: Sync dependencies
+ run: uv sync --locked --extra dev
- name: Run free live web smokes
env:
@@ -40,7 +43,7 @@ jobs:
run: |
set +e
mkdir -p live-reports
- python -m pytest tests/test_live_web_smoke.py -q | tee live-reports/live-web-smoke.log
+ uv run --locked python -m pytest tests/test_live_web_smoke.py -q | tee live-reports/live-web-smoke.log
status=${PIPESTATUS[0]}
python - "$status" <<'PY'
import json
diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml
index 4e571db..c349f5a 100644
--- a/.github/workflows/metrics.yml
+++ b/.github/workflows/metrics.yml
@@ -1,5 +1,8 @@
name: Update metrics
+env:
+ MISE_LOCKED: "1"
+
# Refreshes METRICS.md and the README downloads chart from PyPI + GitHub APIs.
#
# Signals:
@@ -48,29 +51,41 @@ jobs:
github.event_name == 'schedule' ||
github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
ref: ${{ github.event.repository.default_branch }}
- - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
-
+ version: 2026.7.1
+ install: true
+ cache: true
- name: Refresh metrics artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
METRICS_TOKEN: ${{ secrets.METRICS_TOKEN }}
run: |
- if [ -n "${METRICS_TOKEN:-}" ] && GH_TOKEN="$METRICS_TOKEN" gh api "repos/${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
- export GH_TOKEN="$METRICS_TOKEN"
- else
- if [ -n "${METRICS_TOKEN:-}" ]; then
+ metrics_token="${METRICS_TOKEN:-}"
+ use_metrics_token=false
+ if [ -n "$metrics_token" ]; then
+ printf -v GH_TOKEN '%s' "$metrics_token"
+ export GH_TOKEN
+ if gh api "repos/${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
+ use_metrics_token=true
+ fi
+ fi
+
+ if [ "$use_metrics_token" != true ]; then
+ if [ -n "$metrics_token" ]; then
echo "::warning::METRICS_TOKEN is present but failed GitHub API validation; falling back to GITHUB_TOKEN."
fi
- export GH_TOKEN="$GITHUB_TOKEN"
+ printf -v GH_TOKEN '%s' "$GITHUB_TOKEN"
+ export GH_TOKEN
fi
+ unset metrics_token use_metrics_token
python .github/scripts/update_metrics.py
- name: Check generated metrics freshness
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 054f2e3..b26e16b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -30,9 +30,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
build:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
outputs:
version: ${{ steps.meta.outputs.version }}
steps:
@@ -40,9 +45,13 @@ jobs:
with:
persist-credentials: false
- - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- name: Read pyproject.toml version (and verify tag match on tag push)
id: meta
@@ -74,17 +83,14 @@ jobs:
fi
echo "version=$PROJECT_VERSION" >> "$GITHUB_OUTPUT"
- - name: Install release build dependencies
+ - name: Build distributions
run: |
# Pinned (see requirements-release.txt) so compromised *latest*
# release tooling cannot be auto-pulled into the wheel-building job.
- python -m pip install -r requirements-release.txt
-
- - name: Build distributions
- run: python scripts/build_release.py --verify-reproducible
+ uv run --locked --with-requirements requirements-release.txt python scripts/build_release.py --verify-reproducible
- name: Verify wheel and sdist
- run: python -m twine check dist/*
+ run: uv run --locked --with-requirements requirements-release.txt python -m twine check dist/*
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
@@ -102,18 +108,16 @@ jobs:
with:
persist-credentials: false
- - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
- with:
- bun-version: "1.3.11"
-
- - name: Install release gate dependencies
- run: |
- python -m pip install -r requirements-release.txt
- pip install --no-build-isolation -e ".[all,dev]"
+ - name: Sync release gate dependencies
+ run: uv sync --locked --all-extras
- name: Install local render runtime
run: |
@@ -122,14 +126,14 @@ jobs:
agent-browser --version
- name: Check generated release metadata
- run: python scripts/sync_release_metadata.py --check
+ run: uv run --locked python scripts/sync_release_metadata.py --check
- name: Run Bandit
- run: python -m bandit -q -c pyproject.toml -r src scripts
+ run: uv run --locked python -m bandit -q -c pyproject.toml -r src scripts
- name: Run A+ real-feature smoke
run: |
- python scripts/real_feature_smoke.py \
+ uv run --locked python scripts/real_feature_smoke.py \
--json \
--full-mcp \
--strict-ci \
@@ -139,7 +143,7 @@ jobs:
- name: Run A+ release scorecard
run: |
- python scripts/release_a_plus_check.py \
+ uv run --locked python scripts/release_a_plus_check.py \
--strict \
--output-dir "$RUNNER_TEMP/release-readiness" \
--smoke-report "$RUNNER_TEMP/release-readiness/real-feature-smoke/real_feature_smoke.report.json"
@@ -151,9 +155,8 @@ jobs:
- name: Smoke install built wheel
run: |
- python -m venv .release-smoke
- .release-smoke/bin/python -m pip install --upgrade pip
- .release-smoke/bin/python -m pip install dist/*.whl
+ uv venv .release-smoke
+ uv pip install --python .release-smoke/bin/python dist/*.whl
.release-smoke/bin/docpull --version
.release-smoke/bin/python -c "import docpull; print(docpull.__version__)"
@@ -168,6 +171,7 @@ jobs:
publish:
needs: [build, release-gates]
runs-on: ubuntu-24.04
+ timeout-minutes: 30
environment:
name: pypi
url: https://pypi.org/project/docpull/
@@ -190,6 +194,11 @@ jobs:
persist-credentials: false
if: github.event_name == 'push' && github.ref_type == 'tag'
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
+ with:
+ version: 2026.7.1
+ install: true
+ cache: true
- name: Create GitHub release
if: github.event_name == 'push' && github.ref_type == 'tag'
env:
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 1f1191e..39ea563 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -14,9 +14,14 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+env:
+ MISE_LOCKED: "1"
+ UV_VERSION: "0.11.6"
+
jobs:
secret-scan:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout full history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -24,6 +29,11 @@ jobs:
persist-credentials: false
fetch-depth: 0
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
+ with:
+ version: 2026.7.1
+ install: true
+ cache: true
- name: Run Gitleaks
# Use the MIT-licensed gitleaks CLI rather than gitleaks-action@v2,
# which requires a paid GITLEAKS_LICENSE secret for organization repos.
@@ -35,33 +45,36 @@ jobs:
python-security:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- - name: Set up Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- python-version: "3.11"
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install uv
+ run: python -m pip install "uv==$UV_VERSION"
- - name: Install Python dependencies
- run: |
- python -m pip install --upgrade pip setuptools==83.0.0
- pip install -e ".[dev]"
+ - name: Sync Python dependencies
+ run: uv sync --locked --extra dev
- name: Audit Python dependencies
- run: python -m pip_audit
+ run: uv run --locked python -m pip_audit
- name: Run Bandit
- run: python -m bandit -q -c pyproject.toml -r src scripts
+ run: uv run --locked python -m bandit -q -c pyproject.toml -r src scripts
- name: Run Python security regression tests
- run: PYTHONPATH=src python -m pytest -q tests/test_security_hardening.py tests/test_discovery.py tests/test_integration.py
+ run: uv run --locked python -m pytest -q tests/test_security_hardening.py tests/test_discovery.py tests/test_integration.py
mcp-security:
runs-on: ubuntu-24.04
+ timeout-minutes: 30
defaults:
run:
working-directory: mcp
@@ -71,13 +84,13 @@ jobs:
with:
persist-credentials: false
- - name: Set up Bun
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
with:
- bun-version: "1.3.11"
-
+ version: 2026.7.1
+ install: true
+ cache: true
- name: Install MCP dependencies
- run: bun install --frozen-lockfile
+ run: mise run setup
- name: Audit MCP dependencies
run: bun audit
@@ -87,3 +100,35 @@ jobs:
- name: Typecheck MCP
run: bun run typecheck
+
+ web-security:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ defaults:
+ run:
+ working-directory: web
+ steps:
+ - name: Checkout
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
+ with:
+ persist-credentials: false
+
+ - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db
+ with:
+ version: 2026.7.1
+ install: true
+ cache: true
+ - name: Install web dependencies
+ run: mise run setup
+
+ - name: Audit web dependencies
+ run: bun audit
+
+ - name: Typecheck web app
+ run: bun run typecheck
+
+ - name: Lint web app
+ run: bun run lint
+
+ - name: Build web app
+ run: mise x -- bun run build
diff --git a/.gitignore b/.gitignore
index a40bf3f..7bd1aa8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,9 +83,6 @@ target/
profile_default/
ipython_config.py
-# pyenv
-.python-version
-
# pipenv
Pipfile.lock
@@ -95,9 +92,6 @@ poetry.lock
# pdm
.pdm.toml
-# uv
-uv.lock
-
# PEP 582
__pypackages__/
@@ -195,9 +189,9 @@ QUALITY_IMPROVEMENTS.md
test_fetch.py
test_error_handling.py
-# Playwright
-.playwright/
-playwright/.auth/
+# Browser automation artifacts
+.browser/
+agent-browser/.auth/
trace.zip
screenshots/
@@ -216,3 +210,10 @@ screenshots/
.agents/
competitors/
docs/internal/
+
+# mise local overrides
+mise.local.toml
+mise.*.local.toml
+mise.local.lock
+mise.*.local.lock
+.mise/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..28d9a01
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12.13
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..142dcf5
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,14 @@
+# DocPull Agent Instructions
+
+DocPull is the open-source context dependency manager for AI agents. Its core
+contract is local-first, browser-free by default, reproducible, cited, and
+budget guarded.
+
+- Preserve CLI, Python SDK, MCP, and artifact-contract compatibility.
+- Do not silently enable browser or paid/cloud rendering. Those paths require
+ explicit user configuration and budgets.
+- Respect robots, source rights, provenance, and existing cache/lock semantics.
+- Never store credentials or fetched private content in fixtures or reports.
+- Use uv for Python work and Bun 1.3.11/Node 24 for the web and MCP packages.
+- Run the focused pytest suite while iterating and `bun run validate:full` from
+ the root for repository-wide changes.
diff --git a/README.md b/README.md
index 5e96060..aaa0de8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,11 @@
+
+
+
+
# docpull
**Context dependencies for AI agents. Browser-free by default.**
@@ -173,6 +181,8 @@ docpull repo-pack psf/requests -o packs/repo --cache
docpull package-pack pypi:requests -o packs/package
docpull standards-pack rfc:9110 -o packs/standard
docpull dataset-pack ./metrics.csv -o packs/dataset
+docpull dataset-pack 'https://data.example.org/rows.json?$limit=100' -o packs/dataset
+docpull relationship-pack example.com --subject "Example Brand" -o packs/relationships
docpull transcript-pack ./meeting.vtt -o packs/transcript
docpull wiki-pack wiki:Web_scraping -o packs/wiki
docpull brand-pack example.com -o packs/brand
@@ -451,7 +461,8 @@ special handling for common web, documentation, and API surfaces.
| Public GitHub repos | `docpull repo-pack` emits repo metadata, README/docs/examples/changelog files, manifests, and releases |
| npm / PyPI packages | `docpull package-pack` emits registry metadata, README/description, versions, license, dependencies, and install commands |
| Standards | `docpull standards-pack` emits RFC, IETF, W3C, and WHATWG metadata plus section-level records |
-| Local datasets | `docpull dataset-pack` emits bounded schema, exact row counts where streamable, column, null-count, and sample summaries |
+| Local/remote datasets | `docpull dataset-pack` emits bounded schema, exact row counts where streamable, column/null/sample summaries, and HTTPS JSON/CSV snapshot provenance |
+| Relationship review | `docpull relationship-pack` emits cited observations and explicit coverage gaps without approving claims or inferring independence |
| Transcripts | `docpull transcript-pack` emits timestamped segment records from VTT, SRT, text, JSON, or direct transcript URLs |
| Wikimedia / Wikipedia | `docpull wiki-pack` emits MediaWiki REST page metadata, license/revision metadata, and section-level records |
| Brand evidence | `docpull brand-pack` emits cited identity, firmographic, social, logo, and color observations |
diff --git a/bench/uv.lock b/bench/uv.lock
index 2a6f631..5b0bac8 100644
--- a/bench/uv.lock
+++ b/bench/uv.lock
@@ -584,7 +584,7 @@ wheels = [
[[package]]
name = "docpull"
-version = "6.2.0"
+version = "6.3.0"
source = { editable = "../" }
dependencies = [
{ name = "aiohttp" },
diff --git a/bun.lock b/bun.lock
index 39021d5..4937dbf 100644
--- a/bun.lock
+++ b/bun.lock
@@ -23,37 +23,354 @@
"typescript": "^6.0.3",
},
},
+ "web": {
+ "name": "docpull-web",
+ "version": "1.0.0",
+ "dependencies": {
+ "next": "^16.2.9",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ },
+ "devDependencies": {
+ "@types/node": "^25.9.1",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9.39.2",
+ "eslint-config-next": "^16.2.9",
+ "typescript": "^6.0.3",
+ },
+ },
},
"overrides": {
"postcss": "^8.5.10",
},
"packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
+
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
+
+ "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
+
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
+
+ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+
+ "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
+
+ "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
+ "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
+
+ "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
+
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
+
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
+
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
+
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
+
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
+
+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
+
+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
+
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
+
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
+
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
+
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
+
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
+
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
+
+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
+
+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
+
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
+
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
+
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
+
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
+
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
+
+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
+
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
+
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
+
+ "@next/env": ["@next/env@16.2.10", "", {}, "sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA=="],
+
+ "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.10", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q=="],
+
+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA=="],
+
+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ=="],
+
+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg=="],
+
+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A=="],
+
+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA=="],
+
+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw=="],
+
+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA=="],
+
+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A=="],
+
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
+
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
+
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
+
+ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
+
+ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
+
+ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+
+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
+
"@types/node": ["@types/node@25.9.4", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
+ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.64.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/type-utils": "8.64.0", "@typescript-eslint/utils": "8.64.0", "@typescript-eslint/visitor-keys": "8.64.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.64.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.64.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/types": "8.64.0", "@typescript-eslint/typescript-estree": "8.64.0", "@typescript-eslint/visitor-keys": "8.64.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.64.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.64.0", "@typescript-eslint/types": "^8.64.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.64.0", "", { "dependencies": { "@typescript-eslint/types": "8.64.0", "@typescript-eslint/visitor-keys": "8.64.0" } }, "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.64.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.64.0", "", { "dependencies": { "@typescript-eslint/types": "8.64.0", "@typescript-eslint/typescript-estree": "8.64.0", "@typescript-eslint/utils": "8.64.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.64.0", "", {}, "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.64.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.64.0", "@typescript-eslint/tsconfig-utils": "8.64.0", "@typescript-eslint/types": "8.64.0", "@typescript-eslint/visitor-keys": "8.64.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.64.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.64.0", "@typescript-eslint/types": "8.64.0", "@typescript-eslint/typescript-estree": "8.64.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.64.0", "", { "dependencies": { "@typescript-eslint/types": "8.64.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw=="],
+
+ "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.2", "", { "os": "android", "cpu": "arm" }, "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w=="],
+
+ "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.12.2", "", { "os": "android", "cpu": "arm64" }, "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ=="],
+
+ "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.12.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w=="],
+
+ "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.12.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA=="],
+
+ "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.12.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg=="],
+
+ "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A=="],
+
+ "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.12.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g=="],
+
+ "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.12.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg=="],
+
+ "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.12.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA=="],
+
+ "@unrs/resolver-binding-linux-loong64-gnu": ["@unrs/resolver-binding-linux-loong64-gnu@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q=="],
+
+ "@unrs/resolver-binding-linux-loong64-musl": ["@unrs/resolver-binding-linux-loong64-musl@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew=="],
+
+ "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.12.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg=="],
+
+ "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A=="],
+
+ "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w=="],
+
+ "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.12.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw=="],
+
+ "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.12.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ=="],
+
+ "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.12.2", "", { "os": "linux", "cpu": "x64" }, "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A=="],
+
+ "@unrs/resolver-binding-openharmony-arm64": ["@unrs/resolver-binding-openharmony-arm64@1.12.2", "", { "os": "none", "cpu": "arm64" }, "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ=="],
+
+ "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.12.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A=="],
+
+ "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.12.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g=="],
+
+ "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.12.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g=="],
+
+ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA=="],
+
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+ "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
+
+ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
+
+ "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
+
+ "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
+
+ "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
+
+ "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
+
+ "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
+
+ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+
+ "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
+
+ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
+
+ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+
+ "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.41", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A=="],
+
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
+ "brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="],
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+ "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001800", "", {}, "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -62,26 +379,100 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
+
+ "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
+
+ "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
+
+ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+
+ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
"docpull-mcp-lab": ["docpull-mcp-lab@workspace:mcp"],
+ "docpull-web": ["docpull-web@workspace:web"],
+
+ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.389", "", {}, "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg=="],
+
+ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+ "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
+
+ "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+ "es-iterator-helpers": ["es-iterator-helpers@1.4.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q=="],
+
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
+
+ "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="],
+
+ "eslint-config-next": ["eslint-config-next@16.2.10", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.10", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w=="],
+
+ "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
+
+ "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
+
+ "eslint-module-utils": ["eslint-module-utils@2.14.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig=="],
+
+ "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
+
+ "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
+
+ "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
@@ -94,76 +485,272 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+ "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
+ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
+
+ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+ "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="],
+
+ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
+
+ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
+
+ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
+
+ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
+ "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
"hono": ["hono@4.12.27", "", {}, "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
+
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
+
+ "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
+
+ "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
+
+ "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
+
+ "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="],
+
+ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
+
+ "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
+
+ "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
+
+ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+
+ "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
+
+ "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
+
+ "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
+
+ "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
+
+ "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
+
+ "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
+
+ "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
+
+ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
+
+ "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
+
+ "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
+
+ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
+
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
+
+ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
+
+ "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
+
+ "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+ "next": ["next@16.2.10", "", { "dependencies": { "@next/env": "16.2.10", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.10", "@next/swc-darwin-x64": "16.2.10", "@next/swc-linux-arm64-gnu": "16.2.10", "@next/swc-linux-arm64-musl": "16.2.10", "@next/swc-linux-x64-gnu": "16.2.10", "@next/swc-linux-x64-musl": "16.2.10", "@next/swc-win32-arm64-msvc": "16.2.10", "@next/swc-win32-x64-msvc": "16.2.10", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA=="],
+
+ "node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="],
+
+ "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
+
+ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
+
+ "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
+
+ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
+
+ "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
+
+ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openai": ["openai@6.45.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw=="],
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
@@ -182,8 +769,16 @@
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
+
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
+ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+
+ "postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
+
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
@@ -192,26 +787,72 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
+
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+ "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
+
+ "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
+
+ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
+
+ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+ "resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
+
+ "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
+
+ "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
+
+ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
+
+ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@@ -224,36 +865,156 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
+ "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
+
+ "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
+
+ "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
+
+ "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
+
+ "string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="],
+
+ "string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="],
+
+ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
+
+ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
+
+ "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
+ "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
+
+ "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
+
+ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
+
+ "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
+
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+ "typescript-eslint": ["typescript-eslint@8.64.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.64.0", "@typescript-eslint/parser": "8.64.0", "@typescript-eslint/typescript-estree": "8.64.0", "@typescript-eslint/utils": "8.64.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ=="],
+
+ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
+
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+ "unrs-resolver": ["unrs-resolver@1.12.2", "", { "dependencies": { "napi-postinstall": "^0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.12.2", "@unrs/resolver-binding-android-arm64": "1.12.2", "@unrs/resolver-binding-darwin-arm64": "1.12.2", "@unrs/resolver-binding-darwin-x64": "1.12.2", "@unrs/resolver-binding-freebsd-x64": "1.12.2", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-musl": "1.12.2", "@unrs/resolver-binding-openharmony-arm64": "1.12.2", "@unrs/resolver-binding-wasm32-wasi": "1.12.2", "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+ "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
+
+ "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
+
+ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
+
+ "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "@unrs/resolver-binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
+ "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
+
+ "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="],
+
+ "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
+
+ "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "is-bun-module/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
+
+ "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
+
+ "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 9f8a2c5..1a06918 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [6.3.0] - 2026-07-16
+
+### Added
+- Add `relationship-pack` across CLI, SDK, MCP, workflow registry, and project
+ sources. It emits cited `owned_by`, `operated_by`, `acquired_by`,
+ `franchised_by`, and `invested_in` observations plus one coverage result per
+ input; missing evidence remains a coverage gap and never becomes an
+ independence claim.
+- Put core `fetch`, `crawl`, and `dataset-pack` on
+ `WorkflowRequest`/`WorkflowResult`; empty and partial crawls now retain a
+ structured current-run manifest, progress, budget usage, hashes, replay
+ configuration, warnings, and typed failures.
+- Support bounded remote HTTPS JSON/CSV dataset snapshots with original and
+ resolved URL provenance, query parameters, content type, and deterministic
+ SHA-256 hashes.
+- Add the DocPull product site with home, pricing, privacy, terms, `llms.txt`,
+ robots, sitemap, manifest, branded icons, and reusable launch assets.
+- Add metadata-only native integration context adapters that produce cited,
+ rights-labeled document records without storing provider secrets or raw
+ customer payloads.
+- Add a committed `uv.lock`, pinned Mise runtimes, Next.js ESLint gates, and
+ dependency review for reproducible local and CI environments.
+
+### Changed
+- Make CLI success strictly current-run scoped by default. Add explicit
+ `--exit-policy usable-output` for consumers that intentionally accept older
+ records in a shared directory.
+- Classify authority per entity/source in multi-company bundles and add
+ `official_corporate`, `government_registry`, `regulatory_filing`,
+ `press_release`, and `local_reporting` roles.
+- Extract relationship observations into intelligence bundles while keeping
+ every candidate unresolved until downstream human review.
+- Load the root SDK and internal package exports lazily while preserving every
+ documented import and CLI, Python SDK, and MCP surface.
+- Coalesce byte-equivalent concurrent HTTP GETs without retaining completed
+ responses, and isolate requests with different timeouts or headers.
+- Journal frontier transitions between compact snapshots so interrupted runs
+ remain resumable without rewriting the full frontier after every URL.
+- Reuse pack reads, citation analysis, entity extraction, and graph indexes
+ across intelligence workflows; use constant-time document/source lookups and
+ bounded top-result selection for local search.
+- Centralize schema versions and export-format registries in lightweight
+ modules to reduce import cost and duplicated contract definitions.
+
+### Fixed
+- Populate retryable acquisition failures with stable codes such as
+ `http_429`, fetch stage, HTTP status, attempt count, and Retry-After seconds.
+- Prevent stale records in a reused output directory from turning a zero-record
+ current run into exit status 0.
+- Restore automatic CI, CodeQL, security, benchmark, live-smoke, and metrics
+ triggers after the runtime-tooling migration, and make the Python matrix
+ select the advertised patch versions explicitly.
+- Extend release-readiness checks to lint, type-check, audit, and build the web
+ workspace, and repair stale repository links in issue templates.
+
+### Compatibility
+- Keep all 6.2 pack, workflow, CLI, SDK, MCP, schema, and tracker contracts
+ readable and importable; 6.3 changes are additive or internal optimizations.
+
## [6.2.0] - 2026-07-16
### Added
@@ -127,7 +186,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`CIThresholds` through the root Python SDK contract.
- Remove provider and observability extras from the public package extras;
internal experiments that need those SDKs should install them directly.
-- Remove the unreleased Playwright renderer runtime and keep `agent-browser` as
+- Remove the unreleased browser renderer runtime and keep `agent-browser` as
the sole local browser-rendering contract for local, Vercel, and E2B paths.
- Make the `sec-filing` profile use the extractor ensemble so base installs
fall back to the built-in extractor when optional Trafilatura is unavailable.
diff --git a/docs/acquisition.routes.json b/docs/acquisition.routes.json
new file mode 100644
index 0000000..68e8441
--- /dev/null
+++ b/docs/acquisition.routes.json
@@ -0,0 +1,18 @@
+{
+ "schema_version": 3,
+ "generated_at": "2026-07-15T04:21:53.366286+00:00",
+ "output_contract_version": 3,
+ "routes": [
+ {
+ "route": "local-fetch",
+ "output_format": "markdown",
+ "fetched_count": 0,
+ "record_count": 0,
+ "domain_count": 0,
+ "skip_counts": {
+ "robots_disallowed": 0
+ }
+ }
+ ],
+ "domains": {}
+}
diff --git a/docs/adr/0001-evidence-acquisition-engine.md b/docs/adr/0001-evidence-acquisition-engine.md
index eeddcef..514b969 100644
--- a/docs/adr/0001-evidence-acquisition-engine.md
+++ b/docs/adr/0001-evidence-acquisition-engine.md
@@ -3,6 +3,7 @@
- Status: Accepted
- Date: 2026-07-16
- Release: 6.2.0
+- Last amended: 6.3.0
## Decision
@@ -35,6 +36,9 @@ flowchart LR
part of acquisition rather than downstream interpretation.
- Machine-derived statements cross the boundary as `observation` or
`candidate`, never as an approved claim.
+- Relationship extraction is limited to cited `owned_by`, `operated_by`,
+ `acquired_by`, `franchised_by`, and `invested_in` review candidates. Missing
+ evidence is a `coverage_gap`, never an `independent` claim.
- Authority tiers describe source relationship only. They are not review or
product-specific approval decisions.
- Policy changes report structural and textual evidence. They do not express a
@@ -42,6 +46,9 @@ flowchart LR
- Result contracts contain progress, warnings, failures, budget use, hashes,
artifacts, and replay settings so another repository does not need to infer
run state from logs.
+- Fetches and crawls are run-scoped. Strict success depends only on records
+ produced by the current run; reuse of older usable output is an explicit
+ compatibility policy.
## Consequences
diff --git a/docs/alternatives.md b/docs/alternatives.md
index 2d52915..f57d3d3 100644
--- a/docs/alternatives.md
+++ b/docs/alternatives.md
@@ -18,7 +18,7 @@ tool.
| Crawl static/server-rendered websites at modest to large scale | DocPull | Async HTTP, framework-aware extraction, manifests, cache support |
| Parse one messy article page in Python | trafilatura | Excellent text extraction library; DocPull can also use it as an optional extractor |
| Build a custom crawling pipeline with queues, middleware, and spiders | Scrapy | Mature scraping framework for custom pipelines and broad crawler control |
-| Automate a real browser or interact with JavaScript-heavy pages | Playwright, Puppeteer, Selenium | Required when useful content only exists after client-side rendering or interaction |
+| Automate a real browser or interact with JavaScript-heavy pages | agent-browser, Puppeteer, Selenium | Required when useful content only exists after client-side rendering or interaction |
| Build browser-backed crawlers in the JavaScript ecosystem | Crawlee | Strong fit for JavaScript/TypeScript crawling stacks |
| Use a hosted web-to-LLM extraction service | Firecrawl, Jina Reader, hosted extraction APIs | Useful when you want an API service to manage crawling/extraction infrastructure |
| Search the live web before building a pack | Dedicated search/extract providers, then DocPull source ingestion | Search providers find candidate sources; DocPull's public release contract starts when selected sources are fetched, parsed, packed, and validated locally |
@@ -55,7 +55,7 @@ tool.
| DocPull | Yes | Yes | Yes | Yes | Yes | No |
| trafilatura | Yes | Yes | Yes | No | Partial | No |
| Scrapy | Yes | Yes by default | Yes | No | No | No |
-| Playwright/Puppeteer/Selenium | Yes | No | Mixed | No | No | No |
+| agent-browser/Puppeteer/Selenium | Yes | No | Mixed | Yes | No | No |
| Crawlee | Yes | Mixed | No | No | No | No |
| Firecrawl/Jina Reader/hosted extraction APIs | No | Hidden/varies | API-first | Partial | Partial | Yes |
diff --git a/docs/competitor-tracker-integration.md b/docs/competitor-tracker-integration.md
index 906eb6d..022b8d0 100644
--- a/docs/competitor-tracker-integration.md
+++ b/docs/competitor-tracker-integration.md
@@ -1,6 +1,6 @@
# Competitor-tracker integration contract
-Pin DocPull `6.2.0` and validate `intelligence.bundle.v1.json` against
+Pin DocPull `6.3.0` and validate `intelligence.bundle.v1.json` against
`intelligence-bundle.v1.schema.json` at import time.
## Producer flow
@@ -39,7 +39,10 @@ deprecated compatibility envelope.
presenting it for review.
7. Keep `change_candidates` in a review queue; do not infer approval from source
authority or confidence.
-8. Surface warnings and retain replay configuration for audit/reproduction.
+8. Keep `relationship_candidates` unresolved. Verify their evidence spans and
+ require human approval before creating ownership or operator claims.
+9. Treat `coverage_gap` as unknown coverage, never as proof of independence.
+10. Surface warnings and retain replay configuration for audit/reproduction.
## Ownership split
diff --git a/docs/context-packs.md b/docs/context-packs.md
index 49e8b1f..34fb0ac 100644
--- a/docs/context-packs.md
+++ b/docs/context-packs.md
@@ -68,7 +68,7 @@ exported, or checked in Context CI.
| `repo-pack` | public GitHub URL or `owner/repo[@ref]` | repo metadata, selected docs/manifests, releases |
| `package-pack` | `npm:` or `pypi:` | registry metadata, README/description, versions, dependencies |
| `standards-pack` | `rfc:`, `ietf:`, `w3c:`, `whatwg:` | standard metadata, section-level records, references |
-| `dataset-pack` | local CSV, TSV, JSON, NDJSON, SQLite, optional Parquet | schema, exact streamable row counts, and bounded data dictionary records |
+| `dataset-pack` | local CSV, TSV, JSON, NDJSON, SQLite, optional Parquet; HTTPS JSON/CSV | schema, exact streamable row counts, bounded data dictionary records, and remote snapshot provenance |
| `transcript-pack` | local VTT, SRT, text, JSON, or direct transcript URL | timestamped transcript segment records |
| `wiki-pack` | `wiki:`, `wikipedia:`, or Wikimedia/MediaWiki page URLs | page metadata, license/revision metadata, lead and section-level records from the MediaWiki REST API |
@@ -78,7 +78,7 @@ matching `async_build_*_pack` helpers when already inside an event loop.
## Evidence Workflow Protocol
-Brand, product, styleguide, visual/image, screenshot, and policy lanes implement
+Brand, product, styleguide, visual/image, screenshot, policy, relationship, and dataset lanes implement
the common workflow protocol. Their existing result JSON and Markdown remain,
and every run additionally writes:
diff --git a/docs/corpus.manifest.json b/docs/corpus.manifest.json
new file mode 100644
index 0000000..cb4d6fd
--- /dev/null
+++ b/docs/corpus.manifest.json
@@ -0,0 +1,29 @@
+{
+ "schema_version": 3,
+ "output_contract_version": 3,
+ "generated_at": "2026-07-15T04:29:31.283115+00:00",
+ "output_format": "markdown",
+ "run": {
+ "schema_version": 1,
+ "profile": "rag",
+ "start_url": "pack --help",
+ "max_pages": null,
+ "max_depth": 5,
+ "include_paths": [],
+ "exclude_paths": [],
+ "output_format": "markdown",
+ "naming_strategy": "full",
+ "rich_metadata": true,
+ "extractor": "default",
+ "enable_special_cases": true,
+ "strict_js_required": false,
+ "max_tokens_per_file": null,
+ "emit_chunks": false,
+ "tokenizer": "cl100k_base",
+ "auth_type": "none"
+ },
+ "document_count": 0,
+ "record_count": 0,
+ "chunk_count": 0,
+ "records": []
+}
diff --git a/docs/launch-assets/README.md b/docs/launch-assets/README.md
index 7d80a46..7afaf93 100644
--- a/docs/launch-assets/README.md
+++ b/docs/launch-assets/README.md
@@ -9,6 +9,22 @@ submissions.
- `logo-square-dark-1024.png` - light mark on dark square background.
- `logo-transparent-dark-1024.png` - dark mark on transparent background.
- `logo-transparent-light-1024.png` - light mark on transparent background.
+- `logo-square-light-400.png` - 400x400 directory and marketplace icon.
+
+## Logo SVGs
+
+- `logo-mark.svg` - primary two-color mark on a transparent background.
+- `logo-mark-reversed.svg` - warm-white and green mark for dark backgrounds.
+- `logo-mark-monochrome-black.svg` - solid black mark.
+- `logo-mark-monochrome-white.svg` - solid white mark.
+- `logo-lockup-dark.svg` - mark and wordmark for light backgrounds.
+- `logo-lockup-light.svg` - reversed mark and wordmark for dark backgrounds.
+- `logo-app-icon.svg` - rounded-square app and social icon.
+- `logo-favicon.svg` - simplified small-size icon.
+
+The mark uses near-black `#101213`, verification green `#0F6B5D`, and warm
+white `#F5F4EF`. Preserve the geometric D silhouette, chamfered outer and inner
+right shoulders, and inset module; do not close the counter at small sizes.
## Screenshots
diff --git a/docs/launch-assets/logo-app-icon.svg b/docs/launch-assets/logo-app-icon.svg
new file mode 100644
index 0000000..0d3331a
--- /dev/null
+++ b/docs/launch-assets/logo-app-icon.svg
@@ -0,0 +1,11 @@
+
diff --git a/docs/launch-assets/logo-favicon.svg b/docs/launch-assets/logo-favicon.svg
new file mode 100644
index 0000000..7970925
--- /dev/null
+++ b/docs/launch-assets/logo-favicon.svg
@@ -0,0 +1,10 @@
+
diff --git a/docs/launch-assets/logo-lockup-dark.svg b/docs/launch-assets/logo-lockup-dark.svg
new file mode 100644
index 0000000..56e447d
--- /dev/null
+++ b/docs/launch-assets/logo-lockup-dark.svg
@@ -0,0 +1,21 @@
+
diff --git a/docs/launch-assets/logo-lockup-light.svg b/docs/launch-assets/logo-lockup-light.svg
new file mode 100644
index 0000000..bc28737
--- /dev/null
+++ b/docs/launch-assets/logo-lockup-light.svg
@@ -0,0 +1,21 @@
+
diff --git a/docs/launch-assets/logo-mark-monochrome-black.svg b/docs/launch-assets/logo-mark-monochrome-black.svg
new file mode 100644
index 0000000..939dfd0
--- /dev/null
+++ b/docs/launch-assets/logo-mark-monochrome-black.svg
@@ -0,0 +1,9 @@
+
diff --git a/docs/launch-assets/logo-mark-monochrome-white.svg b/docs/launch-assets/logo-mark-monochrome-white.svg
new file mode 100644
index 0000000..e9446b8
--- /dev/null
+++ b/docs/launch-assets/logo-mark-monochrome-white.svg
@@ -0,0 +1,9 @@
+
diff --git a/docs/launch-assets/logo-mark-reversed.svg b/docs/launch-assets/logo-mark-reversed.svg
new file mode 100644
index 0000000..839f280
--- /dev/null
+++ b/docs/launch-assets/logo-mark-reversed.svg
@@ -0,0 +1,10 @@
+
diff --git a/docs/launch-assets/logo-mark.svg b/docs/launch-assets/logo-mark.svg
new file mode 100644
index 0000000..5ade7c6
--- /dev/null
+++ b/docs/launch-assets/logo-mark.svg
@@ -0,0 +1,10 @@
+
diff --git a/docs/launch-assets/logo-square-dark-1024.png b/docs/launch-assets/logo-square-dark-1024.png
index 4401996..43cc966 100644
Binary files a/docs/launch-assets/logo-square-dark-1024.png and b/docs/launch-assets/logo-square-dark-1024.png differ
diff --git a/docs/launch-assets/logo-square-light-1024.png b/docs/launch-assets/logo-square-light-1024.png
index 752d05d..a64ddc6 100644
Binary files a/docs/launch-assets/logo-square-light-1024.png and b/docs/launch-assets/logo-square-light-1024.png differ
diff --git a/docs/launch-assets/logo-square-light-400.png b/docs/launch-assets/logo-square-light-400.png
new file mode 100644
index 0000000..a06c320
Binary files /dev/null and b/docs/launch-assets/logo-square-light-400.png differ
diff --git a/docs/launch-assets/logo-transparent-dark-1024.png b/docs/launch-assets/logo-transparent-dark-1024.png
index d8382e9..cbaaa64 100644
Binary files a/docs/launch-assets/logo-transparent-dark-1024.png and b/docs/launch-assets/logo-transparent-dark-1024.png differ
diff --git a/docs/launch-assets/logo-transparent-light-1024.png b/docs/launch-assets/logo-transparent-light-1024.png
index a5420f1..368d477 100644
Binary files a/docs/launch-assets/logo-transparent-light-1024.png and b/docs/launch-assets/logo-transparent-light-1024.png differ
diff --git a/docs/migrations/6.3.md b/docs/migrations/6.3.md
new file mode 100644
index 0000000..fcc9e74
--- /dev/null
+++ b/docs/migrations/6.3.md
@@ -0,0 +1,54 @@
+# Migrating to DocPull 6.3
+
+No pack, workflow, CLI, SDK, MCP, or tracker migration is required. DocPull
+6.3 preserves the 6.2 public surface and artifact contracts.
+
+## Locked development environments
+
+The repository now commits `uv.lock`. Use `uv run --locked --all-extras` for
+the full Python suite or the root Bun scripts (`bun run validate:full`) to run
+the supported Python, MCP, web, and release-build gates.
+
+## Lazy SDK imports
+
+Root imports such as `from docpull import Fetcher` keep the same behavior. The
+owning module is now imported on first attribute access, so applications that
+only inspect versions or lightweight models no longer load HTTP, HTML, or
+optional workflow stacks.
+
+## Frontier journals
+
+Resumable crawls may create a `frontier.json.journal` beside the existing
+frontier snapshot. DocPull replays complete journal records after a restart and
+compacts them into the established snapshot during flush. Consumers should
+continue treating cache internals as opaque.
+
+## Product site
+
+The public site lives in the private `web` workspace and does not change the
+PyPI package or MCP runtime. Its pricing page describes the open-source package;
+it does not enable paid or browser-backed acquisition paths.
+
+## Acquisition results and exit policy
+
+Core fetch and crawl operations now write `workflow.request.json`,
+`workflow.result.json`, `artifact.manifest.json`, and
+`current-run.manifest.json`, including empty and partial runs. The default
+`--exit-policy strict` succeeds only when the current run produced usable
+records. Use `--exit-policy usable-output` only when intentionally retaining
+the pre-6.3 behavior that accepts older records in a shared output directory.
+
+Failures now expose typed `stage`, `http_status`, `attempts`,
+`retry_after_seconds`, and `retryable` fields. Consumers should stop parsing
+terminal text and inspect `WorkflowResult.failures`.
+
+## Relationship and dataset workflows
+
+`relationship-pack` emits observation-only relationship candidates and exactly
+one coverage result per input. Inputs without a cited candidate are marked as
+coverage gaps; DocPull does not infer independence. The workflow is available
+through CLI, SDK, MCP, the generic workflow registry, and declarative projects.
+
+`dataset-pack` accepts bounded HTTPS JSON and CSV sources in addition to local
+files. Remote schema entries retain the original URL and query parameters,
+resolved URL, content type, status, and SHA-256 snapshot hash.
diff --git a/docs/release-post-v6.3.md b/docs/release-post-v6.3.md
new file mode 100644
index 0000000..1c5afe3
--- /dev/null
+++ b/docs/release-post-v6.3.md
@@ -0,0 +1,32 @@
+# DocPull 6.3: faster local evidence workflows and a complete product surface
+
+DocPull 6.3 preserves the 6.2 evidence/workflow contracts while reducing the
+cost of using them. Public Python exports now load lazily, identical concurrent
+GETs share one in-flight request, resumable frontiers use a compact journal,
+and pack intelligence workflows reuse source, citation, entity, and graph
+indexes instead of rereading the same artifacts.
+
+This release also completes the common workflow boundary for core acquisition
+and knowledge workflows. `fetch`, `crawl`, and `dataset-pack` now emit the same
+run-scoped `WorkflowResult` envelope as pack builders, including current-run
+manifests, typed retryable failures, budgets, hashes, and replay settings.
+Remote HTTPS JSON and CSV datasets retain their original URL, query parameters,
+snapshot hash, and provenance.
+
+The new generic `relationship-pack` emits evidence-backed review candidates for
+`owned_by`, `operated_by`, `acquired_by`, `franchised_by`, and `invested_in`.
+Every input receives exactly one coverage result. Missing evidence is a
+`coverage_gap`, never a negative ownership or independence claim, and all
+candidates remain observations until a downstream human approves them.
+
+The repository now includes a production-ready DocPull site with pricing,
+privacy, terms, machine-readable `llms.txt`, metadata, robots, sitemap, icons,
+and reusable launch assets. The complete Python, MCP, and web surface is covered
+by locked dependencies, automatic CI/security triggers, lint, type checks,
+audits, tests, and reproducible release builds.
+
+No migration is required for existing packs or consumers. CLI, Python SDK,
+MCP, JSON Schema, `intelligence.bundle.v1`, change-event, and
+`company_brain.bundle.json` compatibility remains intact. Strict CLI exit
+behavior is now explicitly run-scoped; consumers that intentionally accept
+partial current-run output can opt into `--exit-policy usable-output`.
diff --git a/docs/scraping-boundary.md b/docs/scraping-boundary.md
index bd95a0c..2fd9304 100644
--- a/docs/scraping-boundary.md
+++ b/docs/scraping-boundary.md
@@ -50,7 +50,7 @@ browser.
- Use Scrapy when you need a programmable spider framework with custom item
pipelines.
-- Use Playwright, Puppeteer, Selenium, or Crawlee when browser automation,
+- Use agent-browser, Puppeteer, Selenium, or Crawlee when browser automation,
sessions, queues, and interaction are the core job.
- Use a hosted extraction service when you want managed rendering, proxies, and
external infrastructure.
diff --git a/docs/social/v6-launch/assets/01-context-drift.png b/docs/social/v6-launch/assets/01-context-drift.png
new file mode 100644
index 0000000..4a934df
Binary files /dev/null and b/docs/social/v6-launch/assets/01-context-drift.png differ
diff --git a/docs/social/v6-launch/assets/02-lockfile-workflow.png b/docs/social/v6-launch/assets/02-lockfile-workflow.png
new file mode 100644
index 0000000..985cf8c
Binary files /dev/null and b/docs/social/v6-launch/assets/02-lockfile-workflow.png differ
diff --git a/docs/social/v6-launch/assets/03-sync-diff.png b/docs/social/v6-launch/assets/03-sync-diff.png
new file mode 100644
index 0000000..81c2479
Binary files /dev/null and b/docs/social/v6-launch/assets/03-sync-diff.png differ
diff --git a/docs/social/v6-launch/assets/04-v3-pack-contract.png b/docs/social/v6-launch/assets/04-v3-pack-contract.png
new file mode 100644
index 0000000..2714754
Binary files /dev/null and b/docs/social/v6-launch/assets/04-v3-pack-contract.png differ
diff --git a/docs/social/v6-launch/assets/05-context-ci.png b/docs/social/v6-launch/assets/05-context-ci.png
new file mode 100644
index 0000000..6c0b8ba
Binary files /dev/null and b/docs/social/v6-launch/assets/05-context-ci.png differ
diff --git a/docs/social/v6-launch/assets/06-export-agent-context.png b/docs/social/v6-launch/assets/06-export-agent-context.png
new file mode 100644
index 0000000..fb5e3f4
Binary files /dev/null and b/docs/social/v6-launch/assets/06-export-agent-context.png differ
diff --git a/docs/social/v6-launch/assets/contact-sheet.png b/docs/social/v6-launch/assets/contact-sheet.png
new file mode 100644
index 0000000..a5b6572
Binary files /dev/null and b/docs/social/v6-launch/assets/contact-sheet.png differ
diff --git a/docs/social/v6-launch/generate_terminal_cards.py b/docs/social/v6-launch/generate_terminal_cards.py
new file mode 100644
index 0000000..d8ed1b4
--- /dev/null
+++ b/docs/social/v6-launch/generate_terminal_cards.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+"""Generate terminal-style social cards for the DocPull v6 launch thread."""
+
+from __future__ import annotations
+
+import textwrap
+from dataclasses import dataclass
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+ROOT = Path(__file__).resolve().parent
+OUT = ROOT / "assets"
+WIDTH = 1600
+HEIGHT = 900
+
+BG = "#0d0e10"
+PANEL = "#17191d"
+HEADER = "#1d2025"
+BORDER = "#32363d"
+PILL = "#181b20"
+PILL_BORDER = "#3a4048"
+STRIPE = "#2b3036"
+TEXT = "#f1f0ec"
+MUTED = "#a3a7ad"
+DIM = "#6c7179"
+ACCENT = "#b9c3cf"
+ACCENT_DIM = "#8793a0"
+SUCCESS = "#d9ddd6"
+WARNING = "#c7b98d"
+ERROR = "#d2a0a0"
+
+
+@dataclass(frozen=True)
+class Line:
+ text: str
+ style: str = "normal"
+
+
+@dataclass(frozen=True)
+class Card:
+ filename: str
+ eyebrow: str
+ title: str
+ subtitle: str
+ terminal_title: str
+ lines: tuple[Line, ...]
+ footer: str = "docpull v6"
+
+
+CARDS: tuple[Card, ...] = (
+ Card(
+ filename="01-context-drift.png",
+ eyebrow="Before",
+ title="The agent bug nobody sees: context drift.",
+ subtitle=(
+ "The model sounds confident, but its source context is stale, "
+ "uncited, or copied from unknown places."
+ ),
+ terminal_title="before: agent-context",
+ lines=(
+ Line("$ ls agent-context", "command"),
+ Line("docs-copy.txt last touched: months ago", "warning"),
+ Line("vector-store/ source: unknown", "warning"),
+ Line("prompt-notes.md citations: none", "warning"),
+ Line("", "dim"),
+ Line("$ ./agent answer 'Did the API change?'", "command"),
+ Line("maybe. based on old docs...", "error"),
+ Line("", "dim"),
+ Line("no lockfile no diff no CI gate no provenance", "error"),
+ ),
+ ),
+ Card(
+ filename="02-lockfile-workflow.png",
+ eyebrow="After",
+ title="Lock agent context like code dependencies.",
+ subtitle=(
+ "Put docs, specs, packages, repos, feeds, and datasets in "
+ "docpull.yaml, then install a reproducible context lockfile."
+ ),
+ terminal_title="project lockfile",
+ lines=(
+ Line("$ docpull init agent-context", "command"),
+ Line("created docpull.yaml", "success"),
+ Line("$ docpull add stripe react openai", "command"),
+ Line("resolved 3 aliases to HTTPS sources", "success"),
+ Line("$ docpull install", "command"),
+ Line("validated dependencies", "success"),
+ Line("wrote .docpull/context.lock.json", "success"),
+ Line("", "dim"),
+ Line("sources, aliases, hashes, run IDs, exports: locked", "info"),
+ ),
+ ),
+ Card(
+ filename="03-sync-diff.png",
+ eyebrow="Review",
+ title="See source changes before agents use them.",
+ subtitle=(
+ "A sync turns external drift into a diff your team can review instead of a silent prompt change."
+ ),
+ terminal_title="docpull diff",
+ lines=(
+ Line("$ docpull sync", "command"),
+ Line("synced 3 context dependencies", "success"),
+ Line("wrote .docpull/runs/", "dim"),
+ Line("$ docpull diff", "command"),
+ Line("Project diff: +4 -2 ~18 api=2 pricing=1", "info"),
+ Line("", "dim"),
+ Line("Changed pages:", "normal"),
+ Line("- /payments/payment-intents likely API behavior change", "warning"),
+ Line("- /billing/subscriptions pricing / billing change", "warning"),
+ Line("- /webhooks likely API behavior change", "warning"),
+ ),
+ ),
+ Card(
+ filename="04-v3-pack-contract.png",
+ eyebrow="Mechanism",
+ title="One contract. Many source types.",
+ subtitle=(
+ "Every lane writes the same v3 pack shape, so validation, "
+ "citations, exports, and CI work the same way."
+ ),
+ terminal_title="packs/stripe-docs",
+ lines=(
+ Line("$ docpull pack validate packs/stripe-docs --level eval", "command"),
+ Line("raw corpus.manifest.json", "success"),
+ Line("raw sources.md", "success"),
+ Line("raw acquisition.routes.json", "success"),
+ Line("agent context.lock.json", "info"),
+ Line("agent coverage.report.json", "info"),
+ Line("agent citation.index.json", "info"),
+ Line("agent pack.score.json + pack.audit.json", "info"),
+ Line("eval rights.manifest.json", "warning"),
+ Line("eval provenance.graph.json", "warning"),
+ Line("eval basis.ndjson + basis.report.json", "warning"),
+ Line("eval PACK_CARD.md", "warning"),
+ ),
+ ),
+ Card(
+ filename="05-context-ci.png",
+ eyebrow="Guardrail",
+ title="Make bad context fail CI.",
+ subtitle=(
+ "Treat weak citation coverage or missing eval artifacts like a failed test, not an agent mystery."
+ ),
+ terminal_title="docpull ci --prepare",
+ lines=(
+ Line("$ docpull ci --prepare", "command"),
+ Line("project_lockfile pass", "success"),
+ Line("pack_score pass 91", "success"),
+ Line("audit_score pass 94", "success"),
+ Line("coverage_confidence pass high", "success"),
+ Line("citation_coverage fail 0.72", "error"),
+ Line("eval_grade_artifacts pass", "success"),
+ Line("rights_status warn unknown", "warning"),
+ Line("", "dim"),
+ Line("Context CI failed: 4 pass, 1 warn, 1 fail", "error"),
+ Line("see context-ci.report.json", "dim"),
+ ),
+ ),
+ Card(
+ filename="06-export-agent-context.png",
+ eyebrow="Ship",
+ title="Send the same pack everywhere agents work.",
+ subtitle=(
+ "Export the locked, cited context to Cursor, Codex, OpenAI, LangChain, MCP, and RAG pipelines."
+ ),
+ terminal_title="agent exports",
+ lines=(
+ Line("$ docpull export context-pack --target cursor", "command"),
+ Line("wrote context.md + citations.json", "success"),
+ Line("wrote Cursor rule references", "success"),
+ Line("$ docpull export context-pack --target codex", "command"),
+ Line("wrote Codex skill references", "success"),
+ Line("$ docpull export context-pack --target openai", "command"),
+ Line("wrote openai-vector.jsonl", "success"),
+ Line("", "dim"),
+ Line("export recorded in .docpull/context.lock.json", "info"),
+ ),
+ footer="pip install docpull",
+ ),
+)
+
+
+def font(path: str, size: int, fallback: str = "") -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
+ candidates = [
+ Path(path),
+ Path(fallback) if fallback else None,
+ Path("/System/Library/Fonts/Supplemental/Arial.ttf"),
+ Path("/System/Library/Fonts/Supplemental/Arial Bold.ttf"),
+ Path("/Library/Fonts/Arial.ttf"),
+ ]
+ for candidate in candidates:
+ if candidate and candidate.exists():
+ try:
+ return ImageFont.truetype(str(candidate), size=size)
+ except OSError:
+ continue
+ return ImageFont.load_default()
+
+
+REG = font("/System/Library/Fonts/Supplemental/Arial.ttf", 34)
+BOLD = font("/System/Library/Fonts/Supplemental/Arial Bold.ttf", 62)
+SMALL = font("/System/Library/Fonts/Supplemental/Arial.ttf", 25)
+MONO = font("/System/Library/Fonts/Menlo.ttc", 28)
+MONO_SMALL = font("/System/Library/Fonts/Menlo.ttc", 24)
+
+
+def text_size(draw: ImageDraw.ImageDraw, value: str, fnt: ImageFont.ImageFont) -> tuple[int, int]:
+ box = draw.textbbox((0, 0), value, font=fnt)
+ return box[2] - box[0], box[3] - box[1]
+
+
+def wrap(draw: ImageDraw.ImageDraw, value: str, fnt: ImageFont.ImageFont, max_width: int) -> list[str]:
+ words = value.split()
+ lines: list[str] = []
+ current: list[str] = []
+ for word in words:
+ attempt = " ".join([*current, word])
+ if text_size(draw, attempt, fnt)[0] <= max_width or not current:
+ current.append(word)
+ else:
+ lines.append(" ".join(current))
+ current = [word]
+ if current:
+ lines.append(" ".join(current))
+ return lines
+
+
+def style_color(style: str) -> str:
+ return {
+ "command": ACCENT,
+ "success": SUCCESS,
+ "warning": WARNING,
+ "error": ERROR,
+ "info": ACCENT_DIM,
+ "dim": DIM,
+ "normal": TEXT,
+ }.get(style, TEXT)
+
+
+def draw_card(card: Card) -> None:
+ image = Image.new("RGB", (WIDTH, HEIGHT), BG)
+ draw = ImageDraw.Draw(image)
+
+ # Background bands.
+ draw.rectangle((0, 0, WIDTH, HEIGHT), fill=BG)
+ draw.rectangle((0, 0, WIDTH, 12), fill=STRIPE)
+ draw.rectangle((0, HEIGHT - 12, WIDTH, HEIGHT), fill=STRIPE)
+
+ left = 86
+ top = 62
+ max_text = 640
+
+ draw.text((left, top), card.eyebrow.upper(), font=MONO_SMALL, fill=ACCENT)
+ y = top + 54
+ for line in wrap(draw, card.title, BOLD, max_text):
+ draw.text((left, y), line, font=BOLD, fill=TEXT)
+ y += 72
+ y += 18
+ for line in wrap(draw, card.subtitle, REG, max_text):
+ draw.text((left, y), line, font=REG, fill=MUTED)
+ y += 46
+
+ # Footer pill.
+ footer_w, footer_h = text_size(draw, card.footer, MONO_SMALL)
+ draw.rounded_rectangle(
+ (left, HEIGHT - 106, left + footer_w + 42, HEIGHT - 58),
+ radius=12,
+ fill=PILL,
+ outline=PILL_BORDER,
+ width=2,
+ )
+ draw.text((left + 21, HEIGHT - 96), card.footer, font=MONO_SMALL, fill=TEXT)
+
+ # Terminal panel.
+ panel_x = 780
+ panel_y = 86
+ panel_w = 730
+ panel_h = 720
+ draw.rounded_rectangle(
+ (panel_x, panel_y, panel_x + panel_w, panel_y + panel_h),
+ radius=24,
+ fill=PANEL,
+ outline=BORDER,
+ width=3,
+ )
+ draw.rounded_rectangle(
+ (panel_x, panel_y, panel_x + panel_w, panel_y + 70),
+ radius=24,
+ fill=HEADER,
+ )
+ draw.rectangle((panel_x, panel_y + 46, panel_x + panel_w, panel_y + 70), fill=HEADER)
+ for i, color in enumerate(("#7c828b", "#a1a6ad", "#c8ccd0")):
+ draw.ellipse((panel_x + 26 + i * 34, panel_y + 26, panel_x + 44 + i * 34, panel_y + 44), fill=color)
+ draw.text((panel_x + 138, panel_y + 22), card.terminal_title, font=MONO_SMALL, fill=MUTED)
+
+ x = panel_x + 34
+ y = panel_y + 104
+ line_h = 40
+ for item in card.lines:
+ if not item.text:
+ y += 22
+ continue
+ max_chars = 38
+ wrapped = textwrap.wrap(item.text, width=max_chars, subsequent_indent=" ") or [item.text]
+ for line in wrapped:
+ draw.text((x, y), line, font=MONO, fill=style_color(item.style))
+ y += line_h
+ if y > panel_y + panel_h - 54:
+ break
+
+ # Tiny source label.
+ draw.text(
+ (panel_x + 34, panel_y + panel_h + 22),
+ "github.com/raintree-technology/docpull",
+ font=MONO_SMALL,
+ fill=DIM,
+ )
+
+ OUT.mkdir(parents=True, exist_ok=True)
+ image.save(OUT / card.filename, optimize=True)
+
+
+def main() -> None:
+ for card in CARDS:
+ draw_card(card)
+ print(f"wrote {len(CARDS)} cards to {OUT}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/social/v6-launch/twitter-thread.md b/docs/social/v6-launch/twitter-thread.md
new file mode 100644
index 0000000..29681d1
--- /dev/null
+++ b/docs/social/v6-launch/twitter-thread.md
@@ -0,0 +1,212 @@
+# DocPull v6 Twitter/X Thread
+
+Use one image per tweet. Put the repo or PyPI link in the final tweet or first reply to keep the early tweets cleaner.
+
+## Assets
+
+Generate or refresh the PNGs:
+
+```bash
+python3 docs/social/v6-launch/generate_terminal_cards.py
+```
+
+Generated images:
+
+- `assets/01-context-drift.png`
+- `assets/02-lockfile-workflow.png`
+- `assets/03-sync-diff.png`
+- `assets/04-v3-pack-contract.png`
+- `assets/05-context-ci.png`
+- `assets/06-export-agent-context.png`
+
+## Thread
+
+### 1/8
+
+The first AI agent bug I keep seeing is not hallucination.
+
+It's context drift.
+
+The model may be fine. The docs it sees are stale, uncited, copied from Slack, or embedded months ago.
+
+DocPull v6 turns agent context into something you can lock, diff, and test.
+
+Attach: `assets/01-context-drift.png`
+
+Alt text: Terminal-style graphic showing stale agent context files with no citations, no diff, no CI gate, and no provenance.
+
+### 2/8
+
+You already solve this for code.
+
+`package-lock.json` tells you exactly what your app depends on.
+
+Agents need the same thing for docs, specs, repos, packages, feeds, datasets, and standards.
+
+That's the category: context dependencies.
+
+Attach: `assets/02-lockfile-workflow.png`
+
+Alt text: Terminal-style graphic showing `docpull init`, `docpull add stripe react openai`, and `docpull install` writing `.docpull/context.lock.json`.
+
+### 3/8
+
+The core workflow:
+
+```bash
+docpull init agent-context
+docpull add stripe react openai
+docpull install
+docpull sync
+docpull diff
+docpull ci --prepare
+docpull export context-pack --target codex
+```
+
+This makes source changes reviewable before they become model behavior.
+
+Attach: none, or reuse `assets/02-lockfile-workflow.png` if you want every tweet to have media.
+
+### 4/8
+
+v6 is built around one mechanism: the v3 context-pack contract.
+
+Every ingestion lane normalizes into the same artifact shape: web, local docs, OpenAPI, feeds, papers, repos, packages, standards, datasets, transcripts, wiki.
+
+One contract. Many sources.
+
+Attach: `assets/04-v3-pack-contract.png`
+
+Alt text: Terminal-style graphic showing v3 pack validation across raw artifacts, agent sidecars including coverage and audit files, and eval-grade rights, provenance, basis, and pack-card artifacts.
+
+### 5/8
+
+That contract matters because agents don't just need text.
+
+They need to know:
+
+- where it came from
+- when it was fetched
+- what changed
+- what can be cited
+- whether it's ready for evals or CI
+
+Markdown is the medium. The artifact is the product.
+
+Attach: none, or `assets/04-v3-pack-contract.png`
+
+### 6/8
+
+Once context is an artifact, CI can enforce it.
+
+`docpull ci --prepare` can fail on stale runs, weak citation coverage, missing eval-grade sidecars, lockfile drift, or scores below your thresholds.
+
+Bad context should fail before it reaches the agent.
+
+Attach: `assets/05-context-ci.png`
+
+Alt text: Terminal-style graphic showing `docpull ci --prepare` with passing lockfile, pack score, audit score, coverage confidence, and eval artifact gates, plus a failing citation coverage gate and a rights status warning.
+
+### 7/8
+
+DocPull isn't a hosted browser farm or a black-box research API.
+
+It's the local artifact layer between selected sources and agent behavior:
+
+sources -> cited pack -> validation -> export -> agents/RAG/CI
+
+Browser rendering and cloud routes stay explicit.
+
+Attach: `assets/03-sync-diff.png`
+
+Alt text: Terminal-style graphic showing `docpull sync` and `docpull diff` detecting added, removed, changed, API, and pricing-related source updates.
+
+### 8/8
+
+Try it:
+
+```bash
+pip install docpull
+docpull init my-agent-context
+docpull add stripe react openai
+docpull sync
+docpull ci --prepare
+```
+
+https://github.com/raintree-technology/docpull
+
+If your agent depends on external sources, make them dependencies.
+
+Attach: `assets/06-export-agent-context.png`
+
+Alt text: Terminal-style graphic showing project context exported with `docpull export context-pack` to Cursor, Codex, and OpenAI targets, with the export recorded in `.docpull/context.lock.json`.
+
+## Shorter 5-Tweet Version
+
+### 1/5
+
+The first AI agent bug I keep seeing is not hallucination.
+
+It's context drift.
+
+DocPull v6 turns agent context into a dependency workflow: declare sources, lock them, sync them, diff them, and check them in CI.
+
+Attach: `assets/01-context-drift.png`
+
+### 2/5
+
+The workflow:
+
+```bash
+docpull init agent-context
+docpull add stripe react openai
+docpull install
+docpull sync
+docpull diff
+```
+
+This gives you `docpull.yaml`, `.docpull/context.lock.json`, run artifacts, and source diffs your team can review before context reaches an agent.
+
+Attach: `assets/02-lockfile-workflow.png`
+
+### 3/5
+
+v6 centers on one mechanism: the v3 context-pack contract.
+
+Web docs, OpenAPI specs, feeds, repos, packages, standards, datasets, transcripts, and local files all normalize into the same artifact shape.
+
+Raw -> agent-ready -> eval-grade.
+
+Attach: `assets/04-v3-pack-contract.png`
+
+### 4/5
+
+Then CI can enforce context quality:
+
+- lockfile matches project
+- citations are present
+- pack/audit scores pass
+- rights/provenance sidecars exist
+- stale or weak context fails before it reaches the agent
+
+```bash
+docpull ci --prepare
+```
+
+Attach: `assets/05-context-ci.png`
+
+### 5/5
+
+DocPull is the local artifact layer for agent context:
+
+sources -> cited pack -> validation -> export -> agents/RAG/CI
+
+```bash
+pip install docpull
+```
+
+https://github.com/raintree-technology/docpull
+
+If your agent depends on external sources, make them dependencies.
+
+Attach: `assets/06-export-agent-context.png`
diff --git a/docs/social/v6.1-benchmark/README.md b/docs/social/v6.1-benchmark/README.md
new file mode 100644
index 0000000..470148c
--- /dev/null
+++ b/docs/social/v6.1-benchmark/README.md
@@ -0,0 +1,39 @@
+# DocPull 6.1 benchmark social images
+
+LinkedIn-ready 4:5 carousel images for the DocPull 6.1.0 benchmark post.
+
+Recommended carousel order:
+
+1. `01-cover.png`
+2. `02-core-results.png`
+3. `03-boundaries.png`
+4. `04-pdf-isolation.png`
+5. `05-integrity.png`
+6. `06-evidence.png`
+
+The `screenshots/` directory contains direct captures of the public PyPI,
+GitHub release, benchmark comparison, and evidence-status pages. These work
+best as supporting images after the designed carousel or in a follow-up post.
+
+For a single-image post, use `assets/02-core-results.png`. For the full story,
+publish all six numbered cards in order. Keep `03-boundaries.png` in any
+carousel that includes the core score so the acquisition boundary is visible.
+
+Additional existing product images that can accompany a follow-up post:
+
+- `../../launch-assets/screenshot-hero-desktop-1280x720.png`
+- `../../launch-assets/docpull-project-diff-demo.png`
+- `../../launch-assets/docpull-routing-table-v5.png`
+- `../../launch-assets/docpull-evidence-formats-table-v5.png`
+- `../v6-launch/assets/contact-sheet.png`
+
+Regenerate the cards from the repository root:
+
+```bash
+uv run --with pillow python docs/social/v6.1-benchmark/generate_cards.py
+```
+
+The benchmark cards reproduce the stored development result. They must remain
+paired with its limitations: the v5 bundle is public development evidence, is
+not claim-grade, and does not establish comparative superiority for the
+released 6.1.0 wheel.
diff --git a/docs/social/v6.1-benchmark/assets/01-cover.png b/docs/social/v6.1-benchmark/assets/01-cover.png
new file mode 100644
index 0000000..9810635
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/01-cover.png differ
diff --git a/docs/social/v6.1-benchmark/assets/02-core-results.png b/docs/social/v6.1-benchmark/assets/02-core-results.png
new file mode 100644
index 0000000..ec192b3
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/02-core-results.png differ
diff --git a/docs/social/v6.1-benchmark/assets/03-boundaries.png b/docs/social/v6.1-benchmark/assets/03-boundaries.png
new file mode 100644
index 0000000..6eb70d4
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/03-boundaries.png differ
diff --git a/docs/social/v6.1-benchmark/assets/04-pdf-isolation.png b/docs/social/v6.1-benchmark/assets/04-pdf-isolation.png
new file mode 100644
index 0000000..8f16c39
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/04-pdf-isolation.png differ
diff --git a/docs/social/v6.1-benchmark/assets/05-integrity.png b/docs/social/v6.1-benchmark/assets/05-integrity.png
new file mode 100644
index 0000000..c7c1ea7
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/05-integrity.png differ
diff --git a/docs/social/v6.1-benchmark/assets/06-evidence.png b/docs/social/v6.1-benchmark/assets/06-evidence.png
new file mode 100644
index 0000000..d0b77c1
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/06-evidence.png differ
diff --git a/docs/social/v6.1-benchmark/assets/contact-sheet.png b/docs/social/v6.1-benchmark/assets/contact-sheet.png
new file mode 100644
index 0000000..d3cd8cf
Binary files /dev/null and b/docs/social/v6.1-benchmark/assets/contact-sheet.png differ
diff --git a/docs/social/v6.1-benchmark/generate_cards.py b/docs/social/v6.1-benchmark/generate_cards.py
new file mode 100644
index 0000000..aee4cbf
--- /dev/null
+++ b/docs/social/v6.1-benchmark/generate_cards.py
@@ -0,0 +1,374 @@
+#!/usr/bin/env python3
+"""Generate the DocPull 6.1 benchmark LinkedIn carousel."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from PIL import Image, ImageDraw, ImageFont
+
+ROOT = Path(__file__).resolve().parent
+OUT = ROOT / "assets"
+WIDTH = 1080
+HEIGHT = 1350
+
+BG = "#0d0e10"
+PANEL = "#17191d"
+PANEL_2 = "#111316"
+BORDER = "#30343a"
+STRIPE = "#2b3036"
+TEXT = "#f5f4ef"
+MUTED = "#a6abb2"
+DIM = "#727780"
+GREEN = "#177d6d"
+GREEN_LIGHT = "#64b9aa"
+WARM = "#c8b98a"
+RED = "#c98f8f"
+
+
+def font(size: int, *, bold: bool = False, mono: bool = False) -> ImageFont.FreeTypeFont:
+ if mono:
+ candidates = [
+ "/System/Library/Fonts/Menlo.ttc",
+ "/System/Library/Fonts/SFNSMono.ttf",
+ ]
+ elif bold:
+ candidates = [
+ "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
+ "/Library/Fonts/Arial Bold.ttf",
+ ]
+ else:
+ candidates = [
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
+ "/Library/Fonts/Arial.ttf",
+ ]
+ for candidate in candidates:
+ path = Path(candidate)
+ if path.exists():
+ return ImageFont.truetype(str(path), size=size)
+ raise RuntimeError("No supported font found")
+
+
+F_LABEL = font(23, mono=True)
+F_SMALL = font(25)
+F_BODY = font(31)
+F_BODY_BOLD = font(31, bold=True)
+F_TITLE = font(67, bold=True)
+F_HUGE = font(150, bold=True)
+F_METRIC = font(45, bold=True)
+F_MONO = font(25, mono=True)
+
+
+def text_width(draw: ImageDraw.ImageDraw, value: str, fnt: ImageFont.ImageFont) -> int:
+ box = draw.textbbox((0, 0), value, font=fnt)
+ return box[2] - box[0]
+
+
+def wrap(draw: ImageDraw.ImageDraw, value: str, fnt: ImageFont.ImageFont, max_width: int) -> list[str]:
+ lines: list[str] = []
+ for paragraph in value.split("\n"):
+ if not paragraph:
+ lines.append("")
+ continue
+ current: list[str] = []
+ for word in paragraph.split():
+ attempt = " ".join([*current, word])
+ if current and text_width(draw, attempt, fnt) > max_width:
+ lines.append(" ".join(current))
+ current = [word]
+ else:
+ current.append(word)
+ if current:
+ lines.append(" ".join(current))
+ return lines
+
+
+def draw_wrapped(
+ draw: ImageDraw.ImageDraw,
+ xy: tuple[int, int],
+ value: str,
+ fnt: ImageFont.ImageFont,
+ fill: str,
+ max_width: int,
+ line_height: int,
+) -> int:
+ x, y = xy
+ for line in wrap(draw, value, fnt, max_width):
+ draw.text((x, y), line, font=fnt, fill=fill)
+ y += line_height
+ return y
+
+
+def rounded_panel(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], *, fill: str = PANEL) -> None:
+ draw.rounded_rectangle(box, radius=22, fill=fill, outline=BORDER, width=2)
+
+
+def base(page: int, eyebrow: str) -> tuple[Image.Image, ImageDraw.ImageDraw]:
+ image = Image.new("RGB", (WIDTH, HEIGHT), BG)
+ draw = ImageDraw.Draw(image)
+ draw.rectangle((0, 0, WIDTH, 12), fill=STRIPE)
+ draw.rectangle((0, HEIGHT - 12, WIDTH, HEIGHT), fill=STRIPE)
+
+ logo_path = ROOT.parent.parent / "launch-assets" / "logo-transparent-light-1024.png"
+ logo = Image.open(logo_path).convert("RGBA")
+ alpha = logo.getchannel("A")
+ bbox = alpha.getbbox()
+ if bbox:
+ logo = logo.crop(bbox)
+ logo.thumbnail((62, 62), Image.Resampling.LANCZOS)
+ image.paste(logo, (68, 55), logo)
+ draw.text((145, 70), "DOCPULL 6.1.0", font=F_LABEL, fill=TEXT)
+ draw.text((68, 157), eyebrow.upper(), font=F_LABEL, fill=GREEN_LIGHT)
+ draw.text((930, 70), f"{page}/6", font=F_LABEL, fill=DIM)
+ return image, draw
+
+
+def footer(draw: ImageDraw.ImageDraw, text: str = "github.com/raintree-technology/docpull") -> None:
+ draw.line((68, 1262, 1012, 1262), fill=BORDER, width=2)
+ draw.text((68, 1283), text, font=F_LABEL, fill=DIM)
+
+
+def save(image: Image.Image, name: str) -> None:
+ OUT.mkdir(parents=True, exist_ok=True)
+ image.save(OUT / name, optimize=True)
+
+
+def card_1() -> None:
+ image, draw = base(1, "Benchmark result")
+ y = draw_wrapped(
+ draw,
+ (68, 220),
+ "Local extraction kept pace with paid APIs.",
+ F_TITLE,
+ TEXT,
+ 920,
+ 79,
+ )
+ y += 35
+ draw.text((68, y), "100.0%", font=F_HUGE, fill=TEXT)
+ y += 170
+ draw.text((73, y), "strict trial pass", font=F_METRIC, fill=GREEN_LIGHT)
+ y += 78
+ rounded_panel(draw, (68, y, 1012, y + 210))
+ draw.text((102, y + 37), "28 core fixed-URL cases", font=F_BODY_BOLD, fill=TEXT)
+ draw.text((102, y + 91), "2 trials per system · all assertions required", font=F_BODY, fill=MUTED)
+ draw.text((102, y + 145), "$0 provider spend · local compute excluded", font=F_BODY, fill=MUTED)
+ draw.text((68, 1170), "Public development evidence — not claim-grade", font=F_SMALL, fill=WARM)
+ footer(draw)
+ save(image, "01-cover.png")
+
+
+def draw_bar(
+ draw: ImageDraw.ImageDraw,
+ y: int,
+ label: str,
+ value: float,
+ *,
+ highlight: bool = False,
+) -> None:
+ draw.text((68, y), label, font=F_BODY_BOLD if highlight else F_BODY, fill=TEXT)
+ value_text = f"{value:.1f}%"
+ tw = text_width(draw, value_text, F_BODY_BOLD)
+ draw.text((1012 - tw, y), value_text, font=F_BODY_BOLD, fill=TEXT if highlight else MUTED)
+ track_y = y + 55
+ draw.rounded_rectangle((68, track_y, 1012, track_y + 34), radius=17, fill="#23262b")
+ end = 68 + int(944 * value / 100)
+ color = GREEN if highlight else "#69717b"
+ draw.rounded_rectangle((68, track_y, end, track_y + 34), radius=17, fill=color)
+
+
+def card_2() -> None:
+ image, draw = base(2, "Core extraction")
+ y = draw_wrapped(draw, (68, 220), "Strict pass rate", F_TITLE, TEXT, 920, 79)
+ draw.text((68, y + 13), "Every predeclared assertion had to pass.", font=F_BODY, fill=MUTED)
+ positions = [430, 575, 720, 865]
+ values = [
+ ("DocPull", 100.0, True),
+ ("Parallel", 96.4, False),
+ ("Exa Full", 94.6, False),
+ ("Tavily", 92.9, False),
+ ]
+ for pos, (label, value, highlight) in zip(positions, values, strict=True):
+ draw_bar(draw, pos, label, value, highlight=highlight)
+ rounded_panel(draw, (68, 1055, 1012, 1198), fill=PANEL_2)
+ draw.text((102, 1089), "DocPull core operational completion", font=F_BODY, fill=MUTED)
+ draw.text((102, 1140), "100.0%", font=F_BODY_BOLD, fill=GREEN_LIGHT)
+ draw.text((360, 1140), "Provider spend $0.00", font=F_BODY_BOLD, fill=TEXT)
+ draw.text((68, 1215), "No significant pairwise difference (Holm p = 1.0).", font=F_SMALL, fill=WARM)
+ footer(draw)
+ save(image, "02-core-results.png")
+
+
+def card_3() -> None:
+ image, draw = base(3, "Boundary conditions")
+ y = draw_wrapped(draw, (68, 220), "Core is not the whole benchmark.", F_TITLE, TEXT, 920, 79)
+ draw.text((68, y + 10), "Strict pass rate across all 32 cases", font=F_BODY, fill=MUTED)
+
+ rows = [
+ ("Parallel", "96.9%"),
+ ("Exa Full", "95.3%"),
+ ("Tavily", "93.8%"),
+ ("DocPull", "87.5%"),
+ ]
+ start_y = 515
+ for index, (label, value) in enumerate(rows):
+ row_y = start_y + index * 93
+ if label == "DocPull":
+ draw.rounded_rectangle((68, row_y - 18, 1012, row_y + 64), radius=14, fill=PANEL)
+ draw.text((93, row_y), label, font=F_BODY_BOLD, fill=TEXT)
+ tw = text_width(draw, value, F_METRIC)
+ draw.text((985 - tw, row_y - 6), value, font=F_METRIC, fill=TEXT)
+
+ rounded_panel(draw, (68, 925, 1012, 1177))
+ draw.text((102, 965), "4 boundary cases", font=F_BODY_BOLD, fill=WARM)
+ draw_wrapped(
+ draw,
+ (102, 1022),
+ "Managed access and robots policy. Hosted systems completed them; DocPull intentionally stopped.",
+ F_BODY,
+ MUTED,
+ 840,
+ 44,
+ )
+ draw.text((68, 1203), "Product boundary, not a hidden quality win.", font=F_SMALL, fill=MUTED)
+ footer(draw)
+ save(image, "03-boundaries.png")
+
+
+def arrow(draw: ImageDraw.ImageDraw, start: tuple[int, int], end: tuple[int, int]) -> None:
+ draw.line((*start, *end), fill=GREEN_LIGHT, width=5)
+ x, y = end
+ draw.polygon(((x, y), (x - 18, y - 12), (x - 18, y + 12)), fill=GREEN_LIGHT)
+
+
+def card_4() -> None:
+ image, draw = base(4, "PDF isolation")
+ y = draw_wrapped(draw, (68, 220), "What changed in 6.1.0", F_TITLE, TEXT, 920, 79)
+ draw.text((68, y + 10), "Remote PDFs now cross a constrained worker boundary.", font=F_BODY, fill=MUTED)
+
+ boxes = [
+ (68, 485, 285, 640, "PDF", "≤ 50 MiB"),
+ (430, 485, 650, 640, "pypdf", "isolated"),
+ (795, 485, 1012, 640, "JSON", "validated"),
+ ]
+ for x1, y1, x2, y2, title, sub in boxes:
+ rounded_panel(draw, (x1, y1, x2, y2))
+ tw = text_width(draw, title, F_BODY_BOLD)
+ draw.text(((x1 + x2 - tw) // 2, y1 + 36), title, font=F_BODY_BOLD, fill=TEXT)
+ sw = text_width(draw, sub, F_SMALL)
+ draw.text(((x1 + x2 - sw) // 2, y1 + 91), sub, font=F_SMALL, fill=MUTED)
+ arrow(draw, (306, 562), (408, 562))
+ arrow(draw, (672, 562), (773, 562))
+
+ draw.text((68, 718), "auto fallback", font=F_LABEL, fill=GREEN_LIGHT)
+ draw.text((68, 766), "pypdf → MarkItDown → Unstructured", font=F_MONO, fill=TEXT)
+
+ items = [
+ "wall, CPU and address-space limits",
+ "credential-stripped worker environment",
+ "process-group cleanup on timeout or cancellation",
+ "100 MiB parsed-output limit",
+ "parser, page count, hash and warning provenance",
+ ]
+ y = 850
+ for item in items:
+ draw.rectangle((70, y + 11, 83, y + 24), fill=GREEN)
+ draw.text((105, y), item, font=F_BODY, fill=MUTED)
+ y += 62
+ footer(draw)
+ save(image, "04-pdf-isolation.png")
+
+
+def card_5() -> None:
+ image, draw = base(5, "Evidence integrity")
+ y = draw_wrapped(draw, (68, 220), "The benchmark was hardened too.", F_TITLE, TEXT, 920, 79)
+ draw.text(
+ (68, y + 10),
+ "6.1.0 makes inflated or stale claims harder to publish.",
+ font=F_BODY,
+ fill=MUTED,
+ )
+
+ items = [
+ ("SCORER V4", "Token boundaries; fused words fail."),
+ ("REPORT V3", "Trial keys and summaries are recomputed."),
+ ("FIXED SCOPE", "Runtime errors cannot move cases out of core."),
+ ("SUBJECT ID", "Wheel hash, version and source revision recorded."),
+ ("SIGNED EVIDENCE", "Publication hashes, GPG verification and escrow."),
+ ]
+ y = 470
+ for label, detail in items:
+ rounded_panel(draw, (68, y, 1012, y + 124), fill=PANEL_2)
+ draw.text((96, y + 25), label, font=F_LABEL, fill=GREEN_LIGHT)
+ draw.text((96, y + 66), detail, font=F_BODY, fill=TEXT)
+ y += 143
+
+ draw.text((68, 1199), "Current v5 bundle status: DATA ONLY", font=F_BODY_BOLD, fill=WARM)
+ footer(draw)
+ save(image, "05-integrity.png")
+
+
+def card_6() -> None:
+ image, draw = base(6, "Read the evidence")
+ y = draw_wrapped(
+ draw,
+ (68, 220),
+ "Results, limitations and release artifacts are public.",
+ F_TITLE,
+ TEXT,
+ 920,
+ 79,
+ )
+
+ entries = [
+ ("BENCHMARK", "bench/results/manual/…/COMPARISON.md"),
+ ("STATUS", "bench/results/STATUS.yaml"),
+ ("RELEASE", "github.com/raintree-technology/docpull/releases/tag/v6.1.0"),
+ ("PYPI", "pypi.org/project/docpull/6.1.0"),
+ ]
+ y = 560
+ for label, path in entries:
+ draw.text((68, y), label, font=F_LABEL, fill=GREEN_LIGHT)
+ draw.text((68, y + 42), path, font=F_MONO, fill=TEXT)
+ draw.line((68, y + 91, 1012, y + 91), fill=BORDER, width=2)
+ y += 143
+
+ rounded_panel(draw, (68, 1120, 1012, 1210))
+ draw.text((102, 1149), 'pip install "docpull[pdf]==6.1.0"', font=F_MONO, fill=TEXT)
+ footer(draw, "Local-first · open source · browser-free by default")
+ save(image, "06-evidence.png")
+
+
+def contact_sheet() -> None:
+ cards = [
+ Image.open(OUT / f"0{index}-{name}.png").convert("RGB")
+ for index, name in [
+ (1, "cover"),
+ (2, "core-results"),
+ (3, "boundaries"),
+ (4, "pdf-isolation"),
+ (5, "integrity"),
+ (6, "evidence"),
+ ]
+ ]
+ thumb_w = 360
+ thumb_h = 450
+ sheet = Image.new("RGB", (thumb_w * 3, thumb_h * 2), BG)
+ for index, card in enumerate(cards):
+ thumb = card.resize((thumb_w, thumb_h), Image.Resampling.LANCZOS)
+ sheet.paste(thumb, ((index % 3) * thumb_w, (index // 3) * thumb_h))
+ sheet.save(OUT / "contact-sheet.png", optimize=True)
+
+
+def main() -> None:
+ card_1()
+ card_2()
+ card_3()
+ card_4()
+ card_5()
+ card_6()
+ contact_sheet()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/sources.md b/docs/sources.md
new file mode 100644
index 0000000..8249a54
--- /dev/null
+++ b/docs/sources.md
@@ -0,0 +1,7 @@
+# Context Pack Sources
+
+Output format: `markdown`.
+
+## Sources
+
+_No records were emitted._
diff --git a/docs/surface-contract.md b/docs/surface-contract.md
index d1fa0ae..4f61c48 100644
--- a/docs/surface-contract.md
+++ b/docs/surface-contract.md
@@ -35,9 +35,9 @@ DocPull targets capability alignment, not 1:1 flag parity. MCP should not mirror
| Capability | CLI | Python SDK/API | MCP | Contract |
| --- | --- | --- | --- | --- |
-| Fetch one URL | `docpull --single` or default crawl entry | `fetch_one`, `fetch_blocking`, `Fetcher` | `fetch_url` | Core-aligned |
+| Fetch one URL | `docpull --single` or `workflow_run(fetch)` | `fetch_one`, `fetch_blocking`, `Fetcher`, `run_workflow` | `fetch_url`, `workflow_run` | Core-aligned |
| Optional rendering | `docpull --render ...`, `docpull render ... --runtime local\|vercel\|e2b`, trusted-target acknowledgement, cloud controls for live smoke, budget caps, prebuilt agent-browser templates, and E2B templates | `RenderConfig`, `Renderer`, `AgentBrowserRenderer`, `VercelSandboxRenderer`, `E2BSandboxRenderer`, `estimate_cloud_render_cost_usd`, `render_url`, `render_url_to_directory`, fetch config | `render_url` with runtime controls; `fetch_url` stays browser-free | Core-aligned |
-| Crawl public web/source | `docpull ` with crawl/output flags | `Fetcher` and config models | `ensure_docs` for named aliases | Adapted |
+| Crawl public web/source | `docpull ` with crawl/output flags and run-scoped result | `Fetcher`, config models, `run_workflow(crawl)` | `ensure_docs`, `workflow_run` | Core-aligned |
| Output Markdown / NDJSON / SQLite / OKF | CLI output flags | Pipeline/config primitives | Indirect through fetched Markdown and pack tools | Adapted |
| List configured sources | Not a primary CLI command | `docpull.mcp.sources` internals, not public SDK | `list_sources` | MCP-focused |
| List cached/indexed sources | Not a primary CLI command | Local filesystem/search helpers | `list_indexed` | MCP-focused |
@@ -48,7 +48,7 @@ DocPull targets capability alignment, not 1:1 flag parity. MCP should not mirror
| Refresh/score/diff/audit context packs | `docpull refresh`, `docpull pack score`, `docpull pack sources`, `docpull pack diff`, `docpull pack audit` | Pack helper modules, `refresh_pack`, `audit_pack` | `refresh_pack`, `pack_score`, `pack_diff`, `audit_pack` | Core-aligned |
| Build pack citations/entities/search/briefs | `docpull pack citations`, `docpull pack entities`, `docpull pack search`, `docpull pack brief` | `build_citation_map`, `extract_pack_entities`, `search_pack`, `build_research_brief` | `pack_citations`, `pack_entities`, `pack_search`, `pack_brief` | Core-aligned |
| Prepare full pack intelligence bundle | `docpull pack prepare` | `prepare_pack` in `docpull.pack_tools` | `pack_prepare` | Core-aligned |
-| Evidence-pack workflow protocol | `brand-pack`, `product-pack`, `styleguide-pack`, `image-pack`, `screenshot-pack`, `policy-pack` | concrete `build_*_pack` builders plus `WorkflowRequest`, `run_workflow`, `async_run_workflow` | `workflow_run` plus dedicated pack tools | Core-aligned |
+| Evidence-pack workflow protocol | `brand-pack`, `product-pack`, `styleguide-pack`, `image-pack`, `screenshot-pack`, `policy-pack`, `relationship-pack`, `dataset-pack` | concrete `build_*_pack` builders plus `WorkflowRequest`, `run_workflow`, `async_run_workflow` | `workflow_run` plus dedicated evidence tools | Core-aligned |
| Tracker import bundle | `docpull pack intelligence-bundle` (`company-brain` alias) | `build_intelligence_bundle` (`build_company_brain_bundle` alias) | `intelligence_bundle` | Core-aligned |
| Context CI and eval-grade context | `docpull ci`, `docpull pack prepare --eval-grade`, `docpull pack validate --level eval` | `run_context_ci`, `validate_pack_contract`, pack preparation helpers | Not exposed yet | Core-aligned |
| Build/query local source graphs | `docpull graph build`, `docpull graph status`, `docpull graph query`, `docpull graph neighbors`, `docpull graph refresh` | `build_graph`, `load_graph`, `graph_status`, `query_graph`, `graph_neighbors`, `refresh_graph` | `graph_build`, `graph_status`, `graph_query`, `graph_neighbors`, `graph_refresh` | Core-aligned |
diff --git a/docs/v6-directory-submission-plan.md b/docs/v6-directory-submission-plan.md
new file mode 100644
index 0000000..fa403d9
--- /dev/null
+++ b/docs/v6-directory-submission-plan.md
@@ -0,0 +1,236 @@
+# DocPull v6 Directory Submission Plan
+
+Verified on 2026-07-04. This plan turns the existing launch kit and visibility
+research into an execution order for DocPull v6.
+
+## Readiness
+
+Ready:
+
+- Public documentation: https://github.com/raintree-technology/docpull
+- GitHub: https://github.com/raintree-technology/docpull
+- PyPI: https://pypi.org/project/docpull/
+- Version: `6.0.0`
+- GitHub topics are filled with relevant Python, MCP, RAG, web extraction, and
+ agent-context terms.
+- PyPI keywords and project URLs include docs, repository, comparison guide,
+ MCP plugin, changelog, releases, and download stats.
+- Launch assets exist under `docs/launch-assets`.
+- v6 X thread assets exist under `docs/social/v6-launch`.
+- GitHub README has package links, MCP install instructions, and clear install
+ CTAs.
+
+Fixed in this launch-prep pass, deploy before submission:
+
+- `/pricing`
+- `/privacy`
+- `/terms`
+- `/llms.txt`
+
+Still recommended before Product Hunt:
+
+- Record a 45-60 second terminal demo from `pip install docpull` to `docpull
+ sync`, `docpull diff`, and `docpull export context-pack --target cursor`.
+- Refresh launch screenshots from the deployed v6 site after the policy pages
+ are live.
+- Decide whether Product Hunt should launch from the website or GitHub URL. Use
+ the website unless the launch story is explicitly "open-source repo first."
+
+## Positioning
+
+Primary category:
+
+- Context dependencies for AI agents.
+
+Short tagline:
+
+- Keep AI agents synced with changing public docs.
+
+Product Hunt tagline:
+
+- Turn public web sources into agent-ready context packs.
+
+Long description:
+
+DocPull is a local-first dependency manager for AI context. Define the public
+docs and web sources an agent depends on, sync them into cited context packs,
+diff what changed, and export reproducible context for Cursor, Claude, OpenAI,
+LlamaIndex, LangChain, MCP clients, and RAG pipelines.
+
+DocPull is browser-free by default and has explicit budget controls for any
+paid-capable cloud route. It fits teams building coding agents,
+RAG systems, MCP tools, and docs-aware automation where stale or uncited context
+should be visible before it changes model behavior.
+
+Third-party API stance:
+
+- Show the adapter ecosystem instead of flattening DocPull into "web scraping."
+ Public v6 adapters include OpenAPI, feeds, repos, packages, papers,
+ standards, datasets, transcripts, wiki pages, parsers, render runtimes, MCP,
+ agent exports, RAG exports, automation exports, and warehouse/table exports.
+- Parallel Web, Tavily, and Exa provider adapter code exists in the repository.
+ Label it as experimental/internal provider code unless the release contract
+ promotes the exact CLI, SDK, MCP, or package-extra surface.
+- Pitch DocPull as the artifact layer that turns selected URLs, files, specs,
+ packages, feeds, datasets, provider records, or research outputs into local
+ packs with lockfiles, citations, validation, diffs, and exports.
+
+## Submission Order
+
+### Batch 0: Metadata and owned surfaces
+
+Do this first because every directory links back here.
+
+1. Deploy the web changes for `/pricing`, `/privacy`, `/terms`, and
+ `/llms.txt`.
+2. Confirm the deployed routes return 200.
+3. Publish or tag the v6 GitHub release if not already done.
+4. Confirm PyPI shows `6.0.0`.
+5. Pin the v6 X thread after posting.
+
+### Batch 1: Developer and MCP surfaces
+
+These are highest fit for DocPull.
+
+1. Official MCP Registry: publish server metadata.
+2. Glama MCP: submit the GitHub repository and inspect the generated server
+ page.
+3. Smithery: publish if the local stdio install flow fits their current server
+ model.
+4. mcp.so: submit through the GitHub issue flow with install command and stdio
+ config.
+5. Cline MCP Marketplace: submit only after a Cline setup test and 400x400 PNG
+ logo are ready.
+6. DevHunt: submit as Developer Tools / Open Source / AI / CLI / Python / MCP.
+7. PyCoder's Weekly: submit the website or GitHub release link.
+8. Python Bytes: send a short email pitch with one terminal example.
+
+### Batch 2: Community launch
+
+Use a technical story, not generic launch copy.
+
+1. r/mcp showcase with install command and MCP tools.
+2. DEV Community or Hashnode tutorial: "Context dependencies for AI agents with
+ DocPull v6."
+3. Hacker News Show HN using `docs/v6-hacker-news-plan.md`; the founder should
+ rewrite the first comment in their own voice before posting.
+4. r/Python or r/webscraping only after the tutorial is live.
+
+### Batch 3: Product launch platforms
+
+Use these after Batch 1 has seeded credibility.
+
+1. Product Hunt.
+2. DevHunt, if not already submitted in Batch 1.
+3. Uneed.
+4. Microlaunch.
+5. Fazier.
+6. BetaList only if we want waitlist/startup exposure; it is weaker for pure
+ OSS devtool usage.
+
+Earliest strong Product Hunt date from today: Tuesday, 2026-07-28. Product Hunt
+allows scheduling up to one month ahead, so schedule during prep rather than
+submitting cold on launch morning.
+
+### Batch 4: GitHub awesome lists and Python directories
+
+Open focused PRs only where DocPull clearly fits.
+
+1. awesome-mcp-servers.
+2. awesome-mcp-devtools.
+3. awesome-web-scraping. Use public web extraction / Python CLI framing only;
+ its contribution rules restrict AI-agent and MCP automation submissions.
+4. best-of-python-dev.
+5. best-of-web-python.
+6. awesome-copilot, only if submitting a real Copilot plugin/skill/tool entry
+ rather than a generic product listing.
+7. awesome-python, as a long-shot after v6 traction is visible.
+8. awesome-agents, awesome-ai-devtools, Awesome-LLMOps, Awesome-RAG, and
+ awesome-harness-engineering only with a precise category fit and proof link.
+
+Detailed GitHub repo targets, suggested categories, and skip/defer notes live in
+[`docs/v6-github-list-targets.md`](v6-github-list-targets.md).
+
+### Batch 5: Long-tail SEO directories
+
+Do these after the core developer surfaces. Avoid paid or reciprocal-badge
+submissions unless there is a clear category fit.
+
+1. AlternativeTo.
+2. SaaSHub.
+3. StackShare.
+4. SourceForge.
+5. Future Tools.
+6. Futurepedia.
+7. There's An AI For That only if paid placement is acceptable.
+
+## Product Hunt Prep Calendar
+
+Assuming a launch on Tuesday, 2026-07-28:
+
+- 2026-07-07: create Product Hunt draft and Upcoming page.
+- 2026-07-08 to 2026-07-20: engage from the maker account with real comments on
+ relevant developer, AI agent, and open-source launches.
+- 2026-07-14: finalize gallery images, demo video, 60-character tagline,
+ 500-character description, maker comment, and first replies.
+- 2026-07-21: send warm heads-up to existing users, friends, and communities.
+ Ask for feedback, not upvotes.
+- 2026-07-27: final route check in incognito: website, GitHub, PyPI, docs,
+ pricing, privacy, terms, demo media, and signup/install path.
+- 2026-07-28: launch at the selected Product Hunt time and keep the maker
+ account available for comments.
+
+## Product Hunt Assets
+
+Name:
+
+- docpull
+
+Tagline:
+
+- Turn public web sources into agent-ready context packs.
+
+Description, under 500 characters:
+
+DocPull is a Python CLI, SDK, and MCP server for turning public docs and web
+sources into cited, refreshable context packs. Declare sources, sync them,
+diff what changed, validate context in CI, and export reproducible context for
+Cursor, Claude, OpenAI, LlamaIndex, LangChain, MCP clients, and RAG.
+
+Launch tags:
+
+- Engineering & Development
+- AI Agents
+- LLMs
+
+Maker comment angle:
+
+DocPull v6 is about making agent context behave more like code dependencies:
+declared, locked, synced, diffed, and tested before it changes model behavior.
+The sharp boundary is intentional: browser-free by default, explicit rendering
+only when needed, and `--budget 0` for no paid-capable routes.
+
+## Measurement
+
+Track weekly:
+
+- Directory submissions sent.
+- Listings live.
+- Backlinks verified.
+- GitHub stars.
+- PyPI downloads.
+- Website referrals.
+- Mentions in ChatGPT, Claude, Perplexity, and Google AI Overviews for
+ "context dependencies", "agent context packs", "Context CI", and
+ "context pack validation".
+
+## Verification Sources
+
+- Product Hunt launch guide: https://www.producthunt.com/launch/preparing-for-launch
+- Product Hunt preparation guide: https://www.producthunt.com/launch/before-launch
+- MCP Registry docs: https://modelcontextprotocol.io/registry/about
+- DevHunt: https://devhunt.org/
+- Smithery publishing docs: https://smithery.ai/docs/build/publish
+- PyCoder's Weekly submissions: https://pycoders.com/submissions
+- Python Bytes contact: https://pythonbytes.fm/home/contact
+- GitHub list target research: docs/v6-github-list-targets.md
diff --git a/docs/v6-directory-todo.md b/docs/v6-directory-todo.md
new file mode 100644
index 0000000..6b35249
--- /dev/null
+++ b/docs/v6-directory-todo.md
@@ -0,0 +1,42 @@
+# DocPull v6 Directory Follow-Ups
+
+## Manual Verification
+
+- Glama MCP: live listing and score badge verified; `punkpeye/awesome-mcp-servers#9276` includes the badge and passes `check-submission`.
+- allMCPservers: complete the Google reCAPTCHA/manual verification for the already-filled official MCP form.
+- MCP.Directory: watch email/listing status because the form cleared after submit but did not show a reliable success banner.
+- HN: add the posted Show HN item URL to `docs/v6-directory-tracker.csv`.
+
+## Site Readiness
+
+- Deploy the restored web routes for `/privacy`, `/terms`, `/pricing`, and `/llms.txt`; they were implemented locally on 2026-07-06 but still need live 200 checks.
+- Add launch screenshots and a 45-60 second demo video before Product Hunt, DevHunt, Fazier, Microlaunch, or Uneed.
+- Product Hunt: schedule only after the launch assets are ready and the founder account is warmed up; do not ask for upvotes, ask for feedback.
+
+## Package / Registry Work
+
+- Cline MCP Marketplace: run a real Cline install test using only `README.md` and/or `llms-install.md`; use the ready `docs/launch-assets/logo-square-light-400.png` asset.
+- Cursor/Windsurf Directory: add Open Plugins metadata (`.mcp.json`) to the GitHub repo, then submit through `https://cursor.directory/plugins/new`.
+- Stacklok ToolHive Catalog: add an OCI image or remote Streamable HTTP MCP server before submitting; current catalog accepts containers/remotes, not PyPI stdio packages.
+- Smithery: add an MCPB bundle or a remote Streamable HTTP MCP server before submitting.
+
+## Account-Gated Launch Surfaces
+
+- AgentLocker: create/verify an account, then submit through `/agent/submit`.
+- OpenTools: sign in and look for an authenticated registry submission path; no public endpoint was found.
+- DevHunt, Fazier, Microlaunch, Uneed: retry after policy/pricing pages and launch assets are deployed.
+
+## Content / Community
+
+- Reddit: post a technical r/mcp/r/SideProject-style walkthrough with install command, tool list, and authorship disclosure.
+- DEV/Hashnode: publish a tutorial, not a raw launch announcement.
+- Awesome-RAG / Awesome-LLMOps / awesome-harness-engineering: submit after a concrete RAG/context-dependency tutorial exists.
+
+## Watchlist
+
+- 2026-07-07 daily monitor: open public PRs/issues remain maintainer-waiting with no requested changes. `punkpeye/awesome-mcp-servers#9276`, `lorien/awesome-web-scraping#262`, and `toolsdk-ai/toolsdk-mcp-registry#381` have passing active checks; `lorien#262` still exposes an older failed run in the rollup. Official MCP Registry, Glama, and MCPRepository listings remain live. mcp.so, mcpservers.org, MCP.Directory, MCP Server Hub, and AiAgents.Directory did not show confirmed live DocPull listing pages in lightweight checks. appcypher still has a one-commit-ahead branch and no PR.
+- 2026-07-06 daily monitor: open PRs/issues remain maintainer-waiting with no requested changes; `punkpeye/awesome-mcp-servers#9276`, `lorien/awesome-web-scraping#262`, and `toolsdk-ai/toolsdk-mcp-registry#381` have passing required checks. MCPRepository is now live with GitHub/PyPI links. mcp.so, mcpservers.org, and PulseMCP did not show new public listing pages in lightweight checks.
+- MCP Market: email request sent; optionally retry the free queue manually, but do not pay for review without approval.
+- MCPCentral: no supported write endpoint found; recheck after Official MCP Registry propagation.
+- AgenticSkills: public submit API returned HTTP 503 because the review queue is not configured; retry later.
+- MCPera and GPTMCP: both appear to be demo-only forms that do not send submissions.
diff --git a/docs/v6-directory-tracker.csv b/docs/v6-directory-tracker.csv
new file mode 100644
index 0000000..441aa58
--- /dev/null
+++ b/docs/v6-directory-tracker.csv
@@ -0,0 +1,57 @@
+Directory,Tier,URL,Category,Priority,Status,Submission Date,Live URL,Backlink Verified,Positioning Variant,Tags,Account Needed,Notes
+Product Hunt,Flagship,https://www.producthunt.com/launch,Launch,High,Blocked - launch assets/deployed pages,,,,Startup,Engineering & Development; AI Agents; LLMs,Yes,"Schedule after /privacy, /terms, /pricing, screenshots, and a 45-60s demo are live; deployed policy/pricing pages returned 404 on 2026-07-04. Local web routes for /privacy, /terms, /pricing, and /llms.txt were restored on 2026-07-06; deploy and verify live 200 responses next."
+Hacker News Show HN,Community,https://news.ycombinator.com/submit,Developer community,High,Submitted,2026-07-04,,,Dev,Show HN; Python; open source; AI agents,Yes,Posted by founder from zacharyr0th; add item URL when available.
+DevHunt,Launch,https://devhunt.org/,Developer tools,High,Ready after deploy,,,,Dev,Developer Tools; Open Source; AI; CLI; Python; MCP,Yes,Best non-PH launch fit.
+Official MCP Registry,MCP,https://modelcontextprotocol.io/registry/about,MCP registry,High,Published,2026-07-04,https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.raintree-technology/docpull,Yes,MCP,MCP; public web context; developer tools; web extraction,Yes,"Published active/latest registry entry for io.github.raintree-technology/docpull version 6.0.1 at 2026-07-04T22:00:42Z after PyPI 6.0.1 marker release and GitHub OIDC workflow publish; reverified 2026-07-07."
+Glama MCP,MCP,https://glama.ai/mcp/servers,MCP registry,High,Published,2026-07-04,https://glama.ai/mcp/servers/raintree-technology/docpull,Yes,MCP,MCP server; web scraping; documentation; RAG,Yes,"Live listing and score badge reverified on 2026-07-07; punkpeye/awesome-mcp-servers #9276 includes the required Glama badge and check-submission passes."
+Smithery,MCP,https://smithery.ai/docs/build/publish,MCP registry,High,Blocked - packaging mismatch,2026-07-04,,,MCP,MCP; local server; developer tools,Yes,Current Smithery publish docs support public HTTPS servers or local MCPB bundles; DocPull is a PyPI stdio server. Needs MCPB bundle or remote Streamable HTTP server before submission.
+mcp.so,MCP,https://mcp.so/,MCP directory,High,Submitted,2026-07-04,https://github.com/chatmcp/mcpso/issues/3016,,MCP,MCP server; web scraping; documentation; developer tools,Yes,"Submitted via GitHub issue from zacharyr0th with install command, stdio config, PyPI link, and 17k+ downloads; issue still open with no maintainer comments on 2026-07-07, and common mcp.so listing paths still show Project not found content."
+Cline MCP Marketplace,MCP,https://github.com/cline/mcp-marketplace,MCP marketplace,Medium,Prep,,,,MCP,MCP; Cline; developer tools; public web context,Yes,"Requires 400x400 PNG logo and confirmation that Cline can install the server using only README.md and/or llms-install.md; ready asset exists at docs/launch-assets/logo-square-light-400.png, but the Cline install test must be completed before submitting."
+PulseMCP,MCP,https://www.pulsemcp.com/servers,MCP directory,High,Outreach Sent,2026-07-04,,,MCP,MCP; public web context; RAG; developer tools,Yes,"Submit page says PulseMCP ingests entries from the Official MCP Registry and to email hello@pulsemcp.com for adjustments; sent listing request from admin@raintree.technology with repo, install command, stdio config, and 17k+ download note."
+mcpservers.org,MCP,https://mcpservers.org/submit,MCP directory,High,Submitted,2026-07-04,,,MCP,MCP; web scraping; development; RAG,Yes,"Submitted free listing with Web Scraping category; site says review within 12 hours and email notification on approval. This is also the required flow for wong2/awesome-mcp-servers; likely listing path checked 2026-07-07 still returned 404."
+MCP Market,MCP,https://mcpmarket.com/submit,MCP directory,Medium,Outreach Sent,2026-07-04,,,MCP,MCP; agent skills; developer tools,Yes,"Chrome showed free queue form, but automation was interrupted before final submit and static access is Vercel-guarded; sent free-queue listing request to support@mcpmarket.com. Do not use $29 paid review without approval."
+MCP Hub / AIMCP,MCP,https://www.aimcp.info/en/submit,MCP directory,Medium,Submitted,2026-07-04,,,MCP,MCP; agent profiles; CLI tools; API docs,Yes,Submitted MCP repository for review via /api/mcps/submit; API returned pending submission id 1450.
+PyCoder's Weekly,Newsletter,https://pycoders.com/submissions,Python newsletter,High,Submitted,2026-07-04,,,Dev,Python; CLI; open source; AI agents,No,"Submitted Google Form with GitHub repo, v6 context-dependencies headline, summary, Zach Roth contact, and admin@raintree.technology."
+Python Bytes,Newsletter,https://pythonbytes.fm/home/contact,Python podcast,High,Outreach Sent,2026-07-04,,,Dev,Python; CLI; web extraction; MCP,No,Sent concise v6 pitch to contact@pythonbytes.fm from admin@raintree.technology.
+r/mcp,Community,https://www.reddit.com/r/mcp/,MCP community,High,Prep,,,,MCP,showcase; MCP; Python; public web context,Yes,"Post install command, tool list, and demo. Disclose authorship."
+DEV Community,Community,https://dev.to/,Developer content,Medium,Prep,,,,Dev,python; showdev; opensource; ai; webscraping,Yes,"Publish tutorial, not announcement."
+awesome-mcp-servers,Awesome list,https://github.com/punkpeye/awesome-mcp-servers,MCP list,High,PR Open,2026-07-04,https://github.com/punkpeye/awesome-mcp-servers/pull/9276,,MCP,MCP; developer tools; documentation,Yes,"Opened fresh PR from zacharyr0th after fork-name collision closed #9275; Glama listing and badge are live, check-submission passes, and PR is mergeable with no reviews requested as of 2026-07-07."
+awesome-mcp-devtools,Awesome list,https://github.com/punkpeye/awesome-mcp-devtools,MCP devtools,High,PR Open,2026-07-04,https://github.com/punkpeye/awesome-mcp-devtools/pull/221,,MCP,MCP; developer tools; CLI; context packs,Yes,Opened PR from zacharyr0th with 17k+ PyPI download proof in PR body.
+awesome-mcp-servers (wong2),Awesome list,https://github.com/wong2/awesome-mcp-servers,MCP list,Medium,Submitted,2026-07-04,,,MCP,MCP; public web context; search; RAG,Yes,"README says PRs are not accepted; submitted through https://mcpservers.org/submit, which is the required route."
+awesome-mcp-servers (appcypher),Awesome list,https://github.com/appcypher/awesome-mcp-servers,MCP list,Medium,Blocked,2026-07-04,https://github.com/zacharyr0th/awesome-mcp-servers-appcypher/tree/add-docpull-appcypher,,MCP,MCP; documentation; search; RAG,Yes,"Renamed fork and branch were pushed, but GitHub PR creation is blocked for this repo from zacharyr0th; compare still shows the branch one commit ahead and no DocPull PR exists as of 2026-07-07. Use compare/manual PR path if available."
+awesome-mcp-servers (TensorBlock),Awesome list,https://github.com/TensorBlock/awesome-mcp-servers,MCP index,High,PR Open,2026-07-04,https://github.com/TensorBlock/awesome-mcp-servers/pull/1064,,MCP,MCP; web scraping; context packs; developer tools,Yes,Opened PR from zacharyr0th using custom fork awesome-mcp-servers-tensorblock; strong fit with Browser Automation & Web Scraping category and hosted searchable profiles.
+awesome-copilot,Awesome list,https://github.com/github/awesome-copilot,Copilot tools,Medium,Needs packaged entry,,,,MCP,Copilot; MCP; skill; plugin; developer tools,Yes,"Only submit a concrete Copilot skill/plugin/tool entry, not a generic repo listing."
+awesome-web-scraping,Awesome list,https://github.com/lorien/awesome-web-scraping,Web scraping list,Medium,PR Open,2026-07-04,https://github.com/lorien/awesome-web-scraping/pull/262,,Dev,Python; crawler; markdown; web extraction,Yes,"Opened PR from zacharyr0th; replaced auto-rejected #259 with #262 using no restricted terms in the PR body; closed duplicate #260. Current PR is mergeable and the latest required check passes as of 2026-07-07; GitHub's rollup still includes an older failed run."
+awesome-ai-web-scraping,Awesome list,https://github.com/h4ckf0r0day/awesome-ai-web-scraping,AI web scraping list,High,PR Open,2026-07-04,https://github.com/h4ckf0r0day/awesome-ai-web-scraping/pull/11,,MCP,MCP; web scraping; LLM; RAG; agents,Yes,Opened PR from zacharyr0th; strong fit because repo has an MCP Servers for Scraping section and explicitly covers LLM-friendly crawlers and MCP servers.
+best-of-web-python,Awesome list,https://github.com/ml-tooling/best-of-web-python,Python web,Medium,PR Open,2026-07-04,https://github.com/ml-tooling/best-of-web-python/pull/272,,Dev,Python; web; html processing; docs extraction,Yes,Opened projects.yaml PR from zacharyr0th with 17k+ PyPI download proof in PR body.
+best-of-python-dev,Awesome list,https://github.com/ml-tooling/best-of-python-dev,Python devtools,Medium,PR Open,2026-07-04,https://github.com/ml-tooling/best-of-python-dev/pull/279,,Dev,Python; CLI; developer tools; docs,Yes,Opened projects.yaml PR from zacharyr0th with 17k+ PyPI download proof in PR body.
+awesome-python,Awesome list,https://github.com/vinta/awesome-python,Python list,Low,Defer,,,,Dev,Python; web scraping; documentation,Yes,Long-shot due strict acceptance bar; try after visible v6 traction.
+awesome-agents,Awesome list,https://github.com/kyrolabs/awesome-agents,Agent tools,Low,Defer,,,,AI,AI agents; knowledge management; developer tools,Yes,Contribution rules prefer agentic frameworks; submit only with clear agent context-dependency proof.
+awesome-ai-devtools,Awesome list,https://github.com/jamesmurdza/awesome-ai-devtools,AI devtools,Low,Defer,,,,AI,AI developer tools; CLI; context management,Yes,Borderline because entries should be AI-powered; use only if pitch centers agent/MCP workflows.
+Awesome-LLMOps,Awesome list,https://github.com/tensorchord/Awesome-LLMOps,LLMOps,Medium,Prep after tutorial,,,,AI,LLMOps; RAG; data management; search,Yes,Submit after a short RAG/context-dependency tutorial exists.
+Awesome-RAG,Awesome list,https://github.com/Danielskry/Awesome-RAG,RAG,Medium,Prep after tutorial,,,,AI,RAG; context packs; data ingestion; citations,Yes,Better with an example RAG pack or tutorial link.
+awesome-harness-engineering,Awesome list,https://github.com/ai-boost/awesome-harness-engineering,Agent harness,Medium,Prep after article,,,,AI,agent harness; context delivery; MCP; verification,Yes,Likely stronger as a technical article/resource submission than a bare repo.
+Uneed,Launch,https://www.uneed.best/submit-a-tool,Product directory,Medium,Blocked - account/CAPTCHA/payment path,,,,Startup,Developer Tools; AI; Open Source,Yes,Static page exposes Turnstile/Supabase account flow; retry after deployed policy/pricing pages and manual account/CAPTCHA step.
+Microlaunch,Launch,https://microlaunch.net/,Product directory,Medium,Blocked - account/launch prep,,,,Startup,Developer Tools; AI Tools; Productivity,Yes,Account-based launch surface; retry after deployed policy/pricing pages and launch assets.
+Fazier,Launch,https://fazier.com/,Product directory,Medium,Blocked - sign-in/launch prep,,,,Startup,Developer Tools; AI; Open Source,Yes,Sign-in/join gated launch surface; retry after deployed policy/pricing pages and launch assets.
+AlternativeTo,SEO,https://alternativeto.net/,Software alternatives,Medium,Defer,,,,SaaS,web scraping; developer tools; open source,Yes,Better after comparison pages are more complete.
+SaaSHub,SEO,https://www.saashub.com/submit/list,SaaS directory,Low,Defer,,,,SaaS,developer tools; web scraping; AI tools,Yes,Only after alternatives/comparison pages are ready.
+Future Tools,AI directory,https://futuretools.io/submit-a-tool,AI directory,Low,Defer,,,,AI,AI agents; developer tools; research,Yes,"Generic AI audience, use after developer surfaces."
+Futurepedia,AI directory,https://www.futurepedia.io/submit-tool,AI directory,Low,Defer,,,,AI,AI agents; developer tools; productivity,Yes,Check current pricing before spending.
+mcp-audit.dev,MCP,https://mcp-audit.dev/submit/,MCP audit registry,Medium,Issue Open,2026-07-04,https://github.com/adudley78/mcp-audit/issues/35,,MCP,MCP; security; audit; PyPI,Yes,"Submitted PyPI package via GitHub issue #35 with repo, package, network-fetch capability, stdio config, and 17k+ download note; issue still open with no maintainer comments on 2026-07-07."
+ToolSDK MCP Registry,MCP,https://github.com/toolsdk-ai/toolsdk-mcp-registry,MCP registry,Medium,PR Open,2026-07-04,https://github.com/toolsdk-ai/toolsdk-mcp-registry/pull/381,,MCP,MCP; Python; search; extraction; RAG,Yes,"Opened PR adding packages/search-data-extraction/docpull.json with python runtime, docpull package, docpull mcp bin args, MIT license, and repo/readme links; biome and package schema checks pass as of 2026-07-07."
+MCP.Directory,MCP,https://mcp.directory/submit,MCP directory,High,Submitted - unconfirmed,2026-07-04,,,MCP,MCP; public web context; RAG; developer tools,No,"Submitted GitHub repo, PyPI package docpull, short description, and admin email; form cleared without visible success or error, so confirm by email/listing."
+ChatMCP MCP Server Market,MCP,https://github.com/chatmcpclient/mcp_server_market,MCP market,Medium,PR Open,2026-07-04,https://github.com/chatmcpclient/mcp_server_market/pull/13,,MCP,MCP; ChatMCP; Python,Yes,"Opened PR adding docpull command config to mcp_server_market.json; PR is mergeable with no maintainer comments as of 2026-07-07."
+MCPRepository,MCP,https://mcprepository.com,MCP directory,Medium,Published,2026-07-04,https://mcprepository.com/raintree-technology/docpull,Yes,MCP,MCP; Python; developer tools,No,"Submitted via npx mcp-index; live listing with GitHub and PyPI links verified on 2026-07-07."
+MCP Server Hub,MCP,https://mcpserverhub.com/submit,MCP directory,Medium,Submitted,2026-07-04,,,MCP,MCP; public web context; developer tools,No,"Submitted embedded Tally form; confirmation said Done and review/display later. Checked 2026-07-07; guessed listing path still returned 404."
+MCP Server Hub .net,MCP,https://mcpserverhub.net/submit,MCP directory,Medium,Submitted,2026-07-04,,,MCP,MCP; developer tools; public web context,No,"Submitted via public /api/submit endpoint; API returned {message: Success}. Checked 2026-07-07; guessed listing path returned 200 but did not expose DocPull content in static HTML."
+allMCPservers,MCP,https://www.allmcpservers.com/,MCP directory,Medium,Blocked - reCAPTCHA,2026-07-04,,,MCP,MCP; developer tools; web extraction,No,"Filled official MCP form with Zach/admin/repo/category Developer Tools, but Google reCAPTCHA loaded and no reliable success/error appeared; needs manual verification."
+MCPCentral,MCP,https://mcpcentral.io,MCP directory,Medium,Blocked - no write endpoint,2026-07-04,,,MCP,MCP; developer tools,No,"Search API shows DocPull not indexed; public API is read-only, mcp-submit provider says integration pending, and openapi.yaml is Cloudflare-gated. Likely wait for official registry auto-index or contact if a submit path appears."
+MCPera,MCP,https://mcpera.com/submit.html,MCP directory,Low,Blocked - demo form,2026-07-04,,,MCP,MCP; developer tools,No,"Submit JavaScript only validates locally, shows an alert, and never posts to a server; no real submission recorded."
+GPTMCP,MCP,https://www.gptmcp.ai/submit,MCP directory,Low,Blocked - demo form,2026-07-04,,,MCP,MCP; developer tools,No,"Submit UI only waits locally and shows a success banner; bundled code makes no network request, so no real submission recorded."
+AgenticSkills,MCP,https://agenticskills.io/submit?type=mcp,MCP and agent skills directory,Medium,Blocked - broken review queue,2026-07-04,,,MCP,MCP; public web context; developer tools,No,"Public form posts to /api/submissions, but the API returned HTTP 503 with 'Submission review queue is not configured.'"
+AiAgents.Directory,AI directory,https://aiagents.directory/submit/,AI agents/tools directory,Medium,Submitted,2026-07-04,,,AI,AI tool; agent infrastructure; RAG; developer tools,No,"Submitted as an AI tool/infrastructure for agents using admin@raintree.technology; site redirected to /submit/success/. Checked 2026-07-07; guessed listing path still returned 404."
+AgentLocker,AI directory,https://agentlocker.ai/submit-your-tool,AI agents/tools directory,Medium,Blocked - account required,2026-07-04,,,AI,AI tools; agent infrastructure; developer tools,Yes,Submission requires sign-in at /auth/login?next=/agent/submit; create/verify account before posting.
+OpenTools,MCP,https://opentools.com/registry,MCP registry,Medium,Blocked - sign-in/no public submit,2026-07-04,,,MCP,MCP; developer tools,Yes,Registry page is Clerk-gated with no public submit endpoint found.
+Stacklok ToolHive Catalog,MCP,https://github.com/stacklok/toolhive-catalog,MCP runtime catalog,Medium,Blocked - packaging mismatch,2026-07-04,,,MCP,MCP; ToolHive; runtime catalog,Yes,Catalog contribution docs support OCI container packages or remote HTTP servers only; DocPull is currently a PyPI stdio server.
+Cursor/Windsurf Directory,MCP,https://cursor.directory/plugins/new,MCP/client plugin directory,Medium,Blocked - needs Open Plugins metadata,2026-07-04,,,MCP,MCP; Cursor; Windsurf; developer tools,Yes,Data is submitted through website after sign-in; repo README says it auto-detects MCP servers from .mcp.json. Add Open Plugins .mcp.json to DocPull and then submit the GitHub repo through cursor.directory/plugins/new.
diff --git a/docs/v6-github-list-targets.md b/docs/v6-github-list-targets.md
new file mode 100644
index 0000000..33745d1
--- /dev/null
+++ b/docs/v6-github-list-targets.md
@@ -0,0 +1,146 @@
+# DocPull v6 GitHub List Targets
+
+Verified on 2026-07-04 from GitHub repository pages, raw contribution files,
+and GitHub API metadata. Use this as the PR target list for the GitHub
+"awesome list" portion of the directory launch.
+
+## Highest-Fit PR Order
+
+1. `punkpeye/awesome-mcp-servers`
+ - URL: https://github.com/punkpeye/awesome-mcp-servers
+ - Fit: MCP server directory with relevant categories for Developer Tools,
+ Knowledge & Memory, Search & Data Extraction, and end-to-end RAG.
+ - Submission angle: local-first MCP server for fetching, indexing,
+ diffing, and exporting cited public docs as agent context packs.
+ - Next step: submit after the official MCP Registry or Glama listing is
+ live so the PR has external validation.
+
+2. `punkpeye/awesome-mcp-devtools`
+ - URL: https://github.com/punkpeye/awesome-mcp-devtools
+ - Fit: MCP developer tools, SDKs, libraries, utilities, and resources.
+ - Suggested category: `Utilities` / `Development Tools`.
+ - Submission angle: Python CLI/SDK/MCP utility for producing reproducible
+ context packs from public docs and web sources.
+
+3. `lorien/awesome-web-scraping`
+ - URL: https://github.com/lorien/awesome-web-scraping
+ - Fit: Python web scraping and command-line web extraction lists.
+ - Suggested files: `python.md` and/or `cli.md`.
+ - Submission angle: Python CLI/SDK for public web-to-Markdown extraction
+ with citations and refreshable local packs.
+ - Caveat: the contribution rules restrict AI-agent and MCP automation
+ content, so do not lead with agent/MCP positioning in this PR.
+
+4. `ml-tooling/best-of-python-dev`
+ - URL: https://github.com/ml-tooling/best-of-python-dev
+ - Fit: Python developer tools, updated weekly, accepts issues or PRs to
+ `projects.yaml`.
+ - Suggested category: `documentation`.
+ - Submission angle: developer documentation/context dependency tooling.
+ - Draft YAML:
+
+ ```yaml
+ - name: docpull
+ github_id: raintree-technology/docpull
+ pypi_id: docpull
+ category: documentation
+ ```
+
+5. `ml-tooling/best-of-web-python`
+ - URL: https://github.com/ml-tooling/best-of-web-python
+ - Fit: Python web libraries, updated weekly, accepts issues or PRs to
+ `projects.yaml`.
+ - Suggested category: `html-processing` first, `url-utils` only if the
+ maintainer prefers the URL discovery angle.
+ - Submission angle: public web and docs extraction to structured Markdown
+ / local context packs.
+
+6. `h4ckf0r0day/awesome-ai-web-scraping`
+ - URL: https://github.com/h4ckf0r0day/awesome-ai-web-scraping
+ - Fit: explicit AI web scraping list with a dedicated `MCP Servers for
+ Scraping` section. The README scope includes LLM-friendly crawlers, MCP
+ servers, RAG pipelines, and agents.
+ - Submission angle: local-first Python CLI/SDK/MCP server for turning public
+ static and server-rendered web sources into Markdown, NDJSON, SQLite, and
+ cited context packs.
+ - Caveat: keep the entry concise and in the MCP scraping section; this is
+ not a generic Python scraper listing.
+
+7. `TensorBlock/awesome-mcp-servers`
+ - URL: https://github.com/TensorBlock/awesome-mcp-servers
+ - Fit: active MCP index with a hosted searchable profile site, issue forms,
+ and direct category markdown PRs.
+ - Suggested category: `Browser Automation & Web Scraping`.
+ - Submission angle: MCP server for fetching, caching, searching, validating,
+ and exporting public static/server-rendered web sources as cited local
+ context packs.
+ - Caveat: use a custom fork name because `zacharyr0th/awesome-mcp-servers`
+ is already the fork used for the punkpeye PR.
+
+8. `github/awesome-copilot`
+ - URL: https://github.com/github/awesome-copilot
+ - Fit: tools section covers MCP servers and developer tooling; repo also
+ accepts agents, skills, hooks, workflows, and plugins.
+ - Submission angle: only submit a concrete DocPull Copilot plugin, skill,
+ or MCP tool entry. Do not submit a generic repo listing.
+ - Next step: package the DocPull workflow as a Copilot-ready skill/plugin
+ before opening a PR.
+
+## Secondary Targets
+
+9. `vinta/awesome-python`
+ - URL: https://github.com/vinta/awesome-python
+ - Fit: huge Python list with a Web Scraping category.
+ - Caveat: strict quality bar. Requires Python-first, active, stable,
+ documented, unique, and established projects. Treat as a long-shot until
+ v6 has visible adoption.
+
+10. `kyrolabs/awesome-agents`
+ - URL: https://github.com/kyrolabs/awesome-agents
+ - Fit: has Knowledge Management and Software Development categories.
+ - Caveat: contribution rules focus on agentic frameworks, so DocPull may be
+ considered adjacent rather than core. Submit only with a clear agent
+ context-dependency framing and proof of traction.
+
+11. `jamesmurdza/awesome-ai-devtools`
+ - URL: https://github.com/jamesmurdza/awesome-ai-devtools
+ - Fit: CLI utilities, configuration/context management, documentation
+ generation, and agent infrastructure sections.
+ - Caveat: PR template says entries must be tools that use AI. DocPull is
+ mostly AI-supporting infrastructure, so this is medium-risk unless the
+ pitch leads with Context CI, agent exports, or MCP workflows.
+
+12. `tensorchord/Awesome-LLMOps`
+ - URL: https://github.com/tensorchord/Awesome-LLMOps
+ - Fit: LLMOps list with Search, Data Management, Observability, and
+ LLMOps categories.
+ - Submission angle: context dependency management for RAG and agent
+ pipelines, especially sync/diff/validate/export.
+
+13. `Danielskry/Awesome-RAG`
+ - URL: https://github.com/Danielskry/Awesome-RAG
+ - Fit: RAG tools and resources.
+ - Submission angle: refreshable cited source packs for RAG ingestion.
+ - Next step: publish a short RAG tutorial or example pack first; a bare
+ tool listing is weaker.
+
+14. `ai-boost/awesome-harness-engineering`
+ - URL: https://github.com/ai-boost/awesome-harness-engineering
+ - Fit: context delivery, MCP, verification/CI, and agent harness design.
+ - Submission angle: DocPull as a context delivery and verification
+ component for agent harnesses.
+ - Caveat: this list is article/resource-heavy. A technical post about
+ context dependencies may be a better submission than the repo alone.
+
+## Skip or Watch
+
+- `e2b-dev/awesome-ai-agents`: large but mostly autonomous agents; DocPull is
+ supporting infrastructure, and the repo has less recent activity than better
+ targets.
+- `Shubhamsaboo/awesome-llm-apps`: very high reach, but it focuses on runnable
+ AI Agent and RAG apps. Consider only if DocPull publishes a runnable demo app.
+- `hyp1231/awesome-llm-powered-agent`: stronger for papers/blogs/repos about
+ agents than for developer infrastructure. Defer unless there is a research or
+ technical article.
+- Fresh `awesome-* 2026` scraper/agent repos with heavy self-promotion: skip
+ unless they show stable maintenance and a non-spammy contribution process.
diff --git a/docs/v6-github-pr-recipes.json b/docs/v6-github-pr-recipes.json
new file mode 100644
index 0000000..b108aaf
--- /dev/null
+++ b/docs/v6-github-pr-recipes.json
@@ -0,0 +1,163 @@
+[
+ {
+ "id": "awesome-mcp-servers",
+ "ready": true,
+ "upstream": "punkpeye/awesome-mcp-servers",
+ "base_branch": "main",
+ "branch": "add-docpull-punkpeye",
+ "commit_message": "Add docpull to Developer Tools",
+ "title": "Add docpull to Developer Tools",
+ "body": "Adds DocPull, a local-first Python CLI and MCP server for fetching, caching, searching, validating, and exporting cited public docs and web sources as agent-ready context packs. The entry is placed under Developer Tools because the primary MCP use case is agent context dependency management for developer workflows. DocPull is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "README.md",
+ "mode": "append_markdown_section",
+ "section": "### 💻 Developer Tools",
+ "entry": "- [raintree-technology/docpull](https://github.com/raintree-technology/docpull) 🐍 🏠 🍎 🪟 🐧 - Local-first MCP server and Python CLI for fetching, caching, searching, validating, and exporting cited public docs and web sources as agent-ready context packs. `pip install 'docpull[mcp]'`"
+ }
+ ]
+ },
+ {
+ "id": "awesome-mcp-devtools",
+ "ready": true,
+ "upstream": "punkpeye/awesome-mcp-devtools",
+ "base_branch": "main",
+ "branch": "add-docpull",
+ "commit_message": "Add docpull to MCP development tools",
+ "title": "Add docpull to MCP development tools",
+ "body": "Adds DocPull to the MCP development tools section. DocPull provides a Python CLI/SDK/MCP workflow for syncing, diffing, validating, and exporting public docs and web sources as cited context packs for agents. It is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "README.md",
+ "mode": "append_markdown_section",
+ "section": "### Development Tools",
+ "entry": "- [docpull](https://github.com/raintree-technology/docpull) 🐍 - Local-first CLI/SDK/MCP server for syncing, diffing, validating, and exporting public docs and web sources as cited agent context packs."
+ }
+ ]
+ },
+ {
+ "id": "awesome-web-scraping",
+ "ready": true,
+ "upstream": "lorien/awesome-web-scraping",
+ "base_branch": "master",
+ "branch": "add-docpull-web-extraction",
+ "commit_message": "Add docpull to web scraping tools",
+ "title": "Add docpull to web scraping tools",
+ "body": "Adds DocPull as a Python and command-line tool for public static/server-rendered web extraction. The wording focuses on standalone public web extraction, matching this repository's contribution rules. DocPull is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "python.md",
+ "mode": "append_markdown_section",
+ "section": "### Web Scraping : Tools",
+ "entry": "* [docpull](https://github.com/raintree-technology/docpull) - Public static/server-rendered web extraction toolkit that writes clean Markdown, NDJSON, SQLite, and local archives."
+ },
+ {
+ "file": "cli.md",
+ "mode": "append_markdown_section",
+ "section": "## Web Scraping",
+ "entry": "* [docpull](https://github.com/raintree-technology/docpull) - Public static/server-rendered web extraction CLI that writes clean Markdown, NDJSON, SQLite, and local archives."
+ }
+ ]
+ },
+ {
+ "id": "best-of-python-dev",
+ "ready": true,
+ "upstream": "ml-tooling/best-of-python-dev",
+ "base_branch": "main",
+ "branch": "add-docpull",
+ "commit_message": "Add docpull to documentation tools",
+ "title": "Add docpull to documentation tools",
+ "body": "Adds DocPull to the documentation category. DocPull is a Python CLI/SDK for declaring, syncing, diffing, and exporting docs and web-source context dependencies for agent and developer workflows. It is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "projects.yaml",
+ "mode": "append_yaml_project",
+ "entry": " - name: docpull\n github_id: raintree-technology/docpull\n pypi_id: docpull\n category: documentation"
+ }
+ ]
+ },
+ {
+ "id": "best-of-web-python",
+ "ready": true,
+ "upstream": "ml-tooling/best-of-web-python",
+ "base_branch": "main",
+ "branch": "add-docpull",
+ "commit_message": "Add docpull to web scraping tools",
+ "title": "Add docpull to web scraping tools",
+ "body": "Adds DocPull to the web scraping and crawling category. DocPull is a Python package for extracting public static/server-rendered web sources into clean Markdown, NDJSON, SQLite, and reproducible local packs. It is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "projects.yaml",
+ "mode": "append_yaml_project",
+ "entry": " - name: docpull\n github_id: raintree-technology/docpull\n pypi_id: docpull\n category: web-scraping"
+ }
+ ]
+ },
+ {
+ "id": "awesome-mcp-servers-appcypher",
+ "ready": true,
+ "upstream": "appcypher/awesome-mcp-servers",
+ "fork_name": "awesome-mcp-servers-appcypher",
+ "base_branch": "main",
+ "branch": "add-docpull-appcypher",
+ "commit_message": "Add docpull to Search & Web",
+ "title": "Add docpull to Search & Web",
+ "body": "Adds DocPull to the Search & Web section. DocPull is a local-first Python CLI and MCP server for fetching, caching, searching, validating, and exporting cited public docs and web sources as agent-ready context packs. It is published on PyPI and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "README.md",
+ "mode": "append_markdown_section",
+ "section": "## 🔍 Search & Web",
+ "entry": "- 🐍 [DocPull](https://github.com/raintree-technology/docpull) - Local-first MCP server and Python CLI for fetching, caching, searching, validating, and exporting cited public docs and web sources as agent-ready context packs."
+ }
+ ]
+ },
+ {
+ "id": "awesome-ai-web-scraping",
+ "ready": true,
+ "upstream": "h4ckf0r0day/awesome-ai-web-scraping",
+ "base_branch": "main",
+ "branch": "add-docpull",
+ "commit_message": "Add DocPull to MCP scraping servers",
+ "title": "Add DocPull to MCP scraping servers",
+ "body": "Adds DocPull to the MCP Servers for Scraping section. The project is a Python CLI/SDK/MCP server for turning public static and server-rendered web sources into Markdown, NDJSON, SQLite, and cited context packs for LLM/RAG/agent workflows. It is open source, published on PyPI, and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "README.md",
+ "mode": "append_markdown_section",
+ "section": "## MCP Servers for Scraping",
+ "entry": "- [DocPull](https://github.com/raintree-technology/docpull) - Local-first Python CLI/SDK/MCP server for turning public static and server-rendered web sources into Markdown, NDJSON, SQLite, and cited context packs. "
+ }
+ ]
+ },
+ {
+ "id": "awesome-mcp-servers-tensorblock",
+ "ready": true,
+ "upstream": "TensorBlock/awesome-mcp-servers",
+ "fork_name": "awesome-mcp-servers-tensorblock",
+ "base_branch": "main",
+ "branch": "add-docpull-tensorblock",
+ "commit_message": "Add DocPull to Browser Automation & Web Scraping",
+ "title": "Add DocPull to Browser Automation & Web Scraping",
+ "body": "Adds DocPull to the Browser Automation & Web Scraping category. DocPull is a Python CLI/SDK/MCP server that lets agents fetch, cache, search, validate, and export public static and server-rendered web sources as cited Markdown, NDJSON, SQLite, and local context packs. It is open source, published on PyPI, and has 17k+ downloads.",
+ "changes": [
+ {
+ "file": "docs/browser-automation--web-scraping.md",
+ "mode": "append_markdown_section",
+ "section": "## 🌐 Browser Automation & Web Scraping",
+ "entry": "- [raintree-technology/docpull](https://github.com/raintree-technology/docpull): Python CLI/SDK/MCP server for fetching, caching, searching, validating, and exporting public static and server-rendered web sources as cited Markdown, NDJSON, SQLite, and local context packs. Install: `pip install 'docpull[mcp]'`."
+ }
+ ]
+ },
+ {
+ "id": "awesome-copilot",
+ "ready": false,
+ "upstream": "github/awesome-copilot",
+ "base_branch": "main",
+ "branch": "add-docpull",
+ "commit_message": "Add docpull",
+ "title": "Add docpull",
+ "body": "Deferred until DocPull has a concrete Copilot skill, plugin, or MCP tool entry ready for this repository's format.",
+ "changes": []
+ }
+]
diff --git a/docs/v6-hacker-news-plan.md b/docs/v6-hacker-news-plan.md
new file mode 100644
index 0000000..49e63b2
--- /dev/null
+++ b/docs/v6-hacker-news-plan.md
@@ -0,0 +1,100 @@
+# DocPull Hacker News Plan
+
+Verified on 2026-07-04.
+
+## Official Constraints
+
+- Use Show HN only because DocPull is something people can run and try:
+ https://news.ycombinator.com/showhn.html
+- Submit the original source or primary try-it page. For DocPull, the GitHub
+ repo is the safest target because it has install commands, source, examples,
+ and no signup wall: https://github.com/raintree-technology/docpull
+- Title must begin with `Show HN`; keep it neutral, no hype, no exclamation
+ points: https://news.ycombinator.com/newsguidelines.html
+- Do not ask anyone for upvotes or comments:
+ https://news.ycombinator.com/showhn.html
+- Submit the link first, then add context as a normal comment:
+ https://news.ycombinator.com/newsfaq.html
+- HN says not to post generated or AI-edited text. Treat the title/comment below
+ as an internal draft only; the founder should rewrite it in their own words
+ before posting.
+
+## Current Submit Status
+
+The public submit page currently requires login:
+https://news.ycombinator.com/submit
+
+Status: ready for a logged-in human post. Do not use automation to submit the
+comment.
+
+## Recommended Link
+
+```text
+https://github.com/raintree-technology/docpull
+```
+
+Why: HN Show HN guidelines prefer something people can try. The repo is
+installable, inspectable, and avoids a landing-page-only first impression.
+
+## Title Draft
+
+```text
+Show HN: DocPull - sync public docs into agent-ready context packs
+```
+
+Alternates:
+
+```text
+Show HN: DocPull - local web-to-Markdown packs for coding agents
+Show HN: DocPull - context dependencies for AI agents
+Show HN: DocPull - browser-free public docs extraction for agents
+```
+
+Use the first title unless the README has shifted to a different primary
+positioning. It is specific, technical, and not over-claiming.
+
+## First Comment Draft
+
+Rewrite this in the founder's own voice before posting.
+
+```text
+Hi HN, I built DocPull because coding agents and RAG pipelines often depend on public docs, changelogs, standards, and API references that change underneath them.
+
+The tool is a Python CLI/SDK/MCP server that fetches public static and server-rendered web sources, extracts the useful content, and writes local Markdown, NDJSON, SQLite, or archive outputs with source metadata. The goal is to make web context behave more like a dependency: declared, synced, diffed, cached, validated, and easy to inspect.
+
+It is browser-free by default. For dynamic pages, the boundary is explicit: either use the local rendering path or use a browser automation tool. I wanted the default path to stay auditable and cheap instead of silently turning every docs crawl into a hosted scraping job.
+
+Try it:
+
+pip install docpull
+docpull https://docs.python.org/3/library/asyncio.html --single
+docpull https://www.python.org/blogs/ --max-pages 25 -o ./python-news
+
+For MCP:
+
+pip install 'docpull[mcp]'
+docpull mcp
+
+I would especially like feedback on the boundary between browser-free extraction and full browser automation. What sources would you expect a tool like this to handle, and where should it intentionally stop?
+```
+
+## Posting Checklist
+
+- Confirm the repo README has the simplest install command in the first screen.
+- Confirm PyPI download count is current if mentioning it in comments.
+- Submit the GitHub link with the selected `Show HN:` title.
+- Immediately add the rewritten first comment as a normal comment.
+- Be available for the first 2-3 hours to answer technical questions.
+- Do not message friends, users, or communities asking for upvotes or comments.
+- Share later as "we posted, feedback welcome" only where the audience would
+ naturally care, without asking for votes.
+
+## Likely Questions To Prepare For
+
+- Why not use Firecrawl, Jina Reader, Crawlee, agent-browser, or Scrapy?
+- What does it do when the useful content is behind JavaScript?
+- What anti-abuse and security boundaries exist for agent-selected URLs?
+- How does it preserve citations/provenance?
+- How does `--budget 0` prevent paid-capable routes?
+- What is the MCP tool surface?
+- What is the smallest real workflow where DocPull beats a one-off script?
diff --git a/docs/v6-sales-pr-automation.md b/docs/v6-sales-pr-automation.md
new file mode 100644
index 0000000..b47d40d
--- /dev/null
+++ b/docs/v6-sales-pr-automation.md
@@ -0,0 +1,161 @@
+# DocPull v6 Sales And PR Automation
+
+This is the systematic operating plan for selling DocPull v6 and turning the
+GitHub list work into repeatable PRs from the `zacharyr0th` account.
+
+## Account Baseline
+
+Local state checked on 2026-07-04:
+
+- Git author: `Zachary Roth <100426704+zacharyr0th@users.noreply.github.com>`
+- `gh` active account: `zacharyr0th`
+- Git protocol: SSH
+- Repo remote: `git@github.com:raintree-technology/docpull.git`
+
+This means PR automation can safely use `gh` and forked branches under
+`zacharyr0th`, as long as each PR is reviewed before pushing.
+
+## Sales Motion
+
+DocPull should not be sold as "another scraper." The strongest shelf is:
+
+> Context dependencies for AI agents.
+
+Use that as the main story on agent, MCP, RAG, and devtool surfaces. Use
+"browser-free public web extraction" only where the audience is explicitly
+web-scraping or Python web tooling.
+
+Primary pitch:
+
+> DocPull lets teams declare the public docs and web sources an agent depends
+> on, sync them into cited context packs, diff what changed, and fail CI when
+> the evidence is stale.
+
+The buying argument:
+
+- Agents are only as reliable as the context loaded into them.
+- Vendor docs, API behavior, pricing pages, changelogs, standards, packages,
+ and repos change constantly.
+- Raw URLs and ad hoc browser scraping are not a dependency system.
+- DocPull makes context behave like code dependencies: declared, locked,
+ synced, diffed, validated, and exported.
+
+## Audience Segments
+
+1. Agent/MCP builders
+ - Pain: agents need current docs/tools without relying on raw browser output.
+ - Lead with: MCP server, context packs, `docpull ci`, local-first workflow.
+ - CTA: install `docpull[mcp]`, add server to MCP client, run a docs sync.
+
+2. RAG/search engineers
+ - Pain: ingestion quality and citations are weak links in RAG systems.
+ - Lead with: Markdown/NDJSON/chunks/citations, provenance, refresh/diff.
+ - CTA: build a pack from vendor docs and export JSONL.
+
+3. Python/web extraction developers
+ - Pain: need clean public web extraction without browser automation.
+ - Lead with: static/server-rendered HTML, async fetching, Markdown/SQLite,
+ safety boundary.
+ - CTA: `pip install docpull` and fetch one real docs site.
+
+4. Coding-agent teams
+ - Pain: generated code breaks because docs in context are stale or uncited.
+ - Lead with: context dependencies, lockfile, Context CI, and agent-ready
+ exports.
+ - CTA: add DocPull to CI as a context gate.
+
+## Surface-Specific Positioning
+
+| Surface | Lead with | Avoid |
+|---|---|---|
+| MCP registries | MCP server for cited context dependencies | Generic scraper copy |
+| GitHub MCP lists | Local MCP server, install command, tool surface | Long marketing descriptions |
+| Python lists | Python package, CLI/SDK, clean web extraction | Agent hype |
+| Web-scraping lists | Public static/server-rendered extraction | MCP/agent automation framing |
+| RAG/LLMOps lists | Refreshable cited ingestion packs | "Crawler" as the whole story |
+| Product Hunt | Agent context dependency manager | Dense implementation details |
+| HN/Reddit | Technical boundary and examples | Launch announcement tone |
+
+## PR Automation Policy
+
+Automate the mechanics, not the judgment.
+
+Do:
+
+- Generate a per-repo branch from a small recipe.
+- Keep one upstream PR per repository.
+- Follow the target repo's contribution format exactly.
+- Use one-sentence entries for awesome lists.
+- Create PR bodies that explain fit and disclose maintainer-facing caveats.
+- Log status in `docs/v6-directory-tracker.csv`.
+
+Do not:
+
+- Bulk-open PRs across every repo in one shot.
+- Submit to weak-fit lists just because automation makes it easy.
+- Lead with AI/MCP on `lorien/awesome-web-scraping`; their rules restrict
+ agent/MCP automation content.
+- Open `github/awesome-copilot` until there is a concrete Copilot skill,
+ plugin, or MCP tool listing to submit.
+
+## Automation Workflow
+
+Dry run all recipes:
+
+```bash
+python scripts/github_list_prs.py --recipes docs/v6-github-pr-recipes.json --dry-run
+```
+
+Prepare one local branch and commit without pushing:
+
+```bash
+python scripts/github_list_prs.py \
+ --recipes docs/v6-github-pr-recipes.json \
+ --id awesome-mcp-devtools \
+ --prepare
+```
+
+Push and create a PR after reviewing the local diff:
+
+```bash
+python scripts/github_list_prs.py \
+ --recipes docs/v6-github-pr-recipes.json \
+ --id awesome-mcp-devtools \
+ --prepare \
+ --push \
+ --create-pr
+```
+
+Recommended launch order:
+
+1. Official MCP Registry / Glama / Smithery listings.
+2. `punkpeye/awesome-mcp-servers`.
+3. `punkpeye/awesome-mcp-devtools`.
+4. `ml-tooling/best-of-python-dev`.
+5. `ml-tooling/best-of-web-python`.
+6. `lorien/awesome-web-scraping`, using web extraction framing.
+7. `h4ckf0r0day/awesome-ai-web-scraping`, using MCP scraping-server framing.
+8. `TensorBlock/awesome-mcp-servers`, using Browser Automation & Web Scraping
+ category framing.
+9. Secondary RAG/LLMOps/agent-harness lists only after a tutorial exists.
+
+## PR Review Checklist
+
+Before pushing each PR:
+
+- The entry is in the correct section.
+- The line is concise and factual.
+- The link points to `https://github.com/raintree-technology/docpull`.
+- The pitch matches that repository's audience.
+- The PR title is specific and not promotional.
+- The PR body mentions why DocPull belongs in that exact section.
+- There are no unrelated file changes from the cloned target repo.
+
+## Tracking
+
+Update `docs/v6-directory-tracker.csv` after every submission:
+
+- `Status`: `PR Open`, `Merged`, `Rejected`, `Needs Changes`, or `Deferred`
+- `Submission Date`: ISO date
+- `Live URL`: PR URL first, merged listing URL after merge
+- `Notes`: maintainer feedback or next action
diff --git a/mcp/package.json b/mcp/package.json
index 0ee1255..81b58a7 100644
--- a/mcp/package.json
+++ b/mcp/package.json
@@ -1,60 +1,64 @@
{
- "name": "docpull-mcp-lab",
- "version": "0.3.0",
- "private": true,
- "description": "Internal lab for docpull semantic-search MCP experiments",
- "type": "module",
- "main": "src/server.ts",
- "scripts": {
- "dev": "bun run src/server.ts",
- "ingest": "bun run src/ingest.ts",
- "test": "bun test",
- "typecheck": "tsc --noEmit"
- },
- "knip": {
- "ignoreBinaries": [
- "docpull"
- ]
- },
- "dependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
- "hono": "4.12.27",
- "ip-address": "^10.2.0",
- "openai": "^6.44.0",
- "pg": "^8.22.0",
- "yaml": "^2.9.0",
- "zod": "^4.4.3"
- },
- "overrides": {
- "@hono/node-server": "^2.0.4",
- "ajv": "^8.18.0",
- "fast-uri": "^3.1.2",
- "hono": "4.12.25",
- "ip-address": "^10.2.0",
- "path-to-regexp": "^8.4.2",
- "qs": "^6.15.2",
- "zod": "^4.4.3"
- },
- "devDependencies": {
- "@types/node": "^25.9.1",
- "@types/pg": "^8.11.10",
- "typescript": "^6.0.3"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/raintree-technology/docpull.git",
- "directory": "mcp"
- },
- "license": "MIT",
- "author": "Raintree Technology",
- "keywords": [
- "mcp",
- "web",
- "web-source",
- "documentation",
- "semantic-search",
- "pgvector",
- "embeddings",
- "claude"
- ]
+ "name": "docpull-mcp-lab",
+ "version": "0.3.0",
+ "private": true,
+ "description": "Internal lab for docpull semantic-search MCP experiments",
+ "type": "module",
+ "main": "src/server.ts",
+ "scripts": {
+ "dev": "bun run src/server.ts",
+ "ingest": "bun run src/ingest.ts",
+ "test": "bun test",
+ "typecheck": "tsc --noEmit"
+ },
+ "knip": {
+ "ignoreBinaries": [
+ "docpull"
+ ]
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "hono": "4.12.27",
+ "ip-address": "^10.2.0",
+ "openai": "^6.44.0",
+ "pg": "^8.22.0",
+ "yaml": "^2.9.0",
+ "zod": "^4.4.3"
+ },
+ "overrides": {
+ "@hono/node-server": "^2.0.4",
+ "ajv": "^8.18.0",
+ "fast-uri": "^3.1.2",
+ "hono": "4.12.25",
+ "ip-address": "^10.2.0",
+ "path-to-regexp": "^8.4.2",
+ "qs": "^6.15.2",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@types/node": "^25.9.1",
+ "@types/pg": "^8.11.10",
+ "typescript": "^6.0.3"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/raintree-technology/docpull.git",
+ "directory": "mcp"
+ },
+ "license": "MIT",
+ "author": "Raintree Technology",
+ "keywords": [
+ "mcp",
+ "web",
+ "web-source",
+ "documentation",
+ "semantic-search",
+ "pgvector",
+ "embeddings",
+ "claude"
+ ],
+ "packageManager": "bun@1.3.11",
+ "engines": {
+ "node": "24.x"
+ }
}
diff --git a/mise.lock b/mise.lock
new file mode 100644
index 0000000..734d84b
--- /dev/null
+++ b/mise.lock
@@ -0,0 +1,276 @@
+# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
+
+[[tools.bun]]
+version = "1.3.11"
+backend = "core:bun"
+
+[tools.bun."platforms.linux-arm64"]
+checksum = "sha256:d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-aarch64.zip"
+
+[tools.bun."platforms.linux-arm64-musl"]
+checksum = "sha256:0f5bf5dc3f276053196274bb84f90a44e2fa40c9432bd6757e3247a8d9476a3d"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-aarch64-musl.zip"
+
+[tools.bun."platforms.linux-x64"]
+checksum = "sha256:8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-x64.zip"
+
+[tools.bun."platforms.linux-x64-baseline"]
+checksum = "sha256:abe346f63414547cdf6b35b7a649a490c728b93d006226156923918a84c0e59b"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-x64-baseline.zip"
+
+[tools.bun."platforms.linux-x64-musl"]
+checksum = "sha256:b0fce3bc4fab52f26a1e0d8886dc07fd0c0eb2a274cb343b59c83a2d5997b5b1"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-x64-musl.zip"
+
+[tools.bun."platforms.linux-x64-musl-baseline"]
+checksum = "sha256:2fa2b697f14ada86a28df771d3876ca7606d7453b2339454893b1937aa9c0c7e"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-linux-x64-musl-baseline.zip"
+
+[tools.bun."platforms.macos-arm64"]
+checksum = "sha256:6f5a3467ed9caec4795bf78cd476507d9f870c7d57b86c945fcb338126772ffc"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-darwin-aarch64.zip"
+
+[tools.bun."platforms.macos-x64"]
+checksum = "sha256:c4fe2b9247218b0295f24e895aaec8fee62e74452679a9026b67eacbd611a286"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-darwin-x64.zip"
+
+[tools.bun."platforms.macos-x64-baseline"]
+checksum = "sha256:fb6739b08bf54550edaa7c824cd5b2dca45b6a06afef408443087a63105f6f8d"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-darwin-x64-baseline.zip"
+
+[tools.bun."platforms.windows-x64"]
+checksum = "sha256:066f8694f8b7d8df592452746d18f01710d4053e93030922dbc6e8c34a8c4b9f"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-windows-x64.zip"
+
+[tools.bun."platforms.windows-x64-baseline"]
+checksum = "sha256:9d0e0f923e9626f3bc6044fc32e0d3ab29039aea753f5678ef8801cf26f75288"
+url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.11/bun-windows-x64-baseline.zip"
+
+[[tools.node]]
+version = "24.18.0"
+backend = "core:node"
+
+[tools.node."platforms.linux-arm64"]
+checksum = "sha256:6b4484c2190274175df9aa8f28e2d758a819cb1c1fe6ab481e2f95b463ab8508"
+url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-arm64.tar.gz"
+
+[tools.node."platforms.linux-arm64-musl"]
+checksum = "sha256:b1c6c2dc31b46dd8fb2322f4fe75b07e775c5120bc37251deeea28f529d4567b"
+url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-arm64-musl.tar.gz"
+
+[tools.node."platforms.linux-x64"]
+checksum = "sha256:783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8"
+url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.gz"
+
+[tools.node."platforms.linux-x64-musl"]
+checksum = "sha256:ea58409911e141ec6b19d9178efa2d9185a13295005b1cbf5521b3157eed1d95"
+url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz"
+
+[tools.node."platforms.macos-arm64"]
+checksum = "sha256:e1a97e14c99c803e96c7339403282ea05a499c32f8d83defe9ef5ec66f979ed1"
+url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-arm64.tar.gz"
+
+[tools.node."platforms.macos-x64"]
+checksum = "sha256:dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080"
+url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-x64.tar.gz"
+
+[tools.node."platforms.windows-x64"]
+checksum = "sha256:0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821"
+url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip"
+
+[[tools.python]]
+version = "3.12.13"
+backend = "core:python"
+
+[tools.python."platforms.linux-arm64"]
+checksum = "sha256:b85154b9c7ca9de3f85f2c9f032d503151db16ef198de86b885fc61890c075ed"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-arm64-musl"]
+checksum = "sha256:66f59eb91a2c7e4fe11201e71d23abf1e14730e37d7ae86c68c50124f7c6d5b7"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64"]
+checksum = "sha256:10a452caac7041357805f0c19a60576df53f1ab06d1abfc9200f1f0157cb3bd1"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64-musl"]
+checksum = "sha256:c97f849b60b7baf80c5fe734e905a777d668be86bf530274041682a7a7b239da"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-arm64"]
+checksum = "sha256:41df7d3ae4757e84b97874f76d634268456aaa271740d33f968d826374998fb7"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-x64"]
+checksum = "sha256:a6bbea996c5f14eb55ab275889d2df45408deec504b4a7219d7b59c045b2555e"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.windows-x64"]
+checksum = "sha256:de3e362376859b060fa8b856c434efa81fcf6d4ede3d6e177c7e2169670cac50"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.12.13+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[[tools.python]]
+version = "3.10.20"
+backend = "core:python"
+
+[tools.python."platforms.linux-arm64"]
+checksum = "sha256:2d9a5d103129bd8155f0332f3f876df90364038e59945369fa73fed15e6a2474"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-arm64-musl"]
+checksum = "sha256:c69776d67791f33f14e00224bc19f013d9a01a857ae26e9b381fb09adf3a03b8"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64"]
+checksum = "sha256:ac308036658cee12537c90bf85985f348160663dfab8550f25fc19bf10695573"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64-musl"]
+checksum = "sha256:ada10acaebec935cf04ee1cd580dc52b47f71064ccca75669b5a3241af3eadc2"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-arm64"]
+checksum = "sha256:1b966fa4f23c0b9a68eafa1c5720c86a622d5fbfedd49a38fe17e18987149caf"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-x64"]
+checksum = "sha256:7532d3a1a1b53dd302bc91a409fe3c04f52b7e2b619f95c20cc3a59cbb873721"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.windows-x64"]
+checksum = "sha256:94bd85f03ae02ee090abd694763e5337293d59ee78aae775c1338ac4cb01661f"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.10.20+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[[tools.python]]
+version = "3.11.15"
+backend = "core:python"
+
+[tools.python."platforms.linux-arm64"]
+checksum = "sha256:9ac18c9a761e91e6c6452bc0ef0082922a00a3fdec734555635d57c3169309b7"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-arm64-musl"]
+checksum = "sha256:bcd414f2e9d8cc5d4ac7fab2c8f912e30379c150fa6b94da19d64daf85cfc5d4"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64"]
+checksum = "sha256:0604cd029b142dc223e131f17f5941c0c8d2d5074997c8178b515b19eea2a6c2"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64-musl"]
+checksum = "sha256:45bffdb320cf4a2c2dbf183b634aeb7e28f9cb0646f715b2f4117c11084d839a"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-arm64"]
+checksum = "sha256:2318799eaf104f8a29bc09a93b0851b05dbbcb4ce9a5f045ddea169c0c7ff3a5"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-x64"]
+checksum = "sha256:4925e5aaa9bc77c85302d350b36c1d9def2002996a6bcfa55c88ba6eb318de29"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.windows-x64"]
+checksum = "sha256:6589ca6d63f520bec4096d62b3ab91da3d0a80b16b594c99a6b677e335814683"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.11.15+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[[tools.python]]
+version = "3.13.14"
+backend = "core:python"
+
+[tools.python."platforms.linux-arm64"]
+checksum = "sha256:e931d7a393f54902503f8745ceb35420e7dd50a067e78e5f45c71404f7a15b30"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-arm64-musl"]
+checksum = "sha256:530453a8d47dcf234dbdebb8fdc8fb7277e1edc0f1b6e326e7a80e064318f019"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64"]
+checksum = "sha256:459ed79967acc207bef2ff5124dac35d74d5108528e37b15395d14e2922f2c92"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64-musl"]
+checksum = "sha256:88a95f01ab0cb402822f803facea2f938d9b4cfe08da3d866629ac21c62c1f83"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-arm64"]
+checksum = "sha256:795a5aeeb050f00aa8a2214d779bad9f1b9113edb6923317a80c042a11a087d7"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-x64"]
+checksum = "sha256:fdbe7198fb32f24ab99c67cb68cb265615e0bb6cb6586ffccb629ef9ca868420"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.windows-x64"]
+checksum = "sha256:7c3159205dda0289a47e6dd1226c4e5800b1f99a03146cdc25a41fed8ef74f5a"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.13.14+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[[tools.python]]
+version = "3.14.6"
+backend = "core:python"
+
+[tools.python."platforms.linux-arm64"]
+checksum = "sha256:f177d40ca931df03f660fc006f86ad8cd2ac6e7d6b5d54edbc625103464fc4aa"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-arm64-musl"]
+checksum = "sha256:11d463be3e2d34ea67722acfd8f3f2d6d6e5b6b4d4ab27240f220628134a0141"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64"]
+checksum = "sha256:c172314f4a8ec137a8f605289010c3d19c8b56867d968f0095074cc68efa1d29"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.linux-x64-musl"]
+checksum = "sha256:f25064ecb3b07cfe2440b178e72001cf1e0d69a5e53625ca3a32b7ae4e2fdcc6"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-arm64"]
+checksum = "sha256:35d774f61d63c1fd4f1bc9495a7ada92e500dc4382a0df8a9910eb87ea48e8cf"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-aarch64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.macos-x64"]
+checksum = "sha256:12fb3ea3d6a855fe6ca1c2241702b06cb7e5939fbef09a5f54bf326e480f66af"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-x86_64-apple-darwin-install_only_stripped.tar.gz"
+provenance = "github-attestations"
+
+[tools.python."platforms.windows-x64"]
+checksum = "sha256:ec05b628ad749682d06d225780fbc02e7bbb5ce2146c9bd8e74a3659b14b693a"
+url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.14.6+20260623-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
+provenance = "github-attestations"
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 0000000..9e69666
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,44 @@
+# Generated by the code-estate Mise policy; do not hand-edit.
+# Estate owners: update the central standards, then run `estate mise sync`.
+min_version = "2026.7.1"
+
+[tools]
+"bun" = "1.3.11"
+"node" = "24.18.0"
+"python" = ["3.12.13", "3.10.20", "3.11.15", "3.13.14", "3.14.6"]
+
+[tasks."setup"]
+description = "Install dependencies"
+run = "bun install --frozen-lockfile"
+
+[tasks."lint"]
+description = "Run lint"
+run = "bun run lint"
+
+[tasks."typecheck"]
+description = "Run type checking"
+run = "bun run typecheck"
+
+[tasks."test"]
+description = "Run tests"
+run = "bun run test"
+
+[tasks."test:unit"]
+description = "Run unit tests"
+run = "bun run test:unit"
+
+[tasks."test:e2e"]
+description = "Run end-to-end tests"
+run = "bun run test:e2e"
+
+[tasks."build"]
+description = "Build the project"
+run = "bun run build"
+
+[tasks."validate"]
+description = "Run repository validation"
+run = "bun run validate"
+
+[tasks."validate:full"]
+description = "Run full repository validation"
+run = "bun run validate:full"
diff --git a/package.json b/package.json
index 08b8c84..2abe555 100644
--- a/package.json
+++ b/package.json
@@ -3,18 +3,24 @@
"private": true,
"packageManager": "bun@1.3.11",
"scripts": {
+ "dev": "bun run --cwd mcp dev",
"build": "uv run --locked --with-requirements requirements-release.txt python scripts/build_release.py --verify-reproducible --clean",
- "test": "uv run --locked python -m pytest -q && bun run --cwd mcp test",
- "lint": "uv run --locked python scripts/sync_release_metadata.py --check && uv run --locked python scripts/generate_contract_schemas.py --check && uv run --locked python -m ruff check . && uv run --locked python -m ruff format --check .",
- "typecheck": "uv run --locked python -m mypy src && bun run --cwd mcp typecheck",
+ "test": "uv run --locked --all-extras python -m pytest -q && bun run --cwd mcp test",
+ "test:unit": "uv run --locked --all-extras python -m pytest -q",
+ "test:e2e": "uv run --locked python scripts/real_feature_smoke.py --json",
+ "lint": "uv run --locked --extra dev python scripts/sync_release_metadata.py --check && uv run --locked --extra dev python scripts/generate_contract_schemas.py --check && uv run --locked --extra dev python -m ruff check . && uv run --locked --extra dev python -m ruff format --check . && bun run --cwd web lint",
+ "lint:fix": "uv run --locked --extra dev python -m ruff check . --fix && uv run --locked --extra dev python -m ruff format . && bun run --cwd web lint:fix",
+ "format": "uv run --locked --extra dev python -m ruff format .",
+ "typecheck": "uv run --locked --extra dev python -m mypy src && bun run --cwd mcp typecheck && bun run --cwd web typecheck",
"validate": "bun run lint && bun run typecheck && bun run test",
- "validate:full": "bun run validate && bun run build"
+ "validate:full": "bun run validate && bun run build && bun run --cwd web build"
},
"overrides": {
"postcss": "^8.5.10"
},
"workspaces": [
- "mcp"
+ "mcp",
+ "web"
],
"engines": {
"node": "24.x"
diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json
index e99afd1..73b5ba8 100644
--- a/plugin/.claude-plugin/plugin.json
+++ b/plugin/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "docpull",
- "version": "6.2.0",
+ "version": "6.3.0",
"description": "Pull public web sources into Claude Code. Indexes static and server-rendered sites as local Markdown with conditional-GET caching, then exposes them as MCP tools. Local, browser-free, no API keys.",
"author": {
"name": "Raintree Technology",
diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json
index a8c7c71..13f7c44 100644
--- a/plugin/.codex-plugin/plugin.json
+++ b/plugin/.codex-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"id": "docpull",
"name": "docpull",
- "version": "6.2.0",
+ "version": "6.3.0",
"description": "Pull public web sources into Codex as local, searchable Markdown through docpull's MCP server.",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
@@ -41,7 +41,9 @@
"Manage user-defined source aliases"
],
"websiteURL": "https://github.com/raintree-technology/docpull",
- "brandColor": "#2563EB",
+ "brandColor": "#0F6B5D",
+ "composerIcon": "./assets/logo.svg",
+ "logo": "./assets/logo.svg",
"defaultPrompt": "Use docpull when I ask about a specific library, framework, SDK, API, website, or public source URL. Prefer cached sources first, fetch only the requested source, and cite source paths when answering."
}
}
diff --git a/plugin/README.md b/plugin/README.md
index e64bd40..d9c36bd 100644
--- a/plugin/README.md
+++ b/plugin/README.md
@@ -1,3 +1,11 @@
+
+
+
+
# docpull plugin
Pull static and server-rendered public web sources into Codex or Claude Code. Local, fast, no API keys.
@@ -9,9 +17,9 @@ for the boundary between the plugin's MCP tools and the broader CLI/SDK.
## What you get
-- **MCP server** (34 tools):
+- **MCP server** (35 tools):
- Read: `fetch_url`, `list_sources`, `list_indexed`, `grep_docs`, `read_doc`, `pack_score`, `pack_diff`, `pack_citations`, `pack_entities`, `pack_search`, `pack_brief`, `graph_status`, `graph_query`, `graph_neighbors`, `validate_policy`, `serve_pack_status`
- - Write: `render_url`, `ensure_docs`, `workflow_run`, `brand_pack`, `product_pack`, `styleguide_pack`, `image_pack`, `screenshot_pack`, `policy_pack`, `intelligence_bundle`, `refresh_pack`, `audit_pack`, `pack_prepare`, `graph_build`, `graph_refresh`, `export_pack`, `add_source`, `remove_source`
+ - Write: `render_url`, `ensure_docs`, `workflow_run`, `brand_pack`, `product_pack`, `styleguide_pack`, `image_pack`, `screenshot_pack`, `policy_pack`, `relationship_pack`, `intelligence_bundle`, `refresh_pack`, `audit_pack`, `pack_prepare`, `graph_build`, `graph_refresh`, `export_pack`, `add_source`, `remove_source`
- All read tools advertise `readOnlyHint` so hosts that auto-approve safe tools won't prompt for them.
- **Claude Code slash commands**:
@@ -31,7 +39,7 @@ MCP server is available:
```bash
pip install 'docpull[mcp]' # or: pipx install 'docpull[mcp]'
# uv tool install 'docpull[mcp]'
-docpull --version # should print 6.2.0 or newer
+docpull --version # should print 6.3.0 or newer
docpull mcp --help # confirm the MCP subcommand is wired
```
diff --git a/plugin/assets/logo.svg b/plugin/assets/logo.svg
new file mode 100644
index 0000000..681438e
--- /dev/null
+++ b/plugin/assets/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/pyproject.toml b/pyproject.toml
index 78d33d6..350e329 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "docpull"
-version = "6.2.0"
+version = "6.3.0"
dynamic = []
description = "Declare, sync, diff, and lock context dependencies for AI agents"
readme = {file = "README.md", content-type = "text/markdown"}
diff --git a/scripts/github_list_prs.py b/scripts/github_list_prs.py
new file mode 100644
index 0000000..799bd1e
--- /dev/null
+++ b/scripts/github_list_prs.py
@@ -0,0 +1,274 @@
+#!/usr/bin/env python3
+"""Prepare GitHub awesome-list PRs from local recipes.
+
+The script is dry-run by default. It only writes to external repositories when
+called with --prepare, and it only pushes/opens PRs when --push/--create-pr are
+also passed.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import shutil
+import subprocess # nosec B404
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+# Bandit B404 is suppressed because this script uses fixed executables and
+# direct argument vectors without a shell.
+
+DEFAULT_WORKDIR = Path(".tmp/github-list-prs")
+
+
+@dataclass(frozen=True)
+class Change:
+ file: str
+ mode: str
+ entry: str
+ section: str | None = None
+
+
+@dataclass(frozen=True)
+class Recipe:
+ id: str
+ ready: bool
+ upstream: str
+ fork_name: str | None
+ base_branch: str
+ branch: str
+ commit_message: str
+ title: str
+ body: str
+ changes: tuple[Change, ...]
+
+
+def main() -> int:
+ args = parse_args()
+ recipes = load_recipes(args.recipes)
+ selected = [recipe for recipe in recipes if args.id in {None, recipe.id}]
+ if args.ready_only:
+ selected = [recipe for recipe in selected if recipe.ready]
+ if not selected:
+ print("No recipes selected.", file=sys.stderr)
+ return 1
+
+ for recipe in selected:
+ print_recipe_summary(recipe)
+ if args.dry_run and not args.prepare:
+ continue
+ prepare_recipe(recipe, args)
+ return 0
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--recipes", type=Path, required=True, help="Path to recipe JSON")
+ parser.add_argument("--id", help="Only process one recipe id")
+ parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
+ parser.add_argument("--ready-only", action="store_true", default=True)
+ parser.add_argument("--include-deferred", dest="ready_only", action="store_false")
+ parser.add_argument("--dry-run", action="store_true", default=True)
+ parser.add_argument("--prepare", action="store_true", help="Clone/fork, edit, and commit locally")
+ parser.add_argument("--push", action="store_true", help="Push branch to zacharyr0th fork")
+ parser.add_argument("--create-pr", action="store_true", help="Open PR with gh")
+ parser.add_argument("--force-reclone", action="store_true", help="Remove existing local clone first")
+ return parser.parse_args()
+
+
+def load_recipes(path: Path) -> list[Recipe]:
+ data = json.loads(path.read_text(encoding="utf-8"))
+ recipes: list[Recipe] = []
+ for raw in data:
+ recipes.append(
+ Recipe(
+ id=str(raw["id"]),
+ ready=bool(raw.get("ready")),
+ upstream=str(raw["upstream"]),
+ fork_name=raw.get("fork_name"),
+ base_branch=str(raw["base_branch"]),
+ branch=str(raw["branch"]),
+ commit_message=str(raw["commit_message"]),
+ title=str(raw["title"]),
+ body=str(raw["body"]),
+ changes=tuple(
+ Change(
+ file=str(change["file"]),
+ mode=str(change["mode"]),
+ entry=str(change["entry"]),
+ section=change.get("section"),
+ )
+ for change in raw.get("changes", [])
+ ),
+ )
+ )
+ return recipes
+
+
+def print_recipe_summary(recipe: Recipe) -> None:
+ status = "ready" if recipe.ready else "deferred"
+ print(f"\n[{recipe.id}] {recipe.upstream} ({status})")
+ print(f" branch: {recipe.branch}")
+ print(f" title: {recipe.title}")
+ for change in recipe.changes:
+ target = f"{change.file} :: {change.section}" if change.section else change.file
+ print(f" change: {change.mode} -> {target}")
+ print(f" {change.entry.splitlines()[0]}")
+
+
+def prepare_recipe(recipe: Recipe, args: argparse.Namespace) -> None:
+ if not recipe.ready:
+ raise SystemExit(f"Recipe {recipe.id} is deferred; pass a ready recipe.")
+ ensure_tools()
+
+ _, upstream_name = recipe.upstream.split("/", 1)
+ fork_name = recipe.fork_name or upstream_name
+ repo_dir = args.workdir / fork_name
+ args.workdir.mkdir(parents=True, exist_ok=True)
+
+ if args.force_reclone and repo_dir.exists():
+ shutil.rmtree(repo_dir)
+
+ ensure_fork(recipe.upstream, fork_name)
+ ensure_clone(repo_dir, recipe.upstream, fork_name)
+ run(["git", "fetch", "upstream", recipe.base_branch], cwd=repo_dir)
+ run(["git", "checkout", "-B", recipe.branch, f"upstream/{recipe.base_branch}"], cwd=repo_dir)
+
+ for change in recipe.changes:
+ apply_change(repo_dir / change.file, change)
+
+ run(["git", "diff", "--", *sorted({change.file for change in recipe.changes})], cwd=repo_dir, check=False)
+ run(["git", "add", *sorted({change.file for change in recipe.changes})], cwd=repo_dir)
+
+ diff_check = run(["git", "diff", "--cached", "--quiet"], cwd=repo_dir, check=False, capture=True)
+ if diff_check.returncode == 0:
+ print(" no staged changes; skipping commit")
+ else:
+ run(["git", "commit", "-m", recipe.commit_message], cwd=repo_dir)
+
+ if args.push or args.create_pr:
+ run(["git", "push", "-u", "origin", recipe.branch, "--force-with-lease"], cwd=repo_dir)
+ if args.create_pr:
+ body_path = (repo_dir / ".docpull-pr-body.md").resolve()
+ body_path.write_text(recipe.body + "\n", encoding="utf-8")
+ run(
+ [
+ "gh",
+ "pr",
+ "create",
+ "--repo",
+ recipe.upstream,
+ "--base",
+ recipe.base_branch,
+ "--head",
+ f"zacharyr0th:{recipe.branch}",
+ "--title",
+ recipe.title,
+ "--body-file",
+ str(body_path),
+ ],
+ cwd=repo_dir,
+ )
+
+
+def ensure_tools() -> None:
+ for tool in ("git", "gh"):
+ if shutil.which(tool) is None:
+ raise SystemExit(f"Missing required tool: {tool}")
+
+
+def ensure_fork(upstream: str, fork_name: str) -> None:
+ fork_repo = f"zacharyr0th/{fork_name}"
+ result = run(["gh", "repo", "view", fork_repo], check=False, capture=True)
+ if result.returncode == 0:
+ return
+ run(["gh", "repo", "fork", upstream, "--clone=false", "--fork-name", fork_name])
+
+
+def ensure_clone(repo_dir: Path, upstream: str, name: str) -> None:
+ if not repo_dir.exists():
+ run(["git", "clone", f"https://github.com/zacharyr0th/{name}.git", str(repo_dir)])
+ remotes = run(["git", "remote"], cwd=repo_dir, capture=True).stdout.splitlines()
+ if "upstream" not in remotes:
+ run(["git", "remote", "add", "upstream", f"https://github.com/{upstream}.git"], cwd=repo_dir)
+ else:
+ run(["git", "remote", "set-url", "upstream", f"https://github.com/{upstream}.git"], cwd=repo_dir)
+ run(["git", "remote", "set-url", "origin", f"https://github.com/zacharyr0th/{name}.git"], cwd=repo_dir)
+
+
+def apply_change(path: Path, change: Change) -> None:
+ text = path.read_text(encoding="utf-8")
+ if "docpull" in text.lower():
+ print(f" {path}: docpull already present; leaving unchanged")
+ return
+ if change.mode == "append_markdown_section":
+ if not change.section:
+ raise ValueError("append_markdown_section requires section")
+ text = append_markdown_section(text, change.section, change.entry)
+ elif change.mode == "append_yaml_project":
+ text = append_yaml_project(text, change.entry)
+ else:
+ raise ValueError(f"Unsupported change mode: {change.mode}")
+ path.write_text(text, encoding="utf-8")
+
+
+def append_markdown_section(text: str, section: str, entry: str) -> str:
+ lines = text.splitlines()
+ try:
+ start = next(i for i, line in enumerate(lines) if line.strip() == section)
+ except StopIteration as exc:
+ raise ValueError(f"Section not found: {section}") from exc
+
+ heading_level = len(section) - len(section.lstrip("#"))
+ insert_at = len(lines)
+ for i in range(start + 1, len(lines)):
+ line = lines[i]
+ if line.startswith("#"):
+ level = len(line) - len(line.lstrip("#"))
+ if level <= heading_level:
+ insert_at = i
+ break
+
+ while insert_at > start and not lines[insert_at - 1].strip():
+ insert_at -= 1
+ if insert_at > start and lines[insert_at - 1].strip().lower() == "
":
+ insert_at -= 1
+ while insert_at > start and not lines[insert_at - 1].strip():
+ insert_at -= 1
+ lines.insert(insert_at, entry)
+ return "\n".join(lines) + "\n"
+
+
+def append_yaml_project(text: str, entry: str) -> str:
+ stripped = text.rstrip()
+ return stripped + "\n" + entry.rstrip() + "\n"
+
+
+def run(
+ cmd: list[str],
+ *,
+ cwd: Path | None = None,
+ check: bool = True,
+ capture: bool = False,
+) -> subprocess.CompletedProcess[str]:
+ print(" $ " + " ".join(cmd))
+ # Arguments are passed directly and shell execution is never enabled.
+ result = subprocess.run( # nosec B603
+ cmd,
+ cwd=cwd,
+ check=False,
+ text=True,
+ stdout=subprocess.PIPE if capture else None,
+ stderr=subprocess.PIPE if capture else None,
+ )
+ if check and result.returncode != 0:
+ if capture:
+ print(result.stdout, end="")
+ print(result.stderr, end="", file=sys.stderr)
+ raise SystemExit(result.returncode)
+ return result
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/server.json b/server.json
index 98571c6..9ca8705 100644
--- a/server.json
+++ b/server.json
@@ -8,12 +8,12 @@
"source": "github"
},
"websiteUrl": "https://github.com/raintree-technology/docpull",
- "version": "6.2.0",
+ "version": "6.3.0",
"packages": [
{
"registryType": "pypi",
"identifier": "docpull",
- "version": "6.2.0",
+ "version": "6.3.0",
"transport": {
"type": "stdio"
}
diff --git a/src/docpull/__init__.py b/src/docpull/__init__.py
index 027bcfd..15ec625 100644
--- a/src/docpull/__init__.py
+++ b/src/docpull/__init__.py
@@ -1,244 +1,287 @@
+"""DocPull — reproducible context dependencies for AI agents.
+
+The public SDK is loaded lazily. Importing :mod:`docpull` is common to every
+CLI command and Python submodule import, so the package root must not eagerly
+load optional workflows or rendering backends. Attribute access and
+``from docpull import ...`` retain the public contract while loading only the
+module that owns the requested symbol.
"""
-docpull - Context dependencies for AI agents.
+# ruff: noqa: F401 - TYPE_CHECKING imports document the lazy public re-exports.
-Usage:
- from docpull import Fetcher, DocpullConfig, ProfileName
+from __future__ import annotations
- config = DocpullConfig(
- url="https://docs.example.com",
- profile=ProfileName.RAG,
- )
+from importlib import import_module
+from typing import TYPE_CHECKING, Any
- async with Fetcher(config) as fetcher:
- async for event in fetcher.run():
- print(event)
-"""
+from .surface import PUBLIC_SDK_EXPORTS
-__version__ = "6.2.0"
+__version__ = "6.3.0"
-from .cache import CacheManager, StreamingDeduplicator
-from .context_ci import CIThresholds, ContextCIError, run_context_ci
-from .context_packs import (
- async_build_dataset_pack,
- async_build_package_pack,
- async_build_paper_pack,
- async_build_repo_pack,
- async_build_standards_pack,
- async_build_transcript_pack,
- async_build_wiki_pack,
- build_brand_pack,
- build_dataset_pack,
- build_feed_pack,
- build_image_pack,
- build_openapi_pack,
- build_package_pack,
- build_paper_pack,
- build_policy_pack,
- build_product_pack,
- build_repo_pack,
- build_standards_pack,
- build_styleguide_pack,
- build_transcript_pack,
- build_wiki_pack,
- capture_screenshot_pack,
-)
-from .contracts import (
- ArtifactManifest,
- ChangeEvent,
- EvidenceSpan,
- IntelligenceBundle,
- SourceAuthority,
- WorkflowRequest,
- WorkflowResult,
- bundled_schema_path,
- write_contract_schemas,
-)
-from .conversion.chunking import Chunk, TokenCounter, chunk_markdown
-from .core.fetcher import Fetcher, fetch_blocking, fetch_one
-from .document_parse import DocumentParseError, ParsedDocument, parse_documents, parse_one_document
-from .exports import ExportResult, export_pack
-from .graph import (
- GraphError,
- build_graph,
- graph_neighbors,
- graph_status,
- load_graph,
- query_graph,
- refresh_graph,
-)
-from .local_workflows import audit_pack, refresh_pack
-from .models.config import (
- BudgetConfig,
- CacheConfig,
- ContentFilterConfig,
- CrawlConfig,
- DocpullConfig,
- NetworkConfig,
- OutputConfig,
- PerformanceConfig,
- ProfileName,
- RenderActionPolicy,
- RenderConfig,
- RenderViewport,
-)
-from .models.events import EventType, FetchEvent, FetchStats
-from .output_contract import validate_pack_contract
-from .pack_reader import LocalPack, PackReadError, PackSource, load_pack
-from .pack_tools import (
- build_citation_map,
- build_company_brain_bundle,
- build_intelligence_bundle,
- build_research_brief,
- diff_packs,
- extract_pack_entities,
- prepare_pack,
- score_pack,
- score_pack_sources,
- search_pack,
-)
-from .pipeline.base import PageContext
-from .pipeline.steps import SqliteSearchResult, search_sqlite_documents
-from .policy import PolicyConfig
-from .rendering import (
- AgentBrowserRenderer,
- E2BSandboxRenderer,
- RenderedPage,
- Renderer,
- RenderError,
- RendererUnavailableError,
- VercelSandboxRenderer,
- agent_browser_binary,
- check_agent_browser_availability,
- check_e2b_sandbox_availability,
- check_render_backend_availability,
- check_vercel_sandbox_availability,
- estimate_cloud_render_cost_usd,
- render_url,
- render_url_to_directory,
-)
-from .server import PackASGIApp, PackServerError, create_pack_app
-from .share import ReportHTTPServer, ShareError, create_report_server, render_report_document
-from .surface import PUBLIC_SDK_EXPORTS
-from .workflows import async_run_workflow, create_workflow_request, run_workflow
+_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
+ **{
+ name: (".context_packs", name)
+ for name in (
+ "async_build_dataset_pack",
+ "async_build_package_pack",
+ "async_build_paper_pack",
+ "async_build_repo_pack",
+ "async_build_standards_pack",
+ "async_build_transcript_pack",
+ "async_build_wiki_pack",
+ "build_brand_pack",
+ "build_dataset_pack",
+ "build_feed_pack",
+ "build_image_pack",
+ "build_openapi_pack",
+ "build_package_pack",
+ "build_paper_pack",
+ "build_policy_pack",
+ "build_relationship_pack",
+ "build_product_pack",
+ "build_repo_pack",
+ "build_standards_pack",
+ "build_styleguide_pack",
+ "build_transcript_pack",
+ "build_wiki_pack",
+ "capture_screenshot_pack",
+ )
+ },
+ **{
+ name: (".contracts", name)
+ for name in (
+ "ArtifactManifest",
+ "ChangeEvent",
+ "EvidenceSpan",
+ "IntelligenceBundle",
+ "CoverageResult",
+ "RelationshipCandidate",
+ "RelationshipPack",
+ "SourceAuthority",
+ "WorkflowRequest",
+ "WorkflowResult",
+ "bundled_schema_path",
+ "write_contract_schemas",
+ )
+ },
+ **{
+ name: (".workflows", name)
+ for name in ("async_run_workflow", "create_workflow_request", "run_workflow")
+ },
+ **{name: (".core.fetcher", name) for name in ("Fetcher", "fetch_blocking", "fetch_one")},
+ **{name: (".conversion.chunking", name) for name in ("Chunk", "TokenCounter", "chunk_markdown")},
+ "PageContext": (".pipeline.base", "PageContext"),
+ **{name: (".pipeline.steps", name) for name in ("SqliteSearchResult", "search_sqlite_documents")},
+ **{
+ name: (".models.config", name)
+ for name in (
+ "BudgetConfig",
+ "CacheConfig",
+ "ContentFilterConfig",
+ "CrawlConfig",
+ "DocpullConfig",
+ "NetworkConfig",
+ "OutputConfig",
+ "PerformanceConfig",
+ "ProfileName",
+ "RenderActionPolicy",
+ "RenderConfig",
+ "RenderViewport",
+ )
+ },
+ **{name: (".models.events", name) for name in ("EventType", "FetchEvent", "FetchStats")},
+ **{name: (".cache", name) for name in ("CacheManager", "StreamingDeduplicator")},
+ "PolicyConfig": (".policy", "PolicyConfig"),
+ **{name: (".local_workflows", name) for name in ("audit_pack", "refresh_pack")},
+ **{
+ name: (".document_parse", name)
+ for name in ("DocumentParseError", "ParsedDocument", "parse_documents", "parse_one_document")
+ },
+ **{
+ name: (".pack_tools", name)
+ for name in (
+ "build_citation_map",
+ "build_company_brain_bundle",
+ "build_intelligence_bundle",
+ "build_research_brief",
+ "diff_packs",
+ "extract_pack_entities",
+ "prepare_pack",
+ "score_pack",
+ "score_pack_sources",
+ "search_pack",
+ )
+ },
+ "validate_pack_contract": (".output_contract", "validate_pack_contract"),
+ **{name: (".context_ci", name) for name in ("CIThresholds", "ContextCIError", "run_context_ci")},
+ **{name: (".exports", name) for name in ("ExportResult", "export_pack")},
+ **{name: (".pack_reader", name) for name in ("LocalPack", "PackReadError", "PackSource", "load_pack")},
+ **{
+ name: (".graph", name)
+ for name in (
+ "GraphError",
+ "build_graph",
+ "graph_neighbors",
+ "graph_status",
+ "load_graph",
+ "query_graph",
+ "refresh_graph",
+ )
+ },
+ **{name: (".server", name) for name in ("PackASGIApp", "PackServerError", "create_pack_app")},
+ **{
+ name: (".share", name)
+ for name in ("ReportHTTPServer", "ShareError", "create_report_server", "render_report_document")
+ },
+ **{
+ name: (".rendering", name)
+ for name in (
+ "AgentBrowserRenderer",
+ "E2BSandboxRenderer",
+ "RenderedPage",
+ "Renderer",
+ "RenderError",
+ "RendererUnavailableError",
+ "VercelSandboxRenderer",
+ "agent_browser_binary",
+ "check_agent_browser_availability",
+ "check_e2b_sandbox_availability",
+ "check_render_backend_availability",
+ "check_vercel_sandbox_availability",
+ "estimate_cloud_render_cost_usd",
+ "render_url",
+ "render_url_to_directory",
+ )
+ },
+}
+
+__all__ = list(PUBLIC_SDK_EXPORTS)
+
+
+def __getattr__(name: str) -> Any:
+ """Load one public SDK symbol on first access and cache it locally."""
+ target = _LAZY_EXPORTS.get(name)
+ if target is None:
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+ module_name, attribute_name = target
+ value = getattr(import_module(module_name, __name__), attribute_name)
+ globals()[name] = value
+ return value
+
+
+def __dir__() -> list[str]:
+ """Include lazy public attributes in interactive discovery."""
+ return sorted(set(globals()) | set(__all__))
+
+
+if TYPE_CHECKING:
+ from .cache import CacheManager, StreamingDeduplicator
+ from .context_ci import CIThresholds, ContextCIError, run_context_ci
+ from .context_packs import (
+ async_build_dataset_pack,
+ async_build_package_pack,
+ async_build_paper_pack,
+ async_build_repo_pack,
+ async_build_standards_pack,
+ async_build_transcript_pack,
+ async_build_wiki_pack,
+ build_brand_pack,
+ build_dataset_pack,
+ build_feed_pack,
+ build_image_pack,
+ build_openapi_pack,
+ build_package_pack,
+ build_paper_pack,
+ build_policy_pack,
+ build_product_pack,
+ build_relationship_pack,
+ build_repo_pack,
+ build_standards_pack,
+ build_styleguide_pack,
+ build_transcript_pack,
+ build_wiki_pack,
+ capture_screenshot_pack,
+ )
+ from .contracts import (
+ ArtifactManifest,
+ ChangeEvent,
+ CoverageResult,
+ EvidenceSpan,
+ IntelligenceBundle,
+ RelationshipCandidate,
+ RelationshipPack,
+ SourceAuthority,
+ WorkflowRequest,
+ WorkflowResult,
+ bundled_schema_path,
+ write_contract_schemas,
+ )
+ from .conversion.chunking import Chunk, TokenCounter, chunk_markdown
+ from .core.fetcher import Fetcher, fetch_blocking, fetch_one
+ from .document_parse import DocumentParseError, ParsedDocument, parse_documents, parse_one_document
+ from .exports import ExportResult, export_pack
+ from .graph import (
+ GraphError,
+ build_graph,
+ graph_neighbors,
+ graph_status,
+ load_graph,
+ query_graph,
+ refresh_graph,
+ )
+ from .local_workflows import audit_pack, refresh_pack
+ from .models.config import (
+ BudgetConfig,
+ CacheConfig,
+ ContentFilterConfig,
+ CrawlConfig,
+ DocpullConfig,
+ NetworkConfig,
+ OutputConfig,
+ PerformanceConfig,
+ ProfileName,
+ RenderActionPolicy,
+ RenderConfig,
+ RenderViewport,
+ )
+ from .models.events import EventType, FetchEvent, FetchStats
+ from .output_contract import validate_pack_contract
+ from .pack_reader import LocalPack, PackReadError, PackSource, load_pack
+ from .pack_tools import (
+ build_citation_map,
+ build_company_brain_bundle,
+ build_intelligence_bundle,
+ build_research_brief,
+ diff_packs,
+ extract_pack_entities,
+ prepare_pack,
+ score_pack,
+ score_pack_sources,
+ search_pack,
+ )
+ from .pipeline.base import PageContext
+ from .pipeline.steps import SqliteSearchResult, search_sqlite_documents
+ from .policy import PolicyConfig
+ from .rendering import (
+ AgentBrowserRenderer,
+ E2BSandboxRenderer,
+ RenderedPage,
+ Renderer,
+ RenderError,
+ RendererUnavailableError,
+ VercelSandboxRenderer,
+ agent_browser_binary,
+ check_agent_browser_availability,
+ check_e2b_sandbox_availability,
+ check_render_backend_availability,
+ check_vercel_sandbox_availability,
+ estimate_cloud_render_cost_usd,
+ render_url,
+ render_url_to_directory,
+ )
+ from .server import PackASGIApp, PackServerError, create_pack_app
+ from .share import ReportHTTPServer, ShareError, create_report_server, render_report_document
+ from .workflows import async_run_workflow, create_workflow_request, run_workflow
-__all__ = [
- "__version__",
- "async_build_dataset_pack",
- "async_build_package_pack",
- "async_build_paper_pack",
- "async_build_repo_pack",
- "async_build_standards_pack",
- "async_build_transcript_pack",
- "async_build_wiki_pack",
- "async_run_workflow",
- "ArtifactManifest",
- "ChangeEvent",
- "EvidenceSpan",
- "IntelligenceBundle",
- "SourceAuthority",
- "WorkflowRequest",
- "WorkflowResult",
- "bundled_schema_path",
- "write_contract_schemas",
- "create_workflow_request",
- "run_workflow",
- "Fetcher",
- "fetch_blocking",
- "fetch_one",
- "refresh_pack",
- "audit_pack",
- "build_dataset_pack",
- "build_brand_pack",
- "build_feed_pack",
- "build_openapi_pack",
- "build_package_pack",
- "build_paper_pack",
- "build_policy_pack",
- "build_product_pack",
- "build_repo_pack",
- "build_standards_pack",
- "build_styleguide_pack",
- "build_transcript_pack",
- "build_wiki_pack",
- "build_image_pack",
- "capture_screenshot_pack",
- "parse_documents",
- "parse_one_document",
- "DocumentParseError",
- "ParsedDocument",
- "score_pack",
- "score_pack_sources",
- "diff_packs",
- "build_citation_map",
- "build_intelligence_bundle",
- "build_company_brain_bundle",
- "extract_pack_entities",
- "search_pack",
- "build_research_brief",
- "prepare_pack",
- "validate_pack_contract",
- "run_context_ci",
- "ContextCIError",
- "CIThresholds",
- "export_pack",
- "ExportResult",
- "GraphError",
- "build_graph",
- "load_graph",
- "graph_status",
- "query_graph",
- "graph_neighbors",
- "refresh_graph",
- "load_pack",
- "LocalPack",
- "PackReadError",
- "PackSource",
- "create_pack_app",
- "PackASGIApp",
- "PackServerError",
- "create_report_server",
- "render_report_document",
- "ReportHTTPServer",
- "ShareError",
- "PageContext",
- "PolicyConfig",
- "DocpullConfig",
- "ProfileName",
- "CrawlConfig",
- "ContentFilterConfig",
- "OutputConfig",
- "NetworkConfig",
- "PerformanceConfig",
- "CacheConfig",
- "BudgetConfig",
- "RenderActionPolicy",
- "RenderConfig",
- "RenderViewport",
- "Renderer",
- "RenderedPage",
- "AgentBrowserRenderer",
- "VercelSandboxRenderer",
- "E2BSandboxRenderer",
- "RenderError",
- "RendererUnavailableError",
- "agent_browser_binary",
- "check_agent_browser_availability",
- "check_vercel_sandbox_availability",
- "check_e2b_sandbox_availability",
- "check_render_backend_availability",
- "estimate_cloud_render_cost_usd",
- "render_url",
- "render_url_to_directory",
- "EventType",
- "FetchEvent",
- "FetchStats",
- "SqliteSearchResult",
- "search_sqlite_documents",
- "CacheManager",
- "StreamingDeduplicator",
- "Chunk",
- "TokenCounter",
- "chunk_markdown",
-]
assert tuple(__all__) == PUBLIC_SDK_EXPORTS
+assert set(_LAZY_EXPORTS) == set(__all__) - {"__version__"}
diff --git a/src/docpull/acquisition_workflows.py b/src/docpull/acquisition_workflows.py
new file mode 100644
index 0000000..22a7b86
--- /dev/null
+++ b/src/docpull/acquisition_workflows.py
@@ -0,0 +1,567 @@
+"""WorkflowRequest/WorkflowResult adapters for core fetch and crawl operations."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Literal, cast
+
+from .contracts import (
+ ArtifactManifest,
+ BudgetUsage,
+ HashDigest,
+ ReplayConfiguration,
+ WorkflowFailure,
+ WorkflowProgressEvent,
+ WorkflowRequest,
+ WorkflowResult,
+ WorkflowWarning,
+ artifact_entries,
+ build_workflow_request,
+ canonical_sha256,
+ file_sha256,
+ new_progress_event,
+ stable_id,
+ workflow_failure_from_mapping,
+)
+from .core.fetcher import Fetcher
+from .models.config import BudgetConfig, CrawlConfig, DocpullConfig, NetworkConfig, OutputConfig, ProfileName
+from .models.events import EventType, FetchEvent, FetchStats, SkipReason
+from .pipeline.base import PageContext
+from .time_utils import utc_now_iso
+
+
+def execute_acquisition_workflow(request: WorkflowRequest, *, crawl: bool) -> dict[str, Any]:
+ """Execute acquisition synchronously and always materialize a structured result."""
+
+ return asyncio.run(_execute_acquisition_workflow(request, crawl=crawl))
+
+
+async def _execute_acquisition_workflow(
+ request: WorkflowRequest,
+ *,
+ crawl: bool,
+) -> dict[str, Any]:
+ output_dir = _output_dir(request)
+ output_dir.mkdir(parents=True, exist_ok=True)
+ value = _input_url(request)
+ options = request.options
+ try:
+ profile = ProfileName(str(options.get("profile") or "rag"))
+ except ValueError as err:
+ raise ValueError(f"Unsupported acquisition profile: {options.get('profile')}") from err
+ config = DocpullConfig(
+ url=value,
+ profile=profile,
+ output=OutputConfig(
+ directory=output_dir,
+ format=str(options.get("format") or "ndjson"), # type: ignore[arg-type]
+ emit_chunks=bool(options.get("emit_chunks", False)),
+ ),
+ crawl=CrawlConfig(
+ max_pages=int(options.get("max_pages") or (50 if crawl else 1)),
+ max_depth=int(options.get("max_depth") or (3 if crawl else 1)),
+ ),
+ network=NetworkConfig(max_retries=int(options.get("max_retries", 3))),
+ budget=BudgetConfig(maximum_paid_cost_usd=0),
+ )
+ started_at = utc_now_iso()
+ events: list[FetchEvent] = []
+ failures: list[WorkflowFailure] = []
+ warnings: list[WorkflowWarning] = []
+ try:
+ async with Fetcher(config) as fetcher:
+ if crawl:
+ async for event in fetcher.run():
+ events.append(event)
+ failure = _failure_from_event(event, default_attempts=config.network.max_retries + 1)
+ if failure is not None:
+ failures.append(failure)
+ warning = _warning_from_event(event)
+ if warning is not None:
+ warnings.append(warning)
+ else:
+ context = await fetcher.fetch_one(value)
+ failure = _failure_from_context(
+ context,
+ default_attempts=config.network.max_retries + 1,
+ )
+ if failure is not None:
+ failures.append(failure)
+ warning = _warning_from_context(context)
+ if warning is not None:
+ warnings.append(warning)
+ stats = fetcher.stats
+ except Exception as err: # noqa: BLE001
+ stats = FetchStats(pages_failed=1)
+ failures.append(
+ workflow_failure_from_mapping(
+ {
+ "code": "workflow_error",
+ "stage": "workflow",
+ "error": str(err),
+ "retryable": False,
+ "url": value,
+ }
+ )
+ )
+
+ return write_acquisition_contracts(
+ request=request,
+ workflow="crawl" if crawl else "fetch",
+ output_dir=output_dir,
+ started_at=started_at,
+ stats=stats,
+ events=events,
+ failures=failures,
+ warnings=warnings,
+ replay_configuration=request.replay,
+ )
+
+
+def write_acquisition_contracts(
+ *,
+ request: WorkflowRequest,
+ workflow: str,
+ output_dir: Path,
+ started_at: str,
+ stats: object,
+ events: list[FetchEvent],
+ failures: list[WorkflowFailure],
+ warnings: list[WorkflowWarning],
+ replay_configuration: ReplayConfiguration,
+) -> dict[str, Any]:
+ """Write run-scoped acquisition contracts without consulting stale output."""
+
+ output_dir = output_dir.resolve()
+ output_dir.mkdir(parents=True, exist_ok=True)
+ finished_at = utc_now_iso()
+ request_path = output_dir / "workflow.request.json"
+ request_path.write_text(
+ json.dumps(request.model_dump(mode="json", exclude_none=True), indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+ current_record_count = int(getattr(stats, "pages_fetched", 0))
+ run_id = stable_id(
+ "run",
+ {
+ "request_id": request.request_id,
+ "workflow": workflow,
+ "started_at": started_at,
+ },
+ )
+ current_artifacts = _current_acquisition_artifacts(
+ output_dir,
+ started_at=started_at,
+ include_records=current_record_count > 0,
+ )
+ current_run_manifest = {
+ "contract_version": "acquisition.run.v1",
+ "schema_version": 1,
+ "run_id": run_id,
+ "request_id": request.request_id,
+ "workflow": workflow,
+ "started_at": started_at,
+ "finished_at": finished_at,
+ "status": _result_status(current_record_count, failures, warnings),
+ "current_run_record_count": current_record_count,
+ "stats": _stats_payload(stats),
+ "artifacts": current_artifacts,
+ "failure_count": len(failures),
+ "warning_count": len(warnings),
+ }
+ run_manifest_path = output_dir / "current-run.manifest.json"
+ run_manifest_path.write_text(
+ json.dumps(current_run_manifest, indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+
+ artifact_map = {
+ "workflow_request": "workflow.request.json",
+ "current_run_manifest": "current-run.manifest.json",
+ **{item["name"]: item["path"] for item in current_artifacts if item.get("name") and item.get("path")},
+ }
+ entries = artifact_entries(output_dir, artifact_map)
+ aggregate = canonical_sha256([entry.model_dump(mode="json") for entry in entries])
+ pack_id = stable_id("pack", {"workflow": workflow, "run_id": run_id, "aggregate": aggregate})
+ manifest = ArtifactManifest(
+ pack_id=pack_id,
+ run_id=run_id,
+ entries=entries,
+ aggregate_sha256=aggregate,
+ )
+ artifact_manifest_path = output_dir / "artifact.manifest.json"
+ artifact_manifest_path.write_text(
+ json.dumps(manifest.model_dump(mode="json"), indent=2, sort_keys=True) + "\n",
+ encoding="utf-8",
+ )
+ progress_events = _progress_events(
+ events, started_at=started_at, finished_at=finished_at, workflow=workflow
+ )
+ result = WorkflowResult(
+ request_id=request.request_id,
+ workflow=workflow,
+ status=cast(
+ Literal["completed", "completed_with_warnings", "failed", "cancelled"],
+ _result_status(current_record_count, failures, warnings),
+ ),
+ started_at=started_at,
+ finished_at=finished_at,
+ pack_identity={"pack_id": pack_id, "aggregate_sha256": aggregate, "workflow": workflow},
+ run_identity={
+ "run_id": run_id,
+ "request_id": request.request_id,
+ "scheduler": None,
+ "current_run_manifest": "current-run.manifest.json",
+ },
+ summary={
+ **_stats_payload(stats),
+ "current_run_record_count": current_record_count,
+ "usable_output": current_record_count > 0,
+ },
+ data={
+ "current_run_manifest": current_run_manifest,
+ "partial_success": current_record_count > 0 and bool(failures),
+ },
+ progress_events=progress_events,
+ warnings=warnings,
+ failures=_dedupe_failures(failures),
+ budget_usage=BudgetUsage(
+ limit_usd=0,
+ estimated_usd=0,
+ paid_request_count=0,
+ http_request_count=int(getattr(stats, "pages_fetched", 0))
+ + int(getattr(stats, "pages_failed", 0))
+ + int(getattr(stats, "pages_skipped", 0)),
+ ),
+ hashes={
+ "request": HashDigest(digest=file_sha256(request_path)),
+ "artifact_manifest": HashDigest(digest=file_sha256(artifact_manifest_path)),
+ "current_run_manifest": HashDigest(digest=file_sha256(run_manifest_path)),
+ "pack": HashDigest(digest=aggregate),
+ },
+ replay_configuration=replay_configuration,
+ compatibility_artifacts={
+ key: value for key, value in artifact_map.items() if key != "workflow_request"
+ },
+ )
+ result_path = output_dir / "workflow.result.json"
+ payload = result.model_dump(mode="json", exclude_none=True)
+ result_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ return payload
+
+
+def _failure_from_context(context: PageContext, *, default_attempts: int) -> WorkflowFailure | None:
+ error = getattr(context, "error", None)
+ should_skip = bool(getattr(context, "should_skip", False))
+ skip_code = getattr(context, "skip_code", None)
+ if not error and not (
+ should_skip
+ and skip_code
+ in {
+ SkipReason.HTTP_ERROR,
+ SkipReason.ROBOTS_DISALLOWED,
+ SkipReason.URL_VALIDATION_FAILED,
+ SkipReason.INVALID_CONTENT_TYPE,
+ SkipReason.NO_CONTENT_EXTRACTED,
+ SkipReason.NO_CONTENT_TO_SAVE,
+ }
+ ):
+ return None
+ return workflow_failure_from_mapping(
+ {
+ "url": getattr(context, "url", None),
+ "error": error or getattr(context, "skip_reason", None) or "Acquisition failed",
+ "code": (
+ f"http_{getattr(context, 'status_code', None)}"
+ if getattr(context, "status_code", None)
+ else (skip_code.value if skip_code else "fetch_error")
+ ),
+ "stage": "fetch",
+ "http_status": getattr(context, "status_code", None),
+ "attempts": getattr(context, "http_attempts", None) or default_attempts,
+ "retry_after_seconds": getattr(context, "retry_after_seconds", None),
+ },
+ default_stage="fetch",
+ default_attempts=default_attempts,
+ )
+
+
+def _failure_from_event(event: FetchEvent, *, default_attempts: int) -> WorkflowFailure | None:
+ event_type = getattr(event, "type", None)
+ skip_reason = getattr(event, "skip_reason", None)
+ status_code = getattr(event, "status_code", None)
+ is_failure_skip = event_type == EventType.FETCH_SKIPPED and skip_reason in {
+ SkipReason.HTTP_ERROR,
+ SkipReason.ROBOTS_DISALLOWED,
+ SkipReason.URL_VALIDATION_FAILED,
+ SkipReason.INVALID_CONTENT_TYPE,
+ SkipReason.NO_CONTENT_EXTRACTED,
+ SkipReason.NO_CONTENT_TO_SAVE,
+ }
+ if event_type != EventType.FETCH_FAILED and not is_failure_skip:
+ return None
+ return workflow_failure_from_mapping(
+ {
+ "url": getattr(event, "url", None),
+ "error": getattr(event, "error", None) or getattr(event, "message", None) or "Acquisition failed",
+ "code": getattr(event, "failure_code", None)
+ or (f"http_{status_code}" if status_code else None)
+ or (skip_reason.value if skip_reason else "fetch_error"),
+ "stage": getattr(event, "failure_stage", None) or "fetch",
+ "retryable": getattr(event, "retryable", None),
+ "http_status": status_code,
+ "attempts": getattr(event, "attempts", None) or default_attempts,
+ "retry_after_seconds": getattr(event, "retry_after_seconds", None),
+ },
+ default_stage="fetch",
+ default_attempts=default_attempts,
+ )
+
+
+def _warning_from_context(context: PageContext) -> WorkflowWarning | None:
+ skip_code = getattr(context, "skip_code", None)
+ if not getattr(context, "should_skip", False) or skip_code in {
+ SkipReason.HTTP_ERROR,
+ SkipReason.ROBOTS_DISALLOWED,
+ SkipReason.URL_VALIDATION_FAILED,
+ SkipReason.INVALID_CONTENT_TYPE,
+ SkipReason.NO_CONTENT_EXTRACTED,
+ SkipReason.NO_CONTENT_TO_SAVE,
+ }:
+ return None
+ return WorkflowWarning(
+ code=skip_code.value if skip_code else "fetch_skipped",
+ message=getattr(context, "skip_reason", None) or "Source was skipped",
+ metadata={"url": getattr(context, "url", None)},
+ )
+
+
+def _warning_from_event(event: FetchEvent) -> WorkflowWarning | None:
+ skip_reason = getattr(event, "skip_reason", None)
+ if getattr(event, "type", None) != EventType.FETCH_SKIPPED or skip_reason in {
+ SkipReason.HTTP_ERROR,
+ SkipReason.ROBOTS_DISALLOWED,
+ SkipReason.URL_VALIDATION_FAILED,
+ SkipReason.INVALID_CONTENT_TYPE,
+ SkipReason.NO_CONTENT_EXTRACTED,
+ SkipReason.NO_CONTENT_TO_SAVE,
+ }:
+ return None
+ return WorkflowWarning(
+ code=skip_reason.value if skip_reason else "fetch_skipped",
+ message=getattr(event, "message", None) or "Source was skipped",
+ metadata={"url": getattr(event, "url", None)},
+ )
+
+
+def _current_acquisition_artifacts(
+ output_dir: Path,
+ *,
+ started_at: str,
+ include_records: bool,
+) -> list[dict[str, Any]]:
+ if not include_records:
+ return []
+
+ started_epoch = datetime.fromisoformat(started_at.replace("Z", "+00:00")).timestamp()
+ rows: list[dict[str, Any]] = []
+ for name, filename in (
+ ("documents_ndjson", "documents.ndjson"),
+ ("corpus_manifest", "corpus.manifest.json"),
+ ("sources", "sources.md"),
+ ("acquisition_routes", "acquisition.routes.json"),
+ ("rights_manifest", "rights.manifest.json"),
+ ("provenance_graph", "provenance.graph.json"),
+ ):
+ path = output_dir / filename
+ if not path.exists() or not path.is_file() or path.stat().st_mtime < started_epoch:
+ continue
+ rows.append(
+ {
+ "name": name,
+ "path": filename,
+ "bytes": path.stat().st_size,
+ "sha256": file_sha256(path),
+ }
+ )
+ return rows
+
+
+def _result_status(
+ current_record_count: int,
+ failures: list[WorkflowFailure],
+ warnings: list[WorkflowWarning],
+) -> str:
+ if current_record_count == 0:
+ return "failed"
+ if failures or warnings:
+ return "completed_with_warnings"
+ return "completed"
+
+
+def _stats_payload(stats: object) -> dict[str, Any]:
+ if hasattr(stats, "to_dict"):
+ payload = stats.to_dict()
+ if isinstance(payload, dict):
+ return payload
+ return {
+ key: getattr(stats, key, 0)
+ for key in (
+ "urls_discovered",
+ "pages_fetched",
+ "pages_skipped",
+ "pages_failed",
+ "files_saved",
+ "bytes_downloaded",
+ "duration_seconds",
+ )
+ }
+
+
+def _progress_events(
+ events: list[FetchEvent],
+ *,
+ started_at: str,
+ finished_at: str,
+ workflow: str,
+) -> list[WorkflowProgressEvent]:
+ rows = [
+ new_progress_event(
+ phase="run",
+ status="started",
+ timestamp=started_at,
+ message=f"Started {workflow}",
+ )
+ ]
+ for event in events:
+ event_type = getattr(event, "type", None)
+ if event_type not in {
+ EventType.DISCOVERY_STARTED,
+ EventType.DISCOVERY_COMPLETE,
+ EventType.FETCH_PROGRESS,
+ EventType.FETCH_FAILED,
+ EventType.FETCH_SKIPPED,
+ }:
+ continue
+ rows.append(
+ new_progress_event(
+ phase="discovery" if "discovery" in event_type.value else "fetch",
+ status="failed"
+ if event_type == EventType.FETCH_FAILED
+ else "warning"
+ if event_type == EventType.FETCH_SKIPPED
+ else "progress",
+ timestamp=(event.timestamp.isoformat() if getattr(event, "timestamp", None) else None),
+ message=getattr(event, "message", None) or getattr(event, "error", None),
+ current=getattr(event, "current", None),
+ total=getattr(event, "total", None),
+ metadata={"url": event.url} if getattr(event, "url", None) else {},
+ )
+ )
+ rows.append(
+ new_progress_event(
+ phase="run",
+ status="completed",
+ timestamp=finished_at,
+ message=f"Completed {workflow}",
+ )
+ )
+ return [WorkflowProgressEvent.model_validate(item) for item in rows]
+
+
+def _dedupe_failures(failures: list[WorkflowFailure]) -> list[WorkflowFailure]:
+ rows: dict[str, WorkflowFailure] = {}
+ for failure in failures:
+ key = canonical_sha256(failure.model_dump(mode="json", exclude_none=True))
+ rows[key] = failure
+ return [rows[key] for key in sorted(rows)]
+
+
+def _input_url(request: WorkflowRequest) -> str:
+ for key in ("url", "value"):
+ value = request.input.get(key)
+ if isinstance(value, str) and value.strip():
+ return value.strip()
+ raise ValueError("Acquisition WorkflowRequest.input must include url or value")
+
+
+def _output_dir(request: WorkflowRequest) -> Path:
+ value = request.output.get("directory")
+ if not isinstance(value, str) or not value.strip():
+ raise ValueError("Acquisition WorkflowRequest.output.directory is required")
+ return Path(value).expanduser().resolve()
+
+
+def write_cli_acquisition_contracts(
+ *,
+ config: DocpullConfig,
+ workflow: str,
+ started_at: str,
+ stats: object,
+ events: list[FetchEvent] | None = None,
+ contexts: list[PageContext] | None = None,
+ extra_failures: list[WorkflowFailure] | None = None,
+) -> dict[str, Any]:
+ """Bridge the compatibility CLI onto the canonical acquisition contracts."""
+
+ options = {
+ "profile": config.profile.value,
+ "format": config.output.format,
+ "max_pages": config.crawl.max_pages,
+ "max_depth": config.crawl.max_depth,
+ "max_retries": config.network.max_retries,
+ "emit_chunks": config.output.emit_chunks,
+ }
+ request = build_workflow_request(
+ workflow=workflow,
+ input_payload={"url": config.url},
+ output_dir=config.output.directory,
+ options=options,
+ source_policy={
+ "robots": "respect",
+ "allowed_schemes": ["https"],
+ "local_first": True,
+ },
+ budget={"maximum_paid_cost_usd": config.budget.maximum_paid_cost_usd},
+ browser_enabled=config.render.enabled,
+ paid_routes_enabled=config.render.enabled
+ and config.render.backend in {"vercel-sandbox", "e2b-sandbox"},
+ )
+ event_rows = list(events or [])
+ failures = list(extra_failures or [])
+ warnings: list[WorkflowWarning] = []
+ for event in event_rows:
+ failure = _failure_from_event(event, default_attempts=config.network.max_retries + 1)
+ if failure is not None:
+ failures.append(failure)
+ warning = _warning_from_event(event)
+ if warning is not None:
+ warnings.append(warning)
+ for context in contexts or []:
+ failure = _failure_from_context(context, default_attempts=config.network.max_retries + 1)
+ if failure is not None:
+ failures.append(failure)
+ warning = _warning_from_context(context)
+ if warning is not None:
+ warnings.append(warning)
+ return write_acquisition_contracts(
+ request=request,
+ workflow=workflow,
+ output_dir=config.output.directory,
+ started_at=started_at,
+ stats=stats,
+ events=event_rows,
+ failures=failures,
+ warnings=warnings,
+ replay_configuration=request.replay,
+ )
+
+
+__all__ = [
+ "execute_acquisition_workflow",
+ "write_acquisition_contracts",
+ "write_cli_acquisition_contracts",
+]
diff --git a/src/docpull/basis.py b/src/docpull/basis.py
index da798b7..dd61b9f 100644
--- a/src/docpull/basis.py
+++ b/src/docpull/basis.py
@@ -7,7 +7,7 @@
from pathlib import Path
from typing import Any, Literal
-from .pack_reader import PackReadError, load_pack
+from .pack_reader import LocalPack, PackReadError, load_pack
from .time_utils import utc_now_iso
BasisConfidence = Literal["high", "medium", "low"]
@@ -249,22 +249,26 @@ def build_pack_basis(
claim: str,
limit: int = 5,
producer: str = "docpull.pack.basis",
+ _pack: LocalPack | None = None,
) -> list[dict[str, Any]]:
"""Build basis records for a claim from local pack search results."""
- try:
- pack = load_pack(pack_dir)
- except PackReadError as err:
- return [
- basis_record(
- claim_path=claim_path,
- claim=claim,
- confidence="low",
- evidence_state="insufficient",
- warnings=[str(err)],
- producer=producer,
- )
- ]
+ if _pack is None:
+ try:
+ pack = load_pack(pack_dir)
+ except PackReadError as err:
+ return [
+ basis_record(
+ claim_path=claim_path,
+ claim=claim,
+ confidence="low",
+ evidence_state="insufficient",
+ warnings=[str(err)],
+ producer=producer,
+ )
+ ]
+ else:
+ pack = _pack
if not pack.documents:
return [
basis_record(
diff --git a/src/docpull/cache/__init__.py b/src/docpull/cache/__init__.py
index 5ed9737..eb956f4 100644
--- a/src/docpull/cache/__init__.py
+++ b/src/docpull/cache/__init__.py
@@ -1,8 +1,19 @@
-"""Caching and deduplication for docpull."""
+"""Caching, durable crawl frontier, and streaming deduplication."""
+# ruff: noqa: F401 - TYPE_CHECKING imports document lazy public re-exports.
-from .frontier import FrontierEntry, FrontierState, FrontierStore
-from .manager import DEFAULT_TTL_DAYS, CacheManager, CacheState, ManifestEntry
-from .streaming_dedup import StreamingDeduplicator
+from __future__ import annotations
+
+from importlib import import_module
+from typing import TYPE_CHECKING, Any
+
+_LAZY_EXPORTS = {
+ **{name: (".frontier", name) for name in ("FrontierEntry", "FrontierState", "FrontierStore")},
+ **{
+ name: (".manager", name)
+ for name in ("DEFAULT_TTL_DAYS", "CacheManager", "CacheState", "ManifestEntry")
+ },
+ "StreamingDeduplicator": (".streaming_dedup", "StreamingDeduplicator"),
+}
__all__ = [
"CacheManager",
@@ -14,3 +25,26 @@
"StreamingDeduplicator",
"DEFAULT_TTL_DAYS",
]
+
+
+def __getattr__(name: str) -> Any:
+ target = _LAZY_EXPORTS.get(name)
+ if target is None:
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+ module_name, attribute_name = target
+ value = getattr(import_module(module_name, __name__), attribute_name)
+ globals()[name] = value
+ return value
+
+
+def __dir__() -> list[str]:
+ return sorted(set(globals()) | set(__all__))
+
+
+if TYPE_CHECKING:
+ from .frontier import FrontierEntry, FrontierState, FrontierStore
+ from .manager import DEFAULT_TTL_DAYS, CacheManager, CacheState, ManifestEntry
+ from .streaming_dedup import StreamingDeduplicator
+
+
+assert set(_LAZY_EXPORTS) == set(__all__)
diff --git a/src/docpull/cache/frontier.py b/src/docpull/cache/frontier.py
index 6d55468..21524b2 100644
--- a/src/docpull/cache/frontier.py
+++ b/src/docpull/cache/frontier.py
@@ -9,7 +9,7 @@
from pathlib import Path
from typing import Any
-from ..models.run import FRONTIER_SCHEMA_VERSION
+from ..models.schema import FRONTIER_SCHEMA_VERSION
from ..time_utils import utc_now_iso
logger = logging.getLogger(__name__)
@@ -82,21 +82,26 @@ class FrontierStore:
def __init__(self, path: Path):
self.path = Path(path)
+ self.journal_path = self.path.with_suffix(self.path.suffix + ".journal")
self.entries: dict[str, FrontierEntry] = {}
self.start_url: str | None = None
self.run_fingerprint: dict[str, object] | None = None
self.created_at: str | None = None
self.updated_at: str | None = None
+ self._journal_needs_separator = False
self._load()
def _load(self) -> None:
- if not self.path.exists():
- return
- try:
- data = json.loads(self.path.read_text(encoding="utf-8"))
- except (OSError, json.JSONDecodeError) as err:
- logger.warning("Could not load frontier store %s: %s", self.path, err)
- return
+ if self.path.exists():
+ try:
+ data = json.loads(self.path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as err:
+ logger.warning("Could not load frontier store %s: %s", self.path, err)
+ else:
+ self._load_snapshot(data)
+ self._replay_journal()
+
+ def _load_snapshot(self, data: object) -> None:
if not isinstance(data, dict) or data.get("schema_version") != FRONTIER_SCHEMA_VERSION:
return
entries = data.get("entries")
@@ -114,6 +119,54 @@ def _load(self) -> None:
if entry:
self.entries[entry.url] = entry
+ def _replay_journal(self) -> None:
+ """Apply complete transition records after the last snapshot."""
+ if not self.journal_path.exists():
+ return
+ try:
+ with self.journal_path.open(encoding="utf-8") as journal:
+ for line_number, line in enumerate(journal, start=1):
+ if not line.strip():
+ continue
+ try:
+ update = json.loads(line)
+ except json.JSONDecodeError:
+ if not line.endswith("\n"):
+ self._journal_needs_separator = True
+ logger.warning(
+ "Ignoring incomplete frontier journal record %s:%d",
+ self.journal_path,
+ line_number,
+ )
+ continue
+ if not isinstance(update, dict):
+ continue
+ if update.get("schema_version") != FRONTIER_SCHEMA_VERSION:
+ continue
+ raw_entry = update.get("entry")
+ if not isinstance(raw_entry, dict):
+ continue
+ entry = FrontierEntry.from_json(raw_entry)
+ if entry is not None:
+ self.entries[entry.url] = entry
+ self.updated_at = entry.updated_at
+ except OSError as err:
+ logger.warning("Could not load frontier journal %s: %s", self.journal_path, err)
+
+ def _append_entry(self, entry: FrontierEntry) -> None:
+ """Durably append one URL transition without rewriting the frontier."""
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+ payload = {
+ "schema_version": FRONTIER_SCHEMA_VERSION,
+ "entry": entry.to_json(),
+ }
+ with self.journal_path.open("a", encoding="utf-8") as journal:
+ if self._journal_needs_separator and self.journal_path.stat().st_size:
+ journal.write("\n")
+ self._journal_needs_separator = False
+ journal.write(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
+ journal.write("\n")
+
def save(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
now = utc_now_iso()
@@ -132,10 +185,17 @@ def save(self) -> None:
try:
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
tmp.replace(self.path)
+ self.journal_path.unlink(missing_ok=True)
+ self._journal_needs_separator = False
except Exception:
tmp.unlink(missing_ok=True)
raise
+ def flush(self) -> None:
+ """Compact pending journal transitions into the stable snapshot."""
+ if self.journal_path.exists():
+ self.save()
+
def initialize(self, *, start_url: str, run_fingerprint: dict[str, object]) -> None:
if self.start_url != start_url or self.run_fingerprint != run_fingerprint:
self.entries.clear()
@@ -164,7 +224,7 @@ def mark_processing(self, url: str) -> None:
entry.state = FrontierState.PROCESSING
entry.attempts += 1
entry.updated_at = utc_now_iso()
- self.save()
+ self._append_entry(entry)
def mark_succeeded(self, url: str) -> None:
self._mark_terminal(url, FrontierState.SUCCEEDED)
@@ -183,7 +243,7 @@ def _mark_terminal(self, url: str, state: FrontierState, error: str | None = Non
entry.state = state
entry.last_error = error
entry.updated_at = utc_now_iso()
- self.save()
+ self._append_entry(entry)
def pending_urls(self) -> list[str]:
terminal = {FrontierState.SUCCEEDED, FrontierState.SKIPPED}
@@ -192,8 +252,10 @@ def pending_urls(self) -> list[str]:
def clear(self) -> None:
if self.path.exists():
self.path.unlink()
+ self.journal_path.unlink(missing_ok=True)
self.entries.clear()
self.start_url = None
self.run_fingerprint = None
self.created_at = None
self.updated_at = None
+ self._journal_needs_separator = False
diff --git a/src/docpull/cache/manager.py b/src/docpull/cache/manager.py
index 097bf71..60a11bb 100644
--- a/src/docpull/cache/manager.py
+++ b/src/docpull/cache/manager.py
@@ -234,6 +234,7 @@ def flush(self) -> None:
"""
self._save_manifest()
self._save_state()
+ self.frontier.flush()
def __enter__(self) -> CacheManager:
"""Context manager entry."""
@@ -415,7 +416,11 @@ def save_discovered_urls(
}
if config_fingerprint is not None:
data["config_fingerprint"] = config_fingerprint
- self.frontier.initialize(start_url=start_url, run_fingerprint=config_fingerprint)
+ if not self.frontier.compatible(
+ start_url=start_url,
+ run_fingerprint=config_fingerprint,
+ ):
+ self.frontier.initialize(start_url=start_url, run_fingerprint=config_fingerprint)
self.frontier.add_many(urls, source="discovery")
self.frontier.save()
try:
diff --git a/src/docpull/cli.py b/src/docpull/cli.py
index 75c6c77..1f30048 100644
--- a/src/docpull/cli.py
+++ b/src/docpull/cli.py
@@ -7,8 +7,9 @@
import json
import sys
import tempfile
+from importlib import import_module
from pathlib import Path
-from typing import Literal, cast
+from typing import TYPE_CHECKING, Any, Literal, cast
if "--doctor" in sys.argv:
from .doctor import run_doctor
@@ -21,54 +22,63 @@
output_dir = Path(sys.argv[flag_idx + 1])
sys.exit(run_doctor(output_dir=output_dir))
-try:
- import aiohttp # noqa: F401
- import bs4 # noqa: F401
- import defusedxml # noqa: F401
- import html2text # noqa: F401
- import rich # noqa: F401
-except ImportError as e:
- print(f"\nERROR: Missing required dependency: {e.name}", file=sys.stderr)
- print("\nDocpull requires all core dependencies to be installed.", file=sys.stderr)
- print("\nRecommended fixes:", file=sys.stderr)
- print(" 1. For pipx users: pipx reinstall docpull --force", file=sys.stderr)
- print(" 2. For pip users: pip install --upgrade --force-reinstall docpull", file=sys.stderr)
- print(" 3. For development: pip install -e .[dev]", file=sys.stderr)
- print("\nTo diagnose issues, run: docpull --doctor", file=sys.stderr)
- sys.exit(1)
-
-from rich.console import Console
-from rich.markup import escape
-from rich.progress import Progress, SpinnerColumn, TextColumn
-
from . import __version__
-from .accounting import (
- BudgetError,
- RunAccounting,
- blocked_action,
- default_route_steps,
- effective_budget_limit,
- enforce_paid_budget,
- maybe_write_run_accounting,
- parse_budget_value,
-)
-from .core.fetcher import Fetcher
-from .models.config import DEFAULT_CLOUD_ARTIFACT_PATH, DocpullConfig, ProfileName, RenderConfig
-from .models.events import EventType, SkipReason
-from .rendering import (
- AgentBrowserRenderer,
- Renderer,
- RenderError,
- VercelSandboxRenderer,
- check_render_backend_availability,
- estimate_cloud_render_cost_usd,
- render_url_to_directory,
-)
-from .skill_export import default_skill_root, expand_skill_agents
from .surface import PRUNED_CLI_COMMANDS, format_cli_subcommands
+if TYPE_CHECKING:
+ from .models.config import DocpullConfig
+ from .models.events import SkipReason
+ from .rendering import Renderer
+
RenderBackend = Literal["agent-browser", "vercel-sandbox", "e2b-sandbox"]
+_CLI_LAZY_EXPORTS = {
+ "Fetcher": (".core.fetcher", "Fetcher"),
+ "check_render_backend_availability": (
+ ".rendering",
+ "check_render_backend_availability",
+ ),
+ "render_url_to_directory": (".rendering", "render_url_to_directory"),
+}
+
+
+def __getattr__(name: str) -> object:
+ """Preserve CLI test/integration seams without eager heavy imports."""
+ target = _CLI_LAZY_EXPORTS.get(name)
+ if target is None:
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+ module_name, attribute_name = target
+ value = getattr(import_module(module_name, __package__), attribute_name)
+ globals()[name] = value
+ return value
+
+
+def _parse_budget_value(value: str) -> float:
+ """Load budget parsing only when argparse sees a budget value."""
+ from .accounting import parse_budget_value
+
+ return parse_budget_value(value)
+
+
+def _core_dependencies_available() -> bool:
+ """Keep the friendly fetch dependency error off unrelated CLI paths."""
+ try:
+ import aiohttp # noqa: F401
+ import bs4 # noqa: F401
+ import defusedxml # noqa: F401
+ import html2text # noqa: F401
+ import rich # noqa: F401
+ except ImportError as err:
+ print(f"\nERROR: Missing required dependency: {err.name}", file=sys.stderr)
+ print("\nDocpull requires all core dependencies to be installed.", file=sys.stderr)
+ print("\nRecommended fixes:", file=sys.stderr)
+ print(" 1. For pipx users: pipx reinstall docpull --force", file=sys.stderr)
+ print(" 2. For pip users: pip install --upgrade --force-reinstall docpull", file=sys.stderr)
+ print(" 3. For development: pip install -e .[dev]", file=sys.stderr)
+ print("\nTo diagnose issues, run: docpull --doctor", file=sys.stderr)
+ return False
+ return True
+
def _write_fetch_accounting(
*,
@@ -79,6 +89,9 @@ def _write_fetch_accounting(
paid_capable: bool,
skip_counts: dict[SkipReason, int] | None = None,
) -> None:
+ from .accounting import RunAccounting, maybe_write_run_accounting
+ from .models.events import SkipReason
+
cache_hits = 0
if skip_counts:
cache_hits = int(skip_counts.get(SkipReason.CACHE_UNCHANGED, 0))
@@ -127,14 +140,22 @@ def _output_dir_has_records(output_dir: Path) -> bool:
return False
-def _fetch_exit_code(stats: object, output_dir: Path, *, allow_empty: bool = False) -> int:
+def _fetch_exit_code(
+ stats: object,
+ output_dir: Path,
+ *,
+ allow_empty: bool = False,
+ exit_policy: str = "strict",
+) -> int:
if int(getattr(stats, "pages_failed", 0)) > 0:
return 1
if allow_empty:
return 0
- if int(getattr(stats, "pages_fetched", 0)) == 0 and not _output_dir_has_records(output_dir):
- return 1
- return 0
+ if int(getattr(stats, "pages_fetched", 0)) > 0:
+ return 0
+ if exit_policy == "usable-output" and _output_dir_has_records(output_dir):
+ return 0
+ return 1
def _add_render_options(parser: argparse.ArgumentParser) -> None:
@@ -251,7 +272,7 @@ def create_parser() -> argparse.ArgumentParser:
)
parser.add_argument(
"--budget",
- type=parse_budget_value,
+ type=_parse_budget_value,
default=None,
metavar="USD",
help="Maximum paid-capable provider/cloud spend for this run. Use 0 for zero paid calls.",
@@ -591,12 +612,43 @@ def create_parser() -> argparse.ArgumentParser:
action="store_true",
help="Suppress output",
)
+ output_group.add_argument(
+ "--exit-policy",
+ choices=["strict", "usable-output"],
+ default="strict",
+ help=(
+ "Exit based on this run only (strict, default), or accept usable records "
+ "already present in the output directory (usable-output)"
+ ),
+ )
return parser
def run_fetcher(args: argparse.Namespace) -> int:
"""Run the fetcher with given arguments."""
+ if not _core_dependencies_available():
+ return 1
+
+ from rich.console import Console
+ from rich.markup import escape
+ from rich.progress import Progress, SpinnerColumn, TextColumn
+
+ from .accounting import (
+ BudgetError,
+ RunAccounting,
+ blocked_action,
+ default_route_steps,
+ enforce_paid_budget,
+ maybe_write_run_accounting,
+ )
+ from .models.config import DocpullConfig, ProfileName
+ from .models.events import EventType, FetchStats, SkipReason
+ from .rendering import estimate_cloud_render_cost_usd
+ from .skill_export import default_skill_root, expand_skill_agents
+ from .time_utils import utc_now_iso
+
+ fetcher_class: Any = globals().get("Fetcher") or __getattr__("Fetcher")
console = Console()
if not args.url:
@@ -866,6 +918,27 @@ def run_fetcher(args: argparse.Namespace) -> int:
console.print("[red]Budget error:[/red] " + escape(str(err)))
return 1
+ acquisition_started_at = utc_now_iso()
+ run_events: list = []
+
+ def write_structured_result(
+ stats: object,
+ *,
+ contexts: list | None = None,
+ extra_failures: list | None = None,
+ ) -> None:
+ from .acquisition_workflows import write_cli_acquisition_contracts
+
+ write_cli_acquisition_contracts(
+ config=config,
+ workflow="fetch" if args.single else "crawl",
+ started_at=acquisition_started_at,
+ stats=stats,
+ events=run_events,
+ contexts=contexts,
+ extra_failures=extra_failures,
+ )
+
async def run() -> int:
if not args.quiet:
console.print(f"[bold blue]docpull[/bold blue] v{__version__}")
@@ -874,7 +947,7 @@ async def run() -> int:
console.print()
try:
- async with Fetcher(config) as fetcher:
+ async with fetcher_class(config) as fetcher:
# Handle --preview-urls mode
if args.preview_urls:
urls = await fetcher.discover()
@@ -890,6 +963,7 @@ async def run() -> int:
return 1
ctx = await fetcher.fetch_one(config.url)
if ctx.error:
+ write_structured_result(fetcher.stats, contexts=[ctx])
console.print(f"[red]Failed:[/red] {ctx.error}")
return 1
if ctx.should_skip:
@@ -902,6 +976,7 @@ async def run() -> int:
SkipReason.NO_CONTENT_EXTRACTED,
SkipReason.NO_CONTENT_TO_SAVE,
}
+ write_structured_result(fetcher.stats, contexts=[ctx])
return 1 if ctx.skip_code in failure_skips else 0
if not args.quiet:
n_chunks = len(ctx.chunks) if ctx.chunks else 0
@@ -916,6 +991,7 @@ async def run() -> int:
render_estimated_cost=render_estimated_cost,
paid_capable=render_is_cloud,
)
+ write_structured_result(fetcher.stats, contexts=[ctx])
return 0
# Track skip reasons for summary
@@ -925,6 +1001,7 @@ async def run() -> int:
if args.quiet:
async for event in fetcher.run():
+ run_events.append(event)
if event.type == EventType.FETCH_SKIPPED and event.skip_reason:
skip_counts[event.skip_reason] += 1
else:
@@ -937,6 +1014,7 @@ async def run() -> int:
task = progress.add_task("Starting...", total=None)
async for event in fetcher.run():
+ run_events.append(event)
if event.type == EventType.STARTED:
progress.update(task, description=f"[cyan]{event.message}")
elif event.type == EventType.RESUMED:
@@ -985,6 +1063,7 @@ async def run() -> int:
paid_capable=render_is_cloud,
skip_counts=skip_counts,
)
+ write_structured_result(stats)
if not args.quiet:
console.print()
console.print("[bold]Results:[/bold]")
@@ -1001,12 +1080,26 @@ async def run() -> int:
for reason, count in sorted(skip_counts.items(), key=lambda x: -x[1]):
console.print(f" {reason.value}: {count}")
- exit_code = _fetch_exit_code(stats, config.output.directory, allow_empty=args.dry_run)
+ exit_code = _fetch_exit_code(
+ stats,
+ config.output.directory,
+ allow_empty=args.dry_run,
+ exit_policy=args.exit_policy,
+ )
if exit_code and not args.quiet and stats.pages_fetched == 0 and stats.pages_failed == 0:
console.print("[yellow]No readable pages were fetched; output pack is empty.[/yellow]")
return exit_code
except Exception as e:
+ from .contracts import workflow_failure_from_mapping
+
+ failure = workflow_failure_from_mapping(
+ {"error": str(e), "stage": "workflow", "code": "workflow_error"}
+ )
+ write_structured_result(
+ FetchStats(pages_failed=1),
+ extra_failures=[failure],
+ )
console.print("[red]Error:[/red] " + escape(str(e)))
if args.verbose:
import traceback
@@ -1023,6 +1116,32 @@ def run_render_cli(argv: list[str]) -> int:
return _run_render_init_cli(argv[1:])
if argv and argv[0] == "doctor":
return _run_render_doctor_cli()
+
+ from rich.console import Console
+ from rich.markup import escape
+
+ from .accounting import (
+ BudgetError,
+ RunAccounting,
+ blocked_action,
+ default_route_steps,
+ effective_budget_limit,
+ enforce_paid_budget,
+ maybe_write_run_accounting,
+ )
+ from .models.config import DEFAULT_CLOUD_ARTIFACT_PATH, RenderConfig
+ from .rendering import (
+ RenderError,
+ estimate_cloud_render_cost_usd,
+ )
+
+ check_render_backend: Any = globals().get("check_render_backend_availability") or __getattr__(
+ "check_render_backend_availability"
+ )
+ render_to_directory: Any = globals().get("render_url_to_directory") or __getattr__(
+ "render_url_to_directory"
+ )
+
output_dir_explicit = any(
arg in {"--output-dir", "-o"}
or arg.startswith("--output-dir=")
@@ -1128,7 +1247,7 @@ def run_render_cli(argv: list[str]) -> int:
)
parser.add_argument(
"--budget",
- type=parse_budget_value,
+ type=_parse_budget_value,
default=None,
metavar="USD",
help="Maximum paid-capable provider/cloud spend for this render. Use 0 for zero paid calls.",
@@ -1166,7 +1285,7 @@ def run_render_cli(argv: list[str]) -> int:
agent_browser_binary=args.agent_browser_bin,
vercel_sandbox_binary=args.vercel_sandbox_bin,
)
- available, message = check_render_backend_availability(backend, binary=binary)
+ available, message = check_render_backend(backend, binary=binary)
style = "green" if available else "yellow"
console.print(f"[{style}]{escape(message)}[/{style}]")
return 0 if available else 1
@@ -1259,7 +1378,7 @@ async def run() -> int:
vercel_sandbox_binary=args.vercel_sandbox_bin,
agent_browser_binary=args.agent_browser_bin,
)
- artifact = await render_url_to_directory(
+ artifact = await render_to_directory(
args.url,
output_dir,
config=config,
@@ -1300,6 +1419,13 @@ async def run() -> int:
def _run_render_doctor_cli() -> int:
+ from rich.console import Console
+ from rich.markup import escape
+
+ check_render_backend: Any = globals().get("check_render_backend_availability") or __getattr__(
+ "check_render_backend_availability"
+ )
+
console = Console()
checks = [
("local", "agent-browser"),
@@ -1308,7 +1434,7 @@ def _run_render_doctor_cli() -> int:
]
exit_code = 0
for runtime, backend in checks:
- available, message = check_render_backend_availability(backend)
+ available, message = check_render_backend(backend)
style = "green" if available else "yellow"
console.print(f"[{style}]{runtime}:[/{style}] {escape(message)}")
if runtime == "local" and not available:
@@ -1321,6 +1447,8 @@ def _run_render_doctor_cli() -> int:
def _run_render_init_cli(argv: list[str]) -> int:
+ from rich.console import Console
+
parser = argparse.ArgumentParser(
prog="docpull render init",
description="Print a sandbox template recipe for agent-browser rendering",
@@ -1404,6 +1532,8 @@ def _renderer_for_render_cli_backend(
vercel_sandbox_binary: str | None,
agent_browser_binary: str | None,
) -> Renderer | None:
+ from .rendering import AgentBrowserRenderer, VercelSandboxRenderer
+
if backend == "agent-browser":
return AgentBrowserRenderer(binary=agent_browser_binary) if agent_browser_binary else None
if backend == "vercel-sandbox":
@@ -1536,12 +1666,14 @@ def main(argv: list[str] | None = None) -> int:
"image-pack",
"screenshot-pack",
"policy-pack",
+ "relationship-pack",
}:
from .context_packs.workflow_cli import (
run_brand_pack_cli,
run_image_pack_cli,
run_policy_pack_cli,
run_product_pack_cli,
+ run_relationship_pack_cli,
run_screenshot_pack_cli,
run_styleguide_pack_cli,
)
@@ -1553,6 +1685,7 @@ def main(argv: list[str] | None = None) -> int:
"image-pack": run_image_pack_cli,
"screenshot-pack": run_screenshot_pack_cli,
"policy-pack": run_policy_pack_cli,
+ "relationship-pack": run_relationship_pack_cli,
}
return workflow_runners[raw_argv[0]](raw_argv[1:])
if raw_argv and raw_argv[0] == "openapi-pack":
diff --git a/src/docpull/context_packs/__init__.py b/src/docpull/context_packs/__init__.py
index 6fc07f8..3045fd4 100644
--- a/src/docpull/context_packs/__init__.py
+++ b/src/docpull/context_packs/__init__.py
@@ -1,46 +1,66 @@
"""Typed local context-pack workflows."""
+# ruff: noqa: F401 - TYPE_CHECKING imports document lazy public re-exports.
from __future__ import annotations
+from importlib import import_module
+from typing import TYPE_CHECKING, Any
+
from ..surface import PUBLIC_CONTEXT_PACK_EXPORTS
-from .brand import build_brand_pack
-from .dataset import async_build_dataset_pack, build_dataset_pack
-from .feed import build_feed_pack
-from .openapi import build_openapi_pack
-from .package import async_build_package_pack, build_package_pack
-from .paper import async_build_paper_pack, build_paper_pack
-from .policy_pack import build_policy_pack
-from .product import build_product_pack
-from .repo import async_build_repo_pack, build_repo_pack
-from .standards import async_build_standards_pack, build_standards_pack
-from .styleguide import build_styleguide_pack
-from .transcript import async_build_transcript_pack, build_transcript_pack
-from .visuals import build_image_pack, capture_screenshot_pack
-from .wiki import async_build_wiki_pack, build_wiki_pack
-
-__all__ = [
- "async_build_dataset_pack",
- "async_build_package_pack",
- "async_build_paper_pack",
- "async_build_repo_pack",
- "async_build_standards_pack",
- "async_build_transcript_pack",
- "async_build_wiki_pack",
- "build_brand_pack",
- "build_dataset_pack",
- "build_feed_pack",
- "build_openapi_pack",
- "build_package_pack",
- "build_paper_pack",
- "build_policy_pack",
- "build_product_pack",
- "build_repo_pack",
- "build_standards_pack",
- "build_styleguide_pack",
- "build_transcript_pack",
- "build_wiki_pack",
- "build_image_pack",
- "capture_screenshot_pack",
-]
+
+_LAZY_EXPORTS = {
+ "build_brand_pack": (".brand", "build_brand_pack"),
+ **{name: (".dataset", name) for name in ("async_build_dataset_pack", "build_dataset_pack")},
+ "build_feed_pack": (".feed", "build_feed_pack"),
+ "build_image_pack": (".visuals", "build_image_pack"),
+ "build_openapi_pack": (".openapi", "build_openapi_pack"),
+ **{name: (".package", name) for name in ("async_build_package_pack", "build_package_pack")},
+ **{name: (".paper", name) for name in ("async_build_paper_pack", "build_paper_pack")},
+ "build_policy_pack": (".policy_pack", "build_policy_pack"),
+ "build_relationship_pack": (".relationship", "build_relationship_pack"),
+ "build_product_pack": (".product", "build_product_pack"),
+ **{name: (".repo", name) for name in ("async_build_repo_pack", "build_repo_pack")},
+ **{name: (".standards", name) for name in ("async_build_standards_pack", "build_standards_pack")},
+ "build_styleguide_pack": (".styleguide", "build_styleguide_pack"),
+ **{name: (".transcript", name) for name in ("async_build_transcript_pack", "build_transcript_pack")},
+ **{name: (".wiki", name) for name in ("async_build_wiki_pack", "build_wiki_pack")},
+ "capture_screenshot_pack": (".visuals", "capture_screenshot_pack"),
+}
+
+__all__ = list(PUBLIC_CONTEXT_PACK_EXPORTS)
+
+
+def __getattr__(name: str) -> Any:
+ target = _LAZY_EXPORTS.get(name)
+ if target is None:
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+ module_name, attribute_name = target
+ value = getattr(import_module(module_name, __name__), attribute_name)
+ globals()[name] = value
+ return value
+
+
+def __dir__() -> list[str]:
+ return sorted(set(globals()) | set(__all__))
+
+
+if TYPE_CHECKING:
+ from .brand import build_brand_pack
+ from .dataset import async_build_dataset_pack, build_dataset_pack
+ from .feed import build_feed_pack
+ from .openapi import build_openapi_pack
+ from .package import async_build_package_pack, build_package_pack
+ from .paper import async_build_paper_pack, build_paper_pack
+ from .policy_pack import build_policy_pack
+ from .product import build_product_pack
+ from .relationship import build_relationship_pack
+ from .repo import async_build_repo_pack, build_repo_pack
+ from .standards import async_build_standards_pack, build_standards_pack
+ from .styleguide import build_styleguide_pack
+ from .transcript import async_build_transcript_pack, build_transcript_pack
+ from .visuals import build_image_pack, capture_screenshot_pack
+ from .wiki import async_build_wiki_pack, build_wiki_pack
+
assert tuple(__all__) == PUBLIC_CONTEXT_PACK_EXPORTS
+assert set(_LAZY_EXPORTS) == set(__all__)
diff --git a/src/docpull/context_packs/cli.py b/src/docpull/context_packs/cli.py
index e727e4c..e025461 100644
--- a/src/docpull/context_packs/cli.py
+++ b/src/docpull/context_packs/cli.py
@@ -184,9 +184,12 @@ def run_standards_pack_cli(argv: list[str] | None = None) -> int:
def run_dataset_pack_cli(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="docpull dataset-pack",
- description="Build a local v3 pack from local CSV, TSV, JSON, NDJSON, SQLite, or Parquet files",
+ description=(
+ "Build a v3 pack from local CSV, TSV, JSON, NDJSON, SQLite, or Parquet files, "
+ "or bounded HTTPS JSON/CSV snapshots"
+ ),
)
- parser.add_argument("sources", nargs="+", help="Local dataset files")
+ parser.add_argument("sources", nargs="+", help="Local dataset files or HTTPS JSON/CSV URLs")
_add_typed_common_args(parser, DEFAULT_DATASET_OUTPUT_DIR, max_items_default=50)
args = parser.parse_args(argv)
return _run_and_print(
diff --git a/src/docpull/context_packs/common.py b/src/docpull/context_packs/common.py
index 346ba5d..350c72e 100644
--- a/src/docpull/context_packs/common.py
+++ b/src/docpull/context_packs/common.py
@@ -20,7 +20,6 @@
BudgetUsage,
HashDigest,
ReplayConfiguration,
- WorkflowFailure,
WorkflowProgressEvent,
WorkflowResult,
WorkflowWarning,
@@ -30,6 +29,7 @@
file_sha256,
new_progress_event,
stable_id,
+ workflow_failure_from_mapping,
)
from ..core.fetcher import Fetcher
from ..evidence import classify_source_authority, document_identity, evidence_span_payload
@@ -37,6 +37,7 @@
from ..http.rate_limiter import PerHostRateLimiter
from ..models.config import DocpullConfig, ProfileName
from ..models.document import DocumentRecord
+from ..models.events import SkipReason
from ..models.run import RunIdentity
from ..output_contract import OUTPUT_CONTRACT_SCHEMA_VERSION, write_raw_contract_sidecars
from ..pack_tools import build_citation_map
@@ -449,12 +450,48 @@ async def fetch_pages(
ctx = await fetcher.fetch_one(url, save=False)
run.http_request_count += 1
if ctx.error:
- run.errors.append({"url": url, "error": ctx.error})
+ run.errors.append(
+ {
+ "url": url,
+ "error": ctx.error,
+ "stage": "fetch",
+ "http_status": ctx.status_code,
+ "attempts": ctx.http_attempts or (config.network.max_retries + 1),
+ "retry_after_seconds": ctx.retry_after_seconds,
+ }
+ )
continue
if ctx.should_skip:
- run.warnings.append(
- {"code": "page_skipped", "message": str(ctx.skip_reason or "skipped"), "url": url}
- )
+ if ctx.skip_code in {
+ SkipReason.HTTP_ERROR,
+ SkipReason.ROBOTS_DISALLOWED,
+ SkipReason.URL_VALIDATION_FAILED,
+ }:
+ run.errors.append(
+ {
+ "url": url,
+ "error": str(ctx.skip_reason or "Page acquisition blocked"),
+ "code": (
+ f"http_{ctx.status_code}"
+ if ctx.status_code
+ else str(ctx.skip_code.value if ctx.skip_code else "page_blocked")
+ ),
+ "stage": "fetch",
+ "http_status": ctx.status_code,
+ "attempts": ctx.http_attempts or (config.network.max_retries + 1),
+ "retry_after_seconds": ctx.retry_after_seconds,
+ "blocked": ctx.skip_code
+ in {SkipReason.ROBOTS_DISALLOWED, SkipReason.URL_VALIDATION_FAILED},
+ }
+ )
+ else:
+ run.warnings.append(
+ {
+ "code": "page_skipped",
+ "message": str(ctx.skip_reason or "skipped"),
+ "url": url,
+ }
+ )
continue
html = (ctx.html or b"").decode("utf-8", errors="replace")
snapshots.append(
@@ -1015,16 +1052,7 @@ def _write_workflow_contract_files(
)
for item in run.warnings
]
- failure_models = [
- WorkflowFailure(
- message=str(item.get("error") or item.get("message") or "Acquisition failure"),
- source_url=str(item.get("url")) if item.get("url") else None,
- metadata={
- str(key): value for key, value in item.items() if key not in {"error", "message", "url"}
- },
- )
- for item in run.errors
- ]
+ failure_models = [workflow_failure_from_mapping(item, default_stage="fetch") for item in run.errors]
status: Literal["completed", "completed_with_warnings", "failed", "cancelled"]
if failure_models and not result_payload.get("summary"):
status = "failed"
diff --git a/src/docpull/context_packs/dataset.py b/src/docpull/context_packs/dataset.py
index d80a66d..398984e 100644
--- a/src/docpull/context_packs/dataset.py
+++ b/src/docpull/context_packs/dataset.py
@@ -4,14 +4,23 @@
import asyncio
import csv
+import hashlib
+import io
import json
import sqlite3
from collections import Counter
from pathlib import Path
from typing import Any
+from urllib.parse import parse_qsl, urlparse
from .common import ContextPackError, write_json
-from .typed import PrepareLevel, TypedPackItem, simple_summary_markdown, write_typed_pack
+from .typed import (
+ PrepareLevel,
+ TypedPackItem,
+ read_https_text,
+ simple_summary_markdown,
+ write_typed_pack,
+)
from .typed_models import DatasetSchemaArtifact
DATASET_WORKFLOW = "dataset-pack"
@@ -27,21 +36,30 @@ def build_dataset_pack(
chunk_tokens: int = 4000,
prepare_level: PrepareLevel = "raw",
) -> dict[str, Any]:
- """Summarize local CSV/TSV/JSON/NDJSON/SQLite/Parquet datasets as a v3 pack."""
+ """Summarize local datasets and bounded remote HTTPS JSON/CSV snapshots."""
if not sources:
- raise ContextPackError("dataset-pack requires at least one local file.")
+ raise ContextPackError("dataset-pack requires at least one local file or HTTPS JSON/CSV URL.")
output_dir = output_dir.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
items: list[TypedPackItem] = []
schemas: list[dict[str, Any]] = []
for source in sources[:max_items]:
+ source_text = str(source)
+ if _is_remote_source(source_text):
+ summaries = [_remote_summary(source_text)]
+ for summary in summaries:
+ schemas.append(summary)
+ items.append(_item_for_summary(None, summary, source_url=source_text))
+ if len(items) >= max_items:
+ break
+ continue
path = Path(source).expanduser().resolve()
if not path.exists() or not path.is_file():
raise ContextPackError(f"Dataset source file does not exist: {path}")
summaries = _summaries_for_path(path, max_items=max_items - len(items))
for summary in summaries:
schemas.append(summary)
- items.append(_item_for_summary(path, summary))
+ items.append(_item_for_summary(path, summary, source_url=path.as_uri()))
if len(items) >= max_items:
break
if len(items) >= max_items:
@@ -69,7 +87,7 @@ def build_dataset_pack(
index_payload={"datasets": schemas},
summary_markdown=simple_summary_markdown(
title="Dataset Pack",
- source=", ".join(str(Path(source)) for source in sources),
+ source=", ".join(str(source) for source in sources),
items=items,
),
result_summary={"dataset_count": len(schemas)},
@@ -104,6 +122,86 @@ def _summaries_for_path(path: Path, *, max_items: int) -> list[dict[str, Any]]:
raise ContextPackError(f"Unsupported dataset file type: {path.suffix or path.name}")
+def _is_remote_source(value: str) -> bool:
+ parsed = urlparse(value)
+ return parsed.scheme == "https" and bool(parsed.netloc)
+
+
+def _remote_summary(url: str) -> dict[str, Any]:
+ parsed = urlparse(url)
+ suffix = Path(parsed.path).suffix.lower()
+ accept = "application/json,text/csv;q=0.9" if suffix == ".json" else "text/csv,application/json;q=0.9"
+ try:
+ response = read_https_text(url, accept=accept, max_bytes=10_000_000)
+ except ValueError as err:
+ raise ContextPackError(str(err)) from err
+ content_type = response.content_type.split(";", 1)[0].strip().lower()
+ snapshot_hash = hashlib.sha256(response.text.encode("utf-8")).hexdigest()
+ provenance = {
+ "original_url": url,
+ "resolved_url": response.url,
+ "query_parameters": [
+ {"name": name, "value": value} for name, value in parse_qsl(parsed.query, keep_blank_values=True)
+ ],
+ "snapshot_hash": snapshot_hash,
+ "hash_algorithm": "sha256",
+ "content_type": response.content_type,
+ "http_status": response.status_code,
+ }
+ name = Path(parsed.path).name or parsed.netloc
+ if suffix == ".json" or "json" in content_type:
+ data = json.loads(response.text)
+ if isinstance(data, list):
+ json_rows = [row for row in data[:MAX_SAMPLE_ROWS] if isinstance(row, dict)]
+ summary = _tabular_summary(
+ path=None,
+ kind="json",
+ name=name,
+ rows=json_rows,
+ fieldnames=sorted({key for row in json_rows for key in row}),
+ row_count=len(data),
+ source=url,
+ )
+ elif isinstance(data, dict):
+ summary = {
+ "kind": "json",
+ "name": name,
+ "path": url,
+ "row_count": 1,
+ "columns": [
+ {"name": key, "types": [type(value).__name__], "null_count": int(value is None)}
+ for key, value in sorted(data.items())
+ ],
+ "sample": data,
+ }
+ else:
+ raise ContextPackError(f"Remote JSON dataset must be an object or array: {url}")
+ elif suffix == ".csv" or content_type in {"text/csv", "application/csv"}:
+ reader = csv.DictReader(io.StringIO(response.text))
+ csv_rows: list[dict[str, Any]] = []
+ row_count = 0
+ for row in reader:
+ row_count += 1
+ if len(csv_rows) < MAX_SAMPLE_ROWS:
+ csv_rows.append(dict(row))
+ summary = _tabular_summary(
+ path=None,
+ kind="csv",
+ name=name,
+ rows=csv_rows,
+ fieldnames=list(reader.fieldnames or []),
+ row_count=row_count,
+ source=url,
+ )
+ else:
+ raise ContextPackError(
+ f"Remote dataset must be HTTPS JSON or CSV; received {response.content_type or 'unknown'}: {url}"
+ )
+ summary["provenance"] = provenance
+ summary["snapshot_hash"] = snapshot_hash
+ return summary
+
+
def _delimited_summary(path: Path, *, delimiter: str) -> dict[str, Any]:
rows: list[dict[str, Any]] = []
row_count = 0
@@ -224,12 +322,13 @@ def _parquet_summary(path: Path) -> dict[str, Any]:
def _tabular_summary(
*,
- path: Path,
+ path: Path | None,
kind: str,
name: str,
rows: list[dict[str, Any]],
fieldnames: list[str],
row_count: int | None,
+ source: str | None = None,
) -> dict[str, Any]:
columns = []
for field in fieldnames:
@@ -246,7 +345,7 @@ def _tabular_summary(
return {
"kind": kind,
"name": name,
- "path": str(path),
+ "path": source or str(path),
"row_count": row_count if row_count is not None else None,
"sample_row_count": len(rows),
"column_count": len(fieldnames),
@@ -255,21 +354,38 @@ def _tabular_summary(
}
-def _item_for_summary(path: Path, summary: dict[str, Any]) -> TypedPackItem:
+def _item_for_summary(
+ path: Path | None,
+ summary: dict[str, Any],
+ *,
+ source_url: str,
+) -> TypedPackItem:
markdown = _summary_markdown(summary)
- title = f"{Path(summary['path']).name}: {summary['name']}"
+ parsed = urlparse(source_url)
+ source_name = Path(parsed.path).name if parsed.scheme else Path(summary["path"]).name
+ title = f"{source_name or parsed.netloc}: {summary['name']}"
+ provenance_raw = summary.get("provenance")
+ provenance: dict[str, Any] = provenance_raw if isinstance(provenance_raw, dict) else {}
return TypedPackItem(
title=title,
- url=path.as_uri(),
+ url=source_url,
markdown=markdown,
source_type="dataset",
item_kind=str(summary["kind"]),
metadata={
- "dataset_path": str(path),
+ "dataset_path": str(path) if path else None,
+ "dataset_url": source_url if path is None else None,
"dataset_name": summary["name"],
"dataset_kind": summary["kind"],
+ "snapshot_hash": summary.get("snapshot_hash"),
+ "provenance": provenance,
+ },
+ route={
+ "source_kind": "https" if path is None else "file",
+ "source_url": source_url,
+ "original_url": provenance.get("original_url"),
+ "resolved_url": provenance.get("resolved_url"),
},
- route={"source_kind": "file", "source_url": path.as_uri()},
public={"row_count": summary.get("row_count"), "column_count": summary.get("column_count")},
)
diff --git a/src/docpull/context_packs/relationship.py b/src/docpull/context_packs/relationship.py
new file mode 100644
index 0000000..7f16f96
--- /dev/null
+++ b/src/docpull/context_packs/relationship.py
@@ -0,0 +1,567 @@
+"""Evidence-backed relationship candidates for human review."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+from pathlib import Path
+from typing import Any, Literal, cast
+from urllib.parse import urlparse
+
+from ..contracts import (
+ CoverageResult,
+ RelationshipCandidate,
+ RelationshipPack,
+ WorkflowWarning,
+ canonical_sha256,
+ stable_id,
+ workflow_failure_from_mapping,
+)
+from ..evidence import evidence_span_payload
+from ..policy import PolicyConfig
+from .common import (
+ ContextPackError,
+ ContextPackRun,
+ PageSnapshot,
+ artifact_ref,
+ domain_from_input,
+ fetch_pages_blocking,
+ homepage_url_for_domain,
+ likely_internal_pages,
+ public_url,
+ status_from_errors,
+ write_basic_pack_files,
+ write_json,
+)
+
+RELATIONSHIP_WORKFLOW = "relationship-pack"
+DEFAULT_RELATIONSHIP_OUTPUT_DIR = Path("packs/relationship")
+SUPPORTED_RELATIONSHIP_PREDICATES = (
+ "owned_by",
+ "operated_by",
+ "acquired_by",
+ "franchised_by",
+ "invested_in",
+)
+
+
+def extract_relationship_candidates_from_records(
+ records: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
+ """Extract deterministic review candidates from existing pack records."""
+
+ candidates: list[dict[str, Any]] = []
+ for index, record in enumerate(records):
+ if not isinstance(record, dict):
+ continue
+ content = str(record.get("content") or "")
+ url = str(record.get("url") or "")
+ if not content or not url:
+ continue
+ metadata_raw = record.get("metadata")
+ extraction_raw = record.get("extraction")
+ metadata: dict[str, Any] = metadata_raw if isinstance(metadata_raw, dict) else {}
+ extraction: dict[str, Any] = extraction_raw if isinstance(extraction_raw, dict) else {}
+ subject = next(
+ (
+ str(value).strip()
+ for value in (
+ metadata.get("entity_name"),
+ metadata.get("brand_name"),
+ metadata.get("company_name"),
+ extraction.get("entity_name"),
+ )
+ if isinstance(value, str) and value.strip()
+ ),
+ "",
+ )
+ if not subject:
+ title = str(record.get("title") or "").strip()
+ subject = re.split(r"\s+[|—–-]\s+", title, maxsplit=1)[0].strip()
+ if not subject:
+ subject = (urlparse(url).hostname or "Unknown entity").removeprefix("www.")
+ page = PageSnapshot(
+ url=url,
+ title=str(record.get("title") or subject),
+ html="",
+ markdown=content,
+ metadata=metadata,
+ extraction=extraction,
+ source_type=str(record.get("source_type") or "relationship-source"),
+ )
+ candidates.extend(
+ _extract_relationship_candidates(
+ {
+ "name": subject,
+ "location_scope": metadata.get("location_scope"),
+ },
+ [page],
+ citation_offset=index,
+ )
+ )
+ deduped = {
+ str(candidate.get("candidate_id")): candidate
+ for candidate in candidates
+ if candidate.get("candidate_id")
+ }
+ return [deduped[key] for key in sorted(deduped)]
+
+
+def build_relationship_pack(
+ sources: list[str | dict[str, Any]] | str,
+ *,
+ output_dir: Path = DEFAULT_RELATIONSHIP_OUTPUT_DIR,
+ policy: PolicyConfig | None = None,
+ max_pages_per_source: int = 4,
+) -> dict[str, Any]:
+ """Extract cited relationship observations and one coverage result per input."""
+
+ raw_sources = [sources] if isinstance(sources, str) else list(sources)
+ if not raw_sources:
+ raise ContextPackError("relationship-pack requires at least one source input.")
+ if max_pages_per_source < 1:
+ raise ContextPackError("max_pages_per_source must be at least 1.")
+
+ specs = [_normalize_source(item, index=index) for index, item in enumerate(raw_sources, start=1)]
+ domains = sorted(
+ {
+ domain
+ for spec in specs
+ for domain in spec["official_domains"]
+ if isinstance(domain, str) and domain
+ }
+ )
+ effective_policy = policy or PolicyConfig(allowed_domains=domains)
+ if policy is not None and not policy.allowed_domains and domains:
+ effective_policy = PolicyConfig.model_validate(
+ {**policy.model_dump(mode="json"), "allowed_domains": domains}
+ )
+ output_dir = output_dir.resolve()
+ run = ContextPackRun(
+ workflow=RELATIONSHIP_WORKFLOW,
+ output_dir=output_dir,
+ policy=effective_policy,
+ input_value=str(specs[0].get("url") or specs[0].get("path") or specs[0]["name"]),
+ )
+
+ all_pages: list[PageSnapshot] = []
+ coverage_rows: list[dict[str, Any]] = []
+ candidates: list[dict[str, Any]] = []
+ for spec in specs:
+ error_start = len(run.errors)
+ warning_start = len(run.warnings)
+ pages = _pages_for_source(spec, run=run, max_pages=max_pages_per_source)
+ page_offset = len(all_pages)
+ all_pages.extend(pages)
+ extracted = _extract_relationship_candidates(
+ spec,
+ pages,
+ citation_offset=page_offset,
+ )
+ candidates.extend(extracted)
+ failures = [
+ workflow_failure_from_mapping(item, default_stage="fetch") for item in run.errors[error_start:]
+ ]
+ warnings = [
+ WorkflowWarning.model_validate(
+ {
+ "code": str(item.get("code") or "warning"),
+ "message": str(item.get("message") or "Workflow warning"),
+ "metadata": item.get("metadata") if isinstance(item.get("metadata"), dict) else {},
+ }
+ )
+ for item in run.warnings[warning_start:]
+ ]
+ if extracted:
+ coverage_status = "candidate_found"
+ elif any(failure.retryable for failure in failures):
+ coverage_status = "retryable_failure"
+ elif pages:
+ coverage_status = "acquired_no_candidate"
+ else:
+ coverage_status = "blocked"
+ if not extracted:
+ warnings.append(
+ WorkflowWarning(
+ code="coverage_gap",
+ message=(
+ "No reviewable relationship candidate was established for this input; "
+ "this is not a negative ownership or independence claim."
+ ),
+ metadata={"input_id": spec["input_id"]},
+ )
+ )
+ coverage = CoverageResult(
+ input_id=spec["input_id"],
+ input={
+ "name": spec["name"],
+ "url": spec.get("url"),
+ "path": spec.get("path"),
+ "location_scope": spec.get("location_scope"),
+ "official_domains": spec["official_domains"],
+ },
+ status=cast(
+ Literal[
+ "candidate_found",
+ "acquired_no_candidate",
+ "retryable_failure",
+ "blocked",
+ ],
+ coverage_status,
+ ),
+ acquired_document_count=len(pages),
+ coverage_gap=not bool(extracted),
+ candidates=[RelationshipCandidate.model_validate(item) for item in extracted],
+ failures=failures,
+ warnings=warnings,
+ )
+ coverage_rows.append(coverage.model_dump(mode="json", exclude_none=True))
+
+ if len(coverage_rows) != len(specs):
+ raise ContextPackError("relationship-pack did not emit exactly one coverage result per input.")
+
+ pack_identity = {
+ "pack_id": stable_id("pack", {"workflow": RELATIONSHIP_WORKFLOW, "coverage": coverage_rows}),
+ "input_count": len(specs),
+ "content_hash": canonical_sha256(coverage_rows),
+ }
+ run_identity = {
+ "run_id": stable_id(
+ "run",
+ {
+ "pack_id": pack_identity["pack_id"],
+ "inputs": [spec["input_id"] for spec in specs],
+ "max_pages_per_source": max_pages_per_source,
+ },
+ ),
+ "scheduler": None,
+ }
+ contract = RelationshipPack(
+ pack_identity=pack_identity,
+ run_identity=run_identity,
+ coverage=[CoverageResult.model_validate(item) for item in coverage_rows],
+ candidates=[RelationshipCandidate.model_validate(item) for item in candidates],
+ summary={
+ "input_count": len(specs),
+ "coverage_count": len(coverage_rows),
+ "candidate_count": len(candidates),
+ "candidate_found_count": sum(item["status"] == "candidate_found" for item in coverage_rows),
+ "acquired_no_candidate_count": sum(
+ item["status"] == "acquired_no_candidate" for item in coverage_rows
+ ),
+ "retryable_failure_count": sum(item["status"] == "retryable_failure" for item in coverage_rows),
+ "blocked_count": sum(item["status"] == "blocked" for item in coverage_rows),
+ },
+ )
+ contract_path = output_dir / "relationship.pack.v1.json"
+ write_json(contract_path, contract.model_dump(mode="json", exclude_none=True))
+ result_payload = {
+ "workflow": RELATIONSHIP_WORKFLOW,
+ "provider": "local",
+ "status": status_from_errors(run.errors),
+ "input": {"sources": specs},
+ "summary": contract.summary,
+ "coverage": coverage_rows,
+ "relationship_candidates": candidates,
+ "warnings": run.warnings,
+ "errors": run.errors,
+ "replay_config": {"max_pages_per_source": max_pages_per_source},
+ }
+ return write_basic_pack_files(
+ run=run,
+ pages=all_pages,
+ result_filename="relationship.result.json",
+ result_payload=result_payload,
+ markdown_filename="RELATIONSHIPS.md",
+ markdown_text=_relationship_markdown(contract),
+ pack_filename="relationship.pack.json",
+ extra_artifacts={
+ "relationship_contract": artifact_ref(output_dir, contract_path),
+ },
+ )
+
+
+def _normalize_source(item: str | dict[str, Any], *, index: int) -> dict[str, Any]:
+ raw = {"value": item} if isinstance(item, str) else dict(item)
+ value = str(raw.get("url") or raw.get("path") or raw.get("value") or "").strip()
+ path = Path(value).expanduser()
+ resolved_path = path.resolve() if value and path.exists() else None
+ url: str | None = None
+ if resolved_path is None:
+ domain = domain_from_input(value)
+ if domain:
+ url = public_url(value if "://" in value else homepage_url_for_domain(domain))
+ else:
+ domain = None
+ supplied_raw = raw.get("official_domains") or (
+ [raw["official_domain"]] if raw.get("official_domain") else []
+ )
+ supplied_domains = [supplied_raw] if isinstance(supplied_raw, str) else list(supplied_raw)
+ official_domains = sorted(
+ {
+ str(domain).lower().removeprefix("www.").rstrip(".")
+ for domain in [*list(supplied_domains), domain]
+ if domain
+ }
+ )
+ name = str(raw.get("name") or "").strip()
+ name_explicit = bool(name)
+ if not name:
+ name = resolved_path.stem if resolved_path else (domain or f"input-{index}")
+ identity = {
+ "name": name,
+ "url": url,
+ "path": str(resolved_path) if resolved_path else None,
+ "location_scope": raw.get("location_scope"),
+ "official_domains": official_domains,
+ }
+ return {
+ **identity,
+ "input_id": str(raw.get("input_id") or stable_id("input", identity)),
+ "name_explicit": name_explicit,
+ }
+
+
+def _pages_for_source(
+ spec: dict[str, Any],
+ *,
+ run: ContextPackRun,
+ max_pages: int,
+) -> list[PageSnapshot]:
+ if spec.get("path"):
+ pages = _pages_from_path(Path(str(spec["path"])))
+ if pages and not spec.get("name_explicit"):
+ entity_name = pages[0].metadata.get("entity_name")
+ if isinstance(entity_name, str) and entity_name.strip():
+ spec["name"] = entity_name.strip()
+ return pages
+ url = str(spec.get("url") or "")
+ if not url:
+ run.errors.append(
+ {
+ "code": "source_unavailable",
+ "stage": "input",
+ "error": "Relationship source has no usable HTTPS URL or local pack path.",
+ "blocked": True,
+ }
+ )
+ return []
+ domain = domain_from_input(url)
+ if not domain:
+ run.errors.append(
+ {"code": "invalid_source", "stage": "input", "error": f"Invalid source: {url}", "blocked": True}
+ )
+ return []
+ allowed, reason = run.policy.allows_url(url)
+ if not allowed:
+ run.errors.append(
+ {
+ "code": "policy_denied",
+ "stage": "policy",
+ "url": url,
+ "error": f"Source policy blocked relationship input: {reason}",
+ "blocked": True,
+ }
+ )
+ return []
+ home = fetch_pages_blocking([url], run=run, max_pages=1)
+ if not home or max_pages == 1:
+ return home
+ candidates = likely_internal_pages(home[0], domain, max_pages=max_pages)
+ pages = fetch_pages_blocking(candidates, run=run, max_pages=max_pages)
+ return pages or home
+
+
+def _pages_from_path(path: Path) -> list[PageSnapshot]:
+ records_path = path / "documents.ndjson" if path.is_dir() else path
+ if not records_path.exists() or not records_path.is_file():
+ return []
+ pages: list[PageSnapshot] = []
+ for line in records_path.read_text(encoding="utf-8").splitlines():
+ if not line.strip():
+ continue
+ try:
+ record = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ if not isinstance(record, dict):
+ continue
+ content = str(record.get("content") or "")
+ url = str(record.get("url") or "")
+ if not content or not url:
+ continue
+ pages.append(
+ PageSnapshot(
+ url=url,
+ title=str(record.get("title") or url),
+ html="",
+ markdown=content,
+ metadata=dict(record.get("metadata") or {}),
+ extraction=dict(record.get("extraction") or {}),
+ source_type=str(record.get("source_type") or "relationship-source"),
+ )
+ )
+ return pages
+
+
+def _extract_relationship_candidates(
+ spec: dict[str, Any],
+ pages: list[PageSnapshot],
+ *,
+ citation_offset: int,
+) -> list[dict[str, Any]]:
+ subject = str(spec["name"])
+ escaped = re.escape(subject)
+ patterns = (
+ (
+ "owned_by",
+ rf"\b{escaped}\b[^.!?\n]{{0,60}}?\b(?:is|was|became|remains)?\s*"
+ rf"(?:wholly\s+)?owned by\s+(?P