failproofai 0.0.11-beta.5 → 0.0.11-beta.7
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13ra2jq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b9z5-i._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1-1804f._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00jkjmt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013du6r._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n5_-1w._.js → [root-of-the-server]__08gyb62._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u8h03r._.js → [root-of-the-server]__08v7sn2._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e85wxv._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gfxvb1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cd25c7._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hlrq6y._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ihxdo5._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1vvfde2._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html2canvas_dist_html2canvas_esm_20uq69-.js → node_modules_html2canvas_dist_html2canvas_esm_0q4m0_h.js} +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{39a26r6z12f79.js → 0avgb78edu0i-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1ttc2tjh-zlpe.js → 0fpw1jzeg66_x.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0v_5uc6i2eumo.js → 0hmxjseydp6as.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1_5m8kaezrw_o.js → 0xkxbaa-oo180.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11vc5dmoyrakc.js → 1eq7f419t6ln3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0_i1vnayvdf0q.js → 2452blhu4bcdr.js} +3 -3
- package/.next/standalone/.next/static/chunks/{2ifhvb-ysi1vv.js → 3n28elq6kwpy3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0lhdvxusp8db3.js → 3osmt-t2hsrng.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1btx2c49fk9xt.css → 3wycox197ouus.css} +1 -1
- package/.next/standalone/.next/static/chunks/{306bhgs07dody.js → 41vwoxpkauj1w.js} +1 -1
- package/.next/standalone/app/actions/get-audit-result.ts +14 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +85 -20
- package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +98 -0
- package/.next/standalone/app/audit/_components/rerun-button.tsx +11 -2
- package/.next/standalone/app/audit/_components/return-section.tsx +17 -29
- package/.next/standalone/app/audit/audit-styles.css +120 -0
- package/.next/standalone/app/audit/page.tsx +19 -9
- package/.next/standalone/lib/fetch-with-timeout.ts +37 -20
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +6 -3
- package/lib/fetch-with-timeout.ts +37 -20
- package/package.json +1 -1
- package/src/audit/archetypes.ts +19 -2
- package/src/audit/cache.ts +20 -1
- package/src/audit/dashboard-cache.ts +40 -0
- package/src/audit/features.ts +10 -2
- /package/.next/standalone/.next/static/{TNfsNLuO5dTuOXES0XK8N → 90NFQus9VOnEzD6sP0EyP}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{TNfsNLuO5dTuOXES0XK8N → 90NFQus9VOnEzD6sP0EyP}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{TNfsNLuO5dTuOXES0XK8N → 90NFQus9VOnEzD6sP0EyP}/_ssgManifest.js +0 -0
|
@@ -2064,3 +2064,123 @@
|
|
|
2064
2064
|
.share-dock-fab { transition: none; }
|
|
2065
2065
|
.share-dock-caret { transition: none; }
|
|
2066
2066
|
}
|
|
2067
|
+
|
|
2068
|
+
/* ─────────────────────────────────────────────────────────────────────
|
|
2069
|
+
audit-progress-strip — sticky banner pinned to the top of the viewport
|
|
2070
|
+
while a re-audit is in flight. Pink hard-offset shadow matches the
|
|
2071
|
+
page's `.share-btn:hover` vocabulary. Right-edge pulse is a CSS-only
|
|
2072
|
+
keyframe (transform-only — no layout thrash) and is not a fake
|
|
2073
|
+
progress bar.
|
|
2074
|
+
───────────────────────────────────────────────────────────────────── */
|
|
2075
|
+
.audit-progress-strip {
|
|
2076
|
+
position: sticky;
|
|
2077
|
+
top: 0;
|
|
2078
|
+
left: 0;
|
|
2079
|
+
right: 0;
|
|
2080
|
+
z-index: 60;
|
|
2081
|
+
background: var(--bg-2);
|
|
2082
|
+
border-bottom: 1px solid var(--accent-pink);
|
|
2083
|
+
box-shadow: 0 4px 0 0 var(--accent-pink-shadow);
|
|
2084
|
+
font-family: var(--font-mono);
|
|
2085
|
+
/* Slide in from above on mount */
|
|
2086
|
+
animation: audit-progress-slide-in 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2087
|
+
}
|
|
2088
|
+
.audit-progress-strip--failed {
|
|
2089
|
+
border-bottom-color: var(--accent-pink);
|
|
2090
|
+
background: linear-gradient(to right, var(--accent-pink-bg), var(--bg-2));
|
|
2091
|
+
}
|
|
2092
|
+
.audit-progress-strip__inner {
|
|
2093
|
+
position: relative;
|
|
2094
|
+
max-width: 1480px;
|
|
2095
|
+
margin: 0 auto;
|
|
2096
|
+
padding: 12px clamp(20px, 3vw, 48px);
|
|
2097
|
+
display: flex;
|
|
2098
|
+
align-items: center;
|
|
2099
|
+
justify-content: space-between;
|
|
2100
|
+
gap: 16px;
|
|
2101
|
+
}
|
|
2102
|
+
.audit-progress-strip__label {
|
|
2103
|
+
display: inline-flex;
|
|
2104
|
+
align-items: center;
|
|
2105
|
+
gap: 10px;
|
|
2106
|
+
color: var(--ink);
|
|
2107
|
+
font-size: 12px;
|
|
2108
|
+
letter-spacing: 0.06em;
|
|
2109
|
+
text-transform: lowercase;
|
|
2110
|
+
}
|
|
2111
|
+
.audit-progress-strip--failed .audit-progress-strip__label { color: var(--accent-pink); }
|
|
2112
|
+
.audit-progress-strip__elapsed {
|
|
2113
|
+
font-variant-numeric: tabular-nums;
|
|
2114
|
+
font-size: 12px;
|
|
2115
|
+
color: var(--accent-pink);
|
|
2116
|
+
letter-spacing: 0.08em;
|
|
2117
|
+
}
|
|
2118
|
+
.audit-progress-strip__spinner {
|
|
2119
|
+
width: 9px;
|
|
2120
|
+
height: 9px;
|
|
2121
|
+
border-radius: 50%;
|
|
2122
|
+
background: transparent;
|
|
2123
|
+
border: 1.5px solid var(--accent-pink);
|
|
2124
|
+
border-top-color: transparent;
|
|
2125
|
+
animation: audit-progress-spin 800ms linear infinite;
|
|
2126
|
+
}
|
|
2127
|
+
.audit-progress-strip__x {
|
|
2128
|
+
display: inline-flex;
|
|
2129
|
+
align-items: center;
|
|
2130
|
+
justify-content: center;
|
|
2131
|
+
width: 14px;
|
|
2132
|
+
height: 14px;
|
|
2133
|
+
border: 1px solid var(--accent-pink);
|
|
2134
|
+
color: var(--accent-pink);
|
|
2135
|
+
font-size: 11px;
|
|
2136
|
+
line-height: 1;
|
|
2137
|
+
}
|
|
2138
|
+
.audit-progress-strip__dismiss {
|
|
2139
|
+
background: transparent;
|
|
2140
|
+
border: 1px solid var(--line-2);
|
|
2141
|
+
color: var(--ink-2);
|
|
2142
|
+
padding: 5px 10px;
|
|
2143
|
+
font-family: var(--font-mono);
|
|
2144
|
+
font-size: 11px;
|
|
2145
|
+
text-transform: lowercase;
|
|
2146
|
+
letter-spacing: 0.06em;
|
|
2147
|
+
cursor: pointer;
|
|
2148
|
+
transition: border-color 140ms ease, color 140ms ease;
|
|
2149
|
+
}
|
|
2150
|
+
.audit-progress-strip__dismiss:hover {
|
|
2151
|
+
border-color: var(--accent-pink);
|
|
2152
|
+
color: var(--accent-pink);
|
|
2153
|
+
}
|
|
2154
|
+
.audit-progress-strip__pulse {
|
|
2155
|
+
position: absolute;
|
|
2156
|
+
bottom: -1px;
|
|
2157
|
+
left: 0;
|
|
2158
|
+
right: 0;
|
|
2159
|
+
height: 1px;
|
|
2160
|
+
background: linear-gradient(
|
|
2161
|
+
90deg,
|
|
2162
|
+
transparent 0%,
|
|
2163
|
+
var(--accent-pink) 20%,
|
|
2164
|
+
var(--accent-pink) 80%,
|
|
2165
|
+
transparent 100%
|
|
2166
|
+
);
|
|
2167
|
+
transform-origin: left center;
|
|
2168
|
+
animation: audit-progress-pulse 2400ms cubic-bezier(0.55, 0.1, 0.45, 0.9) infinite;
|
|
2169
|
+
}
|
|
2170
|
+
@keyframes audit-progress-spin {
|
|
2171
|
+
to { transform: rotate(360deg); }
|
|
2172
|
+
}
|
|
2173
|
+
@keyframes audit-progress-pulse {
|
|
2174
|
+
0% { transform: scaleX(0) translateX(0%); opacity: 0.4; }
|
|
2175
|
+
50% { transform: scaleX(0.6) translateX(20%); opacity: 1; }
|
|
2176
|
+
100% { transform: scaleX(0) translateX(100%); opacity: 0.2; }
|
|
2177
|
+
}
|
|
2178
|
+
@keyframes audit-progress-slide-in {
|
|
2179
|
+
from { transform: translateY(-100%); opacity: 0; }
|
|
2180
|
+
to { transform: translateY(0); opacity: 1; }
|
|
2181
|
+
}
|
|
2182
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2183
|
+
.audit-progress-strip { animation: none; }
|
|
2184
|
+
.audit-progress-strip__spinner { animation: none; border-top-color: var(--accent-pink); }
|
|
2185
|
+
.audit-progress-strip__pulse { animation: none; opacity: 0.4; transform: scaleX(0.4) translateX(30%); }
|
|
2186
|
+
}
|
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
34
|
-
* `AbortSignal.timeout` firing. Both surface as
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
40
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.11-beta.7",
|
|
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.
|
|
12
|
+
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.11-beta.7"},"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/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.
|
|
19
|
+
var version2 = "0.0.11-beta.7";
|
|
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
|
-
|
|
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.
|
|
9906
|
+
var version = "0.0.11-beta.7";
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
34
|
-
* `AbortSignal.timeout` firing. Both surface as
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
40
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.11-beta.7",
|
|
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"
|
package/src/audit/archetypes.ts
CHANGED
|
@@ -826,6 +826,14 @@ const ARCHITECT_COWBOY_MAX_LIFT = 1.0;
|
|
|
826
826
|
* vector, and 0.75 left goldfish under-reachable (< 4% of the population). */
|
|
827
827
|
const GOLDFISH_ENTROPY = 0.70;
|
|
828
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;
|
|
829
837
|
/** When the top-two lifts are within this ratio, resolve deterministically by
|
|
830
838
|
* the behaviour fingerprint instead of always taking the arithmetic winner —
|
|
831
839
|
* spreads near-ties across the population without any RNG. */
|
|
@@ -891,9 +899,18 @@ export function classifyAgent(result: AuditResult, seed = ""): Classification {
|
|
|
891
899
|
return { archetype: "architect", secondary: ARCHETYPES.architect.secondary, ...base };
|
|
892
900
|
}
|
|
893
901
|
|
|
894
|
-
// 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.
|
|
895
908
|
const nonZero = MAPPABLE_KEYS.filter((k) => f.clusterLift[k] > 0).length;
|
|
896
|
-
if (
|
|
909
|
+
if (
|
|
910
|
+
nonZero >= GOLDFISH_MIN_NONZERO &&
|
|
911
|
+
f.entropy > GOLDFISH_ENTROPY &&
|
|
912
|
+
f.secondLift >= GOLDFISH_MIN_SECOND_LIFT
|
|
913
|
+
) {
|
|
897
914
|
const strongest = rankByLift(f, MAPPABLE_KEYS)[0];
|
|
898
915
|
return { archetype: "goldfish", secondary: strongest, ...base };
|
|
899
916
|
}
|
package/src/audit/cache.ts
CHANGED
|
@@ -61,12 +61,26 @@ function getCachePathFor(transcriptPath: string): string {
|
|
|
61
61
|
* silently rendered them as `cwd: undefined` (dropped from
|
|
62
62
|
* `projectsScanned`) and `eventsScanned: 0`. Rejecting v1 forces a
|
|
63
63
|
* re-scan so the new fields are populated correctly.
|
|
64
|
+
*
|
|
65
|
+
* v3: `CacheEntry` gained `cachedAt` to support the 7-day TTL below. v2
|
|
66
|
+
* entries lack the field; without a bump the age check would treat
|
|
67
|
+
* `cachedAt: undefined` as `NaN` (passes the `<= TTL` comparison
|
|
68
|
+
* accidentally on some paths) so we reject them outright and force a
|
|
69
|
+
* re-scan.
|
|
64
70
|
*/
|
|
65
|
-
export const CACHE_SCHEMA_VERSION =
|
|
71
|
+
export const CACHE_SCHEMA_VERSION = 3;
|
|
72
|
+
|
|
73
|
+
/** Hard expiry: a cached transcript result is rejected on read once it's
|
|
74
|
+
* this old. Keeps the audit pipeline honest about long-lived results that
|
|
75
|
+
* may no longer reflect current detector intent even if the transcript
|
|
76
|
+
* bytes and policy hashes haven't changed. */
|
|
77
|
+
export const CACHE_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
66
78
|
|
|
67
79
|
interface CacheEntry {
|
|
68
80
|
/** Bumped whenever the on-disk shape changes incompatibly. */
|
|
69
81
|
schemaVersion: number;
|
|
82
|
+
/** ms since epoch when the entry was written. Drives the 7-day TTL. */
|
|
83
|
+
cachedAt: number;
|
|
70
84
|
mtimeMs: number;
|
|
71
85
|
sizeBytes: number;
|
|
72
86
|
engineVersion: string;
|
|
@@ -90,6 +104,10 @@ export function readCachedTranscriptResult(
|
|
|
90
104
|
if (entry.sizeBytes !== sizeBytes) return null;
|
|
91
105
|
if (entry.engineVersion !== getEngineVersion()) return null;
|
|
92
106
|
if (entry.detectorVersion !== getDetectorVersion()) return null;
|
|
107
|
+
// Number.isFinite (not typeof) so a malformed JSON `Infinity` /
|
|
108
|
+
// `NaN` is rejected — those would otherwise bypass the TTL check
|
|
109
|
+
// and pin a stale entry as valid forever.
|
|
110
|
+
if (!Number.isFinite(entry.cachedAt) || Date.now() - (entry.cachedAt as number) > CACHE_TTL_MS) return null;
|
|
93
111
|
return entry.result ?? null;
|
|
94
112
|
} catch {
|
|
95
113
|
return null;
|
|
@@ -108,6 +126,7 @@ export function writeCachedTranscriptResult(
|
|
|
108
126
|
mkdirSync(join(homedir(), ".failproofai", "cache", "audit"), { recursive: true });
|
|
109
127
|
const entry: CacheEntry = {
|
|
110
128
|
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
129
|
+
cachedAt: Date.now(),
|
|
111
130
|
mtimeMs,
|
|
112
131
|
sizeBytes,
|
|
113
132
|
engineVersion: getEngineVersion(),
|
|
@@ -18,6 +18,12 @@ import type { AuditResult, RunAuditOptions } from "./types";
|
|
|
18
18
|
|
|
19
19
|
const DEFAULT_MAX_AGE_MINUTES = 30;
|
|
20
20
|
|
|
21
|
+
/** Hard expiry: the dashboard cache is rejected on read once it's this old.
|
|
22
|
+
* Matches the per-transcript TTL in `cache.ts` so the whole audit surface
|
|
23
|
+
* has a consistent 7-day freshness window — past that, `/audit` falls
|
|
24
|
+
* through to its empty state and nudges the user to re-audit. */
|
|
25
|
+
const DASHBOARD_CACHE_TTL_MINUTES = 7 * 24 * 60;
|
|
26
|
+
|
|
21
27
|
/**
|
|
22
28
|
* Bump whenever the on-disk shape of a cached entry changes in a way the
|
|
23
29
|
* reader can't tolerate (added required field, renamed key, swapped result
|
|
@@ -75,6 +81,9 @@ export function readDashboardCache(): DashboardCacheEntry | null {
|
|
|
75
81
|
if (entry.schemaVersion !== DASHBOARD_CACHE_SCHEMA_VERSION) {
|
|
76
82
|
return null;
|
|
77
83
|
}
|
|
84
|
+
if (isCacheStale(entry.cachedAt, DASHBOARD_CACHE_TTL_MINUTES)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
78
87
|
return entry;
|
|
79
88
|
} catch {
|
|
80
89
|
return null;
|
|
@@ -109,3 +118,34 @@ export function isCacheStale(cachedAt: string, maxAgeMinutes: number = DEFAULT_M
|
|
|
109
118
|
const ageMs = Date.now() - cachedMs;
|
|
110
119
|
return ageMs > maxAgeMinutes * 60_000;
|
|
111
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read just the `cachedAt` timestamp from the dashboard cache file,
|
|
124
|
+
* **bypassing** the TTL check. Used by the empty-state path to tell apart
|
|
125
|
+
* "no audit has ever run" from "your last audit aged out". A non-null
|
|
126
|
+
* return whose age exceeds `DASHBOARD_CACHE_TTL_MINUTES` means the cache
|
|
127
|
+
* was rejected by `readDashboardCache()` for being expired (rather than
|
|
128
|
+
* missing or schema-incompatible).
|
|
129
|
+
*/
|
|
130
|
+
export function readDashboardCacheMeta(): { cachedAt: string } | null {
|
|
131
|
+
const cachePath = getCachePath();
|
|
132
|
+
if (!existsSync(cachePath)) return null;
|
|
133
|
+
try {
|
|
134
|
+
const raw = readFileSync(cachePath, "utf-8");
|
|
135
|
+
const entry = JSON.parse(raw) as Partial<DashboardCacheEntry>;
|
|
136
|
+
// Require schema-version match + a parseable ISO timestamp before
|
|
137
|
+
// surfacing the entry as "this user's audit aged out". A
|
|
138
|
+
// schema-incompatible entry is structurally indistinguishable from
|
|
139
|
+
// "no audit has ever run", so it should fall through to the
|
|
140
|
+
// first-run empty copy, not the expired banner.
|
|
141
|
+
if (
|
|
142
|
+
!entry
|
|
143
|
+
|| entry.schemaVersion !== DASHBOARD_CACHE_SCHEMA_VERSION
|
|
144
|
+
|| typeof entry.cachedAt !== "string"
|
|
145
|
+
|| Number.isNaN(new Date(entry.cachedAt).getTime())
|
|
146
|
+
) return null;
|
|
147
|
+
return { cachedAt: entry.cachedAt };
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/audit/features.ts
CHANGED
|
@@ -249,6 +249,11 @@ export interface AuditFeatures {
|
|
|
249
249
|
clusterLift: Record<ArchetypeKey, number>;
|
|
250
250
|
/** Normalised entropy of the lift vector (goldfish detector). */
|
|
251
251
|
entropy: number;
|
|
252
|
+
/** Second-highest cluster lift. Goldfish requires this to be elevated:
|
|
253
|
+
* high entropy alone is satisfied by a uniform-at-baseline lift vector
|
|
254
|
+
* (every cluster fires at ambient rate → lifts ≈ 1.0 → entropy = max),
|
|
255
|
+
* which would otherwise collapse every "typical" agent onto goldfish. */
|
|
256
|
+
secondLift: number;
|
|
252
257
|
/** Share of total signal coming from the two architect caution detectors. */
|
|
253
258
|
cautionShare: number;
|
|
254
259
|
cowboyLift: number;
|
|
@@ -288,7 +293,10 @@ export function deriveFeatures(result: AuditResult, seed = ""): AuditFeatures {
|
|
|
288
293
|
clusterLift[k] = BASELINE_SHARE[k] > 0 ? share / BASELINE_SHARE[k] : 0;
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
const
|
|
296
|
+
const liftVec = MAPPABLE_KEYS.map((k) => clusterLift[k]);
|
|
297
|
+
const entropy = normalizedEntropy(liftVec);
|
|
298
|
+
const sortedLifts = [...liftVec].sort((a, b) => b - a);
|
|
299
|
+
const secondLift = sortedLifts[1] ?? 0;
|
|
292
300
|
const cautionShare = totalSignal > 0 ? cautionRaw / totalSignal : 0;
|
|
293
301
|
const faultRate = totalHits / events;
|
|
294
302
|
|
|
@@ -300,7 +308,7 @@ export function deriveFeatures(result: AuditResult, seed = ""): AuditFeatures {
|
|
|
300
308
|
|
|
301
309
|
return {
|
|
302
310
|
events, sessions, totalHits, faultRate,
|
|
303
|
-
clusterRaw, totalSignal, clusterLift, entropy,
|
|
311
|
+
clusterRaw, totalSignal, clusterLift, entropy, secondLift,
|
|
304
312
|
cautionShare, cowboyLift: clusterLift.cowboy, fingerprint,
|
|
305
313
|
};
|
|
306
314
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|