- Describes version 1.2.0. Page last
- reviewed .
+ Describes version 1.3.0. Page last
+ reviewed .
diff --git a/docs/self-assessment.md b/docs/self-assessment.md
index 641bea0..bc8f5b8 100644
--- a/docs/self-assessment.md
+++ b/docs/self-assessment.md
@@ -2,11 +2,16 @@
Assessed against the
[trsdn Repository Quality Standard](https://github.com/trsdn/.github/blob/main/docs/repository-quality-standard.md)
-v1.11.1 on 2026-09-17. The machine-readable result is
+v1.14.0 on 2026-09-20, by an AI agent reading the repository, the GitHub API and
+the published v1.3.0 release. The machine-readable result is
[`.github/conformance.yml`](../.github/conformance.yml); this document is the
evidence for every criterion that isn't a clean pass. A clean pass isn't
repeated here — see `standard.yml`'s catalog for what each ID means.
+Overall state: **Needs work**. The only failing criterion is `P09`, which the
+record cannot excuse and which is low impact; there is no critical or
+high-priority gap. Nine criteria are partial.
+
## Profiles claimed
Baseline, Public, Software, Package And Release, Product Identity, Agent
@@ -21,47 +26,71 @@ Not claimed: Documentation (the primary product is the app, not documentation
or content), Deployable (a distributed desktop app, not a deployed service),
Archived (the repository is active).
+## Fail
+
+- **P09** — no repository-activity card exists. The standard now says a
+ repository with a runner and no card has not met the criterion (before 1.12
+ this was recorded as partial on the grounds that it was unclear whether it
+ was expected). The card has to be regenerated by a scheduled workflow, which
+ needs a write-capable token; `AGENTS.md` forbids adding a write permission to
+ a workflow here, so this is a decision to make, not just work to do.
+
## Partial
- **B05** — `AGENTS.md` and the README document the individual commands
- (`swift build`, `swift test`, `swift format lint`, `./build-app.sh`), but
- there's no single combined gate script. Running all of them is a few lines,
- not one command.
+ (`swift build`, `swift test`, `swift format lint`, `./build-app.sh`, and
+ `npm test` for the plugin), but there's no single combined gate script.
- **I03** — the app bundle's `NSHumanReadableCopyright` names the copyright
holder, but no license identifier (e.g. "MIT") is embedded in the bundle
itself, only in the repository's `LICENSE` file.
- **L03** — the primary language is English in practice (`CONTRIBUTING.md`
states it for contributions) and there is exactly one locale, but nothing
states outright "this app is English-only, no localization is planned."
-- **P08** — README badges (CI, License, Platform, Swift) exist but haven't
- been diffed against the org's specific badge convention document.
-- **P09** — no self-hosted, generated repository-activity visualization
- exists; unclear whether this specific evidence is expected for a repo this
- size, so recorded as partial rather than guessed at either way.
-- **R01, R02** — the README states the current version and macOS
- compatibility informally; there's no separate package manifest (this isn't
- a distributed package) and no explicit written compatibility/versioning
- policy beyond "tags are semver, see CHANGELOG."
+- **P08** — the badge block is CI, License, Platform, Swift. The standard's
+ block is license, platform, CI, latest release, conformance, in that order:
+ the order differs, the release and conformance badges are missing (the
+ conformance badge now exists under `.github/badges/`), and the Swift badge has
+ no stated reason.
+- **R01** — SwiftPM has no manifest fields for description, licence or
+ repository URL, so under 1.13 they belong in the app's `Info.plist`. The
+ plist carries the name, version and copyright, but not the licence
+ identifier (see `I03`), and no document says which property lives where.
+- **R02** — the README states the current version and macOS compatibility
+ informally; there's no written versioning and compatibility policy beyond
+ "tags are semver, see CHANGELOG."
+- **R05** — the standard now asks for a *recorded* smoke test of the published
+ file. On 2026-09-20 the published `OpenPromptr-v1.3.0-macOS-arm64.dmg` was
+ mounted: `spctl` accepts the app as Notarized Developer ID, `codesign
+ --verify --deep --strict` passes, and the binary launches and prints
+ `OpenPromptr 1.3.0 (64)`. The core function (capturing a source and showing
+ it on a target display) was **not** exercised: that needs a display and a
+ Screen Recording grant, and nobody has run 1.3.0 that way. A
+ maintainer installed 1.2.0 on their own machine, but no record says which
+ version was tested when, or what was exercised.
+- **S03** — Swift is checked (`swift format lint --strict` in CI), but the
+ plugin's JavaScript under `Tools/` has no formatter or linter.
+- **S04** — CI builds and tests the app on macOS. The plugin's tests
+ (`npm test` in `Tools/openpromptr-streamdeck`, Node 20+) exist and pass
+ locally but no workflow runs them, so a supported runtime is not covered by CI.
## Resolved since the last pass
-- **R03–R08** — `v1.2.0` (2026-09-17) is a real, signed, notarized release:
- `trsdn/macos-notarization-broker`'s `openpromptr` profile now builds the
- correct source (#49, #50) and produces `OpenPromptr-v{version}-macOS-arm64.{zip,dmg}`
- plus the AppUpdater-required `OpenPromptr-{version}.dmg` copy (R03, R04).
- The broker's own preflight/`validate_app_tree` smoke-tests the bundle
- before signing (R05). Release notes come from the CHANGELOG entry (R06).
- As of `scripts/request.sh` on the broker (PR #53), this is enforced, not
- just practiced: `--publish` fetches `CHANGELOG.md` at the tag through the
- API and fails the release outright if the entry for that version is
- missing, empty, or still sitting under `## Unreleased` (R07 — the trsdn
- standard's decision 0010 gate, mandatory for every profile on that
- broker, not just this one). `provenance.json` is uploaded as a release
- asset, recording the source commit, tag, and signing identity, so a
- consumer can verify where the artifact came from (R08). Verified live:
- `xcrun stapler validate` and `spctl --assess` both accept the published
- DMG as "Notarized Developer ID", and the R07 gate was tested against
- `v1.2.0`'s real `CHANGELOG.md` entry before merging.
+- **R03–R08** — `v1.2.0` and `v1.3.0` are real, signed, notarized releases
+ produced by `trsdn/macos-notarization-broker`'s `openpromptr` profile, started
+ for the tag with the command in `RELEASE_CHECKLIST.md` (R03). Tag, version and
+ release title agree (R04). Release notes are the CHANGELOG entry for the
+ version, and the broker refuses to publish without it (R06, R07).
+ `provenance.json` is a release asset, and `stapler validate` and `spctl` both
+ accept the DMG (R08). `R05` moved to partial only because 1.14.0 asks for more
+ than the broker's own preflight (see above).
+- **W03** — the site still described v1.2.0 after v1.3.0 was published. Fixed in
+ the same change as this assessment (download links, version badge, "describes
+ version" line).
+- **G02** — `AGENTS.md`'s layout and command list did not mention
+ `Tools/openpromptr-streamdeck` or its `npm test`. Fixed in the same change.
+- **W05, W06** — retired in standard 1.12.0 and recorded `na`. **W09** (a site
+ designed for the project) passes: `docs/index.html` is a bespoke page, see
+ [decision 0013](https://github.com/trsdn/.github/blob/main/docs/decisions/0013-sites-are-designed-not-templated.md).
## Not applicable
@@ -82,12 +111,10 @@ Archived (the repository is active).
## Worth noting on otherwise-passing criteria
- **X01** (keyboard operability) — verified by code review, not live
- end-to-end testing: every interactive control in `ControlView.swift` is a
+ end-to-end testing: every interactive control in `ControlView.swift` and `SettingsView.swift` is a
native SwiftUI `Button`/`Toggle`/`Picker`, which macOS gives standard Tab
focus order and a focus ring for free, and there's no custom-drawn
hit-testing that would bypass it. Not tested with an actual screen reader
or a physical keyboard walkthrough.
- **W01** — "repeatable, documented process" here is simply: GitHub Pages
- serves `docs/` from `main` directly, no build step. That's the whole
- process; there's nothing to document beyond what `docs/assets/VENDORED.md`
- already says about updating the vendored assets.
+ serves `docs/` from `main` directly, no build step and no vendored assets.
From 209138786d5039ebd12b7862c53cf73cc71c2c95 Mon Sep 17 00:00:00 2001
From: Torsten Mahr
Date: Sun, 20 Sep 2026 21:40:01 +0200
Subject: [PATCH 2/3] docs: reassess against standard 1.15.0; add a smoke kit
for published releases
1.15.0 asks for a smoke kit (R05), accepts SemVer as the versioning policy (R02),
and grades stated-but-not-allowed and restated facts. Adds
Scripts/smoke-published.sh, run against v1.3.0 (9/9), states SemVer and
English-only in the README, and re-records the results: R05, R02, L03, B05 pass;
S02 and B13 partial. Still Needs work because of P09.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01EGjbE4ZXFZyXFd2GQ7xRAp
---
.github/badges/conformance.svg | 6 +--
.github/conformance.yml | 14 +++---
AGENTS.md | 3 ++
README.md | 12 +++++
RELEASE_CHECKLIST.md | 23 +++++++++
Scripts/smoke-published.sh | 83 ++++++++++++++++++++++++++++++++
docs/self-assessment.md | 86 +++++++++++++++++++---------------
7 files changed, 180 insertions(+), 47 deletions(-)
create mode 100755 Scripts/smoke-published.sh
diff --git a/.github/badges/conformance.svg b/.github/badges/conformance.svg
index 9bdd92a..585d29e 100644
--- a/.github/badges/conformance.svg
+++ b/.github/badges/conformance.svg
@@ -1,10 +1,10 @@
-
@@ -99,21 +100,16 @@ Releases are tagged `vMAJOR.MINOR.PATCH` and follow
fixes bugs, a minor release adds behavior without breaking what exists, and a
major release may change or remove it. That covers the app, its local HTTP API
and the Stream Deck plugin. Every version's changes are in
-[`CHANGELOG.md`](CHANGELOG.md). The app supports macOS 13 and later.
+[`CHANGELOG.md`](CHANGELOG.md).
The app's interface is **English only**; there are no localizations and none are
planned.
## Building and testing
-```bash
-git clone https://github.com/trsdn/OpenPromptr.git
-cd OpenPromptr
-swift test
-./build-app.sh
-```
-
-The script creates `dist/OpenPromptr.app`. It automatically prefers an
+Clone the repository, then build and validate with the commands in
+[`AGENTS.md`](AGENTS.md#build--validate) — that is their one home.
+`./build-app.sh` creates `dist/OpenPromptr.app`. It automatically prefers an
available identity of type **Developer ID Application**, falls back to **Apple
Development**, and only falls back to an ad-hoc signature with a warning if no
stable identity is available. There is no hard-coded team or certificate
@@ -390,6 +386,16 @@ now also carries an icon. This has not been tested end-to-end with VoiceOver.
Automatic-recovery and status changes are conveyed visually and in text only;
there are no sound cues.
+## Repository statistics
+
+
+
+
+
+
+Generated daily by [`stats.yml`](.github/workflows/stats.yml) and committed to
+the `stats` branch, because `main` is protected.
+
## Contributing
Contributions are welcome. The development workflow, language and commit
diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md
index c34eda8..4871c0c 100644
--- a/RELEASE_CHECKLIST.md
+++ b/RELEASE_CHECKLIST.md
@@ -23,6 +23,22 @@ layout, architecture, entitlements, or minimum macOS version — is not a
local decision. It requires a reviewed pull request against the broker's
`profiles/apps.json`, and the release fails until that lands.
+## Where package metadata lives
+
+SwiftPM's `Package.swift` has no fields for a description, a licence or a
+repository URL, so those live in the file the app itself carries,
+`Config/Info.plist`, and the GitHub repository settings mirror them:
+
+| Property | Home | Mirrored in |
+| --- | --- | --- |
+| Name | `CFBundleName`, `CFBundleDisplayName` | repository name |
+| Version | the git tag, stamped into `CFBundleShortVersionString` and `CFBundleVersion` by the build | release title |
+| Description | `OPRProductDescription` | GitHub repository description |
+| Licence | `OPRLicenseIdentifier` (and `NSHumanReadableCopyright` for the holder) | `LICENSE`, GitHub licence |
+| Repository, issue tracker | `OPRRepositoryURL`, `OPRIssueTrackerURL` | GitHub |
+
+When one changes, change the others in the same pull request.
+
## Per release
1. Update `CHANGELOG.md`: move entries out of *Unreleased* into a new
diff --git a/Tools/openpromptr-streamdeck/package.json b/Tools/openpromptr-streamdeck/package.json
index 8958c65..377d1f2 100644
--- a/Tools/openpromptr-streamdeck/package.json
+++ b/Tools/openpromptr-streamdeck/package.json
@@ -5,7 +5,7 @@
"description": "A plugin for OpenDeck and the Elgato Stream Deck, so hardware keys can start and stop OpenPromptr's output and change its rotation and mirroring.",
"type": "module",
"scripts": {
- "test": "node test/manifest.js && node test/deck-socket.js && node test/logic.js && node test/client.js && node test/smoke.js",
+ "test": "node test/style.js && node test/manifest.js && node test/deck-socket.js && node test/logic.js && node test/client.js && node test/smoke.js",
"install-plugin": "./install.sh"
},
"license": "MIT"
diff --git a/Tools/openpromptr-streamdeck/test/smoke.js b/Tools/openpromptr-streamdeck/test/smoke.js
index 9373c72..3186ac0 100644
--- a/Tools/openpromptr-streamdeck/test/smoke.js
+++ b/Tools/openpromptr-streamdeck/test/smoke.js
@@ -106,8 +106,10 @@ check("the plugin registers with the deck app", async () => {
});
check("a key that appears is painted from the app's state, with a custom label", async () => {
- tell({ event: "willAppear", context: "out", action: "com.trsdn.openpromptr.output", payload: { settings: { title: "Prompter" } } });
- tell({ event: "willAppear", context: "rot", action: "com.trsdn.openpromptr.rotation", payload: { settings: {} } });
+ const output = "com.trsdn.openpromptr.output";
+ const rotation = "com.trsdn.openpromptr.rotation";
+ tell({ event: "willAppear", context: "out", action: output, payload: { settings: { title: "Prompter" } } });
+ tell({ event: "willAppear", context: "rot", action: rotation, payload: { settings: {} } });
await until(() => last("setTitle", "out") && last("setTitle", "rot"), "first paint");
assert.equal(last("setTitle", "out").payload.title, "Prompter");
assert.equal(last("setState", "out").payload.state, 0);
@@ -121,9 +123,11 @@ check("a change in the app repaints the key", async () => {
check("a press becomes the matching request", async () => {
requests.length = 0;
- tell({ event: "keyDown", context: "out", action: "com.trsdn.openpromptr.output", payload: { settings: { title: "Prompter" } } });
+ const output = "com.trsdn.openpromptr.output";
+ tell({ event: "keyDown", context: "out", action: output, payload: { settings: { title: "Prompter" } } });
await until(() => requests.includes("POST /v1/output/stop"), "the stop request");
- tell({ event: "keyDown", context: "rot", action: "com.trsdn.openpromptr.rotation", payload: { settings: {} } });
+ const rotation = "com.trsdn.openpromptr.rotation";
+ tell({ event: "keyDown", context: "rot", action: rotation, payload: { settings: {} } });
await until(() => requests.filter((r) => r === "POST /v1/transform").length === 1, "the transform request");
});
diff --git a/Tools/openpromptr-streamdeck/test/style.js b/Tools/openpromptr-streamdeck/test/style.js
new file mode 100644
index 0000000..982eb81
--- /dev/null
+++ b/Tools/openpromptr-streamdeck/test/style.js
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+import { fileURLToPath } from "node:url";
+
+/**
+ * The plugin's format and static checks, without a linter to install.
+ *
+ * The plugin has no dependencies and no build step on purpose, so a check that
+ * needs `npm install` would be the only thing that did. This is the small part
+ * of one that has caught real problems here: source that does not parse, tabs,
+ * trailing whitespace, a missing final newline, and lines too long to read in a
+ * diff.
+ */
+
+const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
+const MAX_LINE = 120;
+
+function files(dir) {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) return [];
+ const full = path.join(dir, entry.name);
+ return entry.isDirectory() ? files(full) : [full];
+ });
+}
+
+const sources = files(root).filter((file) => /\.(js|json|html|css|sh)$/.test(file));
+const scripts = sources.filter((file) => file.endsWith(".js"));
+
+const checks = [];
+const check = (name, body) => checks.push([name, body]);
+const relative = (file) => path.relative(root, file);
+
+check("every script parses", () => {
+ for (const file of scripts) {
+ const result = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
+ assert.equal(result.status, 0, `${relative(file)} does not parse:\n${result.stderr}`);
+ }
+});
+
+check("no tabs and no trailing whitespace", () => {
+ for (const file of sources) {
+ fs.readFileSync(file, "utf8")
+ .split("\n")
+ .forEach((line, index) => {
+ assert.ok(!line.includes("\t"), `${relative(file)}:${index + 1} has a tab`);
+ assert.ok(!/\s$/.test(line), `${relative(file)}:${index + 1} has trailing whitespace`);
+ });
+ }
+});
+
+check("every file ends with exactly one newline", () => {
+ for (const file of sources) {
+ const text = fs.readFileSync(file, "utf8");
+ const single = text.endsWith("\n") && !text.endsWith("\n\n");
+ assert.ok(single, `${relative(file)} does not end in one newline`);
+ }
+});
+
+check(`no script line is longer than ${MAX_LINE} characters`, () => {
+ for (const file of scripts) {
+ fs.readFileSync(file, "utf8")
+ .split("\n")
+ .forEach((line, index) => {
+ assert.ok(line.length <= MAX_LINE, `${relative(file)}:${index + 1} is ${line.length} characters`);
+ });
+ }
+});
+
+check("scripts are modules that import only what they use", () => {
+ // A cheap stand-in for a no-unused-imports rule: a named import that never
+ // appears again in the file is dead weight and usually a leftover.
+ for (const file of scripts) {
+ const text = fs.readFileSync(file, "utf8");
+ for (const match of text.matchAll(/^import\s+(?:(\w+)|\{([^}]+)\})\s+from/gm)) {
+ const listed = match[2]?.split(",").map((name) => name.trim().split(/\s+as\s+/).pop());
+ const names = match[1] ? [match[1]] : listed;
+ for (const name of names.filter(Boolean)) {
+ const uses = text.match(new RegExp(`\\b${name}\\b`, "g")).length;
+ assert.ok(uses > 1, `${relative(file)} imports ${name} and never uses it`);
+ }
+ }
+ }
+});
+
+const failures = [];
+for (const [name, body] of checks) {
+ try {
+ body();
+ console.log(` ok ${name}`);
+ } catch (error) {
+ failures.push(name);
+ console.log(` FAIL ${name}\n ${error.message}`);
+ }
+}
+console.log(
+ failures.length
+ ? `\n${failures.length} of ${checks.length} checks failed`
+ : `\n${checks.length}/${checks.length} checks passed`,
+);
+process.exit(failures.length ? 1 : 0);
diff --git a/docs/self-assessment.md b/docs/self-assessment.md
index 7ea90ae..627ed19 100644
--- a/docs/self-assessment.md
+++ b/docs/self-assessment.md
@@ -8,9 +8,8 @@ the published v1.3.0 release. The machine-readable result is
evidence for every criterion that isn't a clean pass. A clean pass isn't
repeated here — see `standard.yml`'s catalog for what each ID means.
-Overall state: **Needs work**. The only failing criterion is `P09`, which
-`scripts/conformance.py` will not let a `Fail` sit under `Healthy`. It is low
-impact; there is no critical criterion failing. Seven criteria are partial.
+Overall state: **Healthy**: no criterion is `Fail`. Two are `Partial`, and both
+are stated below.
## Profiles claimed
@@ -26,34 +25,16 @@ Not claimed: Documentation (the primary product is the app, not documentation
or content), Deployable (a distributed desktop app, not a deployed service),
Archived (the repository is active).
-## Fail
-
-- **P09** — no repository-activity card exists, and the repository has a runner.
- Since 1.15.0 the card may be published to a dedicated branch (e.g. `stats`) by
- a scheduled workflow, which needs a write-capable token for that job.
- `AGENTS.md` forbids adding a write permission to any workflow here, so this is
- a decision to make: either allow that one job, or accept the `Fail`.
-
## Partial
-- **B13** — the build commands (`swift build`, `swift test`, `./build-app.sh`)
- are restated by hand in `README.md`, `CONTRIBUTING.md` and `AGENTS.md`. The
- copies agree today, which is `Partial` under the standard, and none links to a
- single home.
-- **I03** — the app bundle's `NSHumanReadableCopyright` names the copyright
- holder, but no license identifier (e.g. "MIT") is embedded in the bundle
- itself, only in the repository's `LICENSE` file. Criterion has two parts; one
- is met.
-- **P08** — the badge block is CI, License, Platform, Swift. The standard's
- block is license, platform, CI, latest release, conformance, in that order:
- the order differs, the release and conformance badges are missing (the
- conformance badge now exists under `.github/badges/`), and the Swift badge has
- no stated reason.
-- **R01** — SwiftPM has no manifest fields for description, licence or
- repository URL, so they belong in the app's `Info.plist`. The plist carries
- the name, version, copyright, repository URL (`OPRRepositoryURL`) and issue
- tracker URL, but no description and no licence identifier, and no document
- says which property lives where.
+- **P09** — the card exists as a mechanism, not yet as a rendered image.
+ `.github/workflows/stats.yml` runs `trsdn/.github`'s reusable `repo-stats`
+ workflow (pinned to a commit) daily and on dispatch, writing the card to the
+ `stats` branch, and the README embeds it with a `` element. A
+ workflow can only be dispatched from the default branch, so the first run
+ happens after this change is merged; until then the README image does not
+ resolve. Becomes `Pass` once the run has produced `repo-card.svg` and
+ `repo-card-dark.svg` on `stats`.
- **S02** — reading applied: the main entry point of a graphical application is
the logic behind its action, reached without its views. The 45 tests
(`OpenPromptrCoreTests`) cover the pure logic — aspect fit, capture sizing,
@@ -62,14 +43,9 @@ Archived (the repository is active).
exhausted retries). They do not cover the capture pipeline, `AppModel` or the
virtual display, which need a real display and a Screen Recording grant.
`AGENTS.md` says so. A suite that covers a supporting part of the action is a
- `Partial`.
-- **S03** — Swift is checked (`swift format lint --strict` in CI, and compiling
- is the type check), but the plugin's JavaScript under `Tools/` has no
- formatter or linter.
-- **S04** — the app's claim (macOS 13 or later) is covered by CI on the runner's
- macOS. The plugin claims Node 20 or newer (README, manifest); its tests
- (`npm test` in `Tools/openpromptr-streamdeck`) pass locally but no workflow
- runs them, so a claimed runtime has no job.
+ `Partial`. Closing this needs the capture path split so its decisions can run
+ without ScreenCaptureKit, which is a change to code that `swift test` cannot
+ verify end to end, so it was not attempted here.
## Resolved since the last pass
@@ -95,6 +71,25 @@ Archived (the repository is active).
localizations planned.
- **B05** — the commands are documented in `AGENTS.md`, and the latest run of
them on `main` (CI, 2026-09-20) is green; 1.15.0 counts that as run.
+- **P08** — the badge block is now license, platform, CI, latest release,
+ conformance, in the standard's order. License and release come from GitHub
+ through shields.io, CI is GitHub's own badge, conformance is the committed
+ badge regenerated by `conformance.py`, and the platform badge is checked
+ against `Package.swift` by a CI step, so it cannot drift. The Swift badge was
+ dropped; the toolchain requirement lives in the README's Requirements.
+- **S03, S04** — the Stream Deck plugin now has a CI job (`Stream Deck plugin`)
+ that runs `npm test`: a syntax check, a format check (no tabs, no trailing
+ whitespace, final newline, line length) and an unused-import check, written
+ without dependencies because the plugin has none, plus its functional tests.
+ Its claim of Node 20 or newer is covered by the runner's Node.
+- **B13** — the build and validation commands, the self-test, the icon
+ generation and the platform requirements each have one home (`AGENTS.md`,
+ README) and `CONTRIBUTING.md` links to them. `CONTRIBUTING.md` had also
+ claimed there were no third-party dependencies, which stopped being true with
+ AppUpdater and Swifter.
+- **I03, R01** — `Config/Info.plist` now embeds `OPRLicenseIdentifier` and
+ `OPRProductDescription`; `RELEASE_CHECKLIST.md` states which property lives
+ where and what mirrors it.
- **W03** — the site still described v1.2.0 after v1.3.0 was published. Fixed in
the same change as this assessment (download links, version badge, "describes
version" line).