ai-saas-guard 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -50,8 +50,9 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
50
50
  | Local CLI from source | Available for development |
51
51
  | JSON and SARIF output | Available |
52
52
  | Composite GitHub Action | Available |
53
- | Versioned Action tags | `v0.1.2` |
54
- | npm package | `ai-saas-guard@0.1.2` |
53
+ | Versioned Action tags | `v0.1.3` |
54
+ | npm package | `ai-saas-guard@0.1.3` |
55
+ | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
55
56
 
56
57
  ## Quick Start
57
58
 
@@ -146,11 +147,14 @@ AI-generated PRs often combine unrelated work:
146
147
  - review-first checklist
147
148
  - suggested PR split
148
149
  - required tests or manual verification
150
+ - explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
149
151
 
150
152
  ```bash
151
153
  node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
152
154
  ```
153
155
 
156
+ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` instead of silently reporting a clean or empty diff. In GitHub Actions, use `actions/checkout` with `fetch-depth: 0` when you need merge-base comparison against `origin/main`.
157
+
154
158
  ## Commands
155
159
 
156
160
  | Command | Purpose |
@@ -181,7 +185,7 @@ jobs:
181
185
  - uses: actions/checkout@v6.0.2
182
186
  with:
183
187
  fetch-depth: 0
184
- - uses: zr9959/ai-saas-guard@v0.1.2
188
+ - uses: zr9959/ai-saas-guard@v0.1.3
185
189
  with:
186
190
  command: pr-risk
187
191
  root: ${{ github.workspace }}
@@ -192,7 +196,7 @@ jobs:
192
196
  For SARIF upload:
193
197
 
194
198
  ```yaml
195
- - uses: zr9959/ai-saas-guard@v0.1.2
199
+ - uses: zr9959/ai-saas-guard@v0.1.3
196
200
  with:
197
201
  command: scan
198
202
  format: sarif
@@ -202,7 +206,7 @@ For SARIF upload:
202
206
  sarif_file: ai-saas-guard.sarif
