From 021c4b7e650537af0d0277b65dda8be5feecc9e8 Mon Sep 17 00:00:00 2001 From: larnsce Date: Sun, 6 Sep 2026 05:51:35 +0200 Subject: [PATCH 1/4] ci(workflow): fixture gate, case suite, lint, washr drift, release tail, plugin manifests (#72, #73, #74, #75, #77) gate.yaml runs the check script on the fixtures and diffs the reports against fixtures/expected/, runs the eight check-script cases under fixtures/cases/, and the repository lint (skill path references, em dashes and emojis, heading spacing, VERSION and manifest agreement). washr-drift.yaml installs CRAN washr weekly and verifies every washr:: call the skills execute still exports, against the floor recorded in skills/pkgreview-core/WASHR_FLOOR, which both release preflights now read. scripts/release.sh tags the merge commit, verifies the pinned raw URLs, creates the GitHub release from NEWS.md, and fast-forwards dev; release.yaml repeats the verification on tag push. The proposal issue template carries an objection window; NEWS.md records non-checklist changes; the CLAUDE.md rules say so. .claude-plugin/ packages the skills as a plugin (manifest validated with claude plugin marketplace add). The permission allowlist of #73 is not committed: a session cannot write permission rules; the JSON is recorded on the issue. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019LJvVaoT3vhaJj7k9LBzEn --- .claude-plugin/marketplace.json | 29 ++++++++ .claude-plugin/plugin.json | 5 ++ .github/ISSUE_TEMPLATE/proposal.md | 26 +++++++ .github/workflows/gate.yaml | 36 ++++++++++ .github/workflows/release.yaml | 28 ++++++++ .github/workflows/washr-drift.yaml | 24 +++++++ CLAUDE.md | 49 +++++++++---- NEWS.md | 26 +++++++ .../ci-triggers-missing-dev/expected.txt | 1 + .../.github/workflows/R-CMD-check.yaml | 6 ++ .../cases/ci-triggers-nested-dev/expected.txt | 1 + .../.github/workflows/R-CMD-check.yaml | 10 +++ fixtures/cases/dictionary-schema/expected.txt | 1 + .../overlay/data-raw/dictionary.csv | 2 + fixtures/cases/docs-tracked/expected.txt | 1 + .../overlay/.github/workflows/pkgdown.yaml | 4 ++ .../docs-tracked/overlay/docs/index.html | 1 + fixtures/cases/docs-tracked/setup.sh | 1 + fixtures/cases/docs-untracked/expected.txt | 1 + .../overlay/.github/workflows/pkgdown.yaml | 4 ++ .../docs-untracked/overlay/docs/index.html | 1 + fixtures/cases/docs-untracked/setup.sh | 1 + fixtures/cases/keywords-missing/expected.txt | 2 + .../keywords-missing/overlay/DESCRIPTION | 28 ++++++++ fixtures/cases/readme-no-links/expected.txt | 1 + .../cases/readme-no-links/overlay/README.md | 3 + .../cases/readme-washr-table/expected.txt | 1 + .../readme-washr-table/overlay/README.md | 7 ++ fixtures/expected/history.md | 50 ++++++++++++++ fixtures/expected/pkgreviewtest.md | 69 +++++++++++++++++++ fixtures/run_cases.sh | 28 ++++++++ fixtures/run_gate.sh | 33 +++++++++ scripts/lint.sh | 56 +++++++++++++++ scripts/lint_text.py | 22 ++++++ scripts/news_section.sh | 10 +++ scripts/release.sh | 32 +++++++++ scripts/verify_release.sh | 28 ++++++++ scripts/washr_drift.R | 37 ++++++++++ skills/add-doi/SKILL.md | 13 ++-- skills/create-release/SKILL.md | 9 +-- skills/pkgreview-core/WASHR_FLOOR | 1 + 41 files changed, 666 insertions(+), 22 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .github/ISSUE_TEMPLATE/proposal.md create mode 100644 .github/workflows/gate.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/washr-drift.yaml create mode 100644 NEWS.md create mode 100644 fixtures/cases/ci-triggers-missing-dev/expected.txt create mode 100644 fixtures/cases/ci-triggers-missing-dev/overlay/.github/workflows/R-CMD-check.yaml create mode 100644 fixtures/cases/ci-triggers-nested-dev/expected.txt create mode 100644 fixtures/cases/ci-triggers-nested-dev/overlay/.github/workflows/R-CMD-check.yaml create mode 100644 fixtures/cases/dictionary-schema/expected.txt create mode 100644 fixtures/cases/dictionary-schema/overlay/data-raw/dictionary.csv create mode 100644 fixtures/cases/docs-tracked/expected.txt create mode 100644 fixtures/cases/docs-tracked/overlay/.github/workflows/pkgdown.yaml create mode 100644 fixtures/cases/docs-tracked/overlay/docs/index.html create mode 100644 fixtures/cases/docs-tracked/setup.sh create mode 100644 fixtures/cases/docs-untracked/expected.txt create mode 100644 fixtures/cases/docs-untracked/overlay/.github/workflows/pkgdown.yaml create mode 100644 fixtures/cases/docs-untracked/overlay/docs/index.html create mode 100644 fixtures/cases/docs-untracked/setup.sh create mode 100644 fixtures/cases/keywords-missing/expected.txt create mode 100644 fixtures/cases/keywords-missing/overlay/DESCRIPTION create mode 100644 fixtures/cases/readme-no-links/expected.txt create mode 100644 fixtures/cases/readme-no-links/overlay/README.md create mode 100644 fixtures/cases/readme-washr-table/expected.txt create mode 100644 fixtures/cases/readme-washr-table/overlay/README.md create mode 100644 fixtures/expected/history.md create mode 100644 fixtures/expected/pkgreviewtest.md create mode 100755 fixtures/run_cases.sh create mode 100755 fixtures/run_gate.sh create mode 100755 scripts/lint.sh create mode 100644 scripts/lint_text.py create mode 100755 scripts/news_section.sh create mode 100755 scripts/release.sh create mode 100755 scripts/verify_release.sh create mode 100644 scripts/washr_drift.R create mode 100644 skills/pkgreview-core/WASHR_FLOOR diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..9ec282b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,29 @@ +{ + "name": "pkgreview", + "owner": { + "name": "openwashdata" + }, + "metadata": { + "description": "The openwashdata package review skills: review-package, review-issue, create-next-issue, review-status, review-complete, review-upgrade, create-release, add-doi.", + "version": "1.6.0" + }, + "plugins": [ + { + "name": "pkgreview", + "description": "Review workflow for R data packages in registered organizations: issue-per-area review on dev, release and DOI skills, standard upgrades. The skills read shared references from skills/pkgreview-core.", + "version": "1.6.0", + "source": "./", + "strict": false, + "skills": [ + "./skills/review-package", + "./skills/review-issue", + "./skills/create-next-issue", + "./skills/review-status", + "./skills/review-complete", + "./skills/review-upgrade", + "./skills/create-release", + "./skills/add-doi" + ] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..4c76681 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "pkgreview", + "version": "1.6.0", + "description": "Review workflow for R data packages in registered organizations (openwashdata, Global Health Engineering): issue-per-area review on dev, release and DOI skills, standard upgrades." +} diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..597ef3f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,26 @@ +--- +name: Proposal +about: A change to the tooling or the standard, decided by an objection window unless the choice is open +title: "" +labels: enhancement +--- + +## Problem + +What is wrong or missing today, with the evidence (a release, a review, a file). + +## Proposal + +The change, concretely: which files, which skills, which check lines. Add an Options section only when the choice is genuinely open; otherwise this section is the decision unless an objection arrives. + +## Tasks + +- [ ] + +## Acceptance + +What must be true afterwards; for standard changes, the fixture gate outcome. + +## Objection window + +Until [date, two weeks out]. Without an objection recorded here by then, the proposal stands and is implemented in the next release; an objection turns this into a decision issue with options. diff --git a/.github/workflows/gate.yaml b/.github/workflows/gate.yaml new file mode 100644 index 0000000..8c66ac7 --- /dev/null +++ b/.github/workflows/gate.yaml @@ -0,0 +1,36 @@ +# Mechanical fixture gate, check-script case suite, and repository lint +# (openwashdata/pkgreview#72). The gate diffs the check script's report on the +# fixtures against fixtures/expected/; a changed FAIL, FLAG, or NOT RUN line +# fails here until the expected report is updated in the same change. +name: gate + +on: + push: + branches: [main, dev] + pull_request: + workflow_dispatch: + +jobs: + fixture-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + - name: Fixture gate against the expected reports + run: bash fixtures/run_gate.sh + - name: Check-script case suite + run: bash fixtures/run_cases.sh + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Repository lint + run: bash scripts/lint.sh + - name: Newest tag matches VERSION (main only) + if: github.ref == 'refs/heads/main' + run: LINT_CHECK_TAG=1 bash scripts/lint.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d11947b --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +# Release verification on every tag push (openwashdata/pkgreview#73): the +# files the skills fetch by stamped version must resolve at the tag, and the +# GitHub release exists with the NEWS.md section as its notes. +name: release + +on: + push: + tags: ["v*"] + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + - name: Pinned raw URLs resolve at the tag + run: bash scripts/verify_release.sh "${GITHUB_REF_NAME}" + - name: GitHub release from NEWS.md + run: | + v="${GITHUB_REF_NAME#v}" + if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then + echo "release exists" + else + gh release create "${GITHUB_REF_NAME}" --title "${GITHUB_REF_NAME}" --notes-file <(bash scripts/news_section.sh "$v") + fi diff --git a/.github/workflows/washr-drift.yaml b/.github/workflows/washr-drift.yaml new file mode 100644 index 0000000..a56d9a4 --- /dev/null +++ b/.github/workflows/washr-drift.yaml @@ -0,0 +1,24 @@ +# washr API drift check (openwashdata/pkgreview#77): installs CRAN washr and +# verifies every washr:: call the skills execute or instruct still exports, and +# reports the CRAN version against skills/pkgreview-core/WASHR_FLOOR. +name: washr-drift + +on: + schedule: + - cron: "0 6 * * 1" + push: + branches: [main, dev] + workflow_dispatch: + +jobs: + drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + - name: Install washr from CRAN + run: Rscript -e 'install.packages("washr")' + - name: Drift check + run: Rscript scripts/washr_drift.R diff --git a/CLAUDE.md b/CLAUDE.md index b564663..619caec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,22 +43,39 @@ ships as Claude Code skills. builds a throwaway repo with a planted add-then-remove history for the scan to catch. - `docs/checklist-reconciliation.md` - the record of how the previously - diverging checklist copies were merged (issue #5) + diverging checklist copies were merged (issue #5) and, per release + section keyed by version, every checklist item reworded since; + `/review-upgrade` reads those sections to build a package's delta +- `NEWS.md` - release notes for everything that is not a checklist item +- `scripts/` - `release.sh` (release tail), `verify_release.sh`, + `news_section.sh`, `lint.sh`, `washr_drift.R` +- `.github/workflows/` - `gate.yaml` (fixture gate, case suite, lint), + `washr-drift.yaml`, `release.yaml` +- `.claude-plugin/` - plugin manifests; the version there mirrors + `skills/pkgreview-core/VERSION` (lint checks they agree) ## Rules for changing the review standard 1. Checklist or template changes get a version bump: update - `skills/pkgreview-core/VERSION` and tag the release commit - (`v[version]`). In-flight reviews finish on the version stamped into - their first review issue; skills fetch stamped-version checklists and - org profiles from raw.githubusercontent.com when they detect a - mismatch. -2. After any significant change to checklists or skills, run the review - workflow against `fixtures/pkgreviewtest/` and confirm every planted - defect in `fixtures/SCORECARD.md` is caught. A missed defect means the - change weakened the standard and must not merge. + `skills/pkgreview-core/VERSION` (and the matching version in + `.claude-plugin/`), then release with `scripts/release.sh [version]` + after the dev-to-main PR merges (it tags, verifies the pinned raw + URLs, creates the GitHub release from NEWS.md, and syncs `dev`). + In-flight reviews finish on the version stamped into their first + review issue; skills fetch stamped-version checklists and org + profiles from raw.githubusercontent.com when they detect a mismatch. +2. The mechanical fixture gate runs in CI (`.github/workflows/gate.yaml`: + the check script against `fixtures/pkgreviewtest/` and the history + fixture diffed against `fixtures/expected/`, plus the case suite in + `fixtures/cases/`). A change that alters a FAIL, FLAG, or NOT RUN + line must update the expected report in the same PR, with the + scorecard mapping re-checked. A missed defect means the change + weakened the standard and must not merge. The interactive run + through the skills is issue #17 (#80 decides its future). 3. If a checklist item is added, dropped, or reworded, record the decision - in `docs/checklist-reconciliation.md`. + in `docs/checklist-reconciliation.md`, one row per item. Everything + else (skills, scripts, templates, fixtures, process) is recorded in + `NEWS.md` under the release version. 4. Never let checklist content be duplicated again: issue bodies quote the canonical files verbatim; commands, docs, and skills reference them by path. @@ -71,4 +88,12 @@ ships as Claude Code skills. - Work happens on `dev` (create off `main` if missing); PRs go from `dev` into `main` -- No emojis and no em dashes in anything committed to this repo +- Changes are proposed with `.github/ISSUE_TEMPLATE/proposal.md`: an + objection window decides unless the choice is open; the release issue + carries one decision table +- The release PR body lists `Closes #N` for every issue it lands (PRs + here target `main`, so the keyword fires); no per-issue status or + closing comments +- After the merge: `scripts/release.sh [version]` from `main` +- No emojis and no em dashes in anything committed to this repo; + `scripts/lint.sh` enforces this and the skill path references diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..fdf5308 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,26 @@ +# pkgreview NEWS + +One section per release. Skill, script, template, fixture, and process changes are recorded here; checklist wording changes are recorded in `docs/checklist-reconciliation.md` (repo rule 3), one row per item. Releases before 1.5.0 are described by their reconciliation sections and tag messages. + +# pkgreview 1.6.0 + +Development workflow release (issues #72 to #80, filed 2026-09-06). No checklist item changed. + +- CI: `.github/workflows/gate.yaml` runs the mechanical fixture gate as a golden-file test against `fixtures/expected/`, the check-script case suite under `fixtures/cases/`, and the repository lint (`scripts/lint.sh`); `.github/workflows/washr-drift.yaml` checks weekly that every `washr::` call the skills execute or instruct still exports from CRAN washr and reports the CRAN version against the recorded floor (#72, #77). +- Release tail: `scripts/release.sh ` tags the merge commit, verifies the pinned raw URLs, creates the GitHub release from this file, and fast-forwards `dev`; `.github/workflows/release.yaml` repeats the verification on every tag push. The permission allowlist for the tail's commands is recorded on #73 for the maintainer to add to `.claude/settings.json` (a session cannot write permission rules) (#73). +- Process: proposals use `.github/ISSUE_TEMPLATE/proposal.md` with an objection window; the release PR closes its issues through `Closes` lines; this NEWS file replaces the reconciliation prose for non-checklist changes (#74). +- Plugin: `.claude-plugin/marketplace.json` and `plugin.json` package the skills as a Claude Code plugin; the copy install stays documented until the plugin path has served one release (#75). +- `/review-status` and `/review-package` compare the installed VERSION with the newest tag and warn (status) or stop (package) when the install lags (#76). +- The washr floor lives in `skills/pkgreview-core/WASHR_FLOOR`; both release preflights read it (#77). +- Org profiles carry a YAML block; the check script takes `--org=` (or `--org-file=`) and derives analytics, the site URL pattern, the funding text, the required keywords, and the brand rule from it; `--analytics` stays as a deprecated alias (#78). +- New `/review-upgrade` skill: one issue listing the items that changed between a package's stamped standard and the installed one, worked through the normal flow; `pkgreview-upgrade` label; recovery.md failure mode 11 (#79). +- `fixtures/make_review_fixture.sh create|delete` stands up and tears down the throwaway repository for the interactive gate in a registered org; an evals case for the intake screen under `evals/` (#80). + +# pkgreview 1.5.0 + +Reconciliation with washr 1.1.0 and the owdata catalog (issues #56 to #68, released 2026-09-05). Checklist rows in the reconciliation section of the same name. + +- Release skills: washr >= 1.1.0 preflight, every 1.0.1 caveat deleted, version validated and set with `desc::desc_set_version()`, `update_citation(build = FALSE)`, conditional `update_metadata()`, badge and website steps reduced to verification, default branch check and dev sync. +- Standard: keywords and coverage in the DESCRIPTION `X-schema.org` fields, dictionary schema advisory item, README export-link item reworded, Website required item means the pkgdown workflow deploys the site and `docs/` is not committed, tests trigger wording, brand as an org profile field. +- Check script: keywords, coverage, dictionary schema, export-link, `docs/`-tracked, and dev-trigger lines; scope note freezing the metadata, docs, and tests sections pending washr's `check_publication_readiness()`. +- Workflow skills never commit `docs/`; review-complete verifies the workflow and names the Pages setting. Guidebook reduced to intake, floor, dictionary, review, publication. Premortem files, `prompts/`, and `commands/` deleted. diff --git a/fixtures/cases/ci-triggers-missing-dev/expected.txt b/fixtures/cases/ci-triggers-missing-dev/expected.txt new file mode 100644 index 0000000..b3980f3 --- /dev/null +++ b/fixtures/cases/ci-triggers-missing-dev/expected.txt @@ -0,0 +1 @@ +- [FAIL] (required) R-CMD-check workflow triggers include dev (push and pull_request): branches: [main, master, dev] [main, master] diff --git a/fixtures/cases/ci-triggers-missing-dev/overlay/.github/workflows/R-CMD-check.yaml b/fixtures/cases/ci-triggers-missing-dev/overlay/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..632f920 --- /dev/null +++ b/fixtures/cases/ci-triggers-missing-dev/overlay/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,6 @@ +on: + push: + branches: [main, master, dev] + pull_request: + branches: [main, master] +name: R-CMD-check diff --git a/fixtures/cases/ci-triggers-nested-dev/expected.txt b/fixtures/cases/ci-triggers-nested-dev/expected.txt new file mode 100644 index 0000000..13e6c0c --- /dev/null +++ b/fixtures/cases/ci-triggers-nested-dev/expected.txt @@ -0,0 +1 @@ +- [PASS] (required) R-CMD-check workflow triggers include dev (push and pull_request): branches: [main, dev] [main, dev] diff --git a/fixtures/cases/ci-triggers-nested-dev/overlay/.github/workflows/R-CMD-check.yaml b/fixtures/cases/ci-triggers-nested-dev/overlay/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..16432b1 --- /dev/null +++ b/fixtures/cases/ci-triggers-nested-dev/overlay/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,10 @@ +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev +name: R-CMD-check diff --git a/fixtures/cases/dictionary-schema/expected.txt b/fixtures/cases/dictionary-schema/expected.txt new file mode 100644 index 0000000..42fcb4c --- /dev/null +++ b/fixtures/cases/dictionary-schema/expected.txt @@ -0,0 +1 @@ +- [FAIL] (advisory) Dictionary schema: five washr columns, UTF-8 without BOM, single-class variable_type: UTF-8 byte order mark at the start of the file; columns are [directory, file_name, variable_name, variable_type, description, extra], expected [directory, file_name, variable_name, variable_type, description]; variable_type is not a single class name for: id diff --git a/fixtures/cases/dictionary-schema/overlay/data-raw/dictionary.csv b/fixtures/cases/dictionary-schema/overlay/data-raw/dictionary.csv new file mode 100644 index 0000000..15b1a46 --- /dev/null +++ b/fixtures/cases/dictionary-schema/overlay/data-raw/dictionary.csv @@ -0,0 +1,2 @@ +directory,file_name,variable_name,variable_type,description,extra +data,pkgreviewtest.rda,id,"c(""POSIXct"", ""POSIXt"")",Water point identifier,x diff --git a/fixtures/cases/docs-tracked/expected.txt b/fixtures/cases/docs-tracked/expected.txt new file mode 100644 index 0000000..462d683 --- /dev/null +++ b/fixtures/cases/docs-tracked/expected.txt @@ -0,0 +1 @@ +- [FAIL] (advisory) docs/ untracked while the pkgdown workflow deploys the site: 1 tracked file(s) under docs/ diff --git a/fixtures/cases/docs-tracked/overlay/.github/workflows/pkgdown.yaml b/fixtures/cases/docs-tracked/overlay/.github/workflows/pkgdown.yaml new file mode 100644 index 0000000..7228e29 --- /dev/null +++ b/fixtures/cases/docs-tracked/overlay/.github/workflows/pkgdown.yaml @@ -0,0 +1,4 @@ +on: + push: + branches: [main, master] +name: pkgdown diff --git a/fixtures/cases/docs-tracked/overlay/docs/index.html b/fixtures/cases/docs-tracked/overlay/docs/index.html new file mode 100644 index 0000000..18ecdcb --- /dev/null +++ b/fixtures/cases/docs-tracked/overlay/docs/index.html @@ -0,0 +1 @@ + diff --git a/fixtures/cases/docs-tracked/setup.sh b/fixtures/cases/docs-tracked/setup.sh new file mode 100644 index 0000000..f9bcd85 --- /dev/null +++ b/fixtures/cases/docs-tracked/setup.sh @@ -0,0 +1 @@ +git init -q -b main && git add -A && git -c user.name=case -c user.email=case@example.org commit -q -m "case" diff --git a/fixtures/cases/docs-untracked/expected.txt b/fixtures/cases/docs-untracked/expected.txt new file mode 100644 index 0000000..ef317c4 --- /dev/null +++ b/fixtures/cases/docs-untracked/expected.txt @@ -0,0 +1 @@ +- [PASS] (advisory) docs/ untracked while the pkgdown workflow deploys the site diff --git a/fixtures/cases/docs-untracked/overlay/.github/workflows/pkgdown.yaml b/fixtures/cases/docs-untracked/overlay/.github/workflows/pkgdown.yaml new file mode 100644 index 0000000..7228e29 --- /dev/null +++ b/fixtures/cases/docs-untracked/overlay/.github/workflows/pkgdown.yaml @@ -0,0 +1,4 @@ +on: + push: + branches: [main, master] +name: pkgdown diff --git a/fixtures/cases/docs-untracked/overlay/docs/index.html b/fixtures/cases/docs-untracked/overlay/docs/index.html new file mode 100644 index 0000000..18ecdcb --- /dev/null +++ b/fixtures/cases/docs-untracked/overlay/docs/index.html @@ -0,0 +1 @@ + diff --git a/fixtures/cases/docs-untracked/setup.sh b/fixtures/cases/docs-untracked/setup.sh new file mode 100644 index 0000000..fa3bcc2 --- /dev/null +++ b/fixtures/cases/docs-untracked/setup.sh @@ -0,0 +1 @@ +echo docs > .gitignore && git init -q -b main && git add -A && git -c user.name=case -c user.email=case@example.org commit -q -m "case" diff --git a/fixtures/cases/keywords-missing/expected.txt b/fixtures/cases/keywords-missing/expected.txt new file mode 100644 index 0000000..a1a0097 --- /dev/null +++ b/fixtures/cases/keywords-missing/expected.txt @@ -0,0 +1,2 @@ +- [FAIL] (advisory) DESCRIPTION carries X-schema.org-keywords: field missing or empty +- [FAIL] (advisory) DESCRIPTION carries X-schema.org spatial and temporal coverage: missing: X-schema.org-spatialCoverage, X-schema.org-temporalCoverage diff --git a/fixtures/cases/keywords-missing/overlay/DESCRIPTION b/fixtures/cases/keywords-missing/overlay/DESCRIPTION new file mode 100644 index 0000000..d04fe55 --- /dev/null +++ b/fixtures/cases/keywords-missing/overlay/DESCRIPTION @@ -0,0 +1,28 @@ +Package: pkgreviewtest +Title: Water Point Observations for Review Workflow Testing +Version: 0.0.1 +Authors@R: c( + person("Ada", "Lovelace", , "ada.lovelace@example.org", role = c("aut", "cre"), + comment = c(ORCID = "0000-0001-8271-5555"))) +Description: Water point observations from four Swiss regions, including + water source type, functional status, installation date, and number of + users. This is a fixture package used to test the openwashdata package + review workflow. +License: MIT + file LICENSE +Depends: + R (>= 3.5.0) +Suggests: + dplyr, + ggplot2, + knitr, + readr, + rmarkdown +Config/Needs/website: rmarkdown +Encoding: UTF-8 +Language: en-GB +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 +Date: 2026-07-08 +URL: https://github.com/openwashdata/pkgreviewtest +BugReports: https://github.com/openwashdata/pkgreviewtest/issues diff --git a/fixtures/cases/readme-no-links/expected.txt b/fixtures/cases/readme-no-links/expected.txt new file mode 100644 index 0000000..d0fae95 --- /dev/null +++ b/fixtures/cases/readme-no-links/expected.txt @@ -0,0 +1 @@ +- [FAIL] (advisory) README links the CSV/XLSX exports in inst/extdata/ for non-R users: no link to a .csv or .xlsx file under inst/extdata/ diff --git a/fixtures/cases/readme-no-links/overlay/README.md b/fixtures/cases/readme-no-links/overlay/README.md new file mode 100644 index 0000000..96c9aaa --- /dev/null +++ b/fixtures/cases/readme-no-links/overlay/README.md @@ -0,0 +1,3 @@ +# pkgreviewtest + +No download links here. diff --git a/fixtures/cases/readme-washr-table/expected.txt b/fixtures/cases/readme-washr-table/expected.txt new file mode 100644 index 0000000..02a807b --- /dev/null +++ b/fixtures/cases/readme-washr-table/expected.txt @@ -0,0 +1 @@ +- [PASS] (advisory) README links the CSV/XLSX exports in inst/extdata/ for non-R users: 1 line(s) link into inst/extdata/ diff --git a/fixtures/cases/readme-washr-table/overlay/README.md b/fixtures/cases/readme-washr-table/overlay/README.md new file mode 100644 index 0000000..55fdc22 --- /dev/null +++ b/fixtures/cases/readme-washr-table/overlay/README.md @@ -0,0 +1,7 @@ +# pkgreviewtest + +Alternatively, you can download the individual datasets as a CSV or XLSX file from the table below. + +|dataset |CSV |XLSX | +|:--|:--|:--| +|pkgreviewtest |[Download CSV](https://github.com/openwashdata/pkgreviewtest/raw/main/inst/extdata/pkgreviewtest.csv) |[Download XLSX](https://github.com/openwashdata/pkgreviewtest/raw/main/inst/extdata/pkgreviewtest.xlsx) | diff --git a/fixtures/expected/history.md b/fixtures/expected/history.md new file mode 100644 index 0000000..54bbf69 --- /dev/null +++ b/fixtures/expected/history.md @@ -0,0 +1,50 @@ +# pkgreview mechanical check report + +Package: `(normalized)` +Standard: mechanical subset of the pkgreview checklists +Organization profile: --org=openwashdata (openwashdata.md) +Result: 2 PASS, 15 FAIL (9 required-tier), 2 FLAG, 2 NOT RUN + +## metadata + +- [FAIL] (required) License: CC BY 4.0: License field: missing +- [FAIL] (required) CITATION.cff present and valid: file missing +- [FAIL] (advisory) DESCRIPTION carries X-schema.org-keywords: field missing or empty; CITATION.cff missing +- [FAIL] (advisory) DESCRIPTION carries X-schema.org spatial and temporal coverage: missing: X-schema.org-spatialCoverage, X-schema.org-temporalCoverage +- [PASS] (advisory) Title is under 65 characters: 20 characters + +## data + +- [FAIL] (required) Raw data files preserved in data-raw/ +- [FAIL] (required) data_processing.R in data-raw/ +- [FAIL] (required) Primary data present in data/ as .rda and loads: 0 file(s), 0 dataset(s) +- [FAIL] (required) CSV and XLSX exports in inst/extdata/: facilities.csv +- [FAIL] (required) data-raw/dictionary.csv present: file missing +- [FLAG] (required) PII signal scan (never auto-certified; human sign-off required): no suspicious column names or value patterns detected; the item still requires the intake screen and human judgment +- [FLAG] (required) Git-history PII signal scan (text data files): identifier-like columns in historical revisions of: inst/extdata/facilities.csv - inspect these revisions and treat as disclosed if confirmed + +## docs + +- [FAIL] (required) README.Rmd and rendered README.md present +- [FAIL] (advisory) Roxygen @source present for the datasets +- [FAIL] (advisory) README links the CSV/XLSX exports in inst/extdata/ for non-R users: no link to a .csv or .xlsx file under inst/extdata/ (the washr README template's download table provides them) +- [PASS] (advisory) No vignettes directly in vignettes/ (they belong in vignettes/articles/) +- [FAIL] (advisory) _pkgdown.yml present: file missing +- [NOT RUN] (advisory) docs/ untracked while the pkgdown workflow deploys the site: no .github/workflows/pkgdown.yaml; the required Website item covers the missing workflow + +## tests + +- [FAIL] (required) GitHub Actions R-CMD-check workflow present +- [NOT RUN] (required) R-CMD-check workflow triggers include dev (push and pull_request): workflow file missing; see the presence line above +- [FAIL] (advisory) R-CMD-check badge in README.Rmd + +Not machine-checked (reviewer judgment, run in the session per the +evidence rule): description and provenance prose quality, tidy-data +structure, plausibility of values, devtools::check(), README rebuild, +website build, the pkgdown workflow's Pages setting, ORCID and +maintainer identification, dictionary +description accuracy (a present description can still be wrong), PII +certification (the FLAG above is a signal, never a verdict), and the +history parts the script does not cover: historical .rda column names +(compressed, not text-searchable) and commit-message wording that +names identifying data being added or removed. diff --git a/fixtures/expected/pkgreviewtest.md b/fixtures/expected/pkgreviewtest.md new file mode 100644 index 0000000..c786066 --- /dev/null +++ b/fixtures/expected/pkgreviewtest.md @@ -0,0 +1,69 @@ +# pkgreview mechanical check report + +Package: `(normalized)` +Standard: mechanical subset of the pkgreview checklists +Organization profile: --org=openwashdata (openwashdata.md) +Result: 17 PASS, 19 FAIL (4 required-tier), 1 FLAG, 3 NOT RUN + +## metadata + +- [FAIL] (required) License: CC BY 4.0: License field: MIT + file LICENSE +- [PASS] (required) CITATION.cff present +- [PASS] (required) CITATION.cff version matches DESCRIPTION: DESCRIPTION 0.0.1 vs CITATION.cff 0.0.1 +- [FAIL] (required) Citation files carry real authors, not template placeholders: "Firstname Lastname" found in citation files +- [PASS] (advisory) DESCRIPTION carries X-schema.org-keywords: 4 keyword(s): open data, washdata, water points, Switzerland; CITATION.cff agrees +- [PASS] (advisory) DESCRIPTION carries X-schema.org spatial and temporal coverage: spatial: Switzerland; temporal: 2018-01-01/2022-02-05 +- [PASS] (advisory) Title is under 65 characters: 52 characters + +## data + +- [PASS] (required) Raw data files preserved in data-raw/ +- [PASS] (required) data_processing.R in data-raw/ +- [PASS] (required) Primary data present in data/ as .rda and loads: 1 file(s), 1 dataset(s) +- [PASS] (required) CSV and XLSX exports in inst/extdata/: pkgreviewtest.csv, pkgreviewtest.xlsx +- [PASS] (required) Dictionary covers every variable in every dataset +- [FAIL] (required) Dictionary descriptions present (no empty or placeholder): defective: status, users_count +- [PASS] (advisory) Dictionary schema: five washr columns, UTF-8 without BOM, single-class variable_type +- [FLAG] (required) PII signal scan (never auto-certified; human sign-off required): suspicious columns: owner_phone +- [NOT RUN] (required) Git-history PII signal scan (text data files): package is a subdirectory of a larger git repository; the visible history is the enclosing repo's, not the package's. Run the scan against the package's own repository +- [FAIL] (advisory) Missing values coded as NA, no sentinels [pkgreviewtest]: sentinel values in: users_count (4) +- [FAIL] (advisory) All text data encoded in UTF-8 [pkgreviewtest]: non-UTF-8: region +- [FAIL] (advisory) Date variables stored as Date class [pkgreviewtest]: not Date class: installation_date +- [FAIL] (advisory) Categorical values consistent (no case-only variants) [pkgreviewtest]: status (5 variants, 2 after case-folding) +- [FAIL] (advisory) Unique identifier `id` is unique [pkgreviewtest]: 1 duplicated value(s): WP-005 +- [PASS] (advisory) Exact duplicate rows (full and non-ID columns) [pkgreviewtest]: full: 0, non-ID: 0 +- [FAIL] (advisory) Column names are snake_case [pkgreviewtest]: not snake_case: waterSource +- [FAIL] (advisory) Hard ranges: counts >= 0, percentages in [0, 100] [pkgreviewtest]: negative counts: users_count (4); out-of-range percentages: none +- [FAIL] (advisory) Coordinates in bounds, no (0, 0) points [pkgreviewtest]: out-of-bounds: 1, (0, 0) points: 1 +- [FAIL] (advisory) Cross-field: women_users <= users_count [pkgreviewtest]: 2 violating row(s) +- [FAIL] (advisory) No commented-out code in data_processing.R: 5 line(s): 27, 28, 29, 35, 36 +- [FAIL] (advisory) Script conventions: read_csv with col_types; readr/writexl exports: read_csv() without col_types; base write.csv() used for export + +## docs + +- [PASS] (required) README.Rmd and rendered README.md present +- [PASS] (advisory) Roxygen @source present for the datasets +- [PASS] (advisory) README links the CSV/XLSX exports in inst/extdata/ for non-R users: 2 line(s) link into inst/extdata/ +- [FAIL] (advisory) No vignettes directly in vignettes/ (they belong in vignettes/articles/): example.Rmd +- [FAIL] (advisory) _pkgdown.yml carries the Plausible analytics header +- [FAIL] (advisory) _pkgdown.yml url is the Pages URL from the org profile: url: https://github.com/openwashdata/pkgreviewtest (expected https://openwashdata.github.io/pkgreviewtest/) +- [PASS] (advisory) _pkgdown.yml carries the funding sidebar text from the org profile +- [PASS] (advisory) _pkgdown.yml brand wiring matches the org profile: not wired; optional (openwashdata/brand via washr::use_brand()) +- [NOT RUN] (advisory) docs/ untracked while the pkgdown workflow deploys the site: no .github/workflows/pkgdown.yaml; the required Website item covers the missing workflow + +## tests + +- [FAIL] (required) GitHub Actions R-CMD-check workflow present +- [NOT RUN] (required) R-CMD-check workflow triggers include dev (push and pull_request): workflow file missing; see the presence line above +- [FAIL] (advisory) R-CMD-check badge in README.Rmd + +Not machine-checked (reviewer judgment, run in the session per the +evidence rule): description and provenance prose quality, tidy-data +structure, plausibility of values, devtools::check(), README rebuild, +website build, the pkgdown workflow's Pages setting, ORCID and +maintainer identification, dictionary +description accuracy (a present description can still be wrong), PII +certification (the FLAG above is a signal, never a verdict), and the +history parts the script does not cover: historical .rda column names +(compressed, not text-searchable) and commit-message wording that +names identifying data being added or removed. diff --git a/fixtures/run_cases.sh b/fixtures/run_cases.sh new file mode 100755 index 0000000..d1f9270 --- /dev/null +++ b/fixtures/run_cases.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Check-script case suite (openwashdata/pkgreview#72). Each case under +# fixtures/cases// is an overlay applied to a copy of pkgreviewtest, +# an optional setup.sh run inside the copy, and an expected.txt whose lines +# must each appear verbatim in the report (grep -F). Cases are the synthetic +# checks that verified new check lines; add one per new line. +set -euo pipefail +cd "$(dirname "$0")/.." +ROOT=$PWD +CHECK=$ROOT/skills/pkgreview-core/check/pkgreview-check.R +status=0 +for case in fixtures/cases/*/; do + name=$(basename "$case") + tmp=$(mktemp -d) + cp -R fixtures/pkgreviewtest/. "$tmp/" + [[ -d "$case/overlay" ]] && cp -R "$case/overlay/." "$tmp/" + if [[ -f "$case/setup.sh" ]]; then (cd "$tmp" && bash "$ROOT/$case/setup.sh"); fi + args=(); [[ -f "$case/args" ]] && read -r -a args < "$case/args" + report=$(Rscript "$CHECK" "$tmp" "${args[@]:---org=openwashdata}" 2>&1 || true) + ok=1 + while IFS= read -r line; do + [[ -z "$line" ]] && continue + if ! grep -F -q -- "$line" <<< "$report"; then echo "case $name: MISSING: $line"; ok=0; fi + done < "$case/expected.txt" + if [[ $ok -eq 1 ]]; then echo "case $name: ok"; else status=1; echo "$report" | sed 's/^/ /' | grep -E "FAIL|PASS|NOT RUN" | head -40; fi + rm -rf "$tmp" +done +exit $status diff --git a/fixtures/run_gate.sh b/fixtures/run_gate.sh new file mode 100755 index 0000000..9917b9b --- /dev/null +++ b/fixtures/run_gate.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Mechanical fixture gate as a golden-file test (openwashdata/pkgreview#72). +# Runs the check script on fixtures/pkgreviewtest and on the history fixture, +# normalizes the package path line, and diffs against fixtures/expected/. +# Pass --update to rewrite the expected reports (a deliberate, reviewed change). +set -euo pipefail +cd "$(dirname "$0")/.." +CHECK=skills/pkgreview-core/check/pkgreview-check.R +update=0; [[ "${1:-}" == "--update" ]] && update=1 +normalize() { sed -E 's|^Package: `.*`|Package: `(normalized)`|'; } +status=0 +run_one() { + local name="$1" dir="$2"; shift 2 + local out; out=$(mktemp) + Rscript "$CHECK" "$dir" "$@" 2>&1 | normalize > "$out" || true + if [[ $update -eq 1 ]]; then + cp "$out" "fixtures/expected/$name.md"; echo "updated fixtures/expected/$name.md" + elif diff -u "fixtures/expected/$name.md" "$out"; then + echo "gate: $name matches fixtures/expected/$name.md" + else + echo "gate: $name DIFFERS from fixtures/expected/$name.md (see diff above)"; status=1 + fi + rm -f "$out" +} +run_one pkgreviewtest fixtures/pkgreviewtest --org=openwashdata +HIST=$(bash fixtures/make_history_fixture.sh) +run_one history "$HIST" --org=openwashdata +rm -rf "$HIST" +# Scorecard reconciliation summary for the log: FAIL and FLAG lines are findings. +n_fail=$(grep -c '^- \[FAIL\]' fixtures/expected/pkgreviewtest.md || true) +n_flag=$(grep -c '^- \[FLAG\]' fixtures/expected/pkgreviewtest.md || true) +echo "gate: pkgreviewtest expected report carries $n_fail FAIL and $n_flag FLAG lines (scorecard: 19 + 1 for D1 to D17)" +exit $status diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..6cbf840 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Repository lint (openwashdata/pkgreview#72): skill path references resolve, +# no em dashes or emojis, a blank line after every markdown heading, VERSION +# is a semver and agrees with the plugin manifests; with LINT_CHECK_TAG=1 the +# newest tag must equal VERSION (run on main only). +set -uo pipefail +cd "$(dirname "$0")/.." +fail=0 +say() { echo "lint: $*"; } + +# 1. ${CLAUDE_SKILL_DIR}/../ references in SKILL.md files resolve under skills/ +while IFS= read -r ref; do + rest=${ref#*\$\{CLAUDE_SKILL_DIR\}/../} + [[ "$rest" == *"["* ]] && continue # placeholder paths such as orgs/[org].md + [[ -e "skills/$rest" ]] || { say "unresolved skill reference: $ref"; fail=1; } +done < <(grep -rhoE '\$\{CLAUDE_SKILL_DIR\}/\.\./[A-Za-z0-9_./\[\]-]+' skills/*/SKILL.md | sed -E 's/[.,;:)]+$//' | sort -u) + +# 2. em dashes and emojis in tracked text files (python: macOS grep has no -P) +python3 scripts/lint_text.py || fail=1 + +# 3. blank line after every heading, outside fenced code blocks +while IFS= read -r f; do + awk -v f="$f" ' + /^```/ { fence = !fence } + { if (!fence && prev ~ /^#{1,6} / && $0 !~ /^$/) { print "lint: heading without a blank line after it: " f ":" NR-1; bad = 1 } prev = $0 } + END { exit bad }' "$f" || fail=1 +done < <(git ls-files '*.md') + +# 4. VERSION is a semver +v=$(tr -d '[:space:]' < skills/pkgreview-core/VERSION) +[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { say "VERSION is not a semver: $v"; fail=1; } + +# 5. plugin manifests agree with VERSION +if [[ -f .claude-plugin/plugin.json ]]; then + pv=$(python3 -c 'import json,sys; print(json.load(open(".claude-plugin/plugin.json"))["version"])') + [[ "$pv" == "$v" ]] || { say "plugin.json version $pv differs from VERSION $v"; fail=1; } +fi +if [[ -f .claude-plugin/marketplace.json ]]; then + mv=$(python3 -c 'import json; m=json.load(open(".claude-plugin/marketplace.json")); print(m["plugins"][0]["version"])') + [[ "$mv" == "$v" ]] || { say "marketplace.json plugin version $mv differs from VERSION $v"; fail=1; } + python3 - <<'PY' || fail=1 +import json, os +m = json.load(open(".claude-plugin/marketplace.json")) +bad = [p for p in m["plugins"][0]["skills"] if not os.path.isfile(os.path.join(p, "SKILL.md"))] +if bad: print("lint: marketplace.json names skills without SKILL.md:", bad); raise SystemExit(1) +PY +fi + +# 6. newest tag equals VERSION (main only) +if [[ "${LINT_CHECK_TAG:-0}" == "1" ]]; then + t=$(git tag --list 'v*' | sort -V | tail -1) + [[ "$t" == "v$v" ]] || { say "newest tag $t does not match VERSION $v"; fail=1; } +fi + +[[ $fail -eq 0 ]] && say "clean" +exit $fail diff --git a/scripts/lint_text.py b/scripts/lint_text.py new file mode 100644 index 0000000..3a4cb97 --- /dev/null +++ b/scripts/lint_text.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Em dash and emoji check over tracked text files (part of scripts/lint.sh).""" +import re +import subprocess +import sys + +files = subprocess.run( + ["git", "ls-files", "*.md", "*.R", "*.sh", "*.py", "*.yml", "*.yaml", "*.json", "*.Rmd"], + capture_output=True, text=True, check=True, +).stdout.split() +pattern = re.compile("[—\U0001F300-\U0001FAFF☀-➿]") +bad = 0 +for path in files: + try: + with open(path, encoding="utf-8", errors="replace") as fh: + for number, line in enumerate(fh, 1): + if pattern.search(line): + print(f"lint: em dash or emoji: {path}:{number}: {line.strip()[:80]}") + bad = 1 + except OSError: + pass +sys.exit(bad) diff --git a/scripts/news_section.sh b/scripts/news_section.sh new file mode 100755 index 0000000..d4bdda4 --- /dev/null +++ b/scripts/news_section.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Print the NEWS.md section for one version (the text between its heading and +# the next top-level heading). Usage: scripts/news_section.sh 1.6.0 +set -euo pipefail +v="${1:?usage: scripts/news_section.sh }" +cd "$(dirname "$0")/.." +awk -v h="# pkgreview $v" ' + $0 == h { on = 1; next } + on && /^# / { exit } + on { print }' NEWS.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..7895645 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Release tail (openwashdata/pkgreview#73): after the dev-to-main PR merged, +# tag the merge commit, verify the pinned raw URLs, create the GitHub release +# from NEWS.md, and fast-forward dev. Idempotent: an existing tag or release +# is kept, the verification and the sync run again. +# bash scripts/release.sh 1.6.0 (from main, clean, at origin/main) +set -euo pipefail +v="${1:?usage: scripts/release.sh }" +cd "$(dirname "$0")/.." +[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "not a semver: $v"; exit 1; } +[[ "$(git branch --show-current)" == "main" ]] || { echo "switch to main first (git checkout main)"; exit 1; } +[[ -z "$(git status --porcelain)" ]] || { echo "working tree not clean"; exit 1; } +git fetch -q origin +[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]] || { echo "main is not at origin/main; pull first"; exit 1; } +[[ "$(tr -d '[:space:]' < skills/pkgreview-core/VERSION)" == "$v" ]] || { echo "skills/pkgreview-core/VERSION reads $(cat skills/pkgreview-core/VERSION), not $v"; exit 1; } +if git rev-parse -q --verify "refs/tags/v$v" >/dev/null; then + echo "tag v$v exists at $(git rev-parse --short "v$v^{commit}"); keeping it" +else + git tag -a "v$v" -m "pkgreview review standard v$v" + echo "tagged v$v at $(git rev-parse --short HEAD)" +fi +git push origin "v$v" +bash scripts/verify_release.sh "v$v" +if gh release view "v$v" >/dev/null 2>&1; then + echo "GitHub release v$v exists; keeping it" +else + gh release create "v$v" --title "v$v" --notes-file <(bash scripts/news_section.sh "$v") >/dev/null + echo "GitHub release v$v created from NEWS.md" +fi +git checkout -q dev && git merge -q --ff-only main && git push -q origin dev && git checkout -q main +echo "dev synced to main at $(git rev-parse --short main)" +echo "install: claude plugin update pkgreview (plugin install) or cp -R skills/* ~/.claude/skills/ (copy install)" diff --git a/scripts/verify_release.sh b/scripts/verify_release.sh new file mode 100755 index 0000000..570e330 --- /dev/null +++ b/scripts/verify_release.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Verify that the files the skills fetch by stamped version resolve at a tag +# (openwashdata/pkgreview#73). Usage: scripts/verify_release.sh v1.6.0 +set -euo pipefail +tag="${1:?usage: scripts/verify_release.sh }" +base="https://raw.githubusercontent.com/openwashdata/pkgreview/$tag" +files=( + skills/pkgreview-core/VERSION + skills/pkgreview-core/WASHR_FLOOR + skills/pkgreview-core/references/checklists/metadata.md + skills/pkgreview-core/references/checklists/data.md + skills/pkgreview-core/references/checklists/docs.md + skills/pkgreview-core/references/checklists/tests.md + skills/pkgreview-core/references/orgs/openwashdata.md + skills/pkgreview-core/references/orgs/global-health-engineering.md + skills/pkgreview-core/references/standards.md + skills/pkgreview-core/references/templates/_pkgdown.yml + docs/checklist-reconciliation.md +) +status=0 +for f in "${files[@]}"; do + code=$(curl -s -o /dev/null -w '%{http_code}' "$base/$f") + printf '%s %-62s %s\n' "$tag" "$f" "$code" + [[ "$code" == "200" ]] || status=1 +done +v=$(curl -s "$base/skills/pkgreview-core/VERSION" | tr -d '[:space:]') +[[ "v$v" == "$tag" ]] || { echo "VERSION at $tag reads $v"; status=1; } +exit $status diff --git a/scripts/washr_drift.R b/scripts/washr_drift.R new file mode 100644 index 0000000..17e57a5 --- /dev/null +++ b/scripts/washr_drift.R @@ -0,0 +1,37 @@ +# washr API drift check (openwashdata/pkgreview#77). +# Every `washr::name` the skills execute or instruct (skills/**: SKILL.md +# bodies, the reference files, and code lines of the check script) must be an +# export of the installed washr; the installed version is reported against +# the floor recorded in skills/pkgreview-core/WASHR_FLOOR. Exit 1 on a missing +# export or an installed version below the floor. Base R plus washr. +# docs/ is not scanned: it records history and proposals (a fixed typo, a +# function that never existed, a function washr has not shipped yet), and +# R comment lines are skipped for the same reason. +root <- normalizePath(file.path(dirname(sub("^--file=", "", grep("^--file=", commandArgs(), value = TRUE)[1])), "..")) +floor <- trimws(readLines(file.path(root, "skills", "pkgreview-core", "WASHR_FLOOR"), warn = FALSE)[1]) +if (!requireNamespace("washr", quietly = TRUE)) stop("washr is not installed") +installed <- as.character(packageVersion("washr")) +cat(sprintf("washr installed: %s; recorded floor: %s\n", installed, floor)) +files <- list.files(file.path(root, "skills"), "\\.(md|R)$", recursive = TRUE, full.names = TRUE) +calls <- list() +for (f in files) { + txt <- readLines(f, warn = FALSE) + if (grepl("\\.R$", f)) txt[grepl("^\\s*#", txt)] <- "" + m <- regmatches(txt, gregexpr("washr::[A-Za-z_.][A-Za-z0-9_.]*", txt)) + for (i in seq_along(m)) for (nm in m[[i]]) calls[[sub("^washr::", "", nm)]] <- c(calls[[sub("^washr::", "", nm)]], sprintf("%s:%d", sub(paste0("^", root, "/"), "", f), i)) +} +exports <- getNamespaceExports("washr") +missing <- setdiff(names(calls), exports) +cat(sprintf("%d distinct washr call(s) named in the repository: %s\n", length(calls), paste(sort(names(calls)), collapse = ", "))) +status <- 0L +if (length(missing)) { + status <- 1L + for (nm in missing) cat(sprintf("DRIFT: washr::%s is not exported by washr %s; named in %s\n", nm, installed, paste(unique(calls[[nm]]), collapse = ", "))) +} else cat("every named call is exported\n") +if (utils::compareVersion(installed, floor) < 0) { + status <- 1L + cat(sprintf("FLOOR: installed washr %s is below the recorded floor %s\n", installed, floor)) +} else if (utils::compareVersion(installed, floor) > 0) { + cat(sprintf("NOTE: CRAN washr %s is newer than the floor %s; review NEWS.md of washr for changes to reconcile\n", installed, floor)) +} +quit(status = status) diff --git a/skills/add-doi/SKILL.md b/skills/add-doi/SKILL.md index 66c6878..d10f69d 100644 --- a/skills/add-doi/SKILL.md +++ b/skills/add-doi/SKILL.md @@ -26,12 +26,13 @@ uncommitted changes, R with `washr` 1.1.0 or newer installed. ## Step 1: Validate -- washr preflight: - `Rscript -e 'stopifnot(packageVersion("washr") >= "1.1.0")'`. If it - fails, stop and tell the user to run `install.packages("washr")`. The - steps below rely on 1.1.0 behavior (badge insertion and repair, README - rebuild, DOI kept on later runs) and must not be adapted to an older - washr. +- washr preflight, against the floor recorded in + `${CLAUDE_SKILL_DIR}/../pkgreview-core/WASHR_FLOOR`: + `Rscript -e 'floor <- readLines("${CLAUDE_SKILL_DIR}/../pkgreview-core/WASHR_FLOOR"); stopifnot(packageVersion("washr") >= floor)'`. + If it fails, stop and tell the user to run `install.packages("washr")`. + The steps below rely on 1.1.0 behavior (badge insertion and repair, + README rebuild, DOI kept on later runs) and must not be adapted to an + older washr. - `$ARGUMENTS` matches `10.XXXX/zenodo.NNNNNNN` (regex `^10\.\d{4,9}/zenodo\.\d+$`). If not, stop and ask for the DOI in that format. diff --git a/skills/create-release/SKILL.md b/skills/create-release/SKILL.md index 7ef769e..22daff8 100644 --- a/skills/create-release/SKILL.md +++ b/skills/create-release/SKILL.md @@ -30,12 +30,13 @@ FALSE)` is new in 1.1.0, and the workarounds older versions needed are gone from this skill). Run: ```bash -Rscript -e 'stopifnot(packageVersion("washr") >= "1.1.0")' +Rscript -e 'floor <- readLines("${CLAUDE_SKILL_DIR}/../pkgreview-core/WASHR_FLOOR"); stopifnot(packageVersion("washr") >= floor)' ``` -If it fails, stop and tell the user to run `install.packages("washr")` -(CRAN carries 1.1.0 since 2026-09-02), then rerun the skill. Do not adapt -the steps below to an older washr. +The floor is recorded once in `pkgreview-core/WASHR_FLOOR` (1.1.0 at +this writing). If the check fails, stop and tell the user to run +`install.packages("washr")`, then rerun the skill. Do not adapt the steps +below to an older washr. ## Step 1: Pre-release checks (PAUSE) diff --git a/skills/pkgreview-core/WASHR_FLOOR b/skills/pkgreview-core/WASHR_FLOOR new file mode 100644 index 0000000..9084fa2 --- /dev/null +++ b/skills/pkgreview-core/WASHR_FLOOR @@ -0,0 +1 @@ +1.1.0 From af3b53bda636edea615b0422696f24bc847b689c Mon Sep 17 00:00:00 2001 From: larnsce Date: Sun, 6 Sep 2026 05:51:35 +0200 Subject: [PATCH 2/4] feat(skills): org profile YAML for the check script, version drift warning, review-upgrade, fixture script (#76, #78, #79, #80) Both org profiles gain a machine-readable yaml block; the check script takes --org= or --org-file=, derives the analytics header, the site URL pattern, the funding text, the required keywords, and the brand rule from it, and keeps --analytics one release as a deprecated alias; review-package and review-issue pass --org. review-package stops and review-status reports when the installed VERSION is older than the newest tag. New /review-upgrade builds one pkgreview-upgrade issue from the version-keyed reconciliation sections between a package's stamp and the installed standard; review-issue, review-complete, create-next-issue, and review-status accept it; recovery.md gains failure mode 11. fixtures/make_review_fixture.sh creates and deletes the throwaway repository for the interactive gate in a registered org. Fixture gate: 19 FAIL + 1 FLAG unchanged under --org=openwashdata; the url line now names the expected Pages URL; funding and brand lines PASS. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019LJvVaoT3vhaJj7k9LBzEn --- docs/checklist-reconciliation.md | 6 + fixtures/SCORECARD.md | 4 +- fixtures/make_review_fixture.sh | 34 +++++ skills/create-next-issue/SKILL.md | 3 + skills/pkgreview-core/VERSION | 2 +- skills/pkgreview-core/check/pkgreview-check.R | 101 +++++++++++-- .../pkgreview-core/references/orgs/README.md | 19 ++- .../orgs/global-health-engineering.md | 20 +++ .../references/orgs/openwashdata.md | 20 +++ skills/pkgreview-core/references/recovery.md | 22 +++ skills/review-complete/SKILL.md | 12 +- skills/review-issue/SKILL.md | 25 +++- skills/review-package/SKILL.md | 18 ++- skills/review-status/SKILL.md | 15 +- skills/review-upgrade/SKILL.md | 140 ++++++++++++++++++ 15 files changed, 413 insertions(+), 28 deletions(-) create mode 100755 fixtures/make_review_fixture.sh create mode 100644 skills/review-upgrade/SKILL.md diff --git a/docs/checklist-reconciliation.md b/docs/checklist-reconciliation.md index b3ba0d2..79fa9c0 100644 --- a/docs/checklist-reconciliation.md +++ b/docs/checklist-reconciliation.md @@ -8,6 +8,12 @@ Date: 2026-07-08. Baseline: commit after the issue #4 fixes (the `washr::compile_citation()` typo and the missing CC BY 4.0 item were fixed before consolidation so that known bugs were not canonicalized). +Sections added after the consolidation are keyed by the standard version +in their heading (`(vX.Y.Z)`); `/review-upgrade` reads the sections +between a package's stamped version and the installed one to build that +package's upgrade issue, so a release that rewords, adds, or drops a +checklist item must record it in a section that names the version. + ## Sources | Key | File | Role before consolidation | diff --git a/fixtures/SCORECARD.md b/fixtures/SCORECARD.md index 290afff..68ab86c 100644 --- a/fixtures/SCORECARD.md +++ b/fixtures/SCORECARD.md @@ -56,13 +56,13 @@ Note on the v1.5.0 lines (washr 1.1.0 and owdata reconciliation, issues #56 to # Run the full review workflow against `fixtures/pkgreviewtest` after every significant change to the checklists in `skills/pkgreview-core/references/checklists/` or to the review skills and commands. -The mechanical layer of the gate is scripted: `Rscript skills/pkgreview-core/check/pkgreview-check.R fixtures/pkgreviewtest` runs the machine-checkable subset and must reproduce the defect mapping exactly (every FAIL/FLAG line maps to a defect ID, no unmapped lines; NOT RUN lines are not-applicable reports and map to nothing). Since v1.2.1 that is 19 FAIL + 1 FLAG lines for D1 to D17, with D4, D5, D7, and D15 as two-line spans. The judgment items, the intake conversation, and the guardrail behavior still require the workflow run. +The mechanical layer of the gate is scripted and runs in CI (`bash fixtures/run_gate.sh`, which diffs against `fixtures/expected/`): `Rscript skills/pkgreview-core/check/pkgreview-check.R fixtures/pkgreviewtest --org=openwashdata` runs the machine-checkable subset and must reproduce the defect mapping exactly (every FAIL/FLAG line maps to a defect ID, no unmapped lines; NOT RUN lines are not-applicable reports and map to nothing). Since v1.2.1 that is 19 FAIL + 1 FLAG lines for D1 to D17, with D4, D5, D7, and D15 as two-line spans. The judgment items, the intake conversation, and the guardrail behavior still require the workflow run. The git-history defect D18 is exercised against its own repository, since `pkgreviewtest/` reports NOT RUN for history: ``` HISTREPO=$(bash fixtures/make_history_fixture.sh) -Rscript skills/pkgreview-core/check/pkgreview-check.R "$HISTREPO" # expect the D18 history FLAG naming facilities.csv +Rscript skills/pkgreview-core/check/pkgreview-check.R "$HISTREPO" --org=openwashdata # expect the D18 history FLAG naming facilities.csv rm -rf "$HISTREPO" ``` diff --git a/fixtures/make_review_fixture.sh b/fixtures/make_review_fixture.sh new file mode 100755 index 0000000..745c522 --- /dev/null +++ b/fixtures/make_review_fixture.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Stand up or tear down the throwaway repository for the interactive fixture +# gate (openwashdata/pkgreview#80, procedure in #17). +# +# bash fixtures/make_review_fixture.sh create [org] [name] [dir] +# bash fixtures/make_review_fixture.sh delete [org] [name] [dir] +# +# Defaults: org openwashdata, name pkgreviewtest, dir ~/tmp/. The org +# must be registered (a profile in skills/pkgreview-core/references/orgs/), +# because /review-package stops on an unregistered one. The repository is +# private: the fixture carries synthetic phone numbers and is defective by +# design. Delete it after the run. +set -euo pipefail +cd "$(dirname "$0")/.." +cmd="${1:-}"; org="${2:-openwashdata}"; name="${3:-pkgreviewtest}"; dir="${4:-$HOME/tmp/$name}" +profile="skills/pkgreview-core/references/orgs/$(echo "$org" | tr '[:upper:]' '[:lower:]').md" +case "$cmd" in + create) + [[ -f "$profile" ]] || { echo "organization $org is not registered (no $profile); /review-package would stop at Step 0"; exit 1; } + [[ -e "$dir" ]] && { echo "$dir exists; delete first"; exit 1; } + mkdir -p "$(dirname "$dir")" + cp -R fixtures/pkgreviewtest "$dir"; rm -f "$dir/.DS_Store" + (cd "$dir" && git init -q -b main && git add -A && git commit -q -m "Fixture package for the pkgreview review workflow test (#17)") + (cd "$dir" && gh repo create "$org/$name" --private --source . --push --description "Throwaway pkgreview fixture for the #17 workflow run; delete after" >/dev/null) + echo "created $org/$name (private) from $dir at $(git -C "$dir" rev-parse --short HEAD)" + echo "next: open a Claude Code session in $dir and type /review-package, paste https://github.com/$org/$name" + echo "after the run: bash fixtures/make_review_fixture.sh delete $org $name $dir" + ;; + delete) + gh repo delete "$org/$name" --yes && echo "deleted $org/$name" + rm -rf "$dir" && echo "removed $dir" + ;; + *) echo "usage: $0 create|delete [org] [name] [dir]"; exit 1 ;; +esac diff --git a/skills/create-next-issue/SKILL.md b/skills/create-next-issue/SKILL.md index ceeb5f7..f5d8e44 100644 --- a/skills/create-next-issue/SKILL.md +++ b/skills/create-next-issue/SKILL.md @@ -53,6 +53,9 @@ Rules: `${CLAUDE_SKILL_DIR}/../pkgreview-core/references/recovery.md`. - If all four areas have issues, say so and suggest `/review-status` or `/review-complete`. +- If the newest review issue carries `pkgreview-upgrade`, there is no next + area: an upgrade is one issue; point at `/review-complete` once its PR + is merged. ## Step 2: Version and organization stamp check diff --git a/skills/pkgreview-core/VERSION b/skills/pkgreview-core/VERSION index bc80560..dc1e644 100644 --- a/skills/pkgreview-core/VERSION +++ b/skills/pkgreview-core/VERSION @@ -1 +1 @@ -1.5.0 +1.6.0 diff --git a/skills/pkgreview-core/check/pkgreview-check.R b/skills/pkgreview-core/check/pkgreview-check.R index 51c611e..f2ac705 100644 --- a/skills/pkgreview-core/check/pkgreview-check.R +++ b/skills/pkgreview-core/check/pkgreview-check.R @@ -4,10 +4,17 @@ # review standard (pkgcheck pattern, openwashdata/pkgreview#13). # # Usage: -# Rscript pkgreview-check.R [package-dir] [--analytics=plausible|none] +# Rscript pkgreview-check.R [package-dir] [--org= | --org-file=] # package-dir default: current directory -# --analytics default plausible; pass none when the org profile -# (references/orgs/) defines no analytics header +# --org registered organization (lowercased); the script reads +# the YAML block of references/orgs/.md next to it +# and derives the analytics header, the site URL pattern, +# the funding text, the required keywords, and the brand +# rule from it (openwashdata/pkgreview#78) +# --org-file a profile file to read instead (a stamped copy fetched +# for a pinned review) +# --analytics deprecated alias kept one release: plausible|none, +# used only when no profile is given # # Output: a Markdown report on stdout, grouped by review area, one line # per check with PASS / FAIL / FLAG / NOT RUN and the observed counts. @@ -43,6 +50,44 @@ if (!analytics %in% c("plausible", "none")) pkg <- if (length(pos) >= 1) pos[[1]] else "." if (!dir.exists(pkg)) stop("Package directory not found: ", pkg) +# Organization profile (--org / --org-file): the fenced yaml block of the +# profile file is the machine-readable source of the org values. +org_arg <- sub("^--org=", "", grep("^--org=", flags, value = TRUE)) +org_file <- sub("^--org-file=", "", grep("^--org-file=", flags, value = TRUE)) +script_file <- sub("^--file=", "", grep("^--file=", commandArgs(), value = TRUE)) +script_dir <- if (length(script_file)) dirname(normalizePath(script_file[[1]])) else "." +read_profile <- function(pf) { + ln <- readLines(pf, warn = FALSE) + s0 <- grep("^```ya?ml\\s*$", ln) + if (!length(s0)) stop("No yaml block in organization profile: ", pf) + e0 <- grep("^```\\s*$", ln); e0 <- e0[e0 > s0[1]][1] + out <- list(); key <- NULL + for (l in ln[(s0[1] + 1L):(e0 - 1L)]) { + if (grepl("^\\s*#", l) || !nzchar(trimws(l))) next + if (grepl("^[A-Za-z_]+:", l)) { + key <- sub(":.*$", "", l) + val <- trimws(sub("^[A-Za-z_]+:\\s*", "", l)) + val <- sub("^['\"](.*)['\"]$", "\\1", val) + out[[key]] <- if (nzchar(val)) val else character() + } else if (grepl("^\\s+-\\s", l) && !is.null(key)) { + out[[key]] <- c(out[[key]], sub("^['\"](.*)['\"]$", "\\1", trimws(sub("^\\s+-\\s*", "", l)))) + } + } + out +} +profile <- NULL; profile_source <- "none (deprecated --analytics path)" +if (length(org_file) || length(org_arg)) { + pf <- if (length(org_file)) org_file[[1]] else + file.path(script_dir, "..", "references", "orgs", paste0(tolower(org_arg[[1]]), ".md")) + if (!file.exists(pf)) + stop("Organization profile not found: ", pf, + " (the organization is not registered; see references/orgs/README.md)") + profile <- read_profile(pf) + profile_source <- if (length(org_file)) paste0("--org-file ", basename(pf)) else paste0("--org=", tolower(org_arg[[1]]), " (", basename(pf), ")") + analytics <- if (identical(tolower(profile$analytics), "plausible")) "plausible" else "none" +} +pval <- function(k) if (!is.null(profile) && !is.null(profile[[k]]) && length(profile[[k]])) profile[[k]] else NULL + CROSS_FIELD_PAIRS <- list( c(part = "women_users", whole = "users_count") ) @@ -130,11 +175,14 @@ kw_agree <- if (length(cff) == 0) { } else { sprintf("CITATION.cff differs (drift, rerun washr::update_citation()): %s", paste(cff_kw, collapse = ", ")) } +kw_req <- pval("keywords_required") +kw_req_missing <- if (length(kw) && length(kw_req)) kw_req[!tolower(kw_req) %in% tolower(kw)] else character() add("metadata", "advisory", - if (length(kw)) "PASS" else "FAIL", + if (length(kw) && length(kw_req_missing) == 0) "PASS" else "FAIL", "DESCRIPTION carries X-schema.org-keywords", - if (length(kw)) sprintf("%d keyword(s): %s; %s", length(kw), paste(kw, collapse = ", "), kw_agree) - else paste("field missing or empty;", kw_agree)) + if (!length(kw)) paste("field missing or empty;", kw_agree) + else sprintf("%d keyword(s): %s; %s%s", length(kw), paste(kw, collapse = ", "), kw_agree, + if (length(kw_req_missing)) paste0("; required by the org profile but missing: ", paste(kw_req_missing, collapse = ", ")) else "")) # Coverage fields (#64): read by washr::update_metadata() and the org catalog sp_cov <- dfield("X-schema.org-spatialCoverage") @@ -502,10 +550,42 @@ if (length(pd)) { "org profile defines no analytics header (--analytics=none)") } url_line <- grep("^url:", pd, value = TRUE) - add("docs", "advisory", - if (length(url_line) && grepl("github\\.io", url_line[1])) "PASS" else "FAIL", - "_pkgdown.yml url is the Pages URL, not the repo URL", - if (length(url_line)) url_line[1] else "no url: line") + url_val <- if (length(url_line)) trimws(sub("^url:\\s*", "", url_line[1])) else "" + site_pattern <- pval("site_url_pattern") + pkgname <- dfield("Package") + if (!is.null(site_pattern) && !is.na(pkgname)) { + expected_url <- sub("", pkgname, site_pattern, fixed = TRUE) + norm <- function(u) sub("/+$", "", u) + add("docs", "advisory", + if (nzchar(url_val) && identical(norm(url_val), norm(expected_url))) "PASS" else "FAIL", + "_pkgdown.yml url is the Pages URL from the org profile", + if (nzchar(url_val)) sprintf("url: %s (expected %s)", url_val, expected_url) else sprintf("no url: line (expected %s)", expected_url)) + } else { + add("docs", "advisory", + if (nzchar(url_val) && grepl("github\\.io", url_val)) "PASS" else "FAIL", + "_pkgdown.yml url is the Pages URL, not the repo URL", + if (length(url_line)) url_line[1] else "no url: line") + } + funding <- pval("funding_text") + if (!is.null(funding)) { + add("docs", "advisory", + if (any(grepl(funding, pd, fixed = TRUE))) "PASS" else "FAIL", + "_pkgdown.yml carries the funding sidebar text from the org profile", + if (any(grepl(funding, pd, fixed = TRUE))) "" else "the profile's funding text was not found verbatim") + } + brand <- pval("brand") + if (!is.null(brand)) { + brand_wired <- any(grepl("^\\s*brand:\\s*_brand\\.yml", pd)) + if (identical(tolower(brand), "none")) { + add("docs", "advisory", if (brand_wired) "FAIL" else "PASS", + "_pkgdown.yml carries no brand (the org profile defines none)", + if (brand_wired) "bslib.brand is wired although the organization defines no brand; remove it" else "") + } else { + add("docs", "advisory", "PASS", + "_pkgdown.yml brand wiring matches the org profile", + if (brand_wired) sprintf("wired to _brand.yml (%s)", brand) else sprintf("not wired; optional (%s via washr::use_brand())", brand)) + } + } } else { add("docs", "advisory", "FAIL", "_pkgdown.yml present", "file missing") } @@ -588,6 +668,7 @@ add("tests", "advisory", cat("# pkgreview mechanical check report\n\n") cat(sprintf("Package: `%s` \n", normalizePath(pkg))) cat(sprintf("Standard: mechanical subset of the pkgreview checklists \n")) +cat(sprintf("Organization profile: %s \n", profile_source)) n_fail_req <- sum(results$status == "FAIL" & results$tier == "required") cat(sprintf("Result: %d PASS, %d FAIL (%d required-tier), %d FLAG, %d NOT RUN\n\n", sum(results$status == "PASS"), sum(results$status == "FAIL"), diff --git a/skills/pkgreview-core/references/orgs/README.md b/skills/pkgreview-core/references/orgs/README.md index 06c33fa..0b9dda5 100644 --- a/skills/pkgreview-core/references/orgs/README.md +++ b/skills/pkgreview-core/references/orgs/README.md @@ -43,6 +43,19 @@ Every profile carries this field table: | Brand | The pkgdown brand for the org's package sites (`_brand.yml` and where it comes from), or `none`; `_pkgdown.yml` wires a brand through `template.bslib.brand` only when the profile defines one | | Zenodo community | The community checked in the add-doi record review | +## Machine-readable block + +Below the table, every profile carries a fenced `yaml` block with the same +values under stable keys, read by the check script (`--org=`, or +`--org-file=` for a stamped copy): `github_org`, `pages_domain`, +`site_url_pattern` (with `` as the placeholder), `analytics` +(`plausible` or `none`), `analytics_domain`, `funding_text` (matched +verbatim in `_pkgdown.yml`), `citation_tooling`, `readme_template`, +`keywords_required` (the literal keywords every package must carry; the +topic and the country or region stay reviewer judgment), `brand` (a +source such as `openwashdata/brand`, or `none`), `zenodo_community`. The +table and the block must agree; the block is what the script reads. + ## Registering a new organization Open a PR to openwashdata/pkgreview adding `orgs/[org].md` with the field @@ -52,6 +65,6 @@ table filled in. Two constraints hold until the standard is extended: express only the washr flow (1.1.0 or newer). Registering an org with different tooling requires reworking those files, which is a standard change with its own version bump (repo rule 1). -- Analytics: the check script supports `--analytics=plausible` (default) - and `--analytics=none`. Other analytics stacks need a check script - extension first. +- Analytics: the check script understands `analytics: plausible` and + `analytics: none` in the profile block. Other analytics stacks need a + check script extension first. diff --git a/skills/pkgreview-core/references/orgs/global-health-engineering.md b/skills/pkgreview-core/references/orgs/global-health-engineering.md index 13bbab2..5d11262 100644 --- a/skills/pkgreview-core/references/orgs/global-health-engineering.md +++ b/skills/pkgreview-core/references/orgs/global-health-engineering.md @@ -17,3 +17,23 @@ so the two profiles are near equal. | Discovery keywords (minimum) | open data, global health, the topic, the country or region | | Brand | none: the openwashdata brand must not appear in a Global-Health-Engineering package, so `washr::use_brand()` is not run and `_pkgdown.yml` carries no `bslib.brand` line | | Zenodo community | `global-health-engineering` (slug assumed; verify on first use, #48 decision 5) | + +## Machine-readable values + +The check script reads this block (`--org=global-health-engineering`); the table above is the human copy and must say the same (openwashdata/pkgreview#78). + +```yaml +github_org: Global-Health-Engineering +pages_domain: global-health-engineering.github.io +site_url_pattern: https://global-health-engineering.github.io// +analytics: plausible +analytics_domain: global-health-engineering.github.io +funding_text: This project was funded by the [Open Research Data Program of the ETH Board](https://ethrat.ch/en/eth-domain/open-research-data/). +citation_tooling: washr +readme_template: washr +keywords_required: + - open data + - global health +brand: none +zenodo_community: global-health-engineering +``` diff --git a/skills/pkgreview-core/references/orgs/openwashdata.md b/skills/pkgreview-core/references/orgs/openwashdata.md index 90e7ea3..e3edf2f 100644 --- a/skills/pkgreview-core/references/orgs/openwashdata.md +++ b/skills/pkgreview-core/references/orgs/openwashdata.md @@ -16,3 +16,23 @@ for reviews that predate org profiles (before v1.3.0). | Discovery keywords (minimum) | open data, washdata, the topic, the country or region | | Brand | `_brand.yml` from [openwashdata/brand](https://github.com/openwashdata/brand), installed and wired into `_pkgdown.yml` with `washr::use_brand()` | | Zenodo community | `openwashdata` | + +## Machine-readable values + +The check script reads this block (`--org=openwashdata`); the table above is the human copy and must say the same (openwashdata/pkgreview#78). + +```yaml +github_org: openwashdata +pages_domain: openwashdata.github.io +site_url_pattern: https://openwashdata.github.io// +analytics: plausible +analytics_domain: openwashdata.github.io +funding_text: This project was funded by the [Open Research Data Program of the ETH Board](https://ethrat.ch/en/eth-domain/open-research-data/). +citation_tooling: washr +readme_template: washr +keywords_required: + - open data + - washdata +brand: openwashdata/brand +zenodo_community: openwashdata +``` diff --git a/skills/pkgreview-core/references/recovery.md b/skills/pkgreview-core/references/recovery.md index 02f3d41..5b1d3f5 100644 --- a/skills/pkgreview-core/references/recovery.md +++ b/skills/pkgreview-core/references/recovery.md @@ -67,6 +67,11 @@ and the organization profile pinned the same way: `.../[stamp]/skills/pkgreview-core/references/orgs/[org].md`. Warn the user, and never mix two standard versions within one review. +Note: an installed tooling version older than the newest release is not a +review-state failure. `/review-package` compares the two before a review +exists and stops; `/review-status` reports it. Update the install and +rerun. + ## 6. No version stamp found (review predates stamping) Symptom: the first review issue body has no "Review standard version" line. @@ -149,3 +154,20 @@ recommends the path and stops, it never rewrites history on its own. Only after the history scan comes back clean does the package clear the PII floor. Record the remediation and the clean re-scan on the review issue. + +## 11. Two standard stamps in one package (upgraded package) + +Symptom: the package CLAUDE.md carries a `Standard version:` line that +differs from the stamp in the first review issue. Since v1.6.0, +`/review-upgrade` brings a published package to a newer standard through +one `pkgreview-upgrade` issue and rewrites the standards file with the +new version; the first review issue keeps its original stamp as history. + +Recovery: this is the expected state when an `Upgraded from: [stamp] on +[date]` line sits under the stamps in CLAUDE.md and a closed +`pkgreview-upgrade` issue with a merged PR exists. The current standard +of the package is the last `Standard version:` line in CLAUDE.md, and +that is the version later skills pin to. Without the `Upgraded from:` +line or without the upgrade issue, someone edited the stamp by hand: +restore the stamp from the first review issue and run `/review-upgrade` +properly. diff --git a/skills/review-complete/SKILL.md b/skills/review-complete/SKILL.md index c952417..5b4df31 100644 --- a/skills/review-complete/SKILL.md +++ b/skills/review-complete/SKILL.md @@ -42,6 +42,14 @@ Do not refuse opaquely; for each problem name the matching failure mode and recovery path from `${CLAUDE_SKILL_DIR}/../pkgreview-core/references/recovery.md`. +Upgrade path: when a `pkgreview-upgrade` issue exists +(`gh issue list --label "pkgreview-upgrade" --state all --json number,state,title`), +the four area issues belong to the original review and are closed; the +upgrade issue must be CLOSED with a merged PR, or OPEN with a merged PR +(the same post-merge state as above: close it and continue). An open +upgrade issue without a merged PR blocks completion; name it. The final +PR then carries the new stamp and an `Upgraded from:` line. + Also verify each closed issue has a merged PR referencing it (`gh pr list --state merged --search "[issue-number]"` or the issue's timeline). A closed issue without a merged PR is failure mode 2: check @@ -100,10 +108,12 @@ Title: `Complete package review for [package-name]`. Base: `main`, head: This PR completes the review of the [package-name] R data package following the pkgreview standards with the [org] organization profile. -Review standard version: [stamp from the metadata issue body] +Review standard version: [stamp from the metadata issue body, or from the upgrade issue when this PR completes an upgrade] Organization profile: [org stamp from the metadata issue body] +[Upgrade only:] Upgraded from: [previous stamp] (issue #[upgrade issue]) + ## Completed Review Issues - #[N]: Data Package Review: General Information & Metadata diff --git a/skills/review-issue/SKILL.md b/skills/review-issue/SKILL.md index 2ee29fa..538f301 100644 --- a/skills/review-issue/SKILL.md +++ b/skills/review-issue/SKILL.md @@ -24,9 +24,13 @@ gh issue view $ARGUMENTS --json title,labels,body,state ``` - The issue must carry exactly one of: `pkgreview-metadata`, - `pkgreview-data`, `pkgreview-docs`, `pkgreview-tests`. If not, stop: - this is not a review issue; list review issues with - `gh issue list --label pkgreview --state all`. + `pkgreview-data`, `pkgreview-docs`, `pkgreview-tests`, or + `pkgreview-upgrade` (a standard upgrade issue created by + `/review-upgrade`; its body is the whole work list, its items name the + version that introduced them, and its "Intake re-screen" section must + be filled before any other item, with a hit stopping the flow as in + review-package Step 2). If not, stop: this is not a review issue; list + review issues with `gh issue list --label pkgreview --state all`. - If the issue is CLOSED, stop and suggest `/review-status`. - Version check: find the `Review standard version` line in the first (metadata) review issue and compare with @@ -38,7 +42,8 @@ gh issue view $ARGUMENTS --json title,labels,body,state The checklist in the issue body is the work list for this invocation. Also read the canonical checklist for the area from `${CLAUDE_SKILL_DIR}/../pkgreview-core/references/checklists/[area].md` for -the suggested tools and file lists. If body and canonical file disagree, the +the suggested tools and file lists (for an upgrade issue: every area's +file, since its items span areas). If body and canonical file disagree, the issue body wins (it records this review's pinned standard); mention the difference to the user. @@ -48,9 +53,14 @@ Run the deterministic check script first and read its section for this issue's area: ```bash -Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . > /tmp/pkgreview-check.md +Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . --org=[org] > /tmp/pkgreview-check.md ``` +`[org]` is the lowercased `Organization profile` stamp from the metadata +issue (`openwashdata` when the review predates the stamp). For a review +pinned to an older version, pass `--org-file=[path]` with the fetched +stamped profile instead. + Its FAIL lines for the area are verified facts and seed the plan; do not re-derive what the script already measured. Its PII line is a FLAG signal, never a verdict. @@ -122,7 +132,7 @@ issue's area that the approved plan addressed are now PASS; post the fresh report on the issue so it records the after-state: ```bash -Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . > /tmp/pkgreview-check.md +Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . --org=[org] > /tmp/pkgreview-check.md gh issue comment $ARGUMENTS --body-file /tmp/pkgreview-check.md ``` @@ -190,6 +200,9 @@ Output exactly this and nothing more: > Please review and merge to dev, then run /create-next-issue to continue > (it syncs dev, closes this issue, and creates the next one)." +For an upgrade issue the last sentence reads: "then run /review-complete +(it closes this issue and opens the dev-to-main PR)". + - Do NOT continue with any other task - Do NOT suggest further next steps - Do NOT start the next issue or create the next issue yourself diff --git a/skills/review-package/SKILL.md b/skills/review-package/SKILL.md index e2c9390..7fc2d62 100644 --- a/skills/review-package/SKILL.md +++ b/skills/review-package/SKILL.md @@ -46,7 +46,16 @@ reply.** not, stop and tell the user. 2. Read the installed standard version from `${CLAUDE_SKILL_DIR}/../pkgreview-core/VERSION`. Call it `[VERSION]` - below. + below. Then read the newest released version: + ```bash + git ls-remote --tags https://github.com/openwashdata/pkgreview.git | sed 's|.*refs/tags/v||; /\^{}$/d' | sort -V | tail -1 + ``` + If `[VERSION]` sorts below it (`printf '%s\n' "[VERSION]" "[latest]" | sort -V | tail -1` is not `[VERSION]`), STOP: + "installed [VERSION], latest [latest]; update the installed skills + before starting a new review (a review pins the standard it starts + on)". Continue on the installed version only when the user says so. + If the command fails (offline), report the check as NOT CHECKED and + continue. 3. Dedupe guard: check for an existing review in ANY state: ```bash gh issue list --label "pkgreview-metadata" --state all --json number,state,title @@ -145,11 +154,12 @@ It stays in the package permanently; never delete it in later steps. - Run the deterministic check script and keep its full report: ```bash - Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . > /tmp/pkgreview-check.md + Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . --org=[org] > /tmp/pkgreview-check.md ``` - Pass `--analytics=none` after the package directory when the org - profile defines no analytics header; the default expects Plausible. + `[org]` is the lowercased organization from Step 0; the script reads + the profile's YAML block for the analytics header, the site URL + pattern, the funding text, the required keywords, and the brand rule. It verifies the mechanical subset of the checklists (file presence, license, citation consistency, sentinel values, encoding, naming, ranges, coordinates) and prints a Markdown report. Its PII line is a diff --git a/skills/review-status/SKILL.md b/skills/review-status/SKILL.md index 8c25147..2515de0 100644 --- a/skills/review-status/SKILL.md +++ b/skills/review-status/SKILL.md @@ -46,7 +46,15 @@ search are empty does "no review exists" hold. Also read the version stamp: view the first (metadata) issue body with `gh issue view [number]` and find the "Review standard version" line, if present. The installed tooling version is in -`${CLAUDE_SKILL_DIR}/../pkgreview-core/VERSION`. +`${CLAUDE_SKILL_DIR}/../pkgreview-core/VERSION`, and the newest released +version comes from the tags: + +```bash +git ls-remote --tags https://github.com/openwashdata/pkgreview.git | sed 's|.*refs/tags/v||; /\^{}$/d' | sort -V | tail -1 +``` + +(offline: report "not checked"). Also list a standard upgrade issue, if +any: `gh issue list --label "pkgreview-upgrade" --state all --json number,state,title`. ## Step 2: Report @@ -57,6 +65,8 @@ Build the report only from the command output above; do not invent progress. **Issues Completed**: [N]/4 **Review standard version**: [stamp from issue 1, or "not stamped"] +**Installed tooling version**: [VERSION] (newest release: [latest], or "not checked") +**Standard upgrade**: [none, or issue #[number] ([state]) from [stamp] to [version]] ### Issue Status @@ -72,6 +82,7 @@ Build the report only from the command output above; do not invent progress. - An issue is open: suggest /review-issue [number] - Latest issue closed, later ones not created: suggest /create-next-issue - All four exist and are closed: suggest /review-complete +- An upgrade issue is open: suggest /review-issue [number]; closed with a merged PR and no open review issue: suggest /review-complete ``` ## Step 3: Anomaly checks @@ -84,6 +95,8 @@ failure mode and the recovery path from - An issue closed without a merged PR referencing it - `dev` behind `main` (`git rev-list --count dev..main` greater than 0) - Version stamp in issue 1 differs from the installed tooling version +- Installed tooling version older than the newest release (not a review-state failure; the install is stale, update before the next review) +- A package CLAUDE.md whose last `Standard version:` line differs from the stamp in issue 1 without an `Upgraded from:` line (failure mode 11) - An open PR whose base branch is `main` while review issues are still open If no review is in progress (no `pkgreview-*` issues AND no diff --git a/skills/review-upgrade/SKILL.md b/skills/review-upgrade/SKILL.md new file mode 100644 index 0000000..282b3b8 --- /dev/null +++ b/skills/review-upgrade/SKILL.md @@ -0,0 +1,140 @@ +--- +name: review-upgrade +description: Bring an already-reviewed package from the standard version stamped in it up to the installed standard, with one issue that lists only the items that changed between the two versions. Stops for approval before creating the issue; the normal review-issue flow works it. +disable-model-invocation: true +argument-hint: "[package-name]" +--- + +# review-upgrade + +Upgrade the reviewed package in the current directory from its stamped +standard version to the installed one. Package name: `$ARGUMENTS` if +given, otherwise `basename "$PWD"`. + +A reviewed package is pinned to the standard it was reviewed against, by +design. This skill is the only path that moves it: it computes the +difference between the two versions from the reconciliation record, +creates exactly ONE issue with the changed items, and stops. `/review-issue` +works that issue with one PR into `dev`; `/review-complete` opens the +dev-to-main PR; `/create-release` makes the patch release. + +**Guardrail: never open a PR against `main` here, and stop for explicit +user approval at the check-in below.** + +## Step 0: Organization profile + +Derive the org from `git remote get-url origin`, lowercase it, and read +`${CLAUDE_SKILL_DIR}/../pkgreview-core/references/orgs/[org].md`. If no +profile exists, STOP: the organization is not registered (registration +path in `orgs/README.md` there). Call the lowercased org `[org]`. + +## Step 1: Read the stamps + +1. The package CLAUDE.md: take the LAST `Standard version:` line as + `[STAMP]` (an upgraded package carries `Upgraded from:` lines under + it; the last stamp is the current one) and the `Organization profile:` + line as `[ORG]` (no line: openwashdata, recovery.md failure mode 9). + Cross-check `[STAMP]` against the first review issue + (`gh issue list --label "pkgreview-metadata" --state all --json number,body`); + if CLAUDE.md and the issue disagree and no `Upgraded from:` line + explains it, STOP with recovery.md failure mode 11. +2. No stamp anywhere (no standards file, no review issue): STOP. The + package was never reviewed; run `/review-package` instead. +3. Installed version `[VERSION]` from + `${CLAUDE_SKILL_DIR}/../pkgreview-core/VERSION`. If `[STAMP]` is + `[VERSION]` or newer (`sort -V`), STOP: nothing to upgrade. +4. Open review issues (`gh issue list --label pkgreview --state open`): + any open `pkgreview-*` issue means a review or an upgrade is in + flight; STOP and point at `/review-status`. + +## Step 2: Compute the delta + +The reconciliation record is keyed by version in its section headings +(`## ... (vX.Y.Z)`). Read it at the installed version: + +```bash +curl -fsSL https://raw.githubusercontent.com/openwashdata/pkgreview/v[VERSION]/docs/checklist-reconciliation.md > /tmp/reconciliation.md +``` + +(`${CLAUDE_SKILL_DIR}/../../docs/checklist-reconciliation.md` works too +when the skills are installed as the plugin, which carries the whole +repository.) Take every section whose heading names a version greater +than `[STAMP]` and at most `[VERSION]`. From each section's table, keep +the rows whose Decision says added, reworded, dropped, or floor changed; +each row names the item and its tier. Also read the section's prose for +rules that changed outside the checklists (standards file rules, skills). + +Always include, whatever the delta: + +- The intake re-screen: the PII and sensitivity check over the current + files and the git history (review-package Step 2), because every + publication standard since v1.4.0 covers the history and the package + is published. +- The standards file rewrite: CLAUDE.md is regenerated from + `${CLAUDE_SKILL_DIR}/../pkgreview-core/references/standards.md` at + `[VERSION]` with the stamps, plus the line + `Upgraded from: [STAMP] on [date]` under them. + +Present the delta as a table: item (current canonical wording, quoted +verbatim from the checklist file at `[VERSION]`), tier, version that +introduced it, and the section it came from. Required items first. + +## Step 3: CHECK-IN and STOP + +> "Upgrade [package] from [STAMP] to [VERSION]: [n] required and [m] +> advisory items changed (table above). On yes I create one issue with +> exactly these items; nothing else is written. Proceed? (yes/no/edit)" + +Wait for the reply. "no" or "edit" means discuss; do not create anything. + +## Step 4: Create the upgrade issue + +Labels `pkgreview` and `pkgreview-upgrade` (create them if missing). +Title: `Standard upgrade [STAMP] to [VERSION]`. Body: + +``` +## Standard upgrade + +Review standard version: [VERSION] + +Upgraded from: [STAMP] + +Organization profile: [ORG] + +### Intake re-screen + +[To be filled by /review-issue: the PII and sensitivity check over the +current files and the git history, each check with its command and +outcome; any hit STOPS the upgrade until resolved.] + +### Tasks + +#### Required + +- [ ] [each changed required item, canonical wording verbatim, with "(since vX.Y.Z)"] +- [ ] Standards file: regenerate CLAUDE.md from standards.md at [VERSION] with the stamps and add `Upgraded from: [STAMP] on [date]` + +#### Advisory + +- [ ] [each changed advisory item, same form] + +### Next Steps + +1. Run `/review-issue [this issue's number]`; the body above is the work list +2. Create a PR to the `dev` branch +3. After merging, run `/review-complete` (it accepts a closed upgrade issue with a merged PR as completion), then `/create-release [patch version]` +``` + +Run the check script and post its report as the first comment, so the +upgrade starts from verified facts: + +```bash +Rscript "${CLAUDE_SKILL_DIR}/../pkgreview-core/check/pkgreview-check.R" . --org=[org] > /tmp/pkgreview-check.md +gh issue comment [number] --body-file /tmp/pkgreview-check.md +``` + +## Step 5: Report and STOP + +Give the user the issue number and URL, the counts, and the next step +(`/review-issue [number]`). **Stop here. Do not start working on the +issue, do not create branches or PRs.** From 99f567c16b9421b8de4f545c931ead0f73c910e1 Mon Sep 17 00:00:00 2001 From: larnsce Date: Sun, 6 Sep 2026 05:51:35 +0200 Subject: [PATCH 3/4] chore(release): version 1.6.0, README for the plugin install, compatibility table, structure tree Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019LJvVaoT3vhaJj7k9LBzEn --- README.md | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bdf0f9d..fbb4322 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ Creating a data package for one of the registered organizations? Start with the git clone https://github.com/openwashdata/pkgreview.git cd pkgreview -# Install the skills (copy; re-run after every git pull to update) +# Install the skills. Either as the plugin (one command to update later): +claude plugin marketplace add openwashdata/pkgreview +claude plugin install pkgreview@pkgreview +# or as copies (re-run after every git pull to update); not both, the two +# would provide the same skill names: mkdir -p ~/.claude/skills cp -R skills/* ~/.claude/skills/ @@ -52,6 +56,7 @@ they run only when you type them, never on the model's own initiative. | `/review-issue [n]` | Work on a review issue (actual issue number) | `/review-issue 42` | | `/create-next-issue` | Create next review issue | `/create-next-issue` | | `/review-complete` | Create final PR to main | `/review-complete` | +| `/review-upgrade` | Bring a reviewed package to the installed standard with one issue of changed items | `/review-upgrade` | | `/create-release` | Create a new release | `/create-release [version]` | | `/add-doi` | Integrate a Zenodo DOI after release (resume or repair path) | `/add-doi 10.5281/zenodo.XXXXXXX` | @@ -79,6 +84,14 @@ Each review addresses 4 key areas: - Git - Claude Code (the workflow ships as skills; see Quick Start) +Compatibility (the floor is recorded in `skills/pkgreview-core/WASHR_FLOOR` +and checked weekly by CI): + +| pkgreview | washr | +|-----------|-------| +| 1.0.0 to 1.4.0 | 1.0.1, with the caveats the skills carried | +| 1.5.0 and later | >= 1.1.0 | + ## Repository Structure ``` @@ -89,10 +102,12 @@ pkgreview/ │ ├── create-next-issue/ │ ├── review-status/ │ ├── review-complete/ +│ ├── review-upgrade/ │ ├── create-release/ │ ├── add-doi/ │ └── pkgreview-core/ # Shared references (not a skill) │ ├── VERSION # Review standard version +│ ├── WASHR_FLOOR # Lowest washr version the skills support │ ├── check/ # Deterministic check script │ └── references/ │ ├── checklists/ # Canonical checklists, one per review area @@ -100,13 +115,18 @@ pkgreview/ │ ├── orgs/ # Registered organization profiles │ ├── standards.md # Package-resident standards file │ └── recovery.md # State failure modes and recovery paths -├── fixtures/ # Defective test package, scorecard, history fixture +├── fixtures/ # Defective test package, scorecard, history fixture, +│ # expected reports, check-script cases, throwaway script +├── scripts/ # release.sh, verify_release.sh, lint.sh, washr_drift.R ├── hooks/ # Optional PreToolUse hook (docs/guardrails.md) ├── docs/ │ ├── guidebook.md # Contributor guidebook │ ├── checklist-reconciliation.md │ ├── guardrails.md │ └── roadmap-v1.1.md +├── .github/workflows/ # gate (fixture gate, cases, lint), washr-drift, release +├── .claude-plugin/ # Plugin manifests (version mirrors VERSION) +├── NEWS.md # Release notes for non-checklist changes ├── CLAUDE.md # Guide for Claude sessions in THIS repo ├── README.md # This file └── pkgreview.Rproj # RStudio project file @@ -153,10 +173,13 @@ previously diverging checklist copies were merged is in `docs/checklist-reconciliation.md`. Checklist and template changes get a version bump -(`skills/pkgreview-core/VERSION` plus a git tag); in-flight reviews finish -on the version stamped into their first review issue. After any significant -change, run the review against `fixtures/pkgreviewtest/` and confirm every -planted defect in `fixtures/SCORECARD.md` is caught. +(`skills/pkgreview-core/VERSION` plus a git tag through +`scripts/release.sh`); in-flight reviews finish on the version stamped into +their first review issue, and `/review-upgrade` moves a published package +to a newer standard with one issue of changed items. CI runs the check +script against `fixtures/pkgreviewtest/` on every push and diffs the +report against `fixtures/expected/`, so every planted defect in +`fixtures/SCORECARD.md` stays caught. ## Contributing From f736b2c8d22f75bd37578038fa214a31e7dd6d58 Mon Sep 17 00:00:00 2001 From: larnsce Date: Sun, 6 Sep 2026 05:53:02 +0200 Subject: [PATCH 4/4] fix(lint): write the em dash and emoji ranges as escape sequences The literal characters in the regex made the lint flag its own source once the file was tracked (CI run 34010046734). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019LJvVaoT3vhaJj7k9LBzEn --- scripts/lint_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint_text.py b/scripts/lint_text.py index 3a4cb97..7421013 100644 --- a/scripts/lint_text.py +++ b/scripts/lint_text.py @@ -8,7 +8,7 @@ ["git", "ls-files", "*.md", "*.R", "*.sh", "*.py", "*.yml", "*.yaml", "*.json", "*.Rmd"], capture_output=True, text=True, check=True, ).stdout.split() -pattern = re.compile("[—\U0001F300-\U0001FAFF☀-➿]") +pattern = re.compile("[\\u2014\\U0001F300-\\U0001FAFF\\u2600-\\u27BF]") bad = 0 for path in files: try: