Metadata-Version: 2.4
Name: paper-scoring-digest
Version: 0.1.0
Summary: Daily ranked Paper Scoring digest for OpenClaw and Slack
Project-URL: Repository, https://github.com/maikeruSan/paper-scoring-digest
Author: Paper Scoring contributors
License: MIT License
        
        Copyright (c) 2026 Paper Scoring contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Requires-Dist: paper-scoring-connectors[all]<0.2.0,>=0.1.0
Requires-Dist: paper-scoring-core<0.2.0,>=0.1.0
Requires-Dist: paper-scoring-pipeline<0.2.0,>=0.1.1
Requires-Dist: paper-scoring-reporting<0.2.0,>=0.1.0
Requires-Dist: pydantic<3,>=2.11
Requires-Dist: python-dotenv<2,>=1.1
Requires-Dist: tzdata>=2025.2; sys_platform == 'win32'
Provides-Extra: security
Requires-Dist: bandit[toml]<2,>=1.8; extra == 'security'
Requires-Dist: detect-secrets<2,>=1.5; extra == 'security'
Requires-Dist: pip-audit<3,>=2.9; extra == 'security'
Provides-Extra: test
Requires-Dist: build<2,>=1.3; extra == 'test'
Requires-Dist: coverage[toml]<8,>=7.10; extra == 'test'
Requires-Dist: mypy<2,>=1.18; extra == 'test'
Requires-Dist: packaging<27,>=25; extra == 'test'
Requires-Dist: pytest<10,>=9.0.3; extra == 'test'
Requires-Dist: ruff<1,>=0.12; extra == 'test'
Requires-Dist: twine<7,>=6.2; extra == 'test'
Description-Content-Type: text/markdown

# Paper Scoring Digest

`paper-scoring-digest` is the scheduling and delivery adapter for Paper
Scoring. It keeps arXiv discovery, local retention, Slack presentation, and
discussion state outside the provider-neutral core packages.

The daily workflow:

1. queries `cs.AI`, `cs.DB`, and `cs.LG` as one arXiv candidate pool;
2. removes cross-list duplicates before scoring;
3. ranks the merged pool and publishes one combined Top 20 (not 20 per
   category);
4. downloads and retains the selected PDFs;
5. renders an email-like Slack card through OpenClaw; and
6. deletes PDFs that have reached seven days of age.

Metadata, scores, and arXiv links remain after the PDFs expire, so an Agent can
still explain an earlier ranking. A discussion that requires an expired PDF
must fetch it again from arXiv explicitly.

## Runtime contract

- Python 3.11, 3.12, or 3.13 on Ubuntu and Windows.
- Docker Engine or Docker Desktop with Docker Compose v2.
- Paper Scoring package-to-package dependencies come only from PyPI. Git,
  local-path, and workspace dependency overrides are not used in production.
- Secrets are injected at process start. They are not copied into wheels,
  container images, reports, GitHub Actions, or OpenClaw cron arguments.
- Scheduling and retention use the configured IANA timezone. Automatic arXiv
  discovery selects the latest completed UTC submission date. The production
  schedule below uses `Asia/Taipei`.
- Slack delivery uses a stable channel ID and a configured OpenClaw Slack
  account. Slack tokens remain in OpenClaw's runtime secret store.
- On native Windows, collection, scoring, state inspection, and retention are
  supported, but Slack delivery rejects npm `.cmd`/`.bat` shims because Windows
  may parse them through `cmd.exe` despite `shell=False`. Run delivery from a
  trusted Linux/WSL/Docker OpenClaw host or a native OpenClaw `.exe` launcher.

## Install from PyPI

Ubuntu:

```shell
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --index-url https://pypi.org/simple "paper-scoring-digest==0.1.0"
```

Windows PowerShell:

```powershell
py -3.13 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --index-url https://pypi.org/simple "paper-scoring-digest==0.1.0"
```

Copy `.env.example` to a location outside the repository, fill only the
provider values you need, and restrict access to that file. On Ubuntu:

```shell
install -d -m 700 "${HOME}/.config/paper-scoring"
install -m 600 .env.example "${HOME}/.config/paper-scoring/digest.env"
```