203
207
  ```
204
208
 
205
- For maximum reproducibility, replace `v0.1.2` with the full commit SHA from the release notes.
209
+ For maximum reproducibility, replace `v0.1.3` with the full commit SHA from the release notes.
206
210
 
207
211
  ## Ignore File
208
212
 
@@ -276,7 +280,6 @@ Open-source core:
276
280
 
277
281
  Near-term priorities:
278
282
 
279
- - npm trusted publishing and provenance
280
283
  - PR comment summary mode
281
284
  - configurable severity and rule toggles
282
285
  - expanded Supabase RLS fixtures
@@ -301,4 +304,4 @@ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not
301
304
 
302
305
  ## npm Publishing
303
306
 
304
- The package is published as [`ai-saas-guard`](https://www.npmjs.com/package/ai-saas-guard). See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions provenance workflow, the first-publish token history, and the trusted-publisher follow-up.
307
+ The package is published as [`ai-saas-guard`](https://www.npmjs.com/package/ai-saas-guard). See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions Trusted Publisher workflow, provenance notes, and first-publish token history.
@@ -195,6 +195,13 @@ export const RULE_CATALOG = {
195
195
  why: "AI-generated PRs often bury trust-boundary changes inside larger diffs.",
196
196
  stability: "default"
197
197
  },
198
+ "pr-risk.diff-unavailable": {
199
+ ruleId: "pr-risk.diff-unavailable",
200
+ severity: "info",
201
+ title: "Git diff could not be read",
202
+ why: "PR classification can be misleading when the requested base ref or Git history is unavailable.",
203
+ stability: "default"
204
+ },
198
205
  "pr-risk.no-diff": {
199
206
  ruleId: "pr-risk.no-diff",
200
207
  severity: "info",
@@ -14,10 +14,13 @@ const categoryWeights = {
14
14
  "large AI-generated/refactor-like diff": 18
15
15
  };
16
16
  export async function classifyPrRisk(options) {
17
- const diffText = options.diffText ?? (await readGitDiff(options.rootDir, options.base));
17
+ const diffResult = options.diffText === undefined
18
+ ? await readGitDiff(options.rootDir, options.base)
19
+ : { diffText: options.diffText, diagnostics: [] };
20
+ const { diffText } = diffResult;
18
21
  const files = parseDiffFiles(diffText);
19
22
  const categories = new Set();
20
- const findings = [];
23
+ const findings = [...diffResult.diagnostics];
21
24
  for (const file of files) {
22
25
  for (const category of file.categories) {
23
26
  categories.add(category);
@@ -44,7 +47,7 @@ export async function classifyPrRisk(options) {
44
47
  suggestedFix: "Split unrelated UI/refactor work away from trust-boundary changes and add focused tests before merge."
45
48
  }));
46
49
  }
47
- if (diffText.trim().length === 0) {
50
+ if (diffText.trim().length === 0 && diffResult.diagnostics.length === 0) {
48
51
  findings.push(finding({
49
52
  ruleId: "pr-risk.no-diff",
50
53
  title: "No git diff found",
@@ -68,13 +71,17 @@ async function readGitDiff(rootDir, base) {
68
71
  if (base) {
69
72
  try {
70
73
  const { stdout } = await execFileAsync("git", ["diff", `${base}...HEAD`], { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
71
- return stdout;
74
+ return { diffText: stdout, diagnostics: [] };
72
75
  }
73
- catch {
74
- return "";
76
+ catch (error) {
77
+ return {
78
+ diffText: "",
79
+ diagnostics: [buildGitDiffFailureFinding(rootDir, ["git", "diff", `${base}...HEAD`], error, base)]
80
+ };
75
81
  }
76
82
  }
77
83
  const parts = [];
84
+ const failures = [];
78
85
  for (const args of [
79
86
  ["diff", "--cached"],
80
87
  ["diff"]
@@ -83,11 +90,62 @@ async function readGitDiff(rootDir, base) {
83
90
  const { stdout } = await execFileAsync("git", args, { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
84
91
  parts.push(stdout);
85
92
  }
86
- catch {
87
- continue;
93
+ catch (error) {
94
+ failures.push({ args: ["git", ...args], error });
88
95
  }
89
96
  }
90
- return parts.join("\n");
97
+ if (parts.length === 0 && failures.length > 0) {
98
+ return {
99
+ diffText: "",
100
+ diagnostics: [buildGitDiffFailureFinding(rootDir, failures[0].args, failures[0].error)]
101
+ };
102
+ }
103
+ return { diffText: parts.join("\n"), diagnostics: [] };
104
+ }
105
+ function buildGitDiffFailureFinding(rootDir, command, error, base) {
106
+ const errorText = redactRootPath(getGitErrorText(error), rootDir);
107
+ const lowerError = errorText.toLowerCase();
108
+ let suggestedVerification = "Run `git status` and confirm the target path is inside a Git repository.";
109
+ let suggestedFix = "Run `pr-risk` from a Git checkout, or pass explicit diff text through the API.";
110
+ if (base) {
111
+ suggestedVerification = `Run \`${buildFetchCommand(base)}\`, then \`git rev-parse --verify ${base}\` to confirm the base ref exists locally.`;
112
+ suggestedFix = "Fetch the branch or pass an existing local base ref, for example `--base origin/main`.";
113
+ }
114
+ if (base && (lowerError.includes("no merge base") || lowerError.includes("shallow"))) {
115
+ suggestedVerification = "Run `git rev-parse --is-shallow-repository` and confirm CI checks out full history before `pr-risk`.";
116
+ suggestedFix = "Use `fetch-depth: 0` in `actions/checkout`, or run `git fetch --unshallow` before invoking `pr-risk`.";
117
+ }
118
+ return finding({
119
+ ruleId: "pr-risk.diff-unavailable",
120
+ title: base ? `Could not read git diff for base ${base}` : "Could not read git diff",
121
+ severity: "info",
122
+ evidence: [
123
+ {
124
+ file: ".",
125
+ match: command.join(" "),
126
+ snippet: errorText.slice(0, 500)
127
+ }
128
+ ],
129
+ why: "PR risk classification needs a readable git diff, but the git command failed for the target repository.",
130
+ suggestedVerification,
131
+ suggestedFix
132
+ });
133
+ }
134
+ function buildFetchCommand(base) {
135
+ const remoteRef = /^([^/\s]+)\/(.+)$/.exec(base);
136
+ if (remoteRef)
137
+ return `git fetch ${remoteRef[1]} ${remoteRef[2]}`;
138
+ return `git fetch origin ${base}`;
139
+ }
140
+ function redactRootPath(text, rootDir) {
141
+ return rootDir ? text.replaceAll(rootDir, ".") : text;
142
+ }
143
+ function getGitErrorText(error) {
144
+ const candidate = error;
145
+ return [candidate.stderr, candidate.stdout, candidate.message]
146
+ .filter((value) => Boolean(value?.trim()))
147
+ .join("\n")
148
+ .trim() || "git diff exited with a non-zero status.";
91
149
  }
