appstore-precheck 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,70 @@ All notable changes to this project are documented here. Versioning follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.11.0] - 2026-07-02
9
+
10
+ Roadmap #3: **rejection-outcome feedback loop** — a third, honestly-scoped measurement
11
+ axis. Reporting layer only; the scan and GREEN/YELLOW/RED verdict are untouched.
12
+
13
+ Alongside the synthetic and real-panel corpora, the tool now tracks **real App Store
14
+ review outcomes** (approved / rejected + Apple's cited guideline) in a committed,
15
+ human-reviewed ledger (`corpus/outcomes/ledger.json`, starts empty) and summarizes them
16
+ in `docs/scorecard.md`. The summary is **honesty-floored**: below 10 recorded outcomes
17
+ it shows only a raw tally and computes **no rate**; at/above the floor it may show a
18
+ directional recall estimate with a permanent survivorship-bias caveat. Because the
19
+ ledger is committed and read offline, the section is deterministic and covered by the
20
+ existing `scorecard.sh --check` — no network, no new CI job.
21
+
22
+ ### Added
23
+ - **`corpus/outcomes/ledger.json`** (empty `[]`) + **`corpus/outcomes/README.md`** —
24
+ record schema, anonymization/privacy rules (no verbatim Apple text, no real identity),
25
+ contribution/review process, and the sample-size floor.
26
+ - **`scripts/scorecard-outcomes.sh`** — pure `bash`+`jq`, offline, deterministic; renders
27
+ the "Real App Store outcomes (n=N)" section with the honesty floor. Also
28
+ `scorecard.sh --outcomes`.
29
+ - The outcomes section is baked into `docs/scorecard.md`; Methodology now describes three
30
+ measurements.
31
+ - **`.github/ISSUE_TEMPLATE/app-store-outcome.md`** — anonymized outcome contribution that
32
+ funnels into a maintainer-reviewed PR.
33
+ - Test: `tests/test-scorecard-outcomes.sh`.
34
+
35
+ ### Notes
36
+ - Reporting/measurement only — outcome data never influences the verdict or which rules
37
+ fire (a future, human-only decision). No verbatim Apple Resolution Center text is stored.
38
+ The scan path (`scan.sh`/`verdict.sh`/`bin/cli.js`/`action.yml`) is unchanged.
39
+
40
+ ## [1.10.0] - 2026-07-02
41
+
42
+ Roadmap #4: **SARIF output + opt-in GitHub PR annotations** — read-only, no auto-fix.
43
+
44
+ The scanner can now emit its deterministic findings as **SARIF 2.1.0**
45
+ (`scan.sh --format sarif`, also `npx appstore-precheck --format sarif`), built from
46
+ the same structured findings as `--format json` with pure `jq` (no new dependency).
47
+ `results[]` contains the non-suppressed FAIL and WARN findings (FAIL → `error`,
48
+ WARN → `warning`); PASS and suppressed findings are excluded; findings that carry a
49
+ `file`/`line` become SARIF `physicalLocation`s so GitHub can anchor annotations. Only
50
+ the deterministic scan findings are included — the agent-mode Pierre deep-review
51
+ findings are not (SARIF is a deterministic CI artifact).
52
+
53
+ The GitHub Action gains two **opt-in** inputs (both default `false`, so existing
54
+ usage is unchanged): `sarif` uploads the SARIF to code-scanning via `upload-sarif`
55
+ (PR annotations + Security tab; needs `permissions: security-events: write`), and
56
+ `annotations` emits inline `::error`/`::warning` PR annotations. Both run even when
57
+ the scan verdict fails, and neither modifies the project.
58
+
59
+ ### Added
60
+ - **`skills/appstore-precheck/scripts/sarif.sh`** — `render_sarif()`, a SARIF 2.1.0
61
+ writer over the findings buffer (pure `jq`).
62
+ - **`scan.sh --format sarif`** (and `bin/cli.js --format text|json|sarif`).
63
+ - **`action.yml`** opt-in inputs `sarif` and `annotations` (default off): SARIF
64
+ upload via `github/codeql-action/upload-sarif@v3` and inline PR annotations.
65
+ - Tests: `tests/test-sarif.sh` and `tests/test-action-sarif.sh`.
66
+
67
+ ### Notes
68
+ - Read-only preserved: the scanner writes only to stdout; the Action redirects SARIF
69
+ to a CI workspace file. No auto-fix. Default `--format text`/`--format json` output
70
+ and the Action's default behavior are byte-identical to before.
71
+
8
72
  ## [1.9.0] - 2026-07-02
9
73
 
10
74
  Smarter analysis (roadmap #2a): **screenshot vision**, in two layers.
package/README.md CHANGED
@@ -264,6 +264,29 @@ job on a RED verdict; set `fail-on: YELLOW` to be stricter:
264
264
  Both inputs are optional: with none set, the action scans the repo root and fails the job only on
265
265
  a RED verdict. No App Store Connect credentials are needed; the action runs the static scan only.
266
266
 
267
+ ### SARIF & PR annotations (opt-in)
268
+
269
+ The Action can surface findings as GitHub code-scanning results and inline PR annotations. Both are
270
+ off by default; enable either or both:
271
+
272
+ ```yaml
273
+ permissions:
274
+ contents: read
275
+ security-events: write # required for SARIF upload
276
+ jobs:
277
+ precheck:
278
+ runs-on: ubuntu-latest
279
+ steps:
280
+ - uses: actions/checkout@v4
281
+ - uses: berkayturk/appstore-precheck@v1
282
+ with:
283
+ sarif: true # upload SARIF to the Security tab + PR annotations
284
+ annotations: true # also emit inline ::error/::warning annotations
285
+ ```
286
+
287
+ Locally / via npx: `npx appstore-precheck --format sarif > results.sarif`. The scan stays read-only;
288
+ nothing is auto-fixed.
289
+
267
290
  ## How it works
268
291
 
269
292
  | Phase | Step |
package/bin/cli.js CHANGED
@@ -36,6 +36,7 @@ function printHelp() {
36
36
  `Options:\n` +
37
37
  ` --dir <path> Directory to scan (default: current directory)\n` +
38
38
  ` --fail-on <level> Exit non-zero at RED (default) or YELLOW\n` +
39
+ ` --format <fmt> Output format: text (default), json, or sarif\n` +
39
40
  ` -v, --version Print the version and exit\n` +
40
41
  ` -h, --help Show this help and exit\n` +
41
42
  `\n` +
@@ -53,7 +54,7 @@ function fail(message, code) {
53
54
  }
54
55
 
55
56
  function parseArgs(argv) {
56
- const opts = { dir: process.cwd(), failOn: 'RED' };
57
+ const opts = { dir: process.cwd(), failOn: 'RED', format: 'text' };
57
58
  for (let i = 0; i < argv.length; i++) {
58
59
  const a = argv[i];
59
60
  if (a === '-h' || a === '--help') { printHelp(); process.exit(0); }
@@ -69,6 +70,12 @@ function parseArgs(argv) {
69
70
  opts.failOn = v;
70
71
  continue;
71
72
  }
73
+ if (a === '--format') {
74
+ const v = (argv[++i] || '').toLowerCase();
75
+ if (v !== 'text' && v !== 'json' && v !== 'sarif') fail('--format must be text, json, or sarif', 64);
76
+ opts.format = v;
77
+ continue;
78
+ }
72
79
  fail(`unknown option: ${a} (try --help)`, 64);
73
80
  }
74
81
  return opts;
@@ -84,7 +91,8 @@ function main() {
84
91
  fail(`not a directory: ${opts.dir}`, 64);
85
92
  }
86
93
 
87
- const scan = spawnSync('bash', [SCAN], {
94
+ const scanArgs = opts.format === 'text' ? [SCAN] : [SCAN, '--format', opts.format];
95
+ const scan = spawnSync('bash', scanArgs, {
88
96
  cwd: opts.dir,
89
97
  encoding: 'utf8',
90
98
  maxBuffer: 32 * 1024 * 1024,
@@ -97,6 +105,10 @@ function main() {
97
105
  const scanOut = scan.stdout || '';
98
106
  process.stdout.write(scanOut);
99
107
 
108
+ if (opts.format !== 'text') {
109
+ process.exit(scan.status === 0 ? 0 : (scan.status || 0));
110
+ }
111
+
100
112
  const verdict = spawnSync('bash', [VERDICT], { input: scanOut, encoding: 'utf8' });
101
113
  if (verdict.error) fail(`failed to compute the verdict: ${verdict.error.message}`, 70);
102
114
  const summary = verdict.stdout || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-precheck",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill with native Claude Code, Cursor, and Codex plugins, plus an npx CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Berkay Turk (https://github.com/berkayturk)",
@@ -4,7 +4,7 @@ description: Read-only pre-submission check for an iOS app before App Store revi
4
4
  license: MIT
5
5
  metadata:
6
6
  author: Berkay Turk
7
- version: 1.9.0
7
+ version: 1.11.0
8
8
  allowed-tools: Bash Read Grep Glob WebFetch
9
9
  ---
10
10
 
@@ -11,6 +11,8 @@ whole file to run the skill.
11
11
  - [Phase 4: Pierre deep review (28 checks)](#phase-4-pierre-deep-review-28-semantic-checks)
12
12
  - [Auto-detection rules](#auto-detection-rules)
13
13
  - [Verdict thresholds](#verdict-thresholds)
14
+ - [SARIF output](#sarif-output---format-sarif)
15
+ - [Real App Store outcomes](#real-app-store-outcomes-corpusoutcomes)
14
16
  - [Pre-submit manual checklist](#pre-submit-manual-checklist)
15
17
 
16
18
  ---
@@ -221,6 +223,29 @@ blocks, but it can be the fifth WARN that tips GREEN into YELLOW.
221
223
 
222
224
  ---
223
225
 
226
+ ## SARIF output (`--format sarif`)
227
+
228
+ `scan.sh --format sarif` emits a SARIF 2.1.0 log built from the same structured findings as
229
+ `--format json` (pure `jq`, no new dependency). `results[]` contains the non-suppressed FAIL and
230
+ WARN findings (FAIL → `error`, WARN → `warning`); PASS and suppressed findings are excluded. Findings
231
+ that carry a `file`/`line` become SARIF `physicalLocation`s so GitHub can anchor PR annotations. Only
232
+ the deterministic scan findings are included — the agent-mode Pierre deep-review findings are not
233
+ (SARIF is a deterministic CI artifact). The GitHub Action uploads this via `upload-sarif` and/or
234
+ emits inline `::error`/`::warning` annotations, both opt-in.
235
+
236
+ ---
237
+
238
+ ### Real App Store outcomes (`corpus/outcomes/`)
239
+
240
+ Beyond the synthetic and real-panel corpora, the tool tracks real Apple review outcomes (approved /
241
+ rejected + cited guideline) in a committed, human-reviewed ledger (`corpus/outcomes/ledger.json`),
242
+ summarized in `docs/scorecard.md` by `scripts/scorecard-outcomes.sh`. It is honesty-floored: no rate
243
+ is computed below 10 records (raw tally only), and a permanent survivorship-bias caveat applies once
244
+ a rate is shown. It is a reporting layer only — it never influences the GREEN/YELLOW/RED verdict or
245
+ which rules fire. Contribute an outcome via the "App Store outcome" issue template.
246
+
247
+ ---
248
+
224
249
  ## Pre-submit manual checklist
225
250
 
226
251
  Things the scanner cannot verify; confirm by hand before you submit:
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # sarif.sh — SARIF 2.1.0 output layer for scan.sh. Sourced; pure jq over the
3
+ # findings buffer. No side effects, no output on load. bash 3.2 compatible.
4
+
5
+ : "${PRECHECK_VERSION:=dev}"
6
+
7
+ # render_sarif -> prints a SARIF 2.1.0 log built from the FINDINGS_TMP buffer.
8
+ # results[] = non-suppressed FAIL/WARN only (PASS + suppressed excluded);
9
+ # level: FAIL->error, WARN->warning. rules[] = distinct non-empty ruleIds present.
10
+ render_sarif() {
11
+ local buf="${FINDINGS_TMP:-/dev/null}"
12
+ local uri="https://github.com/berkayturk/appstore-precheck"
13
+ local help="https://github.com/berkayturk/appstore-precheck/blob/main/skills/appstore-precheck/references/methodology.md"
14
+ if [[ ! -s "$buf" ]]; then
15
+ jq -nc --arg v "$PRECHECK_VERSION" --arg u "$uri" '
16
+ {"$schema":"https://json.schemastore.org/sarif-2.1.0.json", "version":"2.1.0",
17
+ "runs":[{"tool":{"driver":{"name":"appstore-precheck","version":$v,"informationUri":$u,"rules":[]}},"results":[]}]}'
18
+ return 0
19
+ fi
20
+ jq -s --arg v "$PRECHECK_VERSION" --arg u "$uri" --arg help "$help" '
21
+ (map(select(.suppressed==false and (.severity=="FAIL" or .severity=="WARN")))) as $issues
22
+ | ($issues
23
+ | map({id:.rule_id, text:.guideline})
24
+ | map(select(.id != "" and .id != null))
25
+ | unique_by(.id)
26
+ | map({id:.id, name:.id, shortDescription:{text:.text}, helpUri:$help})) as $rules
27
+ | ($issues | map(
28
+ ( (if (.rule_id // "") == "" then {} else {ruleId:.rule_id} end)
29
+ + {level:(if .severity=="FAIL" then "error" else "warning" end),
30
+ message:{text:.message}}
31
+ + (if .file != null
32
+ then {locations:[{physicalLocation:(
33
+ {artifactLocation:{uri:.file}}
34
+ + (if .line != null then {region:{startLine:.line}} else {} end))}]}
35
+ else {locations:[]} end) )
36
+ )) as $results
37
+ | {"$schema":"https://json.schemastore.org/sarif-2.1.0.json", "version":"2.1.0",
38
+ "runs":[{"tool":{"driver":{"name":"appstore-precheck","version":$v,"informationUri":$u,"rules":$rules}},"results":$results}]}
39
+ ' "$buf"
40
+ }
@@ -16,6 +16,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/findings.sh"
16
16
  source "$(dirname "${BASH_SOURCE[0]}")/suppress.sh"
17
17
  source "$(dirname "${BASH_SOURCE[0]}")/project-model.sh"
18
18
  source "$(dirname "${BASH_SOURCE[0]}")/image-dims.sh"
19
+ source "$(dirname "${BASH_SOURCE[0]}")/sarif.sh"
19
20
  FINDINGS_TMP="$(mktemp)"; export FINDINGS_TMP
20
21
  trap 'rm -f "$FINDINGS_TMP"' EXIT
21
22
  FORMAT="text"
@@ -28,13 +29,13 @@ PRECHECK_VERSION="$(grep -m1 -E '^[[:space:]]*version:' "$(dirname "${BASH_SOURC
28
29
  while [[ $# -gt 0 ]]; do
29
30
  case "$1" in
30
31
  --format)
31
- if [[ $# -lt 2 ]]; then echo "scan.sh: --format needs a value (text|json)" >&2; exit 64; fi
32
+ if [[ $# -lt 2 ]]; then echo "scan.sh: --format needs a value (text|json|sarif)" >&2; exit 64; fi
32
33
  FORMAT="$2"; shift 2 ;;
33
34
  --format=*) FORMAT="${1#*=}"; shift ;;
34
35
  *) shift ;;
35
36
  esac
36
37
  done
37
- [[ "$FORMAT" == json || "$FORMAT" == text ]] || { echo "scan.sh: --format must be text|json" >&2; exit 64; }
38
+ [[ "$FORMAT" == json || "$FORMAT" == text || "$FORMAT" == sarif ]] || { echo "scan.sh: --format must be text|json|sarif" >&2; exit 64; }
38
39
 
39
40
  CONFIG="${APPSTORE_PRECHECK_CONFIG:-.appstore-precheck.json}"
40
41
  have_jq() { command -v jq >/dev/null 2>&1; }
@@ -202,7 +203,7 @@ SUB_KEY="$(cfg '.disclosureKeys.subscription' 'subscription_disclosure')"
202
203
  TRIAL_KEY="$(cfg '.disclosureKeys.trial' 'subscription_trial_disclosure')"
203
204
  CHECK_FAMILY="$(cfg_bool '.optionalChecks.familyControls')"
204
205
 
205
- if [[ "$FORMAT" == json ]]; then exec 4>&1 1>/dev/null; fi
206
+ if [[ "$FORMAT" != text ]]; then exec 4>&1 1>/dev/null; fi
206
207
 
207
208
  echo "PASS: layout — ios='${IOS_DIR:-?}' metadata='${META_DIR:-?}' xcstrings='${XCSTRINGS:-?}' locales=${#LOCALES[@]}"
208
209
 
@@ -1011,4 +1012,5 @@ echo "---END-OF-SCAN---"
1011
1012
  if [[ "$FORMAT" == text && "${_SUPPRESSED_COUNT:-0}" -gt 0 ]]; then
1012
1013
  printf '(%s finding(s) suppressed via .precheck-ignore)\n' "$_SUPPRESSED_COUNT"
1013
1014
  fi
1014
- if [[ "$FORMAT" == json ]]; then exec 1>&4 4>&-; render_json; fi
1015
+ if [[ "$FORMAT" == json ]]; then exec 1>&4 4>&-; render_json;
1016
+ elif [[ "$FORMAT" == sarif ]]; then exec 1>&4 4>&-; render_sarif; fi