@polishd/next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +262 -0
- package/DATABASE.md +147 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/init.mjs +224 -0
- package/dist/ai/actions.d.ts +23 -0
- package/dist/ai/actions.d.ts.map +1 -0
- package/dist/ai/actions.js +58 -0
- package/dist/ai/actions.js.map +1 -0
- package/dist/ai/digest.d.ts +9 -0
- package/dist/ai/digest.d.ts.map +1 -0
- package/dist/ai/digest.js +108 -0
- package/dist/ai/digest.js.map +1 -0
- package/dist/ai/github.d.ts +51 -0
- package/dist/ai/github.d.ts.map +1 -0
- package/dist/ai/github.js +175 -0
- package/dist/ai/github.js.map +1 -0
- package/dist/ai/guard.d.ts +43 -0
- package/dist/ai/guard.d.ts.map +1 -0
- package/dist/ai/guard.js +57 -0
- package/dist/ai/guard.js.map +1 -0
- package/dist/ai/issues.d.ts +21 -0
- package/dist/ai/issues.d.ts.map +1 -0
- package/dist/ai/issues.js +291 -0
- package/dist/ai/issues.js.map +1 -0
- package/dist/ai/profile.d.ts +26 -0
- package/dist/ai/profile.d.ts.map +1 -0
- package/dist/ai/profile.js +160 -0
- package/dist/ai/profile.js.map +1 -0
- package/dist/ai/providers.d.ts +40 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +142 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/scan.d.ts +38 -0
- package/dist/ai/scan.d.ts.map +1 -0
- package/dist/ai/scan.js +274 -0
- package/dist/ai/scan.js.map +1 -0
- package/dist/ai/settings.d.ts +31 -0
- package/dist/ai/settings.d.ts.map +1 -0
- package/dist/ai/settings.js +182 -0
- package/dist/ai/settings.js.map +1 -0
- package/dist/ai/summary.d.ts +26 -0
- package/dist/ai/summary.d.ts.map +1 -0
- package/dist/ai/summary.js +260 -0
- package/dist/ai/summary.js.map +1 -0
- package/dist/ai/types.d.ts +188 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +13 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/client/init.d.ts +14 -0
- package/dist/client/init.d.ts.map +1 -0
- package/dist/client/init.js +274 -0
- package/dist/client/init.js.map +1 -0
- package/dist/client/monitor.d.ts +23 -0
- package/dist/client/monitor.d.ts.map +1 -0
- package/dist/client/monitor.js +176 -0
- package/dist/client/monitor.js.map +1 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/elements.d.ts +6 -0
- package/dist/dashboard/elements.d.ts.map +1 -0
- package/dist/dashboard/elements.js +35 -0
- package/dist/dashboard/elements.js.map +1 -0
- package/dist/dashboard/features.d.ts +6 -0
- package/dist/dashboard/features.d.ts.map +1 -0
- package/dist/dashboard/features.js +28 -0
- package/dist/dashboard/features.js.map +1 -0
- package/dist/dashboard/index.d.ts +54 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +140 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/journeys.d.ts +5 -0
- package/dist/dashboard/journeys.d.ts.map +1 -0
- package/dist/dashboard/journeys.js +158 -0
- package/dist/dashboard/journeys.js.map +1 -0
- package/dist/dashboard/pages.d.ts +5 -0
- package/dist/dashboard/pages.d.ts.map +1 -0
- package/dist/dashboard/pages.js +104 -0
- package/dist/dashboard/pages.js.map +1 -0
- package/dist/dashboard/summary.d.ts +13 -0
- package/dist/dashboard/summary.d.ts.map +1 -0
- package/dist/dashboard/summary.js +333 -0
- package/dist/dashboard/summary.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +29 -0
- package/dist/proxy.js.map +1 -0
- package/dist/route.d.ts +24 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +37 -0
- package/dist/route.js.map +1 -0
- package/dist/server/ingest.d.ts +14 -0
- package/dist/server/ingest.d.ts.map +1 -0
- package/dist/server/ingest.js +73 -0
- package/dist/server/ingest.js.map +1 -0
- package/dist/server/queries.d.ts +245 -0
- package/dist/server/queries.d.ts.map +1 -0
- package/dist/server/queries.js +547 -0
- package/dist/server/queries.js.map +1 -0
- package/dist/server/store.d.ts +29 -0
- package/dist/server/store.d.ts.map +1 -0
- package/dist/server/store.js +359 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +27 -0
- package/dist/shared/types.js.map +1 -0
- package/docs/SETUP.md +315 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# @polishd/next
|
|
2
|
+
|
|
3
|
+
> **What gets measured gets improved.**
|
|
4
|
+
|
|
5
|
+
Drop-in **product analytics** for Next.js (App Router). Capture real user
|
|
6
|
+
behavioral signals — rage clicks, dead clicks, scroll depth, JS errors, web
|
|
7
|
+
vitals, component engagement — and explore them on a built-in, Vercel-style
|
|
8
|
+
dashboard at `/polishd`.
|
|
9
|
+
|
|
10
|
+
No third-party service. Events go to your own database: **SQLite in dev**
|
|
11
|
+
(zero-config, built into Node), **Postgres in production**. Anonymous by design —
|
|
12
|
+
one httpOnly UUID cookie per session, no fingerprinting, no PII.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
| | |
|
|
19
|
+
|---|---|
|
|
20
|
+
| **[docs/SETUP.md](./docs/SETUP.md)** | Full walkthrough — every variant, verification, troubleshooting |
|
|
21
|
+
| **[AGENTS.md](./AGENTS.md)** | Install procedure written for AI coding agents |
|
|
22
|
+
| **[DATABASE.md](./DATABASE.md)** | Production database setup, schema, retention |
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Next.js **15+**, App Router (no Pages Router support)
|
|
27
|
+
- Node **22.5+** (the dev store uses the built-in `node:sqlite`)
|
|
28
|
+
- Tailwind CSS v3 or v4, if you want the dashboard styled — see [Styling](#styling-required)
|
|
29
|
+
- Postgres in production; nothing extra for local development
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @polishd/next
|
|
35
|
+
npx polishd init
|
|
36
|
+
npm run dev # then visit /polishd
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's it locally — events write to `.polishd/analytics.db` with no
|
|
40
|
+
configuration. `init` detects your layout (`src/` or root), writes the four
|
|
41
|
+
files below, adds `.polishd/` to `.gitignore`, wires Tailwind, and skips
|
|
42
|
+
anything that already exists (`--force` to overwrite, `--dry-run` to preview,
|
|
43
|
+
`--js` for JavaScript, `--config` to also emit a `polishd.config.ts`).
|
|
44
|
+
|
|
45
|
+
`pg` installs automatically as an optional dependency and is loaded only when
|
|
46
|
+
you point Polishd at a Postgres URL.
|
|
47
|
+
|
|
48
|
+
## What gets wired up
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/proxy.ts (middleware.ts on Next 15) — sets the session cookie
|
|
52
|
+
import type { NextRequest } from "next/server";
|
|
53
|
+
import { withPolishdSession } from "@polishd/next/proxy";
|
|
54
|
+
|
|
55
|
+
export function proxy(request: NextRequest) {
|
|
56
|
+
return withPolishdSession(request);
|
|
57
|
+
}
|
|
58
|
+
// Must be an inline literal — Next can't import config.matcher.
|
|
59
|
+
export const config = {
|
|
60
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/instrumentation-client.ts — starts capture before hydration
|
|
64
|
+
import { initPolishd } from "@polishd/next/client";
|
|
65
|
+
initPolishd();
|
|
66
|
+
|
|
67
|
+
// src/app/api/polishd/route.ts — the ingest endpoint
|
|
68
|
+
import { createPolishdRoute } from "@polishd/next/route";
|
|
69
|
+
export const runtime = "nodejs"; // must be declared here, not re-exported
|
|
70
|
+
export const dynamic = "force-dynamic";
|
|
71
|
+
export const POST = createPolishdRoute();
|
|
72
|
+
|
|
73
|
+
// src/app/polishd/page.tsx — the dashboard (unguarded by default)
|
|
74
|
+
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
75
|
+
export const runtime = "nodejs"; // Next requires these inline in the page
|
|
76
|
+
export const dynamic = "force-dynamic";
|
|
77
|
+
export default createPolishdPage();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **Route segment config can't be re-exported.** Next statically parses
|
|
81
|
+
> `runtime` and `dynamic`, so `export { POST, runtime, dynamic } from
|
|
82
|
+
> "@polishd/next/route"` fails the build — declare them inline as above. The same
|
|
83
|
+
> applies to the dashboard page.
|
|
84
|
+
|
|
85
|
+
> **Get the matcher right.** Next statically parses `config.matcher`, so it must
|
|
86
|
+
> be an inline literal in your proxy file — it can't be imported. `npx @polishd/next
|
|
87
|
+
> init` writes the correct one for you. Excluding all of `/api` is load-bearing:
|
|
88
|
+
> under Next 16 + Turbopack, a proxy matcher that touches any `/api/*` route
|
|
89
|
+
> breaks resolution for the entire `/api` segment.
|
|
90
|
+
|
|
91
|
+
## Styling (required)
|
|
92
|
+
|
|
93
|
+
The dashboard is built from Tailwind utility classes and ships **no stylesheet
|
|
94
|
+
of its own**, so Tailwind has to scan the package's `dist` — it won't find it by
|
|
95
|
+
default. `init` wires this up for you; if you're doing it by hand:
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
/* Tailwind v4 — in the CSS file that imports tailwind */
|
|
99
|
+
@import "tailwindcss";
|
|
100
|
+
@source "../../node_modules/@polishd/next/dist"; /* relative to this file */
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
/* Tailwind v3 — in tailwind.config.ts */
|
|
105
|
+
content: [
|
|
106
|
+
"./src/**/*.{ts,tsx}",
|
|
107
|
+
"./node_modules/@polishd/next/dist/**/*.js",
|
|
108
|
+
],
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Skip this and `/polishd` renders as unstyled HTML. Tailwind v4's automatic content
|
|
112
|
+
detection deliberately ignores `node_modules`, so the `@source` line is not
|
|
113
|
+
optional.
|
|
114
|
+
|
|
115
|
+
## Protecting the dashboard
|
|
116
|
+
|
|
117
|
+
By default `/polishd` is **public** (a dev-only console warning reminds you).
|
|
118
|
+
Gate it with an `authenticate` callback, and optionally render your own sign-in
|
|
119
|
+
UI when it fails:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
123
|
+
import { isAuthenticated } from "@/lib/auth";
|
|
124
|
+
import { MyLogin } from "./login";
|
|
125
|
+
|
|
126
|
+
export const runtime = "nodejs";
|
|
127
|
+
export const dynamic = "force-dynamic";
|
|
128
|
+
|
|
129
|
+
export default createPolishdPage({
|
|
130
|
+
authenticate: isAuthenticated, // () => boolean | Promise<boolean>
|
|
131
|
+
unauthorized: <MyLogin />, // optional; a minimal screen otherwise
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The same callback also guards the AI **server actions** (generate summary, save
|
|
136
|
+
settings, scan codebase, file GitHub issues). That matters: a server action is
|
|
137
|
+
an addressable POST endpoint whose id ships in the public client bundle, so
|
|
138
|
+
gating only the page would leave those actions callable by anyone. They re-run
|
|
139
|
+
`authenticate` against the caller's cookies on every call, and deny when no
|
|
140
|
+
dashboard page has registered a policy.
|
|
141
|
+
|
|
142
|
+
If you pass no `authenticate`, the dashboard **and** its actions are public —
|
|
143
|
+
anyone who finds the URL can read your analytics, change the configured model
|
|
144
|
+
and API base URL, and spend your model tokens. Always gate it in production.
|
|
145
|
+
|
|
146
|
+
## AI summary
|
|
147
|
+
|
|
148
|
+
The dashboard's top card turns the captured signals into a plain-English story
|
|
149
|
+
of how people are actually using your site, followed by a **wins** list (what's
|
|
150
|
+
working) and a **losses** list (specific problems). Losses are held to a higher
|
|
151
|
+
bar: each must cite the exact page, selector, or component from the data —
|
|
152
|
+
citations that don't appear in the analytics are discarded as hallucinations,
|
|
153
|
+
and the rest are matched to your source files so every loss shows the file it
|
|
154
|
+
lives in (or an honest "not matched to source" tag for dynamic content). It's
|
|
155
|
+
**bring-your-own-key**: on first visit the card walks you through a four-step
|
|
156
|
+
setup — connect a model (Anthropic, OpenAI, any OpenAI-compatible endpoint, or
|
|
157
|
+
Google), describe your site, scan your codebase, and pick a refresh cadence.
|
|
158
|
+
After that the card is just the story: the summary, when it was generated, a
|
|
159
|
+
**refresh** icon to force a regenerate, and a **gear** that opens the full
|
|
160
|
+
settings when you need them. Nothing is sent to a model until you ask.
|
|
161
|
+
|
|
162
|
+
It's built to spend as few tokens as possible:
|
|
163
|
+
|
|
164
|
+
- The model never sees raw events — only a compact, server-side **digest** of the
|
|
165
|
+
already-aggregated numbers (a few hundred tokens for an entire site).
|
|
166
|
+
- The digest is **fingerprinted** and the summary is **cached**. Re-opening the
|
|
167
|
+
dashboard costs nothing, and a regenerate with no new data returns the cached
|
|
168
|
+
text without a model call.
|
|
169
|
+
- The output is capped to a single tight paragraph.
|
|
170
|
+
|
|
171
|
+
**Auto-refresh.** Pick a cadence (manual / daily / weekly) during setup or in
|
|
172
|
+
Settings. When you open the dashboard past the cadence and the data has
|
|
173
|
+
actually changed, the summary regenerates in the background after the page is
|
|
174
|
+
served — no external cron, serverless-friendly. Unchanged data never triggers
|
|
175
|
+
a model call, whatever the cadence.
|
|
176
|
+
|
|
177
|
+
The API key is stored server-side (in your database) and is **never** sent back
|
|
178
|
+
to the browser — the UI only ever shows whether one is set. You can also preset
|
|
179
|
+
everything from the environment (handy for CI / shared deploys); dashboard
|
|
180
|
+
settings take precedence.
|
|
181
|
+
|
|
182
|
+
**Where to get a key:**
|
|
183
|
+
|
|
184
|
+
| Provider | Get a key | Notes |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Anthropic (Claude) | [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) | default model `claude-opus-4-8` |
|
|
187
|
+
| OpenAI | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) | default model `gpt-4o-mini` |
|
|
188
|
+
| OpenAI-compatible | e.g. [openrouter.ai/keys](https://openrouter.ai/keys), [console.groq.com/keys](https://console.groq.com/keys) | also set **Base URL** |
|
|
189
|
+
| Google (Gemini) | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) | default model `gemini-1.5-flash` |
|
|
190
|
+
|
|
191
|
+
### Project profile — the one-time setup scan
|
|
192
|
+
|
|
193
|
+
To make the summary understand your *code* — not just the numbers — run the
|
|
194
|
+
one-time **Scan codebase** step from the card's Project profile strip. Polishd
|
|
195
|
+
reads your app's source from disk (pages and layouts first, then component
|
|
196
|
+
files, under a hard token budget), and asks the model to write a compact
|
|
197
|
+
profile: what the site is for, a map of its routes, and every interactive
|
|
198
|
+
component by its exact identifier. Optionally tell it your **target audience**
|
|
199
|
+
and **ideology / values** in Settings so the analysis judges the site by your
|
|
200
|
+
goals.
|
|
201
|
+
|
|
202
|
+
The profile is cached in your database and injected into every summary as
|
|
203
|
+
authoritative context, so:
|
|
204
|
+
|
|
205
|
+
- **Summaries never re-read source.** One scan, then pure digest + profile.
|
|
206
|
+
- **The codebase is only touched again** when analytics mention a component the
|
|
207
|
+
profile doesn't cover — a tiny targeted read of just the files naming it —
|
|
208
|
+
or when you explicitly re-scan (the strip shows a hint when new components
|
|
209
|
+
appear).
|
|
210
|
+
- On serverless hosts where source isn't on disk, scan in local dev; the saved
|
|
211
|
+
profile keeps serving in production.
|
|
212
|
+
|
|
213
|
+
> **What the model sees.** A compact, server-side digest of the aggregated
|
|
214
|
+
> analytics (page paths, element selectors, `data-component` names, sample
|
|
215
|
+
> text, error messages), the cached project profile, and the description you
|
|
216
|
+
> provide. Your source is read only during an explicit scan (or a targeted
|
|
217
|
+
> gap-fill), and always under strict size budgets.
|
|
218
|
+
|
|
219
|
+
## Component-level tracking
|
|
220
|
+
|
|
221
|
+
Wrap any element to track it explicitly. Interactive controls get hover + click
|
|
222
|
+
attribution; pass `content` for articles/regions to also measure viewport time,
|
|
223
|
+
scroll depth, and rendered size.
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import { PolishdMonitor } from "@polishd/next/client";
|
|
227
|
+
|
|
228
|
+
<PolishdMonitor name="listen-button">
|
|
229
|
+
<ListenButton />
|
|
230
|
+
</PolishdMonitor>
|
|
231
|
+
|
|
232
|
+
<PolishdMonitor name={slug} content className="block">
|
|
233
|
+
<Article />
|
|
234
|
+
</PolishdMonitor>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Configuration
|
|
238
|
+
|
|
239
|
+
Defaults suit a low-traffic site. Override via `polishd.config.ts` and pass it to
|
|
240
|
+
`initPolishd`:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// polishd.config.ts
|
|
244
|
+
import { definePolishdConfig } from "@polishd/next";
|
|
245
|
+
export default definePolishdConfig({
|
|
246
|
+
sampleRate: 1,
|
|
247
|
+
rageClick: { count: 3, windowMs: 500 },
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// src/instrumentation-client.ts
|
|
253
|
+
import { initPolishd } from "@polishd/next/client";
|
|
254
|
+
import config from "../polishd.config";
|
|
255
|
+
initPolishd(config);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Environment variables
|
|
259
|
+
|
|
260
|
+
| Var | When | Purpose |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `POLISHD_DATABASE_URL` | production | Pooled Postgres connection string — enables capture |
|
|
263
|
+
| `POLISHD_DB_PATH` | dev (optional) | Custom SQLite path (default `.polishd/analytics.db`) |
|
|
264
|
+
| `POLISHD_AI_PROVIDER` | AI (optional) | `anthropic` \| `openai` \| `openai-compatible` \| `google` |
|
|
265
|
+
| `POLISHD_AI_MODEL` | AI (optional) | Model id (defaults per provider, e.g. `claude-opus-4-8`) |
|
|
266
|
+
| `POLISHD_AI_API_KEY` | AI (optional) | Model API key — preset instead of using the dashboard |
|
|
267
|
+
| `POLISHD_AI_BASE_URL` | AI (optional) | Base URL for `openai-compatible` providers |
|
|
268
|
+
| `POLISHD_AI_INSTRUCTIONS` / `POLISHD_AI_CONTEXT` | AI (optional) | Default instructions / site description |
|
|
269
|
+
| `POLISHD_AI_AUDIENCE` / `POLISHD_AI_IDEOLOGY` | AI (optional) | Target audience / product values for the profile scan |
|
|
270
|
+
| `POLISHD_AI_SOURCE_DIRS` | AI (optional) | Comma-separated folders to scan (default `src`/`app`/`components`/`pages`/`lib`) |
|
|
271
|
+
| `POLISHD_AI_REFRESH_CADENCE` | AI (optional) | `manual` \| `daily` \| `weekly` — summary auto-refresh cadence |
|
|
272
|
+
| `POLISHD_GITHUB_REPO` | GitHub (optional) | Repository as `owner/repo` — enables filing bugs from AI-found losses |
|
|
273
|
+
| `POLISHD_GITHUB_TOKEN` | GitHub (optional) | Fine-grained PAT: Contents (read), Issues & Pull requests (write) |
|
|
274
|
+
| `POLISHD_GITHUB_AUTO_ISSUES` | GitHub (optional) | `true` — file a GitHub issue automatically for each new problem a summary finds |
|
|
275
|
+
|
|
276
|
+
AI settings are optional — the dashboard's Settings panel configures the same
|
|
277
|
+
fields, and what you save there overrides these env defaults.
|
|
278
|
+
|
|
279
|
+
Local dev needs nothing. If no writable store is available the package degrades
|
|
280
|
+
to a safe no-op (a console warning, dashboard notice) — your app never breaks.
|
|
281
|
+
See [DATABASE.md](./DATABASE.md) for production setup.
|
|
282
|
+
|
|
283
|
+
## Entry points
|
|
284
|
+
|
|
285
|
+
| Import | Contents |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `@polishd/next` | config, `definePolishdConfig`, event types (isomorphic) |
|
|
288
|
+
| `@polishd/next/client` | `initPolishd`, `PolishdMonitor` |
|
|
289
|
+
| `@polishd/next/server` | store, ingest, queries, `withPolishdSession` (Node only) |
|
|
290
|
+
| `@polishd/next/route` | `POST`, `createPolishdRoute` |
|
|
291
|
+
| `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` |
|
|
292
|
+
| `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard`, `loadPolishdDashboardData` |
|
|
293
|
+
|
|
294
|
+
## Development
|
|
295
|
+
|
|
296
|
+
This repository contains the package only. To work on it:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npm install
|
|
300
|
+
npm run build # tsc -> dist (ESM + .d.ts)
|
|
301
|
+
npm run typecheck
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
To test a change against a real app, `npm pack` and install the tarball into a
|
|
305
|
+
scratch Next.js project — that exercises the published `exports` map and the
|
|
306
|
+
"use client"/"use server" boundaries the way a consumer will.
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT — see [LICENSE](./LICENSE).
|
package/bin/init.mjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx @polishd/next init` — scaffold the glue files a Next.js app needs to wire
|
|
4
|
+
* up Polishd. Node builtins only; zero runtime dependencies.
|
|
5
|
+
*
|
|
6
|
+
* Flags:
|
|
7
|
+
* --force overwrite existing files
|
|
8
|
+
* --dry-run print what would happen, write nothing
|
|
9
|
+
* --js emit .js/.jsx instead of TypeScript
|
|
10
|
+
* --config also write a starter polishd.config.ts
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const args = new Set(process.argv.slice(2));
|
|
16
|
+
const FORCE = args.has("--force");
|
|
17
|
+
const DRY = args.has("--dry-run");
|
|
18
|
+
const JS = args.has("--js");
|
|
19
|
+
const WANT_CONFIG = args.has("--config");
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
|
|
22
|
+
const c = {
|
|
23
|
+
ok: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
24
|
+
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
25
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
26
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── Detect project layout ────────────────────────────────────────────────────
|
|
30
|
+
const hasSrcApp = existsSync(join(cwd, "src", "app"));
|
|
31
|
+
const hasRootApp = existsSync(join(cwd, "app"));
|
|
32
|
+
if (!hasSrcApp && !hasRootApp) {
|
|
33
|
+
console.error(
|
|
34
|
+
c.warn("✗ Could not find an `app/` or `src/app/` directory.\n") +
|
|
35
|
+
" Run this from the root of a Next.js App Router project.",
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const base = hasSrcApp ? "src" : ""; // where proxy/instrumentation live
|
|
40
|
+
const appDir = hasSrcApp ? join("src", "app") : "app";
|
|
41
|
+
const ext = JS ? "tsx" : "tsx"; // page is always JSX
|
|
42
|
+
const codeExt = JS ? "js" : "ts";
|
|
43
|
+
const pageExt = JS ? "jsx" : "tsx";
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
c.ok(`✔ Detected Next.js App Router`) +
|
|
47
|
+
c.dim(` (${hasSrcApp ? "src/ layout" : "root layout"})`),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ── File templates ───────────────────────────────────────────────────────────
|
|
51
|
+
const files = [
|
|
52
|
+
{
|
|
53
|
+
path: join(base, `proxy.${codeExt}`),
|
|
54
|
+
merge: join(base, `middleware.${codeExt}`),
|
|
55
|
+
body:
|
|
56
|
+
(JS ? "" : `import type { NextRequest } from "next/server";\n`) +
|
|
57
|
+
`import { withPolishdSession } from "@polishd/next/proxy";\n\n` +
|
|
58
|
+
`export function proxy(request${JS ? "" : ": NextRequest"}) {\n` +
|
|
59
|
+
` return withPolishdSession(request);\n` +
|
|
60
|
+
`}\n\n` +
|
|
61
|
+
`// Next statically parses config.matcher, so it must be an inline literal\n` +
|
|
62
|
+
`// here. Excluding all of /api is required — under Next 16 + Turbopack a\n` +
|
|
63
|
+
`// matcher that touches /api breaks the whole /api segment.\n` +
|
|
64
|
+
`export const config = {\n` +
|
|
65
|
+
` matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],\n` +
|
|
66
|
+
`};\n`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: join(base, `instrumentation-client.${codeExt}`),
|
|
70
|
+
body: `import { initPolishd } from "@polishd/next/client";\n\ninitPolishd();\n`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: join(appDir, "api", "polishd", `route.${codeExt}`),
|
|
74
|
+
// runtime/dynamic must be declared here, not re-exported: Next statically
|
|
75
|
+
// parses route segment config and rejects `export { runtime } from ...`
|
|
76
|
+
// ("Next.js can't recognize the exported `runtime` field in route").
|
|
77
|
+
body:
|
|
78
|
+
`import { createPolishdRoute } from "@polishd/next/route";\n\n` +
|
|
79
|
+
`// node:sqlite / pg need the Node runtime — never the Edge runtime.\n` +
|
|
80
|
+
`export const runtime = "nodejs";\n` +
|
|
81
|
+
`// This route mutates per-request; it must never be statically cached.\n` +
|
|
82
|
+
`export const dynamic = "force-dynamic";\n\n` +
|
|
83
|
+
`export const POST = createPolishdRoute();\n`,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: join(appDir, "polishd", `page.${pageExt}`),
|
|
87
|
+
body:
|
|
88
|
+
`import { createPolishdPage } from "@polishd/next/dashboard";\n\n` +
|
|
89
|
+
`// Next requires these to be declared inline in the page module.\n` +
|
|
90
|
+
`export const runtime = "nodejs";\n` +
|
|
91
|
+
`export const dynamic = "force-dynamic";\n\n` +
|
|
92
|
+
`// Unguarded by default. To protect it, pass an authenticate callback:\n` +
|
|
93
|
+
`// export default createPolishdPage({ authenticate: isAuthenticated });\n` +
|
|
94
|
+
`export default createPolishdPage();\n`,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
if (WANT_CONFIG) {
|
|
99
|
+
files.push({
|
|
100
|
+
path: `polishd.config.${codeExt}`,
|
|
101
|
+
body:
|
|
102
|
+
`import { definePolishdConfig } from "@polishd/next";\n\n` +
|
|
103
|
+
`export default definePolishdConfig({\n` +
|
|
104
|
+
` // sampleRate: 1,\n` +
|
|
105
|
+
` // rageClick: { count: 3, windowMs: 500 },\n` +
|
|
106
|
+
`});\n`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Write ────────────────────────────────────────────────────────────────────
|
|
111
|
+
function write(file) {
|
|
112
|
+
const abs = join(cwd, file.path);
|
|
113
|
+
if (existsSync(abs) && !FORCE) {
|
|
114
|
+
console.log(c.warn(`⚠ ${file.path} exists`) + c.dim(" — skipped (use --force)"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// If a middleware.ts already exists for the proxy file, print a merge snippet.
|
|
118
|
+
if (file.merge && existsSync(join(cwd, file.merge)) && !FORCE) {
|
|
119
|
+
console.log(
|
|
120
|
+
c.warn(`⚠ ${file.merge} exists`) +
|
|
121
|
+
c.dim(" — add this to it (and keep the matcher):\n") +
|
|
122
|
+
` import { withPolishdSession, polishdMatcher } from "@polishd/next/proxy";\n` +
|
|
123
|
+
` // call withPolishdSession(request) in your handler;\n` +
|
|
124
|
+
` // and merge polishdMatcher into your config.matcher`,
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (DRY) {
|
|
129
|
+
console.log(c.dim(`• would write ${file.path}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
133
|
+
writeFileSync(abs, file.body);
|
|
134
|
+
console.log(c.ok(`✔ Created ${file.path}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const f of files) write(f);
|
|
138
|
+
|
|
139
|
+
// ── .gitignore ───────────────────────────────────────────────────────────────
|
|
140
|
+
const giPath = join(cwd, ".gitignore");
|
|
141
|
+
if (!DRY) {
|
|
142
|
+
const gi = existsSync(giPath) ? readFileSync(giPath, "utf8") : "";
|
|
143
|
+
if (!gi.includes(".polishd")) {
|
|
144
|
+
appendFileSync(giPath, `${gi.endsWith("\n") || gi === "" ? "" : "\n"}.polishd/\n`);
|
|
145
|
+
console.log(c.ok("✔ Added .polishd/ to .gitignore"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Tailwind ─────────────────────────────────────────────────────────────────
|
|
150
|
+
// The dashboard is styled entirely with Tailwind utility classes and ships no
|
|
151
|
+
// stylesheet of its own, so Tailwind has to be told to scan the package's dist.
|
|
152
|
+
// Neither version finds it by default: v4's automatic content detection skips
|
|
153
|
+
// node_modules, and v3 only scans the globs it is given. Without this the
|
|
154
|
+
// dashboard renders completely unstyled.
|
|
155
|
+
const DIST_GLOB = "node_modules/@polishd/next/dist";
|
|
156
|
+
|
|
157
|
+
function wireTailwind() {
|
|
158
|
+
const v3Config = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"]
|
|
159
|
+
.map((f) => join(cwd, f))
|
|
160
|
+
.find((f) => existsSync(f));
|
|
161
|
+
|
|
162
|
+
if (v3Config) {
|
|
163
|
+
console.log(
|
|
164
|
+
c.warn(`⚠ Tailwind v3 config detected`) +
|
|
165
|
+
c.dim(" — add this to its `content` array:\n") +
|
|
166
|
+
` "./${DIST_GLOB}/**/*.js"`,
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Tailwind v4: find the entry stylesheet that imports tailwind.
|
|
172
|
+
const cssCandidates = [
|
|
173
|
+
join("src", "app", "globals.css"),
|
|
174
|
+
join("app", "globals.css"),
|
|
175
|
+
join("src", "styles", "globals.css"),
|
|
176
|
+
join("styles", "globals.css"),
|
|
177
|
+
join("src", "app", "global.css"),
|
|
178
|
+
];
|
|
179
|
+
for (const rel of cssCandidates) {
|
|
180
|
+
const abs = join(cwd, rel);
|
|
181
|
+
if (!existsSync(abs)) continue;
|
|
182
|
+
const css = readFileSync(abs, "utf8");
|
|
183
|
+
if (!/@import\s+["']tailwindcss["']/.test(css)) continue;
|
|
184
|
+
if (css.includes("@polishd/next")) {
|
|
185
|
+
console.log(c.dim(`• ${rel} already sources Polishd's styles`));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// `@source` is resolved relative to the stylesheet, so walk back up to root.
|
|
189
|
+
const depth = dirname(rel).split(/[\\/]/).filter(Boolean).length;
|
|
190
|
+
const source = `${"../".repeat(depth)}${DIST_GLOB}`;
|
|
191
|
+
if (DRY) {
|
|
192
|
+
console.log(c.dim(`• would add @source "${source}" to ${rel}`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(
|
|
196
|
+
abs,
|
|
197
|
+
css.replace(
|
|
198
|
+
/(@import\s+["']tailwindcss["'];?)/,
|
|
199
|
+
`$1\n/* Generate the Polishd dashboard's utility classes (Tailwind skips node_modules). */\n@source "${source}";`,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
console.log(c.ok(`✔ Added @source "${source}" to ${rel}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(
|
|
207
|
+
c.warn("⚠ No Tailwind entry stylesheet found") +
|
|
208
|
+
c.dim(" — the dashboard needs Tailwind to render styled.\n") +
|
|
209
|
+
c.dim(" v4: add to your CSS → ") +
|
|
210
|
+
`@source "<path-to>/${DIST_GLOB}";\n` +
|
|
211
|
+
c.dim(" v3: add to content → ") +
|
|
212
|
+
`"./${DIST_GLOB}/**/*.js"`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
wireTailwind();
|
|
217
|
+
|
|
218
|
+
// ── Next steps ───────────────────────────────────────────────────────────────
|
|
219
|
+
console.log(`
|
|
220
|
+
${c.bold("Next steps:")}
|
|
221
|
+
1. Local dev needs nothing — events write to ${c.dim(".polishd/analytics.db")} (SQLite).
|
|
222
|
+
2. For production, set ${c.dim("POLISHD_DATABASE_URL")} (pooled Postgres). See DATABASE.md.
|
|
223
|
+
3. Visit ${c.bold("/polishd")} to see the dashboard.
|
|
224
|
+
${DRY ? c.warn("\n(dry run — nothing was written)") : ""}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SaveAISettingsInput } from "./settings";
|
|
2
|
+
import type { PolishdAISettingsPublic, PolishdLossItem, CreateIssueResult, GenerateProfileResult, GenerateSummaryResult, VerifyGithubResult } from "./types";
|
|
3
|
+
/** Generate or refresh the narrative. `force` re-asks the model even if data is unchanged. */
|
|
4
|
+
export declare function generateSummaryAction(force?: boolean): Promise<GenerateSummaryResult>;
|
|
5
|
+
/** Persist provider/model/key/instructions/context; returns the no-secret view. */
|
|
6
|
+
export declare function saveAISettingsAction(input: SaveAISettingsInput): Promise<PolishdAISettingsPublic>;
|
|
7
|
+
/**
|
|
8
|
+
* Scan the codebase and (re)build the project profile — the explicit setup
|
|
9
|
+
* step. Passes current analytics so known component names steer the scan.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateProfileAction(): Promise<GenerateProfileResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Check the saved GitHub repo + token live against the API — called by the
|
|
14
|
+
* onboarding step right after saving, so a bad token fails at setup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function verifyGithubAction(): Promise<VerifyGithubResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Verify one loss against the repository's source and — when it holds up —
|
|
19
|
+
* file a GitHub issue with the technical analysis and fix suggestions. A
|
|
20
|
+
* disproved report returns `not-a-bug` with the reasoning instead of filing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createIssueFromLossAction(loss: Pick<PolishdLossItem, "issue" | "evidence" | "location">): Promise<CreateIssueResult>;
|
|
23
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/ai/actions.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,KAAK,EACV,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,8FAA8F;AAC9F,wBAAsB,qBAAqB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAGzF;AAED,mFAAmF;AACnF,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,uBAAuB,CAAC,CAGlC;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAI5E;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAGtE;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC,GAC7D,OAAO,CAAC,iBAAiB,CAAC,CAG5B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
/**
|
|
3
|
+
* Polishd — server actions for the AI summary card.
|
|
4
|
+
*
|
|
5
|
+
* These are the only AI entry points the client touches. They run on the
|
|
6
|
+
* server (Node runtime), so the API key never crosses to the browser, and they
|
|
7
|
+
* return only serializable, non-secret data.
|
|
8
|
+
*
|
|
9
|
+
* Every one of them gates on `requirePolishdAuth()` first. A server action is an
|
|
10
|
+
* addressable POST endpoint whose id ships in the public client bundle, so it
|
|
11
|
+
* is reachable directly regardless of whether the dashboard page rendered —
|
|
12
|
+
* the page's `authenticate` gate protects the render, not these. The guard
|
|
13
|
+
* re-runs that same callback against the caller's cookies on every call.
|
|
14
|
+
*/
|
|
15
|
+
import { requirePolishdAuth } from "./guard";
|
|
16
|
+
import { verifyGithubConnection } from "./github";
|
|
17
|
+
import { createIssueFromLoss } from "./issues";
|
|
18
|
+
import { generateProjectProfile } from "./profile";
|
|
19
|
+
import { loadPolishdDashboardData } from "../server/queries";
|
|
20
|
+
import { saveAISettings } from "./settings";
|
|
21
|
+
import { generateSummary } from "./summary";
|
|
22
|
+
/** Generate or refresh the narrative. `force` re-asks the model even if data is unchanged. */
|
|
23
|
+
export async function generateSummaryAction(force = false) {
|
|
24
|
+
await requirePolishdAuth();
|
|
25
|
+
return generateSummary({ force });
|
|
26
|
+
}
|
|
27
|
+
/** Persist provider/model/key/instructions/context; returns the no-secret view. */
|
|
28
|
+
export async function saveAISettingsAction(input) {
|
|
29
|
+
await requirePolishdAuth();
|
|
30
|
+
return saveAISettings(input);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scan the codebase and (re)build the project profile — the explicit setup
|
|
34
|
+
* step. Passes current analytics so known component names steer the scan.
|
|
35
|
+
*/
|
|
36
|
+
export async function generateProfileAction() {
|
|
37
|
+
await requirePolishdAuth();
|
|
38
|
+
const data = await loadPolishdDashboardData();
|
|
39
|
+
return generateProjectProfile(data);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check the saved GitHub repo + token live against the API — called by the
|
|
43
|
+
* onboarding step right after saving, so a bad token fails at setup.
|
|
44
|
+
*/
|
|
45
|
+
export async function verifyGithubAction() {
|
|
46
|
+
await requirePolishdAuth();
|
|
47
|
+
return verifyGithubConnection();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Verify one loss against the repository's source and — when it holds up —
|
|
51
|
+
* file a GitHub issue with the technical analysis and fix suggestions. A
|
|
52
|
+
* disproved report returns `not-a-bug` with the reasoning instead of filing.
|
|
53
|
+
*/
|
|
54
|
+
export async function createIssueFromLossAction(loss) {
|
|
55
|
+
await requirePolishdAuth();
|
|
56
|
+
return createIssueFromLoss(loss);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/ai/actions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAA4B,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAU5C,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,KAAK,GAAG,KAAK;IACvD,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAA0B;IAE1B,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,MAAM,kBAAkB,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,MAAM,wBAAwB,EAAE,CAAC;IAC9C,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,sBAAsB,EAAE,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAA8D;IAE9D,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PolishdDashboardData } from "../server/queries";
|
|
2
|
+
/**
|
|
3
|
+
* Render the captured analytics as a compact, model-readable brief. Sections
|
|
4
|
+
* are omitted when empty so the prompt only ever contains real signal.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildDigest(data: PolishdDashboardData, context?: string): string;
|
|
7
|
+
/** Stable fingerprint of a digest — drives the "data unchanged" short-circuit. */
|
|
8
|
+
export declare function fingerprintDigest(digest: string): string;
|
|
9
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/ai/digest.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAS9D;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA4FhF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD"}
|