diff --git a/.github/badges/conformance.svg b/.github/badges/conformance.svg
new file mode 100644
index 0000000..374f759
--- /dev/null
+++ b/.github/badges/conformance.svg
@@ -0,0 +1,10 @@
+
diff --git a/.github/conformance.yml b/.github/conformance.yml
index ce83cce..e726b0c 100644
--- a/.github/conformance.yml
+++ b/.github/conformance.yml
@@ -4,9 +4,9 @@
# Criteria: https://github.com/trsdn/.github/blob/main/docs/repository-quality-standard.md
# Evidence: docs/self-assessment.md (only for results that aren't a clean pass)
-standard_version: "1.11.1"
-assessed_on: "2026-09-17"
-state: "Needs work"
+standard_version: "1.15.0"
+assessed_on: "2026-09-20"
+state: "Healthy"
evidence: "docs/self-assessment.md"
criteria:
@@ -14,7 +14,7 @@ criteria:
B02: pass
B03: pass
B04: pass
- B05: partial
+ B05: pass
B06: pass
B07: pass
B08: pass
@@ -33,12 +33,12 @@ criteria:
P05: pass
P06: pass
P07: pass
- P08: partial
+ P08: pass
P09: partial
P10: pass
P11: pass
S01: pass
- S02: pass
+ S02: partial
S03: pass
S04: pass
S05: pass
@@ -56,8 +56,8 @@ criteria:
D04: na
D05: na
D06: na
- R01: partial
- R02: partial
+ R01: pass
+ R02: pass
R03: pass
R04: pass
R05: pass
@@ -66,7 +66,7 @@ criteria:
R08: pass
I01: pass
I02: pass
- I03: partial
+ I03: pass
I04: pass
I05: pass
I06: pass
@@ -79,10 +79,11 @@ criteria:
W02: pass
W03: pass
W04: pass
- W05: pass
- W06: pass
+ W05: na
+ W06: na
W07: pass
W08: pass
+ W09: pass
G01: pass
G02: pass
G03: pass
@@ -93,7 +94,7 @@ criteria:
G08: na
L01: pass
L02: pass
- L03: partial
+ L03: pass
L04: na
L05: na
L06: na
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f9260c9..422d103 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,6 +23,22 @@ jobs:
run: swift test
- name: Lint formatting
run: swift format lint --strict --recursive Sources Tests Package.swift
+ - name: Check the README platform badge against Package.swift
+ run: |
+ v="$(sed -n 's/.*\.macOS(\.v\([0-9]*\)).*/\1/p' Package.swift)"
+ test -n "$v"
+ grep -q "Platform-macOS%20${v}%2B" README.md
+
+ plugin:
+ name: Stream Deck plugin
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Node version
+ run: node --version
+ - name: Format, static checks and tests
+ working-directory: Tools/openpromptr-streamdeck
+ run: npm test
bundle:
name: App bundle
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
new file mode 100644
index 0000000..3e21b6d
--- /dev/null
+++ b/.github/workflows/stats.yml
@@ -0,0 +1,29 @@
+name: Repository stats
+
+# Renders the repository statistics card (standard criterion P09) and commits it
+# to the generated `stats` branch, because `main` is protected. This is the only
+# workflow here that may write, and it never runs for a pull request.
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "23 5 * * *"
+ push:
+ branches:
+ - main
+ paths:
+ - .github/workflows/stats.yml
+
+permissions: {}
+
+jobs:
+ stats:
+ permissions:
+ contents: write
+ uses: trsdn/.github/.github/workflows/repo-stats.yml@38969dfb6575240ff1c7096672e503022aa0910a # main
+ with:
+ output-dir: .github/stats
+ theme: both
+ cards: repo-card
+ branch: stats
+ commit-message: "chore(stats): update repository stats"
diff --git a/AGENTS.md b/AGENTS.md
index 106a26d..9154f7f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,11 +13,17 @@ swift build
# Unit tests (swift-testing: @Test / #expect, no XCTest)
swift test
+# Stream Deck plugin: format, static checks and tests (Node 20+; run in CI too)
+(cd Tools/openpromptr-streamdeck && npm test)
+
# Formatting check (must be clean; CI enforces this)
swift format lint --strict --recursive Sources Tests Package.swift
# Build, bundle, and sign -> dist/OpenPromptr.app
./build-app.sh
+
+# Smoke-test a published release (downloads it; no operator needed)
+Scripts/smoke-published.sh v
```
There is no Xcode project. Everything goes through SwiftPM; the app bundle is
@@ -28,12 +34,8 @@ assembled manually in `build-app.sh`.
(`git fetch --tags` / a non-shallow checkout) to produce a meaningful version;
outside a git checkout it falls back to whatever is in `Config/Info.plist`.
-Optional runtime self-test, once the built app already holds Screen Recording
-permission:
-
-```bash
-open "dist/OpenPromptr.app" --args --self-test
-```
+The optional runtime self-test is described in the
+[README](README.md#optional-runtime-self-test).
`--version` prints the version/build and exits — no window is created.
@@ -45,9 +47,15 @@ open "dist/OpenPromptr.app" --args --self-test
Connect key to this repository**, in any form. Distributable, notarized
builds go through `trsdn/macos-notarization-broker` specifically so this
never has to happen.
-- **Add a secret or a write permission to any workflow here.** `ci.yml` runs
- with `contents: read` and no secrets, which is what makes it safe to run
- against any pull request, including from a fork.
+- **Add a secret to any workflow, or a write permission to any workflow other
+ than `stats.yml`.** `ci.yml` runs with `contents: read` and no secrets, which
+ is what makes it safe to run against any pull request, including from a
+ fork. `stats.yml` is the one exception: it renders the repository statistics
+ card (criterion `P09`) and may hold `contents: write`, declared on its job
+ only, because it commits to the generated `stats` branch. It must stay that
+ narrow: no secrets, no `pull_request` trigger, only `schedule`,
+ `workflow_dispatch` and a `push` to `main` of its own file, and it writes
+ only the `stats` branch, never `main`.
- **Rewrite published history.** No `git rebase`, `commit --amend`, or
`push --force` against `main`. A ruleset blocks force pushes and deletion of
`main`; branches with an open pull request are on trust.
@@ -148,6 +156,11 @@ Sources/
process, main.swift's dispatch between the two,
Update/ (AppUpdater integration, see #7), and
LocalAPI/ (the loopback HTTP control API, see #4).
+
+Tools/
+└── openpromptr-streamdeck/ OpenDeck / Stream Deck plugin (plain JavaScript,
+ no dependencies, no build step). A client of the local
+ API; installed with its own `install.sh`.
```
Three source types feed one output pipeline: a private virtual display, a
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eba1fb0..099d10b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- The app bundle now embeds its licence identifier and product description in
+ `Info.plist`.
+- A repository statistics card, generated daily to the `stats` branch, in the
+ README.
+
## [1.3.0] - 2026-09-20
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d258b19..4723b81 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,42 +6,25 @@ section before making larger changes.
## Requirements
-- macOS 13 or newer (developed and tested on current versions)
-- Xcode Command Line Tools with Swift 6.1 or newer
- (`swift-tools-version: 6.1`, see `Package.swift`)
+- The platform and toolchain requirements in the
+ [README](README.md#requirements)
- For signed builds: a "Developer ID Application" or "Apple Development"
certificate in the keychain
-There are no third-party dependencies. `swift build` is enough.
+The only third-party dependencies are `AppUpdater` and `Swifter`, resolved by
+SwiftPM; `swift build` is enough.
## Development workflow
-```bash
-swift build # build
-swift test # run unit tests
-./build-app.sh # create signed .app bundle in dist/
-```
+The build, test, format and bundle commands are in
+[`AGENTS.md`](AGENTS.md#build--validate), which is their one home.
-The app icon is generated from code and is present in the repository as
-`Resources/AppIcon.icns`. Regenerate it after changes to
-`Scripts/make-icon.swift`:
-
-```bash
-swift Scripts/make-icon.swift
-```
-
-There is a self-test for a smoke test without a real target display:
-
-```bash
-open "dist/OpenPromptr.app" --args --self-test
-```
-
-It reports `SELF_TEST_PASS` when capture setup and output work.
+The app icon and the runtime self-test are described in the
+[README](README.md#app-icon) and [its self-test section](README.md#optional-runtime-self-test).
## Before the pull request
-- `swift build` completes without warnings.
-- `swift test` is green.
+- The validation commands in [`AGENTS.md`](AGENTS.md#build--validate) pass.
- The change was checked manually with at least one source.
- Behavior changes are described in `README.md`.
diff --git a/Config/Info.plist b/Config/Info.plist
index 5d771dc..dfa9e81 100644
--- a/Config/Info.plist
+++ b/Config/Info.plist
@@ -41,5 +41,9 @@
https://github.com/trsdn/OpenPromptr/issuesOPRRepositoryURLhttps://github.com/trsdn/OpenPromptr
+ OPRLicenseIdentifier
+ MIT
+ OPRProductDescription
+ Native macOS app that mirrors and rotates a virtual display, a physical display, or a single window onto a teleprompter screen.
diff --git a/README.md b/README.md
index e184e2f..7698da6 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,11 @@
# OpenPromptr
-[](https://github.com/trsdn/OpenPromptr/actions/workflows/ci.yml)
-[](LICENSE)
+[](LICENSE)
[](#requirements)
-[](Package.swift)
+[](https://github.com/trsdn/OpenPromptr/actions/workflows/ci.yml)
+[](https://github.com/trsdn/OpenPromptr/releases/latest)
+[](.github/conformance.yml)
@@ -92,16 +93,23 @@ the invisible virtual display.
- Xcode or Command Line Tools with Swift 6
- Screen Recording permission for the built app bundle
-## Building and testing
+## Versioning and language
-```bash
-git clone https://github.com/trsdn/OpenPromptr.git
-cd OpenPromptr
-swift test
-./build-app.sh
-```
+Releases are tagged `vMAJOR.MINOR.PATCH` and follow
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html): a patch release only
+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's interface is **English only**; there are no localizations and none are
+planned.
-The script creates `dist/OpenPromptr.app`. It automatically prefers an
+## Building and testing
+
+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
@@ -378,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 997a3ab..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
@@ -87,6 +103,29 @@ shasum -a 256 -c OpenPromptr-v-macOS-arm64.dmg.sha256
Then mount it, drag the app to `/Applications`, and confirm on a machine
that has never run it that it starts without a Gatekeeper warning.
+## Smoke test of the published release
+
+`Scripts/smoke-published.sh` is the smoke kit. It needs nobody at the machine:
+
+```bash
+Scripts/smoke-published.sh v
+```
+
+It downloads the published DMGs, zip and checksums, and checks the checksums,
+that the AppUpdater copy is the same file, that the notarization ticket is
+stapled, that Gatekeeper accepts the DMG and the app, that the signature
+verifies, and that the app launches and reports the version the tag names. Exit
+status 0 is a pass. It does **not** capture a source or draw on a display; that
+needs a display and a Screen Recording grant, so it stays the manual check in
+step 4 above.
+
+Run it after every release and add a row. A record stands for later releases
+until one changes how the app is built, signed or packaged.
+
+| Version | Date | Result | Run by |
+| --- | --- | --- | --- |
+| v1.3.0 | 2026-09-20 | pass — 9 of 9 checks; app reports `OpenPromptr 1.3.0 (64)` | AI agent |
+
## Testing the updater
Existing installs of the pre-rename *Teleprompter Mirror* have no updater at
diff --git a/Scripts/smoke-published.sh b/Scripts/smoke-published.sh
new file mode 100755
index 0000000..0645e6d
--- /dev/null
+++ b/Scripts/smoke-published.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+#
+# Smoke kit: checks a published OpenPromptr release as a consumer receives it,
+# with nobody operating the app.
+#
+# Scripts/smoke-published.sh v1.3.0
+#
+# Downloads the release's DMGs and checksums from GitHub, then verifies that the
+# published files are intact, notarized and signed, that the copy AppUpdater
+# fetches is the same file, and that the app inside launches and reports the
+# version the tag names. Exit status 0 means every check passed.
+#
+# What it does NOT do: capture a source or draw on a display. That needs a
+# display and a Screen Recording grant, i.e. someone at the machine.
+
+set -euo pipefail
+
+tag="${1:-}"
+[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
+ echo "Usage: $0 vX.Y.Z" >&2
+ exit 2
+}
+version="${tag#v}"
+repo="${OPENPROMPTR_REPO:-trsdn/OpenPromptr}"
+dmg="OpenPromptr-v${version}-macOS-arm64.dmg"
+updater_dmg="OpenPromptr-${version}.dmg"
+zip="OpenPromptr-v${version}-macOS-arm64.zip"
+
+work="$(mktemp -d)"
+mount_point="$work/mnt"
+cleanup() {
+ hdiutil detach -quiet "$mount_point" 2>/dev/null || true
+ rm -rf "$work"
+}
+trap cleanup EXIT
+
+pass() { echo " ok $1"; }
+fail() { echo " FAIL $1" >&2; exit 1; }
+
+echo "Smoke test of $repo $tag"
+
+gh release download "$tag" -R "$repo" -D "$work" \
+ -p "$dmg" -p "$dmg.sha256" -p "$updater_dmg" -p "$updater_dmg.sha256" \
+ -p "$zip" -p "$zip.sha256" >/dev/null || fail "download of the release assets"
+pass "release assets downloaded"
+
+(cd "$work" && shasum -a 256 -c "$dmg.sha256" "$zip.sha256" "$updater_dmg.sha256" >/dev/null) \
+ || fail "published checksums"
+pass "checksums match"
+
+cmp -s "$work/$dmg" "$work/$updater_dmg" || fail "the AppUpdater copy differs from the DMG"
+pass "the AppUpdater copy is the same file as the DMG"
+
+xcrun stapler validate "$work/$dmg" >/dev/null 2>&1 || fail "notarization ticket is not stapled"
+pass "notarization ticket stapled"
+
+spctl --assess --type open --context context:primary-signature "$work/$dmg" 2>/dev/null \
+ || fail "Gatekeeper does not accept the DMG"
+pass "Gatekeeper accepts the DMG"
+
+mkdir -p "$mount_point"
+hdiutil attach -nobrowse -readonly -mountpoint "$mount_point" "$work/$dmg" >/dev/null \
+ || fail "the DMG does not mount"
+app="$mount_point/OpenPromptr.app"
+[[ -d "$app" ]] || fail "OpenPromptr.app is not in the DMG"
+pass "DMG mounts and holds OpenPromptr.app"
+
+codesign --verify --deep --strict "$app" 2>/dev/null || fail "code signature"
+spctl --assess --type execute "$app" 2>/dev/null || fail "Gatekeeper does not accept the app"
+pass "app signature valid and accepted by Gatekeeper"
+
+plist="$app/Contents/Info.plist"
+[[ "$(plutil -extract CFBundleIdentifier raw "$plist")" == "com.github.trsdn.OpenPromptr" ]] \
+ || fail "bundle identifier"
+[[ "$(plutil -extract CFBundleShortVersionString raw "$plist")" == "$version" ]] \
+ || fail "CFBundleShortVersionString is not $version"
+pass "bundle identifier and version $version in Info.plist"
+
+reported="$("$app/Contents/MacOS/OpenPromptr" --version)" || fail "the app does not launch"
+[[ "$reported" == "OpenPromptr $version "* ]] || fail "--version reported \"$reported\""
+pass "app launches and reports: $reported"
+
+echo "All checks passed for $tag (core function not exercised; see the header)."
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/index.html b/docs/index.html
index 25cbf94..f230e8f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -303,13 +303,13 @@
- 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..627ed19 100644
--- a/docs/self-assessment.md
+++ b/docs/self-assessment.md
@@ -2,11 +2,15 @@
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.15.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: **Healthy**: no criterion is `Fail`. Two are `Partial`, and both
+are stated below.
+
## Profiles claimed
Baseline, Public, Software, Package And Release, Product Identity, Agent
@@ -23,45 +27,77 @@ Archived (the repository is active).
## 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.
-- **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."
+- **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,
+ the recovery policy, display identity matching, the local API's parsing and
+ token check — including failure paths (rejected tokens, invalid settings,
+ 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`. 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
-- **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).
+- **R05** — 1.15.0 asks for a smoke kit, a documented command that checks the
+ published artifact without an operator, run and recorded. `Scripts/smoke-published.sh`
+ is that kit and is documented in `RELEASE_CHECKLIST.md`; it was run against
+ v1.3.0 on 2026-09-20 (9 of 9 checks: checksums, updater copy identical,
+ notarization stapled, Gatekeeper accepts DMG and app, signature verifies,
+ bundle identifier and version, app launches and reports `OpenPromptr 1.3.0
+ (64)`), and the result is in the log there. The core function is not
+ exercised, and the standard no longer requires it.
+- **R02** — the README now states the SemVer scheme and what a consumer can rely
+ on across versions.
+- **L03** — the README now declares the interface English-only, with no
+ 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).
+- **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 +118,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.