92
150
  function parseDiffFiles(diffText) {
93
151
  const files = [];
@@ -5,27 +5,25 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.1.2`
8
+ - Current version: `0.1.3`
9
9
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
10
10
  - First npm-published version: `0.1.1`
11
- - GitHub Release: `v0.1.2`
11
+ - GitHub Release: `v0.1.3`
12
12
  - Publish workflow: `.github/workflows/npm-publish.yml`
13
+ - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
14
+ - Long-lived npm publish token: not required
13
15
 
14
16
  ## Preferred Path
15
17
 
16
- Use GitHub Actions with npm provenance:
18
+ Use GitHub Actions with npm Trusted Publisher/OIDC:
17
19
 
18
- 1. Create and review a release tag such as `v0.1.2`.
19
- 2. Run the `Publish npm` workflow manually with `ref` set to that tag.
20
- 3. Configure npm Trusted Publisher for future releases:
21
- - Provider: GitHub Actions
22
- - Organization or user: `zr9959`
23
- - Repository: `ai-saas-guard`
24
- - Workflow filename: `npm-publish.yml`
25
- - Allowed action: `npm publish`
26
- 4. Once trusted publishing is verified, remove or rotate any long-lived npm publish token.
20
+ 1. Create and review a release tag such as `v0.1.3`.
21
+ 2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
22
+ 3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
23
+ 4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
24
+ 5. Keep npm package publishing access set to require 2FA and disallow traditional tokens. Trusted publishers continue to work because they use OIDC instead of npm auth tokens.
27
25
 
28
- The first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured. The workflow sets `id-token: write`, uses Node 24, and runs `npm publish --provenance --access public`, so it is ready for npm Trusted Publisher OIDC publishing.
26
+ The first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured. That temporary automation token and the GitHub `NPM_TOKEN` secret were removed after the Trusted Publisher migration.
29
27
 
30
28
  ## Release Gate
31
29
 
@@ -45,9 +45,11 @@ Implemented surfaces:
45
45
  - MCP config side-effect and secret-bearing risk inventory
46
46
  - Next/Vercel deploy and runtime footguns
47
47
  - PR diff risk triage for auth, billing, RLS, env, tests removed, and large mixed diffs
48
+ - PR diff diagnostics when a base ref or shallow checkout prevents comparison
48
49
  - JSON output
49
50
  - SARIF output
50
51
  - composite GitHub Action wrapper
52
+ - npm publishing through GitHub Actions Trusted Publisher/OIDC
51
53
 
52
54
  Existing commands:
53
55
 
@@ -111,6 +113,14 @@ CI:
111
113
  - Uses `permissions: contents: read`
112
114
  - Latest verified run after setup succeeded
113
115
 
116
+ Publishing:
117
+
118
+ - npm package: `ai-saas-guard`
119
+ - Current release line: `v0.1.3`
120
+ - Publish workflow: `.github/workflows/npm-publish.yml`
121
+ - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
122
+ - Long-lived npm publish tokens should not be required.
123
+
114
124
  ## Repository Boundaries
115
125
 
116
126
  Allowed in this public repository:
@@ -133,14 +143,13 @@ Not allowed:
133
143
 
134
144
  Recommended order:
135
145
 
136
- 1. Prepare npm publishing plan with trusted publishing/provenance.
137
- 2. Add GitHub Action release packaging and example workflow.
138
- 3. Add PR comment summary mode.
139
- 4. Add configurable severity and rule toggles.
140
- 5. Expand Supabase RLS fixtures and ownership patterns.
141
- 6. Write Stripe webhook replay cookbook.
142
- 7. Add SARIF upload workflow example.
143
- 8. Improve false-positive suppression and rule stability labels.
146
+ 1. Add PR comment summary mode.
147
+ 2. Add configurable severity and rule toggles.
148
+ 3. Expand Supabase RLS fixtures and ownership patterns.
149
+ 4. Write Stripe webhook replay cookbook.
150
+ 5. Add SARIF upload workflow example.
151
+ 6. Improve false-positive suppression and rule stability labels.
152
+ 7. Add a GitHub App design note for the potential hosted layer.
144
153
 
145
154
  For every feature, keep the scanner evidence-first:
146
155
 
package/docs/rules.md CHANGED
@@ -61,6 +61,7 @@ Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so S
61
61
  | Rule ID | Severity | Why it exists |
62
62
  | --- | --- | --- |
63
63
  | `pr-risk.sensitive-surface` | medium/high | Highlights files reviewers should inspect before cosmetic or refactor files. |
64
+ | `pr-risk.diff-unavailable` | info | Explains when the requested base ref or Git history prevents PR diff classification. |
64
65
  | `pr-risk.no-diff` | info | Explains that PR classification needs a diff. |
65
66
 
66
67
  ## Adding Rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",