skilldrop-cli 0.16.31 → 0.16.32

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.
Files changed (37) hide show
  1. package/bin/skilldrop.js +133 -18
  2. package/contracts/skill.schema.json +3 -3
  3. package/package.json +1 -1
  4. package/packs/converters/skills/mermaid-render/manifest.json +11 -1
  5. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +2 -2
  6. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +2 -2
  7. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +52 -34
  8. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +33 -0
  9. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/ci.yml +39 -0
  10. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/integration.yml +20 -0
  11. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/PR.md +39 -0
  12. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/go.mod +17 -0
  13. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/notify/client.go +35 -0
  14. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client.go +66 -0
  15. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_integration_test.go +38 -0
  16. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_test.go +53 -0
  17. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/settle.go +30 -0
  18. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/pr.diff +42 -0
  19. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/vendor-notes/httpkit-CHANGELOG.md +43 -0
  20. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.github/workflows/ci.yml +67 -0
  21. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.lintwright.json +7 -0
  22. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.nvmrc +1 -0
  23. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/PR.md +110 -0
  24. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/package.json +25 -0
  25. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/pr.diff +146 -0
  26. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/due-date.ts +16 -0
  27. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/totals.ts +11 -0
  28. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/due-date.test.ts +17 -0
  29. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/totals.test.ts +12 -0
  30. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +23 -14
  31. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +17 -5
  32. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +36 -1
  33. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +22 -4
  34. package/packs/dev-team/skills/sonar-review/manifest.json +12 -0
  35. package/packs/infra-as-code/skills/terraform-module/manifest.json +11 -0
  36. package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +8 -1
  37. package/packs/solution-architect/skills/figma-diagrams/manifest.json +12 -1
