@silverassist/agents-toolkit 2.5.1 → 2.6.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/README.md +23 -3
- package/bin/cli.js +6 -5
- package/package.json +1 -1
- package/src/index.js +6 -1
- package/templates/agents/AGENTS.codex.md +2 -0
- package/templates/agents/AGENTS.md +9 -5
- package/templates/shared/instructions/tsdoc-standards.instructions.md +182 -0
- package/templates/shared/prompts/README.md +22 -10
- package/templates/shared/prompts/create-github-pr.prompt.md +47 -3
- package/templates/shared/prompts/finalize-github-pr.prompt.md +7 -1
- package/templates/shared/prompts/resolve-github-reviews.prompt.md +323 -0
- package/templates/shared/skills/README.md +21 -3
- package/templates/shared/skills/ai-seo-optimization/SKILL.md +11 -2
- package/templates/shared/skills/core-review/SKILL.md +251 -0
- package/templates/shared/skills/domain-driven-design/SKILL.md +8 -3
- package/templates/shared/skills/github-review-management/SKILL.md +215 -0
- package/templates/shared/skills/nextjs-caching/SKILL.md +31 -8
- package/templates/shared/skills/release-management/SKILL.md +25 -5
- package/templates/shared/skills/testing-patterns/SKILL.md +38 -0
- package/templates/shared/skills/tsdoc-standards/SKILL.md +225 -0
|
@@ -25,9 +25,9 @@ This skill provides guidelines for organizing code following Domain-Driven Desig
|
|
|
25
25
|
|
|
26
26
|
### ❌ DON'T
|
|
27
27
|
|
|
28
|
-
- Create generic folders like
|
|
28
|
+
- Create generic folders like `src/helpers/`, `src/services/`, or `src/utils/` directly under `src/` (a scoped `src/lib/utils/` for minimal cross-domain primitives is allowed)
|
|
29
29
|
- Mix different domain concerns in the same folder
|
|
30
|
-
- Create
|
|
30
|
+
- Create folder hierarchies deeper than 3 levels below `src/` (e.g., `src/components/auth/login-form/` is allowed; `src/components/auth/login-form/subform/` is not)
|
|
31
31
|
- Use abbreviations in folder names
|
|
32
32
|
|
|
33
33
|
## Project Structure
|
|
@@ -228,7 +228,7 @@ Ask these questions to identify domains:
|
|
|
228
228
|
### ❌ Generic Folder Anti-Patterns
|
|
229
229
|
|
|
230
230
|
```
|
|
231
|
-
# ❌ BAD: Generic folders
|
|
231
|
+
# ❌ BAD: Generic folders at src/ root
|
|
232
232
|
src/
|
|
233
233
|
├── components/
|
|
234
234
|
├── helpers/ # What kind of helpers?
|
|
@@ -237,6 +237,11 @@ src/
|
|
|
237
237
|
└── types/ # Types should live with their domain
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
+
### Shared TypeScript Types
|
|
241
|
+
|
|
242
|
+
- If a type is used by **3 or more domains**, place it in `src/lib/types/` — the shared-types exception lives under the allowed `src/lib/` scope, **not** a generic `src/types/` root (which the anti-pattern above forbids). Export it from that folder's own barrel (`src/lib/types/index.ts`) and import it as `@/lib/types`. A centrally shared primitive has **no owning domain**, so do not route it through a domain barrel — that would invert the intended dependency direction (domains → shared, never shared → a domain).
|
|
243
|
+
- Otherwise, **colocate the type with its primary domain** (e.g., `src/lib/payment/types.ts`, `src/components/auth/types.ts`) and re-export it through that domain's barrel when other domains need it.
|
|
244
|
+
|
|
240
245
|
### ✅ Domain-Oriented Structure
|
|
241
246
|
|
|
242
247
|
```
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github-review-management
|
|
3
|
+
description: Fetch, reply to, resolve, and close GitHub pull-request review threads with the gh CLI and GraphQL API. Use when handling Copilot or human PR reviews end-to-end.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Silver Assist — GitHub Review Management
|
|
7
|
+
|
|
8
|
+
Reference knowledge for clearing GitHub pull-request reviews programmatically. This skill
|
|
9
|
+
backs the `/resolve-github-reviews` prompt: it explains the data model, which API does what,
|
|
10
|
+
the exact commands, and the Copilot-specific edge cases. Use it when a PR has review threads
|
|
11
|
+
(Copilot or human) that need to be addressed, replied to, resolved, and verified to `0`.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- A PR has open Copilot or human review comments to clear before merge
|
|
16
|
+
- You need to **resolve** review threads (not just reply) — which REST cannot do
|
|
17
|
+
- Debugging why a reply or resolve call fails (404 on replies, permissions on the mutation)
|
|
18
|
+
- Handling Copilot suppressed / low-confidence notes or `isOutdated` threads
|
|
19
|
+
- Automating the "reply → resolve → assert 0 unresolved" loop across per-commit review rounds
|
|
20
|
+
|
|
21
|
+
## The data model (get this right first)
|
|
22
|
+
|
|
23
|
+
GitHub exposes three distinct things — conflating them is the usual source of bugs:
|
|
24
|
+
|
|
25
|
+
| Concept | What it is | API surface |
|
|
26
|
+
|---------|-----------|-------------|
|
|
27
|
+
| **Review (submission)** | The top-level verdict on a PR: `APPROVED` / `CHANGES_REQUESTED` / `COMMENTED` | `gh pr review`, REST `/pulls/{n}/reviews` |
|
|
28
|
+
| **Review comment** | A single inline comment anchored to a file/line (has a `databaseId`) | REST `/pulls/{n}/comments` |
|
|
29
|
+
| **Review thread** | An inline comment **plus its replies**, carrying the `isResolved` flag | **GraphQL `reviewThreads` only** |
|
|
30
|
+
|
|
31
|
+
Only **threads** have `isResolved`, and **only GraphQL can flip it**. The REST comments API
|
|
32
|
+
lists and replies to comments but has no concept of resolving.
|
|
33
|
+
|
|
34
|
+
## Which API does what
|
|
35
|
+
|
|
36
|
+
| Operation | API | Command |
|
|
37
|
+
|-----------|-----|---------|
|
|
38
|
+
| List unresolved threads | GraphQL | `reviewThreads(first:100){ nodes{ id isResolved path line comments(first:1){ nodes{ databaseId } } } }` |
|
|
39
|
+
| Reply to a thread | REST | `POST /pulls/{n}/comments/{comment_id}/replies` |
|
|
40
|
+
| Reply fallback (404) | REST | `POST /pulls/{n}/comments` with `in_reply_to={comment_id}` |
|
|
41
|
+
| Acknowledge suppressed notes | REST | `gh pr comment {n} --body "…"` (PR-level) |
|
|
42
|
+
| **Resolve a thread** | **GraphQL** | `resolveReviewThread(input:{threadId:$id})` |
|
|
43
|
+
| Verify 0 unresolved | GraphQL | re-query `reviewThreads`, count `isResolved == false` |
|
|
44
|
+
|
|
45
|
+
## Resolve locations
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
REPO_SLUG=$(gh repo view --json nameWithOwner -q .nameWithOwner) # owner/repo
|
|
49
|
+
OWNER=${REPO_SLUG%/*}; REPO=${REPO_SLUG#*/}
|
|
50
|
+
PR=$(gh pr view --json number -q .number) # current branch's PR
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 1. List unresolved threads (paginated)
|
|
54
|
+
|
|
55
|
+
A PR can have more than 100 threads, so a single `first:100` page is not enough. Walk the
|
|
56
|
+
connection with `pageInfo { endCursor hasNextPage }` and collect every node before filtering.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
> /tmp/review-threads.jsonl
|
|
60
|
+
CURSOR=null
|
|
61
|
+
while : ; do
|
|
62
|
+
# First page: there is no cursor yet ($CURSOR is the string "null"). Passing it as
|
|
63
|
+
# `-f after="null"` would send the literal string "null" — an invalid cursor, not a
|
|
64
|
+
# "start from the beginning" signal. Omit `after` on this pass so the query starts at the
|
|
65
|
+
# beginning; pass the real endCursor afterward. (gh's typed `-F after=null` instead sends
|
|
66
|
+
# a real GraphQL null.)
|
|
67
|
+
if [ "$CURSOR" = "null" ]; then
|
|
68
|
+
AFTER_ARGS=()
|
|
69
|
+
else
|
|
70
|
+
AFTER_ARGS=(-f after="$CURSOR")
|
|
71
|
+
fi
|
|
72
|
+
PAGE=$(gh api graphql -F owner="$OWNER" -F repo="$REPO" -F pr="$PR" "${AFTER_ARGS[@]}" -f query='
|
|
73
|
+
query($owner:String!, $repo:String!, $pr:Int!, $after:String) {
|
|
74
|
+
repository(owner:$owner, name:$repo) {
|
|
75
|
+
pullRequest(number:$pr) {
|
|
76
|
+
reviewThreads(first:100, after:$after) {
|
|
77
|
+
pageInfo { endCursor hasNextPage }
|
|
78
|
+
nodes {
|
|
79
|
+
id
|
|
80
|
+
isResolved
|
|
81
|
+
isOutdated
|
|
82
|
+
path
|
|
83
|
+
line
|
|
84
|
+
comments(first:1) { nodes { databaseId body author { login } } }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}')
|
|
90
|
+
# Fail fast: a GraphQL/auth/network error must not be mistaken for "no threads".
|
|
91
|
+
if ! echo "$PAGE" | jq -e '.data.repository.pullRequest.reviewThreads' >/dev/null 2>&1; then
|
|
92
|
+
echo "ERROR: GraphQL request failed or returned an unexpected shape:" >&2
|
|
93
|
+
echo "$PAGE" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[]' >> /tmp/review-threads.jsonl
|
|
97
|
+
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
|
|
98
|
+
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
|
|
99
|
+
[ "$HAS_NEXT" = "true" ] || break
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
jq -r '
|
|
103
|
+
select(.isResolved == false)
|
|
104
|
+
| "\(.id)\t\(.comments.nodes[0].databaseId)\t\(.path):\(.line)"
|
|
105
|
+
' /tmp/review-threads.jsonl
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Each row gives `threadId` (to resolve) and `commentId` = `databaseId` (to reply). A `null`
|
|
109
|
+
`databaseId` marks a suppressed note — see below.
|
|
110
|
+
|
|
111
|
+
## 2. Reply to a thread
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Primary: reply directly on the thread.
|
|
115
|
+
gh api "repos/$OWNER/$REPO/pulls/$PR/comments/$COMMENT_ID/replies" \
|
|
116
|
+
-f body="Fixed in <sha>: <what changed>."
|
|
117
|
+
|
|
118
|
+
# Fallback when the replies endpoint 404s: link via in_reply_to.
|
|
119
|
+
gh api "repos/$OWNER/$REPO/pulls/$PR/comments" \
|
|
120
|
+
-f body="Fixed in <sha>: <what changed>." \
|
|
121
|
+
-F in_reply_to="$COMMENT_ID"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 3. Resolve the thread (GraphQL — the only way)
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gh api graphql -f id="$THREAD_ID" -f query='
|
|
128
|
+
mutation($id:ID!) {
|
|
129
|
+
resolveReviewThread(input:{threadId:$id}) {
|
|
130
|
+
thread { isResolved }
|
|
131
|
+
}
|
|
132
|
+
}'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Batch all addressed threads:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
for THREAD_ID in $THREAD_IDS; do
|
|
139
|
+
gh api graphql -f id="$THREAD_ID" -f query='mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ thread { isResolved } } }'
|
|
140
|
+
done
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> To reopen a thread (rarely needed), the counterpart mutation is `unresolveReviewThread`.
|
|
144
|
+
|
|
145
|
+
## 4. Verify 0 unresolved (the close-the-loop check)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
> /tmp/review-threads-remaining.jsonl
|
|
149
|
+
CURSOR=null
|
|
150
|
+
while : ; do
|
|
151
|
+
if [ "$CURSOR" = "null" ]; then
|
|
152
|
+
AFTER_ARGS=()
|
|
153
|
+
else
|
|
154
|
+
AFTER_ARGS=(-f after="$CURSOR")
|
|
155
|
+
fi
|
|
156
|
+
PAGE=$(gh api graphql -F owner="$OWNER" -F repo="$REPO" -F pr="$PR" "${AFTER_ARGS[@]}" -f query='
|
|
157
|
+
query($owner:String!, $repo:String!, $pr:Int!, $after:String) {
|
|
158
|
+
repository(owner:$owner, name:$repo) {
|
|
159
|
+
pullRequest(number:$pr) {
|
|
160
|
+
reviewThreads(first:100, after:$after) {
|
|
161
|
+
pageInfo { endCursor hasNextPage }
|
|
162
|
+
nodes { isResolved }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}')
|
|
167
|
+
# Fail fast: a GraphQL/auth/network error must not be mistaken for "0 remaining".
|
|
168
|
+
if ! echo "$PAGE" | jq -e '.data.repository.pullRequest.reviewThreads' >/dev/null 2>&1; then
|
|
169
|
+
echo "ERROR: GraphQL request failed or returned an unexpected shape:" >&2
|
|
170
|
+
echo "$PAGE" >&2
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[]' >> /tmp/review-threads-remaining.jsonl
|
|
174
|
+
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
|
|
175
|
+
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
|
|
176
|
+
[ "$HAS_NEXT" = "true" ] || break
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
REMAINING=$(jq -s '[.[] | select(.isResolved == false)] | length' /tmp/review-threads-remaining.jsonl)
|
|
180
|
+
if [ "$REMAINING" -eq 0 ]; then
|
|
181
|
+
echo "✅ all resolved"
|
|
182
|
+
else
|
|
183
|
+
echo "❌ $REMAINING remaining"
|
|
184
|
+
exit 1
|
|
185
|
+
fi
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> If `isResolved` remains `false` immediately after a successful `resolveReviewThread`
|
|
189
|
+
> mutation, wait 2–3 seconds and re-query — GitHub's GraphQL API can exhibit brief
|
|
190
|
+
> consistency lag after bulk resolves, so a thread already resolved server-side may
|
|
191
|
+
> still report as open on the next read.
|
|
192
|
+
|
|
193
|
+
## Copilot-specific handling
|
|
194
|
+
|
|
195
|
+
- **Suppressed / low-confidence notes** — Copilot posts some findings without an inline comment
|
|
196
|
+
(`databaseId` is `null`), so there is **no inline comment to reply to**. The thread itself still
|
|
197
|
+
has an `id` and is resolvable — acknowledge the note with a single PR-level comment
|
|
198
|
+
(`gh pr comment`), then resolve the thread by its `id` like any other.
|
|
199
|
+
- **`isOutdated` threads** — when the underlying code moves, the thread is flagged `isOutdated`
|
|
200
|
+
but remains **unresolved**. It never clears itself; resolve it explicitly once handled.
|
|
201
|
+
- **Per-commit review rounds** — Copilot re-reviews after each push, potentially opening new
|
|
202
|
+
threads. Re-run list → reply → resolve → verify until the count is `0`. Trigger a fresh pass
|
|
203
|
+
with `gh pr comment $PR --body "@copilot review"` (unquoted `$PR` — it is the integer PR
|
|
204
|
+
number set earlier via `gh pr view --json number -q .number`, and `gh pr comment` takes
|
|
205
|
+
it as a positional argument, not a string flag value).
|
|
206
|
+
|
|
207
|
+
## Common failures
|
|
208
|
+
|
|
209
|
+
| Symptom | Cause | Fix |
|
|
210
|
+
|---------|-------|-----|
|
|
211
|
+
| Resolve mutation returns a permissions error | Token lacks `repo` scope | Re-auth: `gh auth refresh -s repo` |
|
|
212
|
+
| `POST …/replies` → 404 | Thread doesn't accept direct replies | Use `in_reply_to` fallback |
|
|
213
|
+
| Reply "works" but thread stays open | Replied via REST, never resolved | Run the GraphQL `resolveReviewThread` |
|
|
214
|
+
| `pr` variable rejected by GraphQL | Passed as a string | Use `-F pr="$PR"` (typed) not `-f pr=...` |
|
|
215
|
+
| Count never reaches 0 | New round opened threads | Re-run the loop after each push |
|
|
@@ -5,7 +5,7 @@ description: Caching strategy for Next.js (App Router) frontends consuming the C
|
|
|
5
5
|
|
|
6
6
|
# Next.js Caching Skill
|
|
7
7
|
|
|
8
|
-
Canonical caching strategy for Silver Side Next.js (App Router) frontends. Use it when touching data
|
|
8
|
+
Canonical caching strategy for Silver Assist Side Next.js (App Router) frontends. Use it when touching data
|
|
9
9
|
fetching, route segment configs, the asset/page proxy, image config, or the on-demand revalidate
|
|
10
10
|
route — and when diagnosing "why isn't this page cached / why is it serving stale".
|
|
11
11
|
|
|
@@ -76,15 +76,25 @@ Pick one (ordered by risk, lowest first):
|
|
|
76
76
|
sets `Cache-Control: public, s-maxage=2592000, stale-while-revalidate=2592000` — the **same**
|
|
77
77
|
header the ISR pages emit (see `expireTime` under ISR tiers), so the CDN policy is uniform across
|
|
78
78
|
static and dynamic routes. The origin stays dynamic; the CDN caches the deterministic-per-URL
|
|
79
|
-
response. Per-repo regex, no build-time risk — this is what WEB-1069 shipped.
|
|
79
|
+
response. Per-repo regex, no build-time risk — this is what WEB-1069 shipped. **Requires:** a
|
|
80
|
+
per-repo path regex in `src/proxy.ts` that matches exactly the routes you want cached **and**
|
|
81
|
+
those routes must be **public and deterministic per URL** — no auth-, cookie-, or session-
|
|
82
|
+
dependent output. A `public` edge cache serves one stored response to *every* visitor, so a
|
|
83
|
+
personalized route matched here would leak one user's content to others. Never match
|
|
84
|
+
authenticated or personalized paths.
|
|
80
85
|
2. **`export const dynamic = "force-static"` (interim).** Forces the POST read to `force-cache` and
|
|
81
86
|
prerenders the route as real ISR (confirmed via `prerender-manifest.json`). Removed in WEB-1058
|
|
82
87
|
because a CCDS failure at build time cached a **blank page**; safer now that reads throw on 5xx at
|
|
83
88
|
runtime (a failed revalidation keeps the last good cache), but full prerender is sensitive to
|
|
84
|
-
null/bad records
|
|
89
|
+
null/bad records. **Requires:** the page is fully null-safe (every field access guarded); the read
|
|
90
|
+
throws on failure **at build/prerender time too** (not only during runtime revalidation) so a failed
|
|
91
|
+
read aborts generation instead of prerendering a cacheable blank/error page — the exact WEB-1058
|
|
92
|
+
regression; AND the client throws on 5xx at runtime (so ISR keeps the previous version instead of
|
|
93
|
+
caching an error). Re-validate this per repository before adopting the strategy.
|
|
85
94
|
3. **`cacheComponents: true` + `use cache` (strategic).** Wrap the POST read in `use cache` so its data
|
|
86
|
-
lands in the static shell (PPR). Next-recommended long term; larger migration
|
|
87
|
-
hoc with route-segment
|
|
95
|
+
lands in the static shell (PPR). Next-recommended long term; larger migration. **Requires:** a
|
|
96
|
+
deliberate PPR migration for the whole route segment; do **not** mix ad hoc with route-segment
|
|
97
|
+
`export const revalidate` (the two models conflict).
|
|
88
98
|
|
|
89
99
|
---
|
|
90
100
|
|
|
@@ -164,7 +174,9 @@ is intentionally **long** (WEB-1069):
|
|
|
164
174
|
30d revalidate). Set it to `5184000` (60d) so a 30d page yields the same header the proxy sets on
|
|
165
175
|
dynamic pages: `s-maxage=2592000, stale-while-revalidate=2592000`.
|
|
166
176
|
- The long default is safe because a missed webhook still self-heals within 30d; use `tags` for
|
|
167
|
-
immediate surgical invalidation. Shorter tiers (24h/7d) are
|
|
177
|
+
immediate surgical invalidation. Shorter tiers (24h/7d) are appropriate per-route when the data
|
|
178
|
+
source updates more frequently than the 30d default — for example, sources that update daily or
|
|
179
|
+
weekly. Sources that update less frequently than 30d should keep the default.
|
|
168
180
|
- Default to `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
|
|
169
181
|
preserves the last good cache instead of overwriting it with a broken page. Pair with a client that
|
|
170
182
|
**throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the
|
|
@@ -185,6 +197,10 @@ revalidateTag(tag); // granular per-state / per-city tags
|
|
|
185
197
|
if (invalidateCDN) await invalidateCloudFrontPaths([path]);
|
|
186
198
|
```
|
|
187
199
|
|
|
200
|
+
If `invalidateCloudFrontPaths` throws, log the error and surface it to the caller — do **not**
|
|
201
|
+
silently swallow it. The Next.js cache will be fresh but the CDN will serve stale until its TTL
|
|
202
|
+
expires; an operator must retry the CloudFront invalidation manually.
|
|
203
|
+
|
|
188
204
|
---
|
|
189
205
|
|
|
190
206
|
## Anti-patterns
|
|
@@ -226,5 +242,12 @@ curl -sI https://<host>/<care-type>/<state>/<city> | grep -i cache-control
|
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
In the build output, `●` (or "Static"/"ISR") means prerendered; `ƒ` ("Dynamic") means it renders per
|
|
229
|
-
request
|
|
230
|
-
|
|
245
|
+
request. Which one to expect depends on the strategy chosen in "Making a POST-read page cacheable":
|
|
246
|
+
strategy **2** (`force-static`) prerenders fully, so it shows `●`; strategy **3** (`use cache` / PPR)
|
|
247
|
+
shows `●` only when the whole route is static — if dynamic holes remain it renders as a **partial
|
|
248
|
+
prerender** and Next marks it `◐` (partial), which is still correct output, so do not reject it.
|
|
249
|
+
Strategy **1** (CDN edge override) intentionally leaves the route as `ƒ` — the page is CDN-cached at
|
|
250
|
+
the edge even though Next classifies it as dynamic. Because the symbol alone is ambiguous across
|
|
251
|
+
strategies, verify with the `curl -sI … | grep -i cache-control` check above and defer to the
|
|
252
|
+
installed Next.js version's build-output legend rather than the symbol. `next.config`
|
|
253
|
+
`logging.fetches.fullUrl: true` shows per-fetch cache decisions in dev.
|
|
@@ -41,7 +41,7 @@ plugin-slug/
|
|
|
41
41
|
|
|
42
42
|
### Key Principles
|
|
43
43
|
|
|
44
|
-
1. **Unified scripts** — `build-release.sh` and `release.yml` are identical across all 6 plugins.
|
|
44
|
+
1. **Unified scripts** — `build-release.sh` and `release.yml` are structurally identical across all 6 plugins. The only permitted per-plugin variation is the Silver Assist package validation block at the end of `build-release.sh` (see [Validation Checks](#validation-checks)), which may omit checks for packages the plugin does not depend on.
|
|
45
45
|
2. **Selective copy** — Only runtime files go into the ZIP (no tests, docs, dev configs).
|
|
46
46
|
3. **Auto-detection** — The build script finds the main plugin file, slug, and version automatically.
|
|
47
47
|
4. **Checksums** — Every ZIP gets MD5 and SHA-256 checksums.
|
|
@@ -80,13 +80,21 @@ git push origin v1.3.6
|
|
|
80
80
|
|
|
81
81
|
### Step 1: Update All Versions
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
**Selection is driven by which script the plugin ships — each plugin ships exactly one variant**,
|
|
84
|
+
and `release.yml` auto-detects the same way (it runs `update-version.sh` if present, else
|
|
85
|
+
`update-version-simple.sh`). Manual runs and CI therefore stay consistent *because only one script
|
|
86
|
+
exists per plugin*; never ship both, or CI's preference for `update-version.sh` would silently
|
|
87
|
+
diverge from a manual `update-version-simple.sh` run.
|
|
88
|
+
|
|
89
|
+
- Full variant (`update-version.sh`): `acf-clone-fields`, `silver-assist-post-revalidate`, `silver-assist-security`.
|
|
90
|
+
- Simple variant (`update-version-simple.sh`): `contact-form-to-api`, `leadgen-app-form`, `nextjs-graphql-hooks`.
|
|
91
|
+
- New plugin: ship **only** `update-version-simple.sh` unless it genuinely needs the full variant.
|
|
84
92
|
|
|
85
93
|
```bash
|
|
86
94
|
# Full variant (acf-clone-fields, silver-assist-post-revalidate, silver-assist-security)
|
|
87
95
|
./scripts/update-version.sh 1.2.0 --no-confirm --force
|
|
88
96
|
|
|
89
|
-
# Simple variant (contact-form-to-api, leadgen-app-form, nextjs-graphql-hooks)
|
|
97
|
+
# Simple variant (contact-form-to-api, leadgen-app-form, nextjs-graphql-hooks, new plugins by default)
|
|
90
98
|
./scripts/update-version-simple.sh 1.2.0 --no-confirm --force
|
|
91
99
|
```
|
|
92
100
|
|
|
@@ -165,7 +173,7 @@ gh run watch <run-id> --exit-status
|
|
|
165
173
|
|
|
166
174
|
## Unified Build Script (`scripts/build-release.sh`)
|
|
167
175
|
|
|
168
|
-
This script is **identical** across all Silver Assist plugins. When setting up a new plugin, copy it verbatim.
|
|
176
|
+
This script is **structurally identical** across all Silver Assist plugins. When setting up a new plugin, copy it and change only the dependency-specific asset-validation checks a plugin does not need — the single allowed per-plugin variation, documented below (see the Silver Assist package-asset table and [Key Principles](#key-principles)). Everything else must remain identical; copying it verbatim for a plugin that lacks one of those dependencies would leave an invalid validation check that fails its release build.
|
|
169
177
|
|
|
170
178
|
### What It Does
|
|
171
179
|
|
|
@@ -406,7 +414,7 @@ The build script validates these Silver Assist package assets exist in the final
|
|
|
406
414
|
| Settings Hub CSS | `vendor/silverassist/wp-settings-hub/assets/css/settings-hub.css` |
|
|
407
415
|
| GitHub Updater JS | `vendor/silverassist/wp-github-updater/assets/js/check-updates.js` |
|
|
408
416
|
|
|
409
|
-
If the plugin does NOT
|
|
417
|
+
If the plugin does NOT depend on one of these packages, omit only that package's validation check when creating the plugin's `build-release.sh`. This is the single allowed per-plugin variation of the script (see [Key Principles](#key-principles)); everything else must remain identical to the template above.
|
|
410
418
|
|
|
411
419
|
---
|
|
412
420
|
|
|
@@ -429,6 +437,14 @@ on:
|
|
|
429
437
|
description: 'Version to release (e.g. 1.2.0). Leave empty to use plugin file version.'
|
|
430
438
|
required: false
|
|
431
439
|
|
|
440
|
+
# Note: `workflow_dispatch` still performs a REAL release — the "Create GitHub Release"
|
|
441
|
+
# step below publishes the release and creates the `v<version>` tag if it does not
|
|
442
|
+
# already exist. It is NOT a dry run. Use it only to release a version whose source is
|
|
443
|
+
# already committed on the checked-out ref: the "Update version" step edits files inside
|
|
444
|
+
# the CI checkout without committing them back, so an uncommitted bump would ship a ZIP
|
|
445
|
+
# that differs from the repository. For a true dry run, run the build steps in a separate
|
|
446
|
+
# non-publishing workflow that omits the "Create GitHub Release" step.
|
|
447
|
+
|
|
432
448
|
permissions:
|
|
433
449
|
contents: write
|
|
434
450
|
|
|
@@ -457,6 +473,10 @@ jobs:
|
|
|
457
473
|
else
|
|
458
474
|
VERSION=$(grep -o 'Version: [0-9]\+\.[0-9]\+\.[0-9]\+' *.php 2>/dev/null | head -1 | cut -d' ' -f2)
|
|
459
475
|
fi
|
|
476
|
+
if [ -z "$VERSION" ]; then
|
|
477
|
+
echo "::error::Could not detect version: no tag, no manual input, and no Version: header found in root PHP files."
|
|
478
|
+
exit 1
|
|
479
|
+
fi
|
|
460
480
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
461
481
|
echo "🏷️ Version: $VERSION"
|
|
462
482
|
|
|
@@ -75,6 +75,24 @@ jest.mock('stripe', () => ({
|
|
|
75
75
|
|
|
76
76
|
## Server Action Testing Pattern
|
|
77
77
|
|
|
78
|
+
### Mocking `next/navigation`
|
|
79
|
+
|
|
80
|
+
Server Actions that call `redirect()` or use routing helpers require `next/navigation` to be mocked, since it is not available in the Jest test environment.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Mock next/navigation BEFORE imports.
|
|
84
|
+
// redirect() must THROW like the real implementation — Next signals a redirect by throwing
|
|
85
|
+
// NEXT_REDIRECT to terminate the action's control flow. A plain `jest.fn()` no-op would let
|
|
86
|
+
// code after redirect() keep running, producing outcomes impossible in production. Throw a
|
|
87
|
+
// sentinel and assert on it (e.g. `await expect(action()).rejects.toThrow(/NEXT_REDIRECT/)`).
|
|
88
|
+
jest.mock('next/navigation', () => ({
|
|
89
|
+
redirect: jest.fn((url) => {
|
|
90
|
+
throw new Error(`NEXT_REDIRECT: ${url}`);
|
|
91
|
+
}),
|
|
92
|
+
useRouter: jest.fn(() => ({ push: jest.fn() })),
|
|
93
|
+
}));
|
|
94
|
+
```
|
|
95
|
+
|
|
78
96
|
### Basic Server Action Test
|
|
79
97
|
|
|
80
98
|
```typescript
|
|
@@ -113,6 +131,26 @@ describe('submitWizardToSalesforce', () => {
|
|
|
113
131
|
expect(result.success).toBe(true);
|
|
114
132
|
expect(result.leadId).toBeDefined();
|
|
115
133
|
});
|
|
134
|
+
|
|
135
|
+
it('should return failure state when the API rejects', async () => {
|
|
136
|
+
// Simulate an error thrown by the mocked dependency
|
|
137
|
+
mockCreateLead.mockRejectedValue(new Error('API error'));
|
|
138
|
+
|
|
139
|
+
const formData = new FormData();
|
|
140
|
+
formData.append('email', 'test@example.com');
|
|
141
|
+
formData.append('firstName', 'John');
|
|
142
|
+
|
|
143
|
+
const result = await submitWizardToSalesforce(
|
|
144
|
+
{ success: false, message: '', timestamp: 0 },
|
|
145
|
+
formData
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Assert the error path for THIS action: it catches the API failure and returns a
|
|
149
|
+
// failure state. Match the action's own contract — actions that instead throw (e.g.
|
|
150
|
+
// via redirect(), see the mock above) must be asserted with `rejects.toThrow(...)`.
|
|
151
|
+
expect(result.success).toBe(false);
|
|
152
|
+
expect(result.message).toContain('error');
|
|
153
|
+
});
|
|
116
154
|
});
|
|
117
155
|
```
|
|
118
156
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tsdoc-standards
|
|
3
|
+
description: Write and enforce TSDoc (not JSDoc) in TypeScript. Use when adding or reviewing doc comments on functions, components, interfaces, or file headers. Covers allowed tags, forbidden JSDoc patterns, and templates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Silver Assist — TSDoc Standards
|
|
7
|
+
|
|
8
|
+
This skill is the deep reference for writing and reviewing documentation comments in
|
|
9
|
+
TypeScript. All doc comments follow the **[TSDoc](https://tsdoc.org/)** standard — **not
|
|
10
|
+
JSDoc**. The enforced, always-on rules live in `tsdoc-standards.instructions.md`
|
|
11
|
+
(`applyTo: "**/*.{ts,tsx}"`); this skill adds the *why*, expanded examples, and a review
|
|
12
|
+
checklist for on-demand use.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Adding doc comments to functions, Server Actions, React components, hooks, or utilities
|
|
17
|
+
- Documenting interfaces, types, enums, or module/file headers
|
|
18
|
+
- Reviewing a PR and deciding whether a comment is TSDoc-correct
|
|
19
|
+
- Migrating an existing file from JSDoc (`{type}` braces, `@template`, `@module`) to TSDoc
|
|
20
|
+
- Answering "which tag do I use for X?" or "is `@fileoverview` allowed?"
|
|
21
|
+
|
|
22
|
+
## Why TSDoc, not JSDoc
|
|
23
|
+
|
|
24
|
+
TypeScript already carries the type information. JSDoc duplicates it in `{braces}`, which:
|
|
25
|
+
|
|
26
|
+
- **Drifts** — the `{string}` in the comment and the `: string` in the signature diverge over time.
|
|
27
|
+
- **Is redundant** — editors and `tsc` read the real types; the comment adds nothing.
|
|
28
|
+
- **Breaks API extractors** — tools like [API Extractor](https://api-extractor.com/) and the
|
|
29
|
+
TSDoc parser reject JSDoc-only tags (`@typedef`, `@callback`, `@fileoverview`).
|
|
30
|
+
|
|
31
|
+
TSDoc keeps prose in the comment and types in the code — one source of truth for each.
|
|
32
|
+
|
|
33
|
+
## Core Rules (with rationale)
|
|
34
|
+
|
|
35
|
+
### 1. No type annotations in `@param` / `@returns`
|
|
36
|
+
|
|
37
|
+
The type is in the signature; the comment describes *meaning*, not *type*.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// ❌ JSDoc — the {string} duplicates the signature
|
|
41
|
+
/** @param {string} slug - Community slug */
|
|
42
|
+
|
|
43
|
+
// ✅ TSDoc — hyphen required after the name; no braces
|
|
44
|
+
/** @param slug - Community slug */
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. `@typeParam` for generics (not `@template`)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// ❌ @template is a JSDoc/Closure tag
|
|
51
|
+
/** @template T */
|
|
52
|
+
|
|
53
|
+
// ✅ TSDoc generic parameter
|
|
54
|
+
/** @typeParam T - The item shape returned by the fetcher */
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. `@packageDocumentation` for file/module headers (not `@module`)
|
|
58
|
+
|
|
59
|
+
Place it in the **first** comment of the file. It marks module-level docs for extractors.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
/**
|
|
63
|
+
* @packageDocumentation
|
|
64
|
+
* CCDS geo-search utilities for state, city, and community lookups.
|
|
65
|
+
*/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Document interface members inline (not `@property`)
|
|
69
|
+
|
|
70
|
+
Each member gets its own leading comment — it shows on hover for that specific field.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface Community {
|
|
74
|
+
/** The community name. */
|
|
75
|
+
name: string;
|
|
76
|
+
/** The city where the community is located. */
|
|
77
|
+
city: string;
|
|
78
|
+
/** @defaultValue `false` */
|
|
79
|
+
featured?: boolean;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Overloaded functions — document each overload signature
|
|
84
|
+
|
|
85
|
+
For overloaded functions, place the TSDoc comment on **each overload signature individually**.
|
|
86
|
+
Do **not** place it on the implementation signature — tooling (editors, `tsc`, API extractors)
|
|
87
|
+
will not surface it to consumers, who only see the overload signatures.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ✅ TSDoc on each overload; implementation signature is undocumented
|
|
91
|
+
/**
|
|
92
|
+
* Fetches a community by its slug.
|
|
93
|
+
*
|
|
94
|
+
* @param slug - The community slug
|
|
95
|
+
* @returns The matching community, or `undefined`
|
|
96
|
+
*/
|
|
97
|
+
export function getCommunity(slug: string): Community | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Fetches a community by its numeric ID.
|
|
100
|
+
*
|
|
101
|
+
* @param id - The community ID
|
|
102
|
+
* @returns The matching community, or `undefined`
|
|
103
|
+
*/
|
|
104
|
+
export function getCommunity(id: number): Community | undefined;
|
|
105
|
+
export function getCommunity(key: string | number): Community | undefined {
|
|
106
|
+
// implementation
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Allowed Tags — quick reference
|
|
111
|
+
|
|
112
|
+
| Tag | Format | Notes |
|
|
113
|
+
|-----|--------|-------|
|
|
114
|
+
| `@param` | `@param name - Description` | Hyphen required; no type braces |
|
|
115
|
+
| `@returns` | `@returns Description` | No type braces |
|
|
116
|
+
| `@remarks` | Block | Extended description / side effects |
|
|
117
|
+
| `@example` | Block | Fenced code (` ```typescript `) |
|
|
118
|
+
| `@throws` | Block | Documented exceptions |
|
|
119
|
+
| `@see` | Block | Cross-reference or URL |
|
|
120
|
+
| `@deprecated` | Block | Add migration guidance |
|
|
121
|
+
| `@defaultValue` | Block | Default of a property |
|
|
122
|
+
| `@typeParam` | `@typeParam T - Description` | Generic parameter |
|
|
123
|
+
| `@packageDocumentation` | Modifier | First comment in the file |
|
|
124
|
+
| `{@link symbol}` | Inline | Hyperlink to a symbol |
|
|
125
|
+
|
|
126
|
+
> `@param` / `@returns` are **optional** — add them only when they add clarity (non-obvious
|
|
127
|
+
> params, complex return shapes). Never add them to interfaces, types, or constants.
|
|
128
|
+
|
|
129
|
+
## Templates
|
|
130
|
+
|
|
131
|
+
### Server Action
|
|
132
|
+
|
|
133
|
+
````typescript
|
|
134
|
+
/**
|
|
135
|
+
* Submits the contact form and sends a notification email.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* Validates server-side, creates a DB record, dispatches email.
|
|
139
|
+
*
|
|
140
|
+
* @param formData - Submitted form data from the contact page
|
|
141
|
+
*
|
|
142
|
+
* @throws When the email service is unavailable
|
|
143
|
+
*/
|
|
144
|
+
export async function submitContactForm(formData: FormData): Promise<void> {}
|
|
145
|
+
````
|
|
146
|
+
|
|
147
|
+
### React Component
|
|
148
|
+
|
|
149
|
+
````typescript
|
|
150
|
+
/**
|
|
151
|
+
* Renders a responsive community card with name and location.
|
|
152
|
+
*
|
|
153
|
+
* @returns The community card JSX element
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* <CommunityCard community={community} className="mt-4" />
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function CommunityCard({ community, className }: CommunityCardProps) {
|
|
161
|
+
return <article className={className}>{community.name}</article>;
|
|
162
|
+
}
|
|
163
|
+
````
|
|
164
|
+
|
|
165
|
+
### Utility Function
|
|
166
|
+
|
|
167
|
+
````typescript
|
|
168
|
+
/**
|
|
169
|
+
* Formats a date string for display.
|
|
170
|
+
*
|
|
171
|
+
* @param dateString - ISO 8601 date string
|
|
172
|
+
* @returns Human-readable date (e.g., "January 15, 2025")
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* formatDate("2025-01-15"); // "January 15, 2025"
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function formatDate(dateString: string): string {
|
|
180
|
+
return new Date(dateString).toLocaleDateString("en-US", { dateStyle: "long" });
|
|
181
|
+
}
|
|
182
|
+
````
|
|
183
|
+
|
|
184
|
+
### Generic Utility (`@typeParam`)
|
|
185
|
+
|
|
186
|
+
````typescript
|
|
187
|
+
/**
|
|
188
|
+
* Returns the first element matching the predicate, or `undefined`.
|
|
189
|
+
*
|
|
190
|
+
* @typeParam T - The element type of the array
|
|
191
|
+
* @param items - The array to search
|
|
192
|
+
* @param predicate - Returns `true` for the desired element
|
|
193
|
+
* @returns The first matching element, or `undefined` if none match
|
|
194
|
+
*/
|
|
195
|
+
export function findFirst<T>(items: T[], predicate: (item: T) => boolean): T | undefined {
|
|
196
|
+
return items.find(predicate);
|
|
197
|
+
}
|
|
198
|
+
````
|
|
199
|
+
|
|
200
|
+
## Forbidden Patterns → Fixes
|
|
201
|
+
|
|
202
|
+
| ❌ Forbidden (JSDoc) | ✅ TSDoc fix |
|
|
203
|
+
|----------------------|--------------|
|
|
204
|
+
| `@param {string} name` | `@param name` |
|
|
205
|
+
| `@returns {boolean} …` | `@returns …` |
|
|
206
|
+
| `@template T` | `@typeParam T` |
|
|
207
|
+
| `@module path/to/mod` | `@packageDocumentation` |
|
|
208
|
+
| `@fileoverview …` | `@packageDocumentation` |
|
|
209
|
+
| `@typedef {Object} X` | Define a TypeScript `interface`/`type` |
|
|
210
|
+
| `@callback X` | Define a TypeScript function type |
|
|
211
|
+
| `@type {X}` | Annotate the value: `const x: X = …` |
|
|
212
|
+
| `@property {T} name` | Inline `/** … */` on the interface member |
|
|
213
|
+
| `@function` / `@async` / `@class` / `@enum` | Remove — redundant with TypeScript |
|
|
214
|
+
|
|
215
|
+
## Review Checklist
|
|
216
|
+
|
|
217
|
+
When reviewing doc comments, confirm:
|
|
218
|
+
|
|
219
|
+
- [ ] No `{type}` braces in `@param` / `@returns`.
|
|
220
|
+
- [ ] `@param name - Description` uses the hyphen and matches the actual parameter name.
|
|
221
|
+
- [ ] Generics use `@typeParam`, not `@template`.
|
|
222
|
+
- [ ] File-level docs use `@packageDocumentation` in the first comment, not `@module` / `@fileoverview`.
|
|
223
|
+
- [ ] Interface/type members are documented inline, not via `@property`.
|
|
224
|
+
- [ ] No redundant `@function` / `@async` / `@class` / `@enum` / `@typedef` / `@callback` / `@type`.
|
|
225
|
+
- [ ] `@param` / `@returns` are present only where they add real clarity.
|