failproofai 0.0.11-beta.4 → 0.0.11-beta.6

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 (122) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +14 -14
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  40. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  41. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  42. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13ra2jq._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b9z5-i._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/_1-1804f._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +8 -8
  66. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00jkjmt._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013du6r._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0-wdurj._.js → [root-of-the-server]__09sb8gm._.js} +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e85wxv._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gfxvb1._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cd25c7._.js +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__06f-yix._.js → [root-of-the-server]__1hldld5._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hlrq6y._.js +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ihxdo5._.js +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1vvfde2._.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +15 -15
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html2canvas_dist_html2canvas_esm_0hmsdod.js → node_modules_html2canvas_dist_html2canvas_esm_0jar0u6.js} +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1d_onnt._.js +2 -2
  85. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  86. package/.next/standalone/.next/server/pages/404.html +1 -1
  87. package/.next/standalone/.next/server/pages/500.html +1 -1
  88. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  89. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  90. package/.next/standalone/.next/static/chunks/{2zjavpea8glet.js → 0faafgt7it9wg.js} +1 -1
  91. package/.next/standalone/.next/static/chunks/{36l507r2mbf5h.js → 1ha7bu3h33vlg.js} +1 -1
  92. package/.next/standalone/.next/static/chunks/{1e0o-6kjfnwzh.js → 28ktbb6r1yts-.js} +1 -1
  93. package/.next/standalone/.next/static/chunks/{41jjdgqqpl7em.js → 2msp6lxlj6y58.js} +1 -1
  94. package/.next/standalone/.next/static/chunks/2posnfuhnwuc9.js +41 -0
  95. package/.next/standalone/.next/static/chunks/{20b5bz43so-1k.js → 3-hps4mh626t-.js} +1 -1
  96. package/.next/standalone/.next/static/chunks/{3y3l184vijlfg.js → 35vxydl4wq7x_.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/{1btx2c49fk9xt.css → 3co9znvxzaj4r.css} +1 -1
  98. package/.next/standalone/.next/static/chunks/{0nbmu4_n1j2-c.js → 3yg0zz4h9pe--.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{0fkntmojmibda.js → 445x6v-0e8vzo.js} +1 -1
  100. package/.next/standalone/app/actions/get-audit-result.ts +14 -3
  101. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +108 -20
  102. package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +98 -0
  103. package/.next/standalone/app/audit/_components/rerun-button.tsx +4 -1
  104. package/.next/standalone/app/audit/_components/return-section.tsx +17 -29
  105. package/.next/standalone/app/audit/_components/top-audit-bar.tsx +153 -0
  106. package/.next/standalone/app/audit/audit-styles.css +258 -0
  107. package/.next/standalone/app/audit/page.tsx +19 -9
  108. package/.next/standalone/lib/fetch-with-timeout.ts +37 -20
  109. package/.next/standalone/package.json +1 -1
  110. package/.next/standalone/server.js +1 -1
  111. package/README.md +6 -0
  112. package/dist/cli.mjs +6 -3
  113. package/lib/fetch-with-timeout.ts +37 -20
  114. package/package.json +1 -1
  115. package/src/audit/archetypes.ts +24 -4
  116. package/src/audit/cache.ts +20 -1
  117. package/src/audit/dashboard-cache.ts +40 -0
  118. package/src/audit/features.ts +65 -19
  119. package/.next/standalone/.next/static/chunks/1bxdpd036n3l9.js +0 -41
  120. /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_buildManifest.js +0 -0
  121. /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_clientMiddlewareManifest.js +0 -0
  122. /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_ssgManifest.js +0 -0
@@ -2064,3 +2064,261 @@
2064
2064
  .share-dock-fab { transition: none; }
2065
2065
  .share-dock-caret { transition: none; }
2066
2066
  }
2067
+
2068
+ /* ─────────────────────────────────────────────────────────────────────
2069
+ top-audit-bar — thin strip above the IdentitySection. Echoes
2070
+ .arch-mast's dashed bottom border so the bar reads as a sibling band.
2071
+ Lowercase mono throughout; bracketed [ ... ] button label keeps it
2072
+ visually paired with the bottom `[ re-audit now ]` in return-section.
2073
+ ───────────────────────────────────────────────────────────────────── */
2074
+ .top-audit-bar {
2075
+ display: grid;
2076
+ grid-template-columns: minmax(0, 1fr) auto auto;
2077
+ align-items: center;
2078
+ gap: 18px;
2079
+ padding: 14px 4px;
2080
+ margin-bottom: 18px;
2081
+ border-bottom: 1px dashed var(--line);
2082
+ font-family: var(--font-mono);
2083
+ font-size: 12px;
2084
+ color: var(--ink-2);
2085
+ /* Subtle left rail mark — keeps the strip from reading as a plain hr */
2086
+ position: relative;
2087
+ }
2088
+ .top-audit-bar::before {
2089
+ content: "";
2090
+ position: absolute;
2091
+ left: -10px;
2092
+ top: 50%;
2093
+ width: 6px;
2094
+ height: 6px;
2095
+ transform: translateY(-50%) rotate(45deg);
2096
+ background: var(--accent-pink);
2097
+ opacity: 0.55;
2098
+ }
2099
+ .top-audit-bar--expired::before { background: var(--amber); opacity: 0.85; }
2100
+ .top-audit-bar--empty::before { background: var(--accent-green); opacity: 0.65; }
2101
+
2102
+ .top-audit-bar__left {
2103
+ display: flex;
2104
+ align-items: baseline;
2105
+ gap: 14px;
2106
+ min-width: 0;
2107
+ }
2108
+ .top-audit-bar__label {
2109
+ text-transform: lowercase;
2110
+ letter-spacing: 0.16em;
2111
+ font-size: 11px;
2112
+ color: var(--dim);
2113
+ white-space: nowrap;
2114
+ }
2115
+ .top-audit-bar__label--warn { color: var(--amber); }
2116
+ .top-audit-bar__glyph {
2117
+ color: var(--accent-pink);
2118
+ letter-spacing: -2px;
2119
+ margin-right: 4px;
2120
+ }
2121
+ .top-audit-bar--expired .top-audit-bar__glyph { color: var(--amber); }
2122
+ .top-audit-bar--empty .top-audit-bar__glyph { color: var(--accent-green); }
2123
+
2124
+ .top-audit-bar__time {
2125
+ display: inline-flex;
2126
+ align-items: center;
2127
+ gap: 8px;
2128
+ color: var(--ink);
2129
+ letter-spacing: 0.02em;
2130
+ white-space: nowrap;
2131
+ }
2132
+ .top-audit-bar__time--fresh { color: var(--accent-green); }
2133
+
2134
+ .top-audit-bar__dot {
2135
+ width: 6px;
2136
+ height: 6px;
2137
+ border-radius: 50%;
2138
+ background: var(--accent-green);
2139
+ display: inline-block;
2140
+ box-shadow: 0 0 0 2px rgba(102, 209, 181, 0.16);
2141
+ }
2142
+ .top-audit-bar__time:not(.top-audit-bar__time--fresh) .top-audit-bar__dot {
2143
+ background: var(--dim);
2144
+ box-shadow: none;
2145
+ }
2146
+
2147
+ .top-audit-bar__expired-copy { color: var(--ink); }
2148
+
2149
+ .top-audit-bar__center {
2150
+ display: inline-flex;
2151
+ align-items: center;
2152
+ gap: 8px;
2153
+ padding: 6px 12px;
2154
+ background: var(--amber-bg);
2155
+ border: 1px dashed var(--amber);
2156
+ color: var(--amber);
2157
+ font-size: 11px;
2158
+ text-transform: lowercase;
2159
+ letter-spacing: 0.04em;
2160
+ justify-self: end;
2161
+ }
2162
+ .top-audit-bar__amber-dot {
2163
+ width: 6px;
2164
+ height: 6px;
2165
+ border-radius: 50%;
2166
+ background: var(--amber);
2167
+ animation: top-audit-amber-pulse 1.6s ease-in-out infinite;
2168
+ }
2169
+ @keyframes top-audit-amber-pulse {
2170
+ 0%, 100% { opacity: 0.5; transform: scale(0.85); }
2171
+ 50% { opacity: 1; transform: scale(1); }
2172
+ }
2173
+
2174
+ .top-audit-bar__right {
2175
+ justify-self: end;
2176
+ }
2177
+ /* Compact override of `.share-btn` so it fits the bar's 14px padding. The
2178
+ parent class drives the colors, hard-offset hover shadow, and disabled
2179
+ state — we only tweak geometry. */
2180
+ .top-audit-bar__btn {
2181
+ grid-template-columns: 1fr;
2182
+ padding: 8px 14px;
2183
+ gap: 0;
2184
+ font-size: 12px;
2185
+ letter-spacing: 0.06em;
2186
+ text-transform: lowercase;
2187
+ color: var(--ink);
2188
+ }
2189
+ .top-audit-bar__btn[data-state="scanning"] {
2190
+ color: var(--accent-pink);
2191
+ border-right-color: var(--accent-pink);
2192
+ }
2193
+
2194
+ @media (max-width: 720px) {
2195
+ .top-audit-bar {
2196
+ grid-template-columns: 1fr;
2197
+ gap: 10px;
2198
+ }
2199
+ .top-audit-bar__right { justify-self: stretch; }
2200
+ .top-audit-bar__btn { text-align: center; }
2201
+ }
2202
+ @media (prefers-reduced-motion: reduce) {
2203
+ .top-audit-bar__amber-dot { animation: none; }
2204
+ }
2205
+
2206
+ /* ─────────────────────────────────────────────────────────────────────
2207
+ audit-progress-strip — sticky banner pinned to the top of the viewport
2208
+ while a re-audit is in flight. Pink hard-offset shadow matches the
2209
+ page's `.share-btn:hover` vocabulary. Right-edge pulse is a CSS-only
2210
+ keyframe (transform-only — no layout thrash) and is not a fake
2211
+ progress bar.
2212
+ ───────────────────────────────────────────────────────────────────── */
2213
+ .audit-progress-strip {
2214
+ position: sticky;
2215
+ top: 0;
2216
+ left: 0;
2217
+ right: 0;
2218
+ z-index: 60;
2219
+ background: var(--bg-2);
2220
+ border-bottom: 1px solid var(--accent-pink);
2221
+ box-shadow: 0 4px 0 0 var(--accent-pink-shadow);
2222
+ font-family: var(--font-mono);
2223
+ /* Slide in from above on mount */
2224
+ animation: audit-progress-slide-in 220ms cubic-bezier(0.22, 1, 0.36, 1);
2225
+ }
2226
+ .audit-progress-strip--failed {
2227
+ border-bottom-color: var(--accent-pink);
2228
+ background: linear-gradient(to right, var(--accent-pink-bg), var(--bg-2));
2229
+ }
2230
+ .audit-progress-strip__inner {
2231
+ position: relative;
2232
+ max-width: 1480px;
2233
+ margin: 0 auto;
2234
+ padding: 12px clamp(20px, 3vw, 48px);
2235
+ display: flex;
2236
+ align-items: center;
2237
+ justify-content: space-between;
2238
+ gap: 16px;
2239
+ }
2240
+ .audit-progress-strip__label {
2241
+ display: inline-flex;
2242
+ align-items: center;
2243
+ gap: 10px;
2244
+ color: var(--ink);
2245
+ font-size: 12px;
2246
+ letter-spacing: 0.06em;
2247
+ text-transform: lowercase;
2248
+ }
2249
+ .audit-progress-strip--failed .audit-progress-strip__label { color: var(--accent-pink); }
2250
+ .audit-progress-strip__elapsed {
2251
+ font-variant-numeric: tabular-nums;
2252
+ font-size: 12px;
2253
+ color: var(--accent-pink);
2254
+ letter-spacing: 0.08em;
2255
+ }
2256
+ .audit-progress-strip__spinner {
2257
+ width: 9px;
2258
+ height: 9px;
2259
+ border-radius: 50%;
2260
+ background: transparent;
2261
+ border: 1.5px solid var(--accent-pink);
2262
+ border-top-color: transparent;
2263
+ animation: audit-progress-spin 800ms linear infinite;
2264
+ }
2265
+ .audit-progress-strip__x {
2266
+ display: inline-flex;
2267
+ align-items: center;
2268
+ justify-content: center;
2269
+ width: 14px;
2270
+ height: 14px;
2271
+ border: 1px solid var(--accent-pink);
2272
+ color: var(--accent-pink);
2273
+ font-size: 11px;
2274
+ line-height: 1;
2275
+ }
2276
+ .audit-progress-strip__dismiss {
2277
+ background: transparent;
2278
+ border: 1px solid var(--line-2);
2279
+ color: var(--ink-2);
2280
+ padding: 5px 10px;
2281
+ font-family: var(--font-mono);
2282
+ font-size: 11px;
2283
+ text-transform: lowercase;
2284
+ letter-spacing: 0.06em;
2285
+ cursor: pointer;
2286
+ transition: border-color 140ms ease, color 140ms ease;
2287
+ }
2288
+ .audit-progress-strip__dismiss:hover {
2289
+ border-color: var(--accent-pink);
2290
+ color: var(--accent-pink);
2291
+ }
2292
+ .audit-progress-strip__pulse {
2293
+ position: absolute;
2294
+ bottom: -1px;
2295
+ left: 0;
2296
+ right: 0;
2297
+ height: 1px;
2298
+ background: linear-gradient(
2299
+ 90deg,
2300
+ transparent 0%,
2301
+ var(--accent-pink) 20%,
2302
+ var(--accent-pink) 80%,
2303
+ transparent 100%
2304
+ );
2305
+ transform-origin: left center;
2306
+ animation: audit-progress-pulse 2400ms cubic-bezier(0.55, 0.1, 0.45, 0.9) infinite;
2307
+ }
2308
+ @keyframes audit-progress-spin {
2309
+ to { transform: rotate(360deg); }
2310
+ }
2311
+ @keyframes audit-progress-pulse {
2312
+ 0% { transform: scaleX(0) translateX(0%); opacity: 0.4; }
2313
+ 50% { transform: scaleX(0.6) translateX(20%); opacity: 1; }
2314
+ 100% { transform: scaleX(0) translateX(100%); opacity: 0.2; }
2315
+ }
2316
+ @keyframes audit-progress-slide-in {
2317
+ from { transform: translateY(-100%); opacity: 0; }
2318
+ to { transform: translateY(0); opacity: 1; }
2319
+ }
2320
+ @media (prefers-reduced-motion: reduce) {
2321
+ .audit-progress-strip { animation: none; }
2322
+ .audit-progress-strip__spinner { animation: none; border-top-color: var(--accent-pink); }
2323
+ .audit-progress-strip__pulse { animation: none; opacity: 0.4; transform: scaleX(0.4) translateX(30%); }
2324
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Suspense } from "react";
10
10
  import { notFound } from "next/navigation";
11
- import { readDashboardCache } from "@/src/audit/dashboard-cache";
11
+ import { readDashboardCache, readDashboardCacheMeta } from "@/src/audit/dashboard-cache";
12
12
  import { BUILTIN_POLICIES } from "@/src/hooks/builtin-policies";
13
13
  import { AUDIT_DETECTORS } from "@/src/audit/detectors";
14
14
  import { AuditDashboard } from "./_components/audit-dashboard";
@@ -32,14 +32,24 @@ export default async function AuditPage({ searchParams }: PageProps) {
32
32
  const { p } = await searchParams;
33
33
 
34
34
  const cache = readDashboardCache();
35
- const initial = cache
36
- ? {
37
- status: "cached" as const,
38
- cachedAt: cache.cachedAt,
39
- params: cache.params,
40
- result: cache.result,
41
- }
42
- : { status: "empty" as const };
35
+ let initial;
36
+ if (cache) {
37
+ initial = {
38
+ status: "cached" as const,
39
+ cachedAt: cache.cachedAt,
40
+ params: cache.params,
41
+ result: cache.result,
42
+ };
43
+ } else {
44
+ // No fresh cache. Probe meta so we can distinguish "first run" from
45
+ // "your last audit expired" and surface the right top-bar nudge.
46
+ const meta = readDashboardCacheMeta();
47
+ initial = {
48
+ status: "empty" as const,
49
+ expired: meta !== null,
50
+ expiredAt: meta?.cachedAt ?? null,
51
+ };
52
+ }
43
53
 
44
54
  return (
45
55
  <Suspense>
@@ -1,14 +1,28 @@
1
1
  /**
2
- * Shared fetch wrapper with per-request timeout via AbortController.
2
+ * Shared fetch wrapper with per-request timeout and external-signal
3
+ * composition.
3
4
  *
4
5
  * Replaces three byte-equivalent implementations that had drifted on the
5
6
  * default timeout (15s in two client components, 10s in the server-side
6
7
  * api-server-client). Co-locates the `isAbortError` predicate so callers
7
8
  * can classify timeout vs network failures consistently.
8
9
  *
9
- * Server-side uses `AbortSignal.timeout` where available (cheaper); this
10
- * helper uses the AbortController+setTimeout form because the client
11
- * code-paths need a guaranteed cleanup hook.
10
+ * Built on the platform primitives `AbortSignal.timeout()` which
11
+ * aborts with a typed `DOMException(..., "TimeoutError")` rather than
12
+ * the default bare abort that surfaces as Next.js 16's dev-overlay
13
+ * "signal is aborted without reason" warning — and `AbortSignal.any()`,
14
+ * which composes the timeout signal with any caller-supplied
15
+ * `init.signal` so a component-unmount or parent-controller abort
16
+ * actually stops the in-flight fetch. The prior `AbortController` +
17
+ * `setTimeout` implementation called `controller.abort()` with no
18
+ * reason AND silently dropped `init.signal` by spreading then
19
+ * overwriting it with the controller's own signal.
20
+ *
21
+ * Both APIs are available in every runtime Next.js 16 supports:
22
+ * `AbortSignal.timeout` since Node 17.3 / 16.14, `AbortSignal.any`
23
+ * since Node 20.3 (also backported to 18.17). Next.js 16's minimum
24
+ * runtime (Node >= 20.9, per its engines field) covers both. Browser
25
+ * support: Chrome 116+, Firefox 124+, Safari 17.4+.
12
26
  */
13
27
 
14
28
  /** Hard cap on every fetch using this helper unless overridden. Picked to
@@ -16,27 +30,30 @@
16
30
  * so a slow but successful upstream response still lands. */
17
31
  export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
18
32
 
19
- export async function fetchWithTimeout(
33
+ export function fetchWithTimeout(
20
34
  input: RequestInfo | URL,
21
35
  init: RequestInit = {},
22
36
  timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS,
23
37
  ): Promise<Response> {
24
- const controller = new AbortController();
25
- const id = setTimeout(() => controller.abort(), timeoutMs);
26
- try {
27
- return await fetch(input, { ...init, signal: controller.signal });
28
- } finally {
29
- clearTimeout(id);
30
- }
38
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
39
+ const signal = init.signal
40
+ ? AbortSignal.any([init.signal, timeoutSignal])
41
+ : timeoutSignal;
42
+ return fetch(input, { ...init, signal });
31
43
  }
32
44
 
33
- /** True when an error came from an AbortController.abort() OR a
34
- * `AbortSignal.timeout` firing. Both surface as `Error` with `name`
35
- * set to "AbortError" or "TimeoutError" respectively. Keeping the two
36
- * in one predicate prevents the "I forgot TimeoutError" drift class. */
45
+ /** True when an error came from an `AbortController.abort()` OR an
46
+ * `AbortSignal.timeout()` firing. Both surface as objects whose `name`
47
+ * is "AbortError" or "TimeoutError" respectively depending on the
48
+ * runtime, they may be `Error` subclasses, `DOMException` instances,
49
+ * both, or neither (the jsdom polyfill, for one, breaks the Error
50
+ * prototype chain), so we duck-type on `name` rather than the
51
+ * provenance. Callers always pass values caught from a try/catch, so
52
+ * the universe of inputs is narrow enough that name-matching is
53
+ * unambiguous. Keeping the two names in one predicate prevents the
54
+ * "I forgot TimeoutError" drift class. */
37
55
  export function isAbortError(err: unknown): boolean {
38
- return (
39
- err instanceof Error
40
- && (err.name === "AbortError" || err.name === "TimeoutError")
41
- );
56
+ if (err === null || typeof err !== "object") return false;
57
+ const name = (err as { name?: unknown }).name;
58
+ return name === "AbortError" || name === "TimeoutError";
42
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.11-beta.4",
3
+ "version": "0.0.11-beta.6",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.11-beta.4"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.11-beta.6"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15
 
package/README.md CHANGED
@@ -166,6 +166,12 @@ MIT with [Commons Clause](https://commonsclause.com/) — free for internal and
166
166
 
167
167
  See [CONTRIBUTING.md](./CONTRIBUTING.md). New policies, edge cases, and translations all welcome.
168
168
 
169
+ > **Build before you start.** Run `bun install && bun run build` first. This repo runs
170
+ > failproofai's own hooks on itself, and they resolve the `failproofai` import against the
171
+ > compiled `dist/` bundle — without a build you'll hit `Cannot find package 'failproofai'`
172
+ > hook errors. Rebuild after changing `src/`. See
173
+ > [Build before the in-repo dev hooks will work](./CONTRIBUTING.md#build-before-the-in-repo-dev-hooks-will-work).
174
+
169
175
  ---
170
176
 
171
177
  Built by [Nivedit Jain](https://github.com/NiveditJain) and [Nikita Agarwal](https://github.com/nk-ag).
package/dist/cli.mjs CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // package.json
19
- var version2 = "0.0.11-beta.4";
19
+ var version2 = "0.0.11-beta.6";
20
20
  var init_package = () => {};
21
21
 
22
22
  // src/posthog-key.ts
@@ -8747,7 +8747,10 @@ var init_telemetry = __esm(() => {
8747
8747
 
8748
8748
  // lib/fetch-with-timeout.ts
8749
8749
  function isAbortError(err) {
8750
- return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
8750
+ if (err === null || typeof err !== "object")
8751
+ return false;
8752
+ const name = err.name;
8753
+ return name === "AbortError" || name === "TimeoutError";
8751
8754
  }
8752
8755
 
8753
8756
  // lib/auth/api-server-client.ts
@@ -9900,7 +9903,7 @@ import { realpathSync as realpathSync3 } from "fs";
9900
9903
  import { dirname as dirname8, resolve as resolve16 } from "path";
9901
9904
  import { fileURLToPath as fileURLToPath3 } from "url";
9902
9905
  // package.json
9903
- var version = "0.0.11-beta.4";
9906
+ var version = "0.0.11-beta.6";
9904
9907
 
9905
9908
  // bin/failproofai.mjs
9906
9909
  if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
@@ -1,14 +1,28 @@
1
1
  /**
2
- * Shared fetch wrapper with per-request timeout via AbortController.
2
+ * Shared fetch wrapper with per-request timeout and external-signal
3
+ * composition.
3
4
  *
4
5
  * Replaces three byte-equivalent implementations that had drifted on the
5
6
  * default timeout (15s in two client components, 10s in the server-side
6
7
  * api-server-client). Co-locates the `isAbortError` predicate so callers
7
8
  * can classify timeout vs network failures consistently.
8
9
  *
9
- * Server-side uses `AbortSignal.timeout` where available (cheaper); this
10
- * helper uses the AbortController+setTimeout form because the client
11
- * code-paths need a guaranteed cleanup hook.
10
+ * Built on the platform primitives `AbortSignal.timeout()` which
11
+ * aborts with a typed `DOMException(..., "TimeoutError")` rather than
12
+ * the default bare abort that surfaces as Next.js 16's dev-overlay
13
+ * "signal is aborted without reason" warning — and `AbortSignal.any()`,
14
+ * which composes the timeout signal with any caller-supplied
15
+ * `init.signal` so a component-unmount or parent-controller abort
16
+ * actually stops the in-flight fetch. The prior `AbortController` +
17
+ * `setTimeout` implementation called `controller.abort()` with no
18
+ * reason AND silently dropped `init.signal` by spreading then
19
+ * overwriting it with the controller's own signal.
20
+ *
21
+ * Both APIs are available in every runtime Next.js 16 supports:
22
+ * `AbortSignal.timeout` since Node 17.3 / 16.14, `AbortSignal.any`
23
+ * since Node 20.3 (also backported to 18.17). Next.js 16's minimum
24
+ * runtime (Node >= 20.9, per its engines field) covers both. Browser
25
+ * support: Chrome 116+, Firefox 124+, Safari 17.4+.
12
26
  */
13
27
 
14
28
  /** Hard cap on every fetch using this helper unless overridden. Picked to
@@ -16,27 +30,30 @@
16
30
  * so a slow but successful upstream response still lands. */
17
31
  export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
18
32
 
19
- export async function fetchWithTimeout(
33
+ export function fetchWithTimeout(
20
34
  input: RequestInfo | URL,
21
35
  init: RequestInit = {},
22
36
  timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS,
23
37
  ): Promise<Response> {
24
- const controller = new AbortController();
25
- const id = setTimeout(() => controller.abort(), timeoutMs);
26
- try {
27
- return await fetch(input, { ...init, signal: controller.signal });
28
- } finally {
29
- clearTimeout(id);
30
- }
38
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
39
+ const signal = init.signal
40
+ ? AbortSignal.any([init.signal, timeoutSignal])
41
+ : timeoutSignal;
42
+ return fetch(input, { ...init, signal });
31
43
  }
32
44
 
33
- /** True when an error came from an AbortController.abort() OR a
34
- * `AbortSignal.timeout` firing. Both surface as `Error` with `name`
35
- * set to "AbortError" or "TimeoutError" respectively. Keeping the two
36
- * in one predicate prevents the "I forgot TimeoutError" drift class. */
45
+ /** True when an error came from an `AbortController.abort()` OR an
46
+ * `AbortSignal.timeout()` firing. Both surface as objects whose `name`
47
+ * is "AbortError" or "TimeoutError" respectively depending on the
48
+ * runtime, they may be `Error` subclasses, `DOMException` instances,
49
+ * both, or neither (the jsdom polyfill, for one, breaks the Error
50
+ * prototype chain), so we duck-type on `name` rather than the
51
+ * provenance. Callers always pass values caught from a try/catch, so
52
+ * the universe of inputs is narrow enough that name-matching is
53
+ * unambiguous. Keeping the two names in one predicate prevents the
54
+ * "I forgot TimeoutError" drift class. */
37
55
  export function isAbortError(err: unknown): boolean {
38
- return (
39
- err instanceof Error
40
- && (err.name === "AbortError" || err.name === "TimeoutError")
41
- );
56
+ if (err === null || typeof err !== "object") return false;
57
+ const name = (err as { name?: unknown }).name;
58
+ return name === "AbortError" || name === "TimeoutError";
42
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.11-beta.4",
3
+ "version": "0.0.11-beta.6",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -820,9 +820,20 @@ const ARCHITECT_CAUTION_MIN = 0.35;
820
820
  /** Architect only applies when cowboy isn't itself over-indexing. */
821
821
  const ARCHITECT_COWBOY_MAX_LIFT = 1.0;
822
822
  /** Normalised lift entropy above this (with enough distinct clusters lit)
823
- * → goldfish (genuinely scattered). */
824
- const GOLDFISH_ENTROPY = 0.75;
823
+ * → goldfish (genuinely scattered). Retuned from 0.75 → 0.70 when the lift
824
+ * denominator moved from catalog weights to empirical firing shares
825
+ * (see features.ts BASELINE_SHARE): the empirical baseline reshapes the lift
826
+ * vector, and 0.75 left goldfish under-reachable (< 4% of the population). */
827
+ const GOLDFISH_ENTROPY = 0.70;
825
828
  const GOLDFISH_MIN_NONZERO = 4;
829
+ /** Goldfish also requires the SECOND-highest cluster lift to be meaningfully
830
+ * above baseline. Entropy on its own is satisfied by a uniform-at-baseline
831
+ * lift vector (every cluster firing at typical ambient rate → all lifts ≈ 1.0
832
+ * → entropy = max), which would otherwise collapse every "typical" agent onto
833
+ * goldfish. Requiring secondLift > 1.3 means at least TWO clusters are
834
+ * genuinely over-indexing — the actual definition of scatter, not just
835
+ * "average-volume noise spread across many clusters". */
836
+ const GOLDFISH_MIN_SECOND_LIFT = 1.3;
826
837
  /** When the top-two lifts are within this ratio, resolve deterministically by
827
838
  * the behaviour fingerprint instead of always taking the arithmetic winner —
828
839
  * spreads near-ties across the population without any RNG. */
@@ -888,9 +899,18 @@ export function classifyAgent(result: AuditResult, seed = ""): Classification {
888
899
  return { archetype: "architect", secondary: ARCHETYPES.architect.secondary, ...base };
889
900
  }
890
901
 
891
- // 3. goldfish — genuinely scattered across many clusters.
902
+ // 3. goldfish — genuinely scattered across many clusters. Both the entropy
903
+ // and the second-lift gate must clear: entropy catches "many clusters
904
+ // contribute", secondLift catches "the contribution is actually elevated
905
+ // relative to ambient". Without secondLift, a high-volume average user
906
+ // (lifts ≈ 1.0 everywhere) passes the entropy gate and collapses onto
907
+ // goldfish — that's the bug fixed alongside this gate.
892
908
  const nonZero = MAPPABLE_KEYS.filter((k) => f.clusterLift[k] > 0).length;
893
- if (nonZero >= GOLDFISH_MIN_NONZERO && f.entropy > GOLDFISH_ENTROPY) {
909
+ if (
910
+ nonZero >= GOLDFISH_MIN_NONZERO &&
911
+ f.entropy > GOLDFISH_ENTROPY &&
912
+ f.secondLift >= GOLDFISH_MIN_SECOND_LIFT
913
+ ) {
894
914
  const strongest = rankByLift(f, MAPPABLE_KEYS)[0];
895
915
  return { archetype: "goldfish", secondary: strongest, ...base };
896
916
  }