@@ -0,0 +1,146 @@
1
+ diff --git a/package.json b/package.json
2
+ index 1c2e8f0..5ad91b3 100644
3
+ --- a/package.json
4
+ +++ b/package.json
5
+ @@ -17,11 +17,11 @@
6
+ "devDependencies": {
7
+ - "bundlecrate": "6.0.1",
8
+ - "lintwright": "4.2.0",
9
+ - "prettyquill": "3.1.2",
10
+ - "tsforge-types": "1.4.0",
11
+ - "vellum-test": "2.7.3"
12
+ + "bundlecrate": "6.1.0",
13
+ + "lintwright": "4.3.1",
14
+ + "prettyquill": "3.1.4",
15
+ + "tsforge-types": "1.4.2",
16
+ + "vellum-test": "2.8.0"
17
+ }
18
+ }
19
+ diff --git a/package-lock.json b/package-lock.json
20
+ index 9e04a1d..c3b7f52 100644
21
+ --- a/package-lock.json
22
+ +++ b/package-lock.json
23
+ @@ -12,11 +12,11 @@
24
+ "devDependencies": {
25
+ - "bundlecrate": "6.0.1",
26
+ - "lintwright": "4.2.0",
27
+ - "prettyquill": "3.1.2",
28
+ - "tsforge-types": "1.4.0",
29
+ - "vellum-test": "2.7.3"
30
+ + "bundlecrate": "6.1.0",
31
+ + "lintwright": "4.3.1",
32
+ + "prettyquill": "3.1.4",
33
+ + "tsforge-types": "1.4.2",
34
+ + "vellum-test": "2.8.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ }
39
+ },
40
+ + "node_modules/@bundlecrate/resolver-core": {
41
+ + "version": "1.0.0",
42
+ + "resolved": "https://registry.npmjs.org/@bundlecrate/resolver-core/-/resolver-core-1.0.0.tgz",
43
+ + "integrity": "sha512-Xk3bP9vQm2LrT7wZ1sNc4HfJ8eYd0GaU6oKiE5rBtWqV2nM7xL1pR9sT3vW8cZ4yH6jF0dK2gA5eN1bQ7uI3oP==",
44
+ + "dev": true,
45
+ + "license": "MIT",
46
+ + "engines": {
47
+ + "node": ">=18.18"
48
+ + }
49
+ + },
50
+ "node_modules/bundlecrate": {
51
+ - "version": "6.0.1",
52
+ - "resolved": "https://registry.npmjs.org/bundlecrate/-/bundlecrate-6.0.1.tgz",
53
+ - "integrity": "sha512-Lm7nQ2vKp9cRz4Wt1bXe8JdH3yGa0fUu6oNiE5rCtYqW2sM7xP1kR9tT3vV8bZ4yJ6hF0dL2gA5eN1cQ7uO3iP==",
54
+ + "version": "6.1.0",
55
+ + "resolved": "https://registry.npmjs.org/bundlecrate/-/bundlecrate-6.1.0.tgz",
56
+ + "integrity": "sha512-Pq4rT8wXn1cLz6Vs2mBe9KdJ7hYf3aGu0oNiE5rDtUqX2nL7wM1pS9vT4bW8cZ3yK6jG0eH2fA5dN1aQ7iU3oR==",
57
+ "dev": true,
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ - "bundlecrate-legacy-resolver": "^2.3.0",
61
+ + "@bundlecrate/resolver-core": "^1.0.0",
62
+ "picolabel": "^0.9.0"
63
+ },
64
+ "bin": {
65
+ "bundlecrate": "bin/bundlecrate.js"
66
+ },
67
+ "engines": {
68
+ - "node": ">=16"
69
+ + "node": ">=18.18"
70
+ }
71
+ },
72
+ - "node_modules/bundlecrate-legacy-resolver": {
73
+ - "version": "2.3.4",
74
+ - "resolved": "https://registry.npmjs.org/bundlecrate-legacy-resolver/-/bundlecrate-legacy-resolver-2.3.4.tgz",
75
+ - "integrity": "sha512-Ab2cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0eF2gH4iJ6kL8mN0oP2qR4sT6uV8wX0yZ2aB4cD6eF8==",
76
+ - "dev": true,
77
+ - "license": "MIT"
78
+ - },
79
+ "node_modules/lintwright": {
80
+ - "version": "4.2.0",
81
+ - "resolved": "https://registry.npmjs.org/lintwright/-/lintwright-4.2.0.tgz",
82
+ - "integrity": "sha512-Hn5mR2xLq8cTz3Wv1bYe7KdF4hJa9gUu0oNiE6rBtWpX3sM2wL1kQ8vT5cV9bZ7yG4hF0dJ2eA6nN1aR3uI7oS==",
83
+ + "version": "4.3.1",
84
+ + "resolved": "https://registry.npmjs.org/lintwright/-/lintwright-4.3.1.tgz",
85
+ + "integrity": "sha512-Ty8kW3vNp2cQz5Ls1mXe6RdJ9hGa4fUu7oNiE0rBtVqY2sK3wM1pL8vS5cT9bZ6yH4jF0eG2dA7nN1bR3iU5oQ==",
86
+ "dev": true,
87
+ "license": "MIT",
88
+ "dependencies": {
89
+ - "picolabel": "0.9.1"
90
+ + "picolabel": "0.9.2"
91
+ },
92
+ "bin": {
93
+ "lintwright": "bin/lintwright.js"
94
+ }
95
+ },
96
+ "node_modules/picolabel": {
97
+ - "version": "0.9.1",
98
+ - "resolved": "https://registry.npmjs.org/picolabel/-/picolabel-0.9.1.tgz",
99
+ - "integrity": "sha512-Cd3eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5aB7cD9eF1gH3iJ5kL7mN9oP1qR3sT5uV7wX9yZ1aB3cD5eF7gH==",
100
+ + "version": "0.9.2",
101
+ + "resolved": "https://registry.npmjs.org/picolabel/-/picolabel-0.9.2.tgz",
102
+ + "integrity": "sha512-Ef4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ==",
103
+ "dev": true,
104
+ "license": "MIT"
105
+ },
106
+ "node_modules/prettyquill": {
107
+ - "version": "3.1.2",
108
+ - "resolved": "https://registry.npmjs.org/prettyquill/-/prettyquill-3.1.2.tgz",
109
+ - "integrity": "sha512-Gh6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ0kL==",
110
+ + "version": "3.1.4",
111
+ + "resolved": "https://registry.npmjs.org/prettyquill/-/prettyquill-3.1.4.tgz",
112
+ + "integrity": "sha512-Ij7kL9mN1oP3qR5sT7uV9wX1yZ3aB5cD7eF9gH1iJ3kL5mN7oP9qR1sT3uV5wX7yZ9aB1cD3eF5gH7iJ9kL1mN==",
113
+ "dev": true,
114
+ "license": "MIT",
115
+ "bin": {
116
+ "prettyquill": "bin/prettyquill.js"
117
+ }
118
+ },
119
+ "node_modules/tsforge-types": {
120
+ - "version": "1.4.0",
121
+ - "resolved": "https://registry.npmjs.org/tsforge-types/-/tsforge-types-1.4.0.tgz",
122
+ - "integrity": "sha512-Kl8mN0oP2qR4sT6uV8wX0yZ2aB4cD6eF8gH0iJ2kL4mN6oP8qR0sT2uV4wX6yZ8aB0cD2eF4gH6iJ8kL0mN2oP==",
123
+ + "version": "1.4.2",
124
+ + "resolved": "https://registry.npmjs.org/tsforge-types/-/tsforge-types-1.4.2.tgz",
125
+ + "integrity": "sha512-Mn9oP1qR3sT5uV7wX9yZ1aB3cD5eF7gH9iJ1kL3mN5oP7qR9sT1uV3wX5yZ7aB9cD1eF3gH5iJ7kL9mN1oP3qR==",
126
+ "dev": true,
127
+ "license": "MIT"
128
+ },
129
+ "node_modules/vellum-test": {
130
+ - "version": "2.7.3",
131
+ - "resolved": "https://registry.npmjs.org/vellum-test/-/vellum-test-2.7.3.tgz",
132
+ - "integrity": "sha512-Op0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0eF2gH4iJ6kL8mN0oP2qR4sT==",
133
+ + "version": "2.8.0",
134
+ + "resolved": "https://registry.npmjs.org/vellum-test/-/vellum-test-2.8.0.tgz",
135
+ + "integrity": "sha512-Qr1sT3uV5wX7yZ9aB1cD3eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5aB7cD9eF1gH3iJ5kL7mN9oP1qR3sT5uV==",
136
+ "dev": true,
137
+ "license": "MIT",
138
+ "bin": {
139
+ "vellum": "bin/vellum.js"
140
+ },
141
+ "engines": {
142
+ "node": ">=18"
143
+ }
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,16 @@
1
+ import type { Brand } from "tsforge-types";
2
+
3
+ export type IsoDate = Brand<string, "IsoDate">;
4
+
5
+ const DAY_MS = 24 * 60 * 60 * 1000;
6
+
7
+ export function isOverdue(inv: { dueAt: string }): boolean {
8
+ return Date.now() > Date.parse(inv.dueAt);
9
+ }
10
+
11
+ export function formatDue(inv: { dueAt: string }): string {
12
+ const days = Math.round((Date.parse(inv.dueAt) - Date.now()) / DAY_MS);
13
+ if (days < 0) return `overdue by ${-days} days`;
14
+ if (days === 0) return "due today";
15
+ return `due in ${days} days`;
16
+ }
@@ -0,0 +1,11 @@
1
+ import type { DeepReadonly } from "tsforge-types";
2
+
3
+ export type Line = DeepReadonly<{ qty: number; unitMinor: number }>;
4
+
5
+ export function lineTotal(l: Line): number {
6
+ return l.qty * l.unitMinor;
7
+ }
8
+
9
+ export function invoiceTotal(lines: readonly Line[]): number {
10
+ return lines.reduce((sum, l) => sum + lineTotal(l), 0);
11
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect, vt } from "vellum-test";
2
+ import { isOverdue, formatDue } from "../src/due-date";
3
+
4
+ describe("due dates", () => {
5
+ it("flags invoices past their due date", () => {
6
+ vt.fakeTimers({ now: new Date("2026-03-15T09:00:00Z") });
7
+ expect(isOverdue({ dueAt: "2026-03-14T23:59:59Z" })).toBe(true);
8
+ expect(isOverdue({ dueAt: "2026-03-16T00:00:00Z" })).toBe(false);
9
+ vt.restoreTimers();
10
+ });
11
+
12
+ it("formats relative due text", () => {
13
+ vt.fakeTimers({ now: new Date("2026-03-15T09:00:00Z") });
14
+ expect(formatDue({ dueAt: "2026-03-18T09:00:00Z" })).toBe("due in 3 days");
15
+ vt.restoreTimers();
16
+ });
17
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from "vellum-test";
2
+ import { lineTotal, invoiceTotal } from "../src/totals";
3
+
4
+ describe("totals", () => {
5
+ it("multiplies quantity by unit price in minor units", () => {
6
+ expect(lineTotal({ qty: 3, unitMinor: 1250 })).toBe(3750);
7
+ });
8
+
9
+ it("sums lines", () => {
10
+ expect(invoiceTotal([{ qty: 1, unitMinor: 100 }, { qty: 2, unitMinor: 50 }])).toBe(200);
11
+ });
12
+ });
@@ -67,25 +67,34 @@ No install scripts, sources, licences or pins appear. The floor says only that t
67
67
 
68
68
  ## Review (what the agent hands back)
69
69
 
70
- **MERGE.** A two-release patch bump whose only behaviour change, the new 10 MB line limit in 2.2.2, raises an error the import code already handles, and CI runs that code.
70
+ **MERGE.** Two patch releases; the only behaviour change, 2.2.2's new 10 MB line limit, raises an error the import code already handles, and the PR's tests run that code.
71
71
 
72
- 🟨 Notes
72
+ - Both releases read. 2.2.1 fixes quoted newlines splitting rows, covered by `test_quoted_newline_stays_one_row` (`tests/test_ingest.py:9`). 2.2.2 is a security fix: lines over 10 MB raise `RowTooLongError`, a subclass of `ParseError`.
73
+ - `git grep -n "tidyparse" -- '*.py'` finds one call, `app/ingest.py:14`, inside `except tidyparse.ParseError` (`app/ingest.py:15`), so the new error becomes `StatementRejected` like any malformed file.
74
+ - `ci.yml` runs `pytest -q` on the PR and `tests/test_ingest.py:3` imports `import_statement`, so the changed code ran.
73
75
 
74
- - Both releases read. 2.2.1 fixes quoted newlines splitting rows, which `test_quoted_newline_stays_one_row` (`tests/test_ingest.py:9`) covers. 2.2.2 is a security fix: lines over 10 MB now raise `RowTooLongError`, a subclass of `ParseError`.
75
- - Search: `git grep -n "tidyparse" -- '*.py'` finds one call, `app/ingest.py:14`, inside `except tidyparse.ParseError` (`app/ingest.py:15`). The new error is caught there and becomes `StatementRejected`, the same path as any malformed file.
76
- - CI: `ci.yml` runs `pytest -q` on the PR, and `tests/test_ingest.py:3` imports `import_statement`, so the changed code ran. No test feeds a 10 MB line; that is the library's limit, not a change in this repo's behaviour.
77
- - Beyond the title: `charset-sniff` 1.4.0 → 1.4.1, a transitive patch, listed and not read.
78
- - No new packages, install scripts, registry changes or pins.
76
+ Also in the lockfile: `charset-sniff` 1.4.0 → 1.4.1 (transitive patch). No new packages, install scripts, sources, runtime changes or pins.
79
77
 
80
- | Package | Old → New | Class | Direct? |
81
- |---|---|---|---|
82
- | tidyparse | 2.2.0 → 2.2.2 | patch | direct |
83
- | charset-sniff | 1.4.0 → 1.4.1 | patch | transitive |
78
+ Rollback: `git revert` the merge commit, or `poetry add tidyparse@2.2.0` and commit `pyproject.toml` and `poetry.lock`; that reopens the 2.2.2 memory issue, so treat it as a stopgap.
84
79
 
85
- Rollback: `git revert` the merge commit, or `poetry add tidyparse@2.2.0` and commit `pyproject.toml` and `poetry.lock`. Nothing persists to undo, but rolling back reopens the 2.2.2 memory issue, so pin back to 2.2.0 only as a stopgap.
86
-
87
- To confirm: nothing.
80
+ About 150 words. No package table: two packages fit in a line. No "To confirm": nothing is open.
88
81
 
89
82
  ## Why this is MERGE and not MERGE AFTER CHECKS
90
83
 
91
84
  Every check in the verdict rule is closed with evidence in the reply: both releases read, the one changed behaviour searched and found handled, the calling code run by a PR test, no pins, no install-time code, no new source. Remove any one of those (no repo to search, a release with no notes, a test that does not import `app/ingest.py`) and the verdict drops to MERGE AFTER CHECKS with that item as the check.
85
+
86
+ ## The same shape when it blocks
87
+
88
+ Suppose instead the PR moved tidyparse to 3.0.0 and the notes (invented for this illustration) said 3.0.0 requires Python 3.11 and `parse_rows()` now returns a `RowSet` instead of a list. The reply keeps the order and stays short:
89
+
90
+ > **DO NOT MERGE.** 3.0.0 needs Python 3.11 and the production image runs 3.10, and the import code indexes a result that is no longer a list.
91
+ >
92
+ > 🟥 1. `Dockerfile:1` builds on `python:3.10-slim`; CI runs 3.12 (`ci.yml:12`), so the PR passed and the image build fails at install. Move the image to 3.11 in this PR.
93
+ > 🟥 2. `app/ingest.py:17` does `rows[0]` on what is now a `RowSet`; no test reads the first row, so CI missed it. Use `rows.first()`.
94
+ >
95
+ > Also in the lockfile: `charset-sniff` 1.4.0 → 1.4.1.
96
+ > Rollback: `git revert` the merge commit and rebuild the image.
97
+ >
98
+ > I can make both changes on this branch.
99
+
100
+ The lead names both blockers; each blocker says what breaks, where, why CI missed it and the fix; nothing is in the reply that does not change the decision.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dependency-upgrade-review",
3
- "version": "0.1.0",
4
- "description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks that each SHA pin is the commit its release tag points to, says whether CI ran the changed code, finds the repo's uses of changed APIs and writes the rollback, ending in MERGE, MERGE AFTER CHECKS or DO NOT MERGE. Use when the user says \"review this Dependabot PR\", \"is this Renovate bump safe to merge\", \"can I merge this dependency upgrade\", \"check this lockfile diff\", or \"is this action SHA pin right\".",
3
+ "version": "0.2.0",
4
+ "description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks each SHA pin against its release tag and against the PR's other pins, checks raised runtime floors against the images and runners that install it, says whether CI ran the changed code, traces what each caller of a changed API now does and writes the rollback. The reply leads with MERGE, MERGE AFTER CHECKS or DO NOT MERGE and the reasons that decide it. Use when the user says \"review this Dependabot PR\", \"is this Renovate bump safe to merge\", \"can I merge this dependency upgrade\", \"check this lockfile diff\", or \"is this action SHA pin right\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
7
7
  "npm": [],
@@ -38,10 +38,22 @@
38
38
  "github-actions"
39
39
  ],
40
40
  "permissions": {
41
- "network": [],
42
- "commands": [],
41
+ "network": [
42
+ "github.com",
43
+ "api.github.com",
44
+ "registry.npmjs.org",
45
+ "pypi.org",
46
+ "proxy.golang.org"
47
+ ],
48
+ "commands": [
49
+ "git",
50
+ "gh",
51
+ "npm",
52
+ "pip",
53
+ "go"
54
+ ],
43
55
  "files": "named-paths",
44
- "notes": "Reads only the diff path it is given, or stdin; writes only the -o path when given. No network: SHA verification and changelog reading are printed as commands for the reviewer to run."
56
+ "notes": "The script reads only the diff path it is given, or stdin, writes only the -o path when given, and makes no network calls. The instructions read the PR with gh or git, list a package's releases with npm, pip, go or gh, and verify a SHA pin with git ls-remote against github.com; gh reaches api.github.com and the package managers their registries; with no network those become commands in the reply for the reviewer to run."
45
57
  },
46
58
  "model": {
47
59
  "tier": "standard",
@@ -1,6 +1,6 @@
1
1
  # dependency-upgrade-review reference
2
2
 
3
- Long-form material for the steps in `SKILL.md`: what each ecosystem's diff shows, where the risky signals live, how to list the releases between two versions, how to verify a pin, and how to roll back.
3
+ Long-form material for the steps in `SKILL.md`: what each ecosystem's diff shows, where the risky signals live, how to list the releases between two versions, how to trace a behaviour change to its callers, where runtime floors bite, how to verify a pin, and how to roll back.
4
4
 
5
5
  ## Reading a lockfile diff, by ecosystem
6
6
 
@@ -44,6 +44,36 @@ What to pull from each release, in this order:
44
44
  3. **Deprecations:** still works, removed later. A deprecated call the repo makes is a 🟧, not a 🟥.
45
45
  4. **Licence:** a changed `license` field or a LICENSE file change. Moves between permissive licences (MIT, BSD, Apache-2.0, ISC) are a note. A move to a copyleft licence (GPL, AGPL, MPL at file level, LGPL) or a source-available licence (BUSL, SSPL, Elastic, "Commons Clause") needs a sign-off from whoever owns licence policy before merge.
46
46
 
47
+ ## Tracing a behaviour change to its callers
48
+
49
+ A search hit is where the review starts. Read the calling function and write what it does under the new version.
50
+
51
+ | Change in the notes | What to look for in the caller | What breaks |
52
+ |---|---|---|
53
+ | Returns `None` / `-1` / `false` → raises | `if result is None`, `== -1`, `if not ok` branches; retry loops and fallbacks built on them | The branch never runs; the first failure escapes as an exception and the retry or fallback is dead code |
54
+ | Raises → returns a sentinel | `try` / `except` around the call | The error is swallowed; the code carries on with the sentinel |
55
+ | Exception class renamed or re-parented | `except OldError`, `catch (e instanceof OldError)` | The handler stops catching it |
56
+ | Tuple or dict → object | `a, b = f(...)`, `result["key"]` | Unpacking or key access fails at runtime |
57
+ | Changed default | Every call that does not pass the option | Each one gets the new behaviour silently |
58
+ | Module or function removed | Imports at the top of a file, and imports inside functions | A top-level import fails at startup and in tests; an import inside a function fails only when that function runs |
59
+
60
+ Name the fix in the repo's code, and any migration aid an intermediate release ships (an opt-in flag that turns on the new behaviour early). Moving to that release first lets the code change land and be tested before the major.
61
+
62
+ ## Runtime floors
63
+
64
+ A release that drops a runtime version breaks wherever the old runtime still runs, and CI usually runs a newer one.
65
+
66
+ | Floor raised in | Compare with |
67
+ |---|---|
68
+ | Release notes ("requires Python 3.10"), `python_requires`, `python-versions` in `poetry.lock`, `requires-python` in `uv.lock` | Dockerfile `FROM`, `setup-python` version and matrix, `.python-version`, the Python the lockfile was compiled on |
69
+ | `engines.node` in `package-lock.json` or `package.json` | `.nvmrc`, `setup-node` / toolchain version, Dockerfile `FROM`, `engine-strict` in `.npmrc` |
70
+ | `go` directive in `go.mod`, `toolchain` line | `setup-go` version, Dockerfile builder image |
71
+ | `rust-version` (MSRV) in a crate | `rust-toolchain.toml`, CI toolchain |
72
+ | Wheels or prebuilt binaries only for some versions or platforms | Every image and runner that installs it; elsewhere it builds from source and needs a compiler or toolchain |
73
+ | An action's runtime (`node20` → `node24`) | Self-hosted runner versions; GitHub-hosted runners update themselves |
74
+
75
+ A lockfile generated on a newer runtime than production can resolve versions production cannot install. `pip install --require-hashes` and `npm ci` fail at build time, not at review time.
76
+
47
77
  ## Verifying a pin
48
78
 
49
79
  ```bash
@@ -57,6 +87,7 @@ git ls-remote https://github.com/<owner>/<repo> refs/tags/<tag> 'refs/tags/<tag>
57
87
  - A SHA with no `# vX.Y.Z` comment: list the tags (`git ls-remote --tags https://github.com/<owner>/<repo>`) and find the SHA. No tag points to it means it is not a release.
58
88
  - Tag-only pin (`uses: owner/repo@v3`): the maintainer, or anyone who takes over the account, can point `v3` at new code after review. Pin to the commit the `^{}` line returns and keep the tag as a comment.
59
89
  - Branch pin (`@main`): every run gets whatever was pushed last. A PR that introduces one is DO NOT MERGE.
90
+ - Two SHAs for one tag: when the same `owner/repo` and `# vX.Y.Z` comment appear with different SHAs across workflow files, at most one is the tag's commit. Run the command once and compare both; until then, the PR is DO NOT MERGE. Read the PR's commit list: a bot writes every pin the same way, so a pin that differs from the bot's usually came from a hand-made commit.
60
91
 
61
92
  Stakes come from the job, not the action: look at `permissions:` (`contents: write`, `id-token: write`, `packages: write`), the `secrets.*` it receives, and the `environment:` it runs in.
62
93
 
@@ -70,6 +101,10 @@ Stakes come from the job, not the action: look at `permissions:` (`contents: wri
70
101
  | A build or publish step (bundler, packager, uploader) not in the PR workflow | No |
71
102
  | A source file no test imports | No |
72
103
  | A test that imports the file but never hits the changed behaviour (a default, an edge case the notes name) | Not the part that changed |
104
+ | A test that mocks the dependency with the old contract (a fake that returns a value where the new version raises) | No; it tests the old behaviour |
105
+ | An import inside a function no test calls | No; it never fails at import or collection |
106
+ | An install script, when the PR job installs with `--ignore-scripts` and the release job does not | No; it first runs in the release job, with that job's secrets and permissions |
107
+ | A runtime older than the one CI uses (production image, self-hosted runner) | No |
73
108
 
74
109
  Ways to exercise an unrun path before merge: a `workflow_dispatch` dry run on the PR branch, a release to a test index (TestPyPI, a scratch npm scope), or a test that covers the changed behaviour, added in the same PR.
75
110
 
@@ -161,6 +161,7 @@ def host_of(url):
161
161
  class Report:
162
162
  def __init__(self):
163
163
  self.changes, self.flags, self.files, self.unattributed = [], [], [], {}
164
+ self.sha_pins = {} # (repo, tag) -> {sha: [files]}, from added `uses:` lines
164
165
 
165
166
  def change(self, path, eco, where, name, old, new, note=None):
166
167
  if old == new:
@@ -256,6 +257,12 @@ def merge_blocks(rep, path, eco, hunks, key_fn, field_res, name_fn, reset_re=Non
256
257
  rep.flag("source", path, f"{label}: {f} moved from {oh} to {nh}")
257
258
  elif nh and not o and f != "index" and nh not in DEFAULT_HOSTS:
258
259
  rep.flag("source", path, f"{label}: new package comes from {nh}, not the default registry")
260
+ for f in ("node", "python-versions"):
261
+ orv, nrv = of.get(f, (None,))[0], nf.get(f, (None,))[0]
262
+ if o and nrv and orv != nrv:
263
+ rt = "Node" if f == "node" else "Python"
264
+ rep.flag("runtime", path, f"{label}: requires {rt} {nrv} (was {orv or 'unstated'}); check every "
265
+ f"place it installs and runs (base images, CI versions, runners)")
259
266
  ol, nl = of.get("license", (None,))[0], nf.get("license", (None,))[0]
260
267
  if ol and nl and ol != nl:
261
268
  rep.flag("licence", path, f"{label}: licence changed from {ol} to {nl}")
@@ -317,7 +324,7 @@ def npm_lock_name(key):
317
324
 
318
325
 
319
326
  NPM_FIELDS = {f: re.compile(rf'^\s*"{f}":\s*"?([^",]+)"?,?\s*$') for f in
320
- ("version", "resolved", "license", "hasInstallScript", "dev", "optional", "gypfile")}
327
+ ("version", "resolved", "license", "hasInstallScript", "dev", "optional", "gypfile", "node")}
321
328
 
322
329
 
323
330
  def h_package_lock(rep, path, hunks):
@@ -360,7 +367,8 @@ def h_pnpm_lock(rep, path, hunks):
360
367
 
361
368
  TOML_PKG_FIELDS = {"version": re.compile(r'^version\s*=\s*"([^"]+)"'),
362
369
  "source": re.compile(r'^source\s*=\s*"([^"]+)"'),
363
- "url": re.compile(r'^(?:url|source\s*=\s*\{\s*(?:registry|git|url))\s*=\s*"([^"]+)"')}
370
+ "url": re.compile(r'^(?:url|source\s*=\s*\{\s*(?:registry|git|url))\s*=\s*"([^"]+)"'),
371
+ "python-versions": re.compile(r'^python-versions\s*=\s*"([^"]+)"')}
364
372
 
365
373
 
366
374
  def h_toml_lock(eco):
@@ -527,6 +535,7 @@ def h_workflow(rep, path, hunks):
527
535
  if n and SHA40.fullmatch(n[0]):
528
536
  tag_ = re.match(r"^(v?[\w.\-]+)", n[1]).group(1) if n[1] else None
529
537
  if tag_:
538
+ rep.sha_pins.setdefault((repo, tag_), {}).setdefault(n[0], []).append(path)
530
539
  rep.flag("pin", path, f"{name}@{n[0]} claims {tag_}. UNVERIFIED until the tag resolves to this commit: "
531
540
  f"`git ls-remote https://github.com/{repo} refs/tags/{tag_} 'refs/tags/{tag_}^{{}}'` "
532
541
  f"(the `^{{}}` line is the commit for an annotated tag; it must equal {n[0]})")
@@ -591,9 +600,16 @@ def analyse(files, title):
591
600
  for r in rep.changes:
592
601
  short = r["name"].split("/")[-1].lower()
593
602
  r["in_title"] = r["name"].lower() in t or (len(short) > 3 and re.search(rf"\b{re.escape(short)}\b", t) is not None)
603
+ for (repo, tag_), shas in rep.sha_pins.items():
604
+ if len(shas) > 1:
605
+ where = "; ".join(f"{s} in {', '.join(dict.fromkeys(ps))}" for s, ps in shas.items())
606
+ rep.flag("pin-conflict", next(iter(shas.values()))[0],
607
+ f"{repo} {tag_} is pinned to {len(shas)} different commits ({where}). A tag points to one "
608
+ f"commit, so at most one of these is {tag_}; find which with "
609
+ f"`git ls-remote https://github.com/{repo} refs/tags/{tag_} 'refs/tags/{tag_}^{{}}'`")
594
610
  reasons_stop, reasons_check = [], []
595
611
  for fl in rep.flags:
596
- if fl["kind"] in ("install-script", "source"):
612
+ if fl["kind"] in ("install-script", "source", "pin-conflict"):
597
613
  reasons_stop.append(f"{fl['kind']}: {fl['text']}")
598
614
  elif fl["kind"] == "pin":
599
615
  reasons_check.append("pin: " + fl["text"].split(". ")[0])
@@ -662,9 +678,11 @@ def render_md(rep, floor, stop, check, title):
662
678
  out += ["", f"## Beyond the title ({len(beyond)})", ""]
663
679
  out += [f"- `{r['name']}` {r['kind']}: {r['old'] or '—'} → {r['new'] or '—'} ({r['file']})" for r in beyond] or ["None."]
664
680
  labels = (("install-script", "Install scripts"), ("native", "Native code"), ("source", "Sources and registries"),
665
- ("licence", "Licences"), ("pin", "Pins (verify every SHA; the script cannot)"))
681
+ ("licence", "Licences"), ("runtime", "Runtime requirements"), ("pin", "Pins (verify every SHA; the script cannot)"))
666
682
  for kind, heading in labels:
667
683
  rows = [f for f in rep.flags if f["kind"] == kind]
684
+ if kind == "pin":
685
+ rows = [f for f in rep.flags if f["kind"] == "pin-conflict"] + rows
668
686
  out += ["", f"## {heading} ({len(rows)})", ""]
669
687
  out += [f"- `{f['file']}`: {f['text']}" for f in rows] or ["None seen in the diff."]
670
688
  if rep.unattributed:
@@ -33,5 +33,17 @@
33
33
  "model": {
34
34
  "tier": "standard",
35
35
  "rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalates on very large diffs."
36
+ },
37
+ "permissions": {
38
+ "network": [
39
+ "sonarcloud.io"
40
+ ],
41
+ "commands": [
42
+ "sonar-scanner",
43
+ "git",
44
+ "curl"
45
+ ],
46
+ "files": "none",
47
+ "notes": "No scripts. Its instructions run sonar-scanner and query the Sonar API with curl, using SONAR_TOKEN from the environment, against sonarcloud.io or the self-hosted SonarQube URL the user configures."
36
48
  }
37
49
  }
@@ -37,5 +37,16 @@
37
37
  "model": {
38
38
  "tier": "standard",
39
39
  "rationale": "Structured generation against fixed module conventions and a secure-defaults checklist; the judgment is bounded to choosing defaults and validation rules for a described need."
40
+ },
41
+ "permissions": {
42
+ "network": [
43
+ "registry.terraform.io",
44
+ "releases.hashicorp.com"
45
+ ],
46
+ "commands": [
47
+ "terraform"
48
+ ],
49
+ "files": "none",
50
+ "notes": "No scripts. Its instructions run `terraform fmt`, `terraform init -backend=false` and `terraform validate` on the module it writes. `init` downloads the providers from the Terraform registry; none of the commands contacts a cloud account."
40
51
  }
41
52
  }
