@silverassist/agents-toolkit 2.5.0 → 2.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverassist/agents-toolkit",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Reusable AI agent prompts for development workflows with Jira integration — supports GitHub Copilot, Claude Code, and Codex",
5
5
  "author": "Santiago Ramirez",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module @silverassist/agents-toolkit
4
4
  */
5
5
 
6
- export const VERSION = "2.5.0";
6
+ export const VERSION = "2.5.1";
7
7
 
8
8
  export const PROMPTS = {
9
9
  workflow: [
@@ -22,10 +22,26 @@ the others.
22
22
  That leaves POST reads uncached and forces the whole route into dynamic rendering
23
23
  (`cache-control: private, no-store`).
24
24
 
25
- 2. **A `POST` IS cacheable cross-request — but only with explicit `next` options.**
26
- Next.js does not cache `POST` by default. Any read `fetch` to CCDS or the WP GraphQL endpoint —
27
- GET or POST — must set `next: { revalidate, tags }`. The request body is part of the cache key, so
28
- different filter bodies cache independently.
25
+ 2. **A `POST` read caches its DATA with `next` options — but the ROUTE still renders dynamically.**
26
+ Next.js does not cache `POST` by default; any read `fetch` (GET or POST) to CCDS or the WP GraphQL
27
+ endpoint must set `next: { revalidate, tags }` (the body is part of the cache key, so different
28
+ filter bodies cache independently). **However** (WEB-1069, Next 16) a page whose data comes from a
29
+ POST read still prerenders as `ƒ` (Dynamic) and serves `cache-control: private, no-store` even with
30
+ those options — the data-fetch cache and the route's static/dynamic classification are independent.
31
+ So `next: { revalidate, tags }` is **necessary but not sufficient** to make a POST-read page
32
+ CDN-cacheable; you must also apply a rendering/edge strategy (rule 2b). GET reads ISR natively and
33
+ need nothing extra.
34
+
35
+ 2b. **Make a POST-read page cacheable at the rendering/edge layer.** Pick one (lowest risk first):
36
+ - **CDN edge override (current/default):** `src/proxy.ts` matches city/community paths and sets
37
+ `Cache-Control: public, s-maxage=2592000, stale-while-revalidate=2592000` — the same header ISR
38
+ pages emit (see rule 7 `expireTime`), so the CDN policy is uniform. Origin stays dynamic; the CDN
39
+ caches the deterministic-per-URL response. No build-time risk. This is what WEB-1069 shipped.
40
+ - **`export const dynamic = "force-static"` (interim):** prerenders the route as real ISR, but a
41
+ bad CCDS record/response at build time caches a blank page (the WEB-1058 regression) — use only
42
+ when the page is fully null-safe and reads throw on 5xx at runtime. Validate per repo.
43
+ - **`cacheComponents: true` + `use cache` (strategic):** PPR migration; do not mix ad hoc with
44
+ route-segment `export const revalidate`.
29
45
 
30
46
  3. **Always pair `tags` with a `revalidate` duration.** `next: { tags }` alone either holds stale data
31
47
  indefinitely or (Next 16 default) does not cache at runtime at all. Use a `CACHE_DURATIONS`-style
@@ -34,18 +50,26 @@ the others.
34
50
  4. **`React.cache()` is request dedup only.** It collapses duplicate calls within a single render. It
35
51
  does **not** cache across requests and is never a substitute for `next: { revalidate }`.
36
52
 
37
- 5. **Every cacheable route exports `revalidate`.** Suggested tiers: listing 24h–30d, detail/city 24h
38
- (`86400`), state/landing 7d (`604800`), WP catch-all 7d. Do **not** add `revalidate` to
39
- mutation/personalized routes (forms, thank-you, wizards). Prefer `export const revalidate` over
40
- `dynamic = "force-static"` so a failed ISR revalidation preserves the last good cache instead of
41
- overwriting it with a broken page.
53
+ 5. **Every cacheable route exports `revalidate`.** CCDS/WP data changes rarely and is refreshed
54
+ on-demand (webhooks + tags), so the default is intentionally **long: `2592000` (30d)** for CCDS
55
+ reads, `WP_CACHE_DURATIONS`, and page segments alike (WEB-1069). Shorter tiers (24h/7d) are fine
56
+ per-route for volatile sources. Do **not** add `revalidate` to mutation/personalized routes (forms,
57
+ thank-you, wizards). Default to `export const revalidate` over `dynamic = "force-static"` so a
58
+ failed ISR revalidation preserves the last good cache instead of overwriting it with a broken page
59
+ (force-static is still a valid POST-read option per rule 2b once null-safety + throw-on-5xx exist).
42
60
 
43
61
  6. **On-demand revalidation dual-invalidates.** `/api/revalidate` must call `revalidateTag`/
44
62
  `revalidatePath` **and** the CDN invalidation (e.g. `invalidateCloudFrontPaths`) in the same
45
63
  request, otherwise the CDN keeps serving stale until its own TTL.
46
64
 
47
- 7. **Image config** in `next.config`: set `minimumCacheTTL: 2592000` (30d), `qualities`, and
48
- `formats: ["image/avif", "image/webp"]`. No malformed `remotePatterns` hostnames.
65
+ 7. **`next.config` cache config.** `expireTime` sets the CDN stale window: Next emits
66
+ `s-maxage=<revalidate>, stale-while-revalidate=<expireTime revalidate>` for ISR pages, so
67
+ `expireTime` **must be ≥ the largest page `revalidate`**. Set `expireTime: 5184000` (60d) so a 30d
68
+ page yields `s-maxage=2592000, stale-while-revalidate=2592000` — the same header the proxy sets on
69
+ dynamic pages (rule 2b). (WEB-1069 bug: `expireTime: 86400` under a 30d revalidate = invalid stale
70
+ window.) Also set image `minimumCacheTTL: 31536000` (1y — optimized images are content-hashed and
71
+ never change), `qualities`, and `formats: ["image/avif", "image/webp"]`. No malformed
72
+ `remotePatterns` hostnames.
49
73
 
50
74
  8. **Asset proxy TTLs** are type-keyed (image/css/font) at `365d + SWR 30d`.
51
75
 
@@ -67,7 +91,7 @@ export async function fetchData<T>({
67
91
  method = "GET",
68
92
  body = null,
69
93
  revalidateTag,
70
- revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
94
+ revalidate = 2592000, // 30d time-based fallback; pair with tags for surgical invalidation
71
95
  mutation = false,
72
96
  }: FetchOptions): Promise<T | null> {
73
97
  const isMutation = mutation || method === "PUT" || method === "DELETE";
@@ -91,7 +115,7 @@ export async function fetchData<T>({
91
115
 
92
116
  ```ts
93
117
  export const WP_CACHE_DURATIONS = {
94
- pages: 86400, posts: 86400, menus: 86400, staticPages: 604800, default: 86400,
118
+ pages: 2592000, posts: 2592000, menus: 2592000, staticPages: 2592000, default: 2592000, // 30d
95
119
  } as const;
96
120
 
97
121
  export async function fetchWPAPI<T>(
@@ -18,8 +18,14 @@ route — and when diagnosing "why isn't this page cached / why is it serving st
18
18
  ## Core principle
19
19
 
20
20
  **Cache by intent (read vs. mutation), never by the HTTP method.** Some reads must use `POST` because
21
- they take a body (e.g. CCDS `geo-search`). They still have to cache like a `GET`. Mutations must never
22
- cache. Getting this wrong silently turns a static page into a per-request dynamic render.
21
+ they take a body (e.g. CCDS `geo-search`). Their *data* still has to cache like a `GET` send
22
+ `next: { revalidate, tags }`, never `no-store`. Mutations must never cache.
23
+
24
+ **But caching the data is not the same as making the page static.** A POST read caches its response
25
+ cross-request, yet Next.js still renders the *route* dynamically (`private, no-store`). The data-fetch
26
+ cache (layer 2) and the route's static/dynamic classification (layers 1/5) are **independent** — see
27
+ the two sections below. This is the WEB-1069 gotcha and the reason city/community pages needed an edge
28
+ override even though their fetches were already cached.
23
29
 
24
30
  ---
25
31
 
@@ -41,20 +47,44 @@ whole route into **dynamic rendering**, which emits `cache-control: private, no-
41
47
 
42
48
  ---
43
49
 
44
- ## How Next.js 16 actually decides (the rule behind the rule)
45
-
46
- From `next/dist/server/lib/patch-fetch.js`:
47
-
48
- - `fetch` is **not cached by default**. A non-`GET` method is "uncacheable" **only when no explicit
49
- cache config is provided**. Concretely, a POST is auto-no-cached only when it has no `next`/`cache`
50
- options *and* the segment `revalidate` is `0`.
51
- - Therefore **a `POST` IS cached cross-request when you pass an explicit `next: { revalidate }`** (and
52
- the segment isn't `revalidate: 0`). This is why the WordPress GraphQL POST caches fine.
53
- - The **request body is part of the cache key** (`incremental-cache` → `generateCacheKey` hashes
54
- `init.body`), so different filter bodies cache independently and never collide.
55
-
56
- **Implication:** you do **not** need `unstable_cache` or a proxy header override to cache POST reads.
57
- Just send `next: { revalidate, tags }`.
50
+ ## POST reads: the data caches, but the route stays dynamic (the key gotcha)
51
+
52
+ Two independent facts, both true:
53
+
54
+ 1. **The data fetch caches.** From `next/dist/server/lib/patch-fetch.js`: a `fetch` is uncacheable
55
+ only when it has no `next`/`cache` options *and* the segment `revalidate` is `0`. So a POST **with**
56
+ `next: { revalidate }` IS cached cross-request — the **request body is part of the cache key**
57
+ (`generateCacheKey` hashes `init.body`), so different filter bodies cache independently. This is
58
+ real and bounds origin/CCDS load; it is why the WordPress GraphQL POST caches fine.
59
+
60
+ 2. **The route still renders dynamically.** Empirically (WEB-1069, Next 16), a page whose data comes
61
+ from a POST read prerenders as `ƒ` (Dynamic) and serves `cache-control: private, no-store` — **even
62
+ with** `next: { revalidate, tags }` on the fetch. Next treats the POST as request-time data for
63
+ prerendering, so ISR (layer 1) never engages. GET reads do not have this problem — they ISR
64
+ natively (`s-maxage`, `x-nextjs-cache: HIT`).
65
+
66
+ **Implication:** `next: { revalidate, tags }` on a POST read is **necessary** (data cache) but **not
67
+ sufficient** to make the page CDN-cacheable. You must additionally pick a rendering/edge strategy
68
+ (next section). `revalidate` + `tags` alone will NOT flip a POST-read page from `private, no-store` to
69
+ `public`.
70
+
71
+ ## Making a POST-read page cacheable (rendering / edge layer)
72
+
73
+ Pick one (ordered by risk, lowest first):
74
+
75
+ 1. **CDN edge override (current, lowest risk).** `src/proxy.ts` matches the city/community paths and
76
+ sets `Cache-Control: public, s-maxage=2592000, stale-while-revalidate=2592000` — the **same**
77
+ header the ISR pages emit (see `expireTime` under ISR tiers), so the CDN policy is uniform across
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.
80
+ 2. **`export const dynamic = "force-static"` (interim).** Forces the POST read to `force-cache` and
81
+ prerenders the route as real ISR (confirmed via `prerender-manifest.json`). Removed in WEB-1058
82
+ because a CCDS failure at build time cached a **blank page**; safer now that reads throw on 5xx at
83
+ runtime (a failed revalidation keeps the last good cache), but full prerender is sensitive to
84
+ null/bad records — keep the page fully null-safe and validate per repo before adopting.
85
+ 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 — do **not** mix ad
87
+ hoc with route-segment `export const revalidate`.
58
88
 
59
89
  ---
60
90
 
@@ -76,7 +106,7 @@ export async function fetchData<T>({
76
106
  method = "GET",
77
107
  body = null,
78
108
  revalidateTag,
79
- revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
109
+ revalidate = 2592000, // 30d time-based fallback; pair with tags for surgical invalidation
80
110
  mutation = false,
81
111
  }: FetchOptions): Promise<T | null> {
82
112
  const isMutation = mutation || method === "PUT" || method === "DELETE";
@@ -116,18 +146,31 @@ await fetch(WP_API_URL, {
116
146
 
117
147
  ## ISR revalidate tiers
118
148
 
119
- | Route type | `export const revalidate` |
120
- |------------|---------------------------|
121
- | Listing / index | `2592000` (30d) — webhook handles freshness |
122
- | Detail / city / community (`[slug]`) | `86400` (24h) |
123
- | State / advisor landing | `604800` (7d) |
124
- | WP catch-all (`[[...uri]]`) | `604800` (7d) |
149
+ CCDS/WP data changes rarely and is refreshed on-demand via webhooks + tags, so the time-based default
150
+ is intentionally **long** (WEB-1069):
151
+
152
+ | Route / config | Value |
153
+ |----------------|-------|
154
+ | CCDS reads (client default) | `2592000` (30d) |
155
+ | WordPress GraphQL reads (`WP_CACHE_DURATIONS`) | `2592000` (30d) |
156
+ | Listing / detail / city / community / state / landing / WP catch-all | `2592000` (30d) |
157
+ | `next.config` `expireTime` (CDN stale window) | `5184000` (60d) → emits `s-maxage=2592000, stale-while-revalidate=2592000` |
158
+ | `next.config` image `minimumCacheTTL` | `31536000` (1y) — optimized images are content-hashed, never change |
125
159
 
126
160
  - Every cacheable route exports `revalidate`. Do **not** add it to form/personalized routes.
127
- - Prefer `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
161
+ - **`expireTime` sets the CDN stale window.** Next emits `s-maxage=<revalidate>,
162
+ stale-while-revalidate=<expireTime − revalidate>` for ISR pages, so `expireTime` **must be ≥ the
163
+ largest `revalidate`** or the stale window is invalid (the WEB-1069 bug: `expireTime: 86400` under a
164
+ 30d revalidate). Set it to `5184000` (60d) so a 30d page yields the same header the proxy sets on
165
+ dynamic pages: `s-maxage=2592000, stale-while-revalidate=2592000`.
166
+ - 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 fine per-route if a source is volatile.
168
+ - Default to `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
128
169
  preserves the last good cache instead of overwriting it with a broken page. Pair with a client that
129
170
  **throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the
130
- build phase (so `generateStaticParams` can skip a bad entry without failing the build).
171
+ build phase (so `generateStaticParams` can skip a bad entry without failing the build). `force-static`
172
+ is still a valid POST-read option (see "Making a POST-read page cacheable") **once** the page is
173
+ fully null-safe and the throw-on-5xx guard is in place.
131
174
 
132
175
  ---
133
176
 
@@ -146,13 +189,16 @@ if (invalidateCDN) await invalidateCloudFrontPaths([path]);
146
189
 
147
190
  ## Anti-patterns
148
191
 
149
- - ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached the route
150
- renders dynamically (`private, no-store`). This is the exact regression that broke city pages.
192
+ - ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` — leaves POST reads uncached (origin hit
193
+ every render). Note: even a *correctly* cached POST read still renders the route dynamically — that
194
+ needs an edge override, not just `next` options (see "Making a POST-read page cacheable").
151
195
  - ❌ Bare `fetch(url, { method: "POST", body })` for a read — refetched from origin every render.
152
196
  - ❌ `next: { tags }` with no `revalidate` — holds stale data or doesn't cache at runtime.
153
197
  - ❌ Caching a mutation (`submit`, lead, `PUT`/`DELETE`/`PATCH`).
154
198
  - ❌ Treating `React.cache()` as cross-request caching (it's request-scoped dedup only).
155
- - ❌ `dynamic = "force-static"` on a page whose revalidation can fail (caches a broken page).
199
+ - ❌ `dynamic = "force-static"` on a page that is **not** fully null-safe or lacks throw-on-5xx — a bad
200
+ CCDS record/response at build time caches a broken/blank page (the WEB-1058 regression). It is a
201
+ valid option only once those guards exist.
156
202
  - ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` —
157
203
  that is a separate, deliberate migration; do not introduce it ad hoc.
158
204