On Windows, store the file under a user-only directory and use the file's
Security properties or `icacls` to remove access for other users. Do not place
the file in the repository. Put the digest state directory under the same
user-only ACL: POSIX `0700`/`0600` mode changes do not configure Windows DACLs.

For an OpenAI-backed run, set `PAPER_SCORING_PROVIDER`,
`PAPER_SCORING_MODEL`, and `OPENAI_API_KEY`. For a local Ollama-backed run,
set the provider to `ollama`, select an installed model, and optionally set
`OLLAMA_BASE_URL`; no cloud API key is needed.

## Run once

The following is an illustrative production-shaped invocation. Replace every
angle-bracket placeholder; never paste a credential into the command line.

```shell
paper-scoring-digest run \
  --env-file <ABSOLUTE_RUNTIME_ENV_FILE> \
  --state-dir <ABSOLUTE_PRIVATE_STATE_DIRECTORY> \
  --category cs.AI \
  --category cs.DB \
  --category cs.LG \
  --top-n 20 \
  --retention-days 7 \
  --timezone Asia/Taipei \
  --slack-channel <SLACK_CHANNEL_ID> \
  --slack-account <OPENCLAW_SLACK_ACCOUNT>
```

By default the service pages through the complete daily category union before
ranking, so "Top 20" covers the full discovered day. The optional
`--candidate-limit 60` changes the meaning to "Top 20 from the latest 60
de-duplicated candidates" and bounds model cost; the Slack context labels that
scope explicitly. Re-running is idempotent: a pending delivery is handled
before a new daily run is created.

Useful read-only commands:

```shell
paper-scoring-digest show --run-id <YYYY-MM-DD> --rank 3
paper-scoring-digest show --run-id latest --paper-id <ARXIV_ID>
paper-scoring-digest presentation --run-id latest
paper-scoring-digest prune --state-dir <ABSOLUTE_PRIVATE_STATE_DIRECTORY> --retention-days 7 --timezone Asia/Taipei
```

Each successful run writes a private manifest plus text, HTML, and PDF reports
under the state directory. Only PDFs are removed by retention. Each manifest
stores `pdf_expires_at`, calculated from the instant when the digest was
created; an old arXiv submission downloaded during catch-up therefore still
receives the full 168-hour retention period.

## OpenClaw and Slack

Install the canonical cross-Agent Skill from PyPI, then install it into the
dedicated OpenClaw workspace:

```shell
python -m pip install --index-url https://pypi.org/simple "paper-scoring-skills[openai]>=0.2,<0.3"
paper-scoring-skills install --client openclaw --scope project
openclaw agents add paper-research \
  --workspace <ABSOLUTE_OPENCLAW_AGENT_WORKSPACE> \
  --model <PROVIDER/MODEL> \
  --non-interactive
```

Configure the target Slack channel with `replyToMode: "all"` and add an exact
peer binding to `paper-research`. The fragment below is illustrative:

```json
{
  "channels": {
    "slack": {
      "channels": {
        "<SLACK_CHANNEL_ID>": { "replyToMode": "all" }
      }
    }
  },
  "bindings": [
    {
      "agentId": "paper-research",
      "match": {
        "channel": "slack",
        "accountId": "<OPENCLAW_SLACK_ACCOUNT>",
        "peer": { "kind": "channel", "id": "<SLACK_CHANNEL_ID>" }
      }
    }
  ]
}
```

`bindings` is an array and a config patch replaces arrays. Export and back up
the current OpenClaw config, merge this item with every existing binding, run
`openclaw config patch --dry-run`, and only then apply it. Do not paste the
fragment directly over a live bindings array.

Create the job disabled, test it, and enable it after the card and fallback
delivery have both been verified:

```shell
openclaw cron add \
  --name paper-scoring-daily-digest \
  --declaration-key paper-scoring-daily-digest-v1 \
  --cron "15 19 * * *" \
  --tz Asia/Taipei \
  --exact \
  --command-argv '["<VENV_BIN>/paper-scoring-digest","run","--env-file","<ABSOLUTE_RUNTIME_ENV_FILE>","--state-dir","<ABSOLUTE_PRIVATE_STATE_DIRECTORY>","--category","cs.AI","--category","cs.DB","--category","cs.LG","--top-n","20","--retention-days","7","--timezone","Asia/Taipei","--slack-channel","<SLACK_CHANNEL_ID>","--slack-account","<OPENCLAW_SLACK_ACCOUNT>","--cron-mode"]' \
  --command-cwd <ABSOLUTE_OPENCLAW_AGENT_WORKSPACE> \
  --timeout-seconds 14400 \
  --announce \
  --channel slack \
  --account <OPENCLAW_SLACK_ACCOUNT> \
  --to channel:<SLACK_CHANNEL_ID> \
  --disabled
```

The digest sends a portable OpenClaw presentation that is rendered as Slack
Block Kit: a title, run context, one section per ranked paper, dividers, and a
thread instruction. The command's normal success output is suppressed in cron
mode; `--announce` is only a fallback if the command fails before direct card
delivery.

After the card arrives, reply in its thread, for example:

```text
3, 7：比較這兩篇的研究問題、方法與實驗結果
```

The exact channel binding routes the reply to `paper-research`. The Skill maps
the displayed rank to that card's stored run, reads a retained PDF when
available, and answers in the same thread. It does not silently re-score a
paper.

## Docker

The image builds the current package locally while resolving every Paper
Scoring dependency from the locked PyPI graph. The Dockerfile copies only the
package inputs; it never copies `.env` or the state directory.

Set the runtime env-file path in the shell that launches Compose:

```shell
export PAPER_SCORING_ENV_FILE="${HOME}/.config/paper-scoring/digest.env"
docker compose build
docker compose run --rm paper-scoring-digest --help
```

PowerShell:

```powershell
$env:PAPER_SCORING_ENV_FILE = "$HOME\.config\paper-scoring\digest.env"
docker compose build
docker compose run --rm paper-scoring-digest --help
```

The Compose service uses a named volume for `/data`, which avoids host
ownership differences between Ubuntu and Docker Desktop. Provider credentials
are loaded only through `env_file` when the container starts.

To collect, score, and persist a digest in the container without attempting to
access the host's OpenClaw CLI, use `--no-deliver`:

```shell
docker compose run --rm paper-scoring-digest run \
  --no-deliver \
  --state-dir /data \
  --category cs.AI --category cs.DB --category cs.LG \
  --top-n 20 --retention-days 7
```

The manifest remains `pending`; a trusted OpenClaw host with access to the
same state directory can later run the normal delivery command without
rescoring.

The Python image intentionally does not bundle OpenClaw or a Slack token. Use
it for collection, ranking, report/state inspection, and retention inside a
container; run Slack delivery from the trusted OpenClaw host (or extend the
official OpenClaw image with this PyPI package). This keeps the Gateway's
credentials and config out of the digest image.

## Verification and release evidence

Pull requests and `main` run the following gates:

- Ubuntu and Windows tests on Python 3.11-3.13, with branch coverage and JUnit
  reports;
- Ruff, formatting, strict mypy, wheel/sdist build, and Twine validation;
- a non-root, read-only Docker smoke test;
- Bandit, `pip-audit`, and `detect-secrets`; and
- a redacted Gitleaks scan of the complete Git history plus a noreply-author
  policy check.

The workflows upload test, package, and security reports as GitHub Actions
artifacts even when a gate fails. Reports must be reviewed before merging,
tagging, or publishing to PyPI. Artifacts are evidence, not a place to store
runtime credentials.

For a local equivalent using only the locked PyPI dependency graph:

```shell
uv sync --locked --no-sources --extra test --extra security
uv run --frozen ruff check .
uv run --frozen ruff format --check .
uv run --frozen mypy
uv run --frozen coverage run --branch -m pytest
uv run --frozen coverage report --show-missing
uv run --frozen bandit -c pyproject.toml -r src
uv run --frozen pip-audit
uv build --no-sources --default-index https://pypi.org/simple
uv run --frozen twine check dist/*
```

Before a release, also scan untracked files and the complete Git history. Never
publish when any report contains an unexplained secret candidate, personal
path, private email address, channel ID, token, or credential.