@@ -40,7 +40,14 @@
40
40
  "iam",
41
41
  "drift"
42
42
  ],
43
- "permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values."},
43
+ "permissions": {
44
+ "network": [],
45
+ "commands": [
46
+ "terraform"
47
+ ],
48
+ "files": "named-paths",
49
+ "notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values. Its instructions run `terraform show -json` to produce that plan JSON."
50
+ },
44
51
  "model": {
45
52
  "tier": "heavy",
46
53
  "rationale": "Adversarial review of a destructive change: judging which replacements lose data, which access changes widen exposure, and what is out of scope needs careful reasoning; a missed destroy costs far more than the model."
@@ -28,7 +28,18 @@
28
28
  "design",
29
29
  "api"
30
30
  ],
31
- "permissions": {"network": ["api.figma.com"], "commands": [], "files": "named-paths", "notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API."},
31
+ "permissions": {
32
+ "network": [
33
+ "api.figma.com",
34
+ "pypi.org",
35
+ "files.pythonhosted.org"
36
+ ],
37
+ "commands": [
38
+ "pip"
39
+ ],
40
+ "files": "named-paths",
41
+ "notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API. Its instructions install the pinned requirements once with `python3 -m pip install -r requirements.txt`, which reaches PyPI."
42
+ },
32
43
  "model": {
33
44
  "tier": "standard",
34
45
  "rationale": "API calls + spec generation. Bounded transformation."