@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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — loss → verified GitHub issue (server only).
|
|
3
|
+
*
|
|
4
|
+
* A loss in the summary is a claim made from analytics data. Before it becomes
|
|
5
|
+
* a bug in the tracker, this module makes it earn that status:
|
|
6
|
+
*
|
|
7
|
+
* 1. Pull the relevant source from the connected GitHub repo — the file the
|
|
8
|
+
* citation was matched to, plus code-search hits for the evidence.
|
|
9
|
+
* 2. Ask the model for a technical verdict against that code: confirmed
|
|
10
|
+
* (specific code explains the problem), rejected (the code proves the
|
|
11
|
+
* report wrong), or inconclusive (not enough source to decide).
|
|
12
|
+
* 3. Rejected → no issue; the verdict's reasoning is surfaced to the owner.
|
|
13
|
+
* Confirmed → an issue with a precise title, the technical analysis, and
|
|
14
|
+
* suggested fixes. Inconclusive → the plain analytics-only issue, marked
|
|
15
|
+
* as unverified, so a search hiccup never blocks a real report.
|
|
16
|
+
*
|
|
17
|
+
* Verdicts and filed issues are logged per evidence citation, so the same
|
|
18
|
+
* problem — refound by a regenerated summary or clicked twice — is neither
|
|
19
|
+
* re-investigated nor re-filed.
|
|
20
|
+
*/
|
|
21
|
+
import { getMeta, setMeta } from "../server/store";
|
|
22
|
+
import { createGithubIssue, getGithubStatus, isGithubConnected, readGithubFile, searchGithubCode, } from "./github";
|
|
23
|
+
import { loadProjectProfile } from "./profile";
|
|
24
|
+
import { callModel } from "./providers";
|
|
25
|
+
import { resolveSettings } from "./settings";
|
|
26
|
+
// ── Issue log (dedupe) ───────────────────────────────────────────────────────
|
|
27
|
+
const ISSUE_LOG_KEY = "github_issue_log";
|
|
28
|
+
function issueKey(evidence) {
|
|
29
|
+
return evidence.trim().toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
async function loadIssueLog() {
|
|
32
|
+
const raw = await getMeta(ISSUE_LOG_KEY);
|
|
33
|
+
if (!raw)
|
|
34
|
+
return {};
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function recordLog(key, entry) {
|
|
43
|
+
const log = await loadIssueLog();
|
|
44
|
+
log[key] = entry;
|
|
45
|
+
await setMeta(ISSUE_LOG_KEY, JSON.stringify(log));
|
|
46
|
+
}
|
|
47
|
+
// ── Investigation ────────────────────────────────────────────────────────────
|
|
48
|
+
const MAX_FILES = 4;
|
|
49
|
+
const MAX_CHARS_PER_FILE = 8_000;
|
|
50
|
+
const INVESTIGATE_PROMPT = "You are a senior engineer verifying a suspected bug before it enters the " +
|
|
51
|
+
"issue tracker. A product-analytics tool watched real users interact with a " +
|
|
52
|
+
"website and flagged a suspected problem. You receive that suspicion, the " +
|
|
53
|
+
"analytics identifier cited as evidence (a page path, CSS selector, or " +
|
|
54
|
+
"component name), and source files from the site's repository.\n\n" +
|
|
55
|
+
"Decide whether the problem is technically real in this code. Respond with " +
|
|
56
|
+
"ONLY a JSON object (no markdown fences, no preamble):\n" +
|
|
57
|
+
'{ "verdict": "confirmed" | "rejected" | "inconclusive", "title": string, ' +
|
|
58
|
+
'"details": string, "fixes": string[], "reasoning": string }\n\n' +
|
|
59
|
+
"verdict: confirmed ONLY when you can point to specific code that explains " +
|
|
60
|
+
"or exhibits the problem. rejected ONLY when the code demonstrates the " +
|
|
61
|
+
"report cannot be right (the interaction is handled correctly, the behavior " +
|
|
62
|
+
"is clearly intentional). inconclusive when the source provided is not " +
|
|
63
|
+
"enough to decide either way.\n" +
|
|
64
|
+
"title: a short, specific bug title (under 80 characters) — becomes the " +
|
|
65
|
+
"GitHub issue title when confirmed.\n" +
|
|
66
|
+
"details: the technical root-cause analysis, citing the file paths and the " +
|
|
67
|
+
"relevant code. Written for the developer who will fix it.\n" +
|
|
68
|
+
"fixes: 1-3 concrete, code-level suggestions for fixing it.\n" +
|
|
69
|
+
"reasoning: one or two sentences on how you reached the verdict — shown to " +
|
|
70
|
+
"the site owner when the report is rejected or unverifiable.";
|
|
71
|
+
/** Terms specific enough to search code for (same spirit as citationTokens). */
|
|
72
|
+
function searchTerms(evidence) {
|
|
73
|
+
return [
|
|
74
|
+
...new Set(evidence
|
|
75
|
+
.split(/[^\w-]+/)
|
|
76
|
+
.filter((t) => t.length >= 4 &&
|
|
77
|
+
!/^(button|input|click|clicks|page|pages|http|https|with|from|that|div|span)$/i.test(t))),
|
|
78
|
+
].slice(0, 3);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Gather the source files that should contain the answer: the file the loss
|
|
82
|
+
* was already matched to, then code-search hits for the evidence.
|
|
83
|
+
*/
|
|
84
|
+
async function collectSource(loss) {
|
|
85
|
+
const paths = [];
|
|
86
|
+
if (loss.location)
|
|
87
|
+
paths.push(loss.location);
|
|
88
|
+
for (const p of await searchGithubCode(searchTerms(loss.evidence), MAX_FILES)) {
|
|
89
|
+
if (!paths.includes(p))
|
|
90
|
+
paths.push(p);
|
|
91
|
+
}
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const path of paths.slice(0, MAX_FILES)) {
|
|
94
|
+
const content = await readGithubFile(path);
|
|
95
|
+
if (content)
|
|
96
|
+
out.push({ path, content: content.slice(0, MAX_CHARS_PER_FILE) });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** Tolerant JSON extraction, same approach as the summary parser. */
|
|
101
|
+
function parseInvestigation(text, files) {
|
|
102
|
+
let t = text.trim();
|
|
103
|
+
const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
104
|
+
if (fence)
|
|
105
|
+
t = fence[1].trim();
|
|
106
|
+
const start = t.indexOf("{");
|
|
107
|
+
const end = t.lastIndexOf("}");
|
|
108
|
+
if (start >= 0 && end > start) {
|
|
109
|
+
try {
|
|
110
|
+
const obj = JSON.parse(t.slice(start, end + 1));
|
|
111
|
+
const verdict = obj.verdict === "confirmed" || obj.verdict === "rejected"
|
|
112
|
+
? obj.verdict
|
|
113
|
+
: "inconclusive";
|
|
114
|
+
return {
|
|
115
|
+
verdict,
|
|
116
|
+
title: typeof obj.title === "string" ? obj.title.trim() || undefined : undefined,
|
|
117
|
+
details: typeof obj.details === "string" ? obj.details.trim() || undefined : undefined,
|
|
118
|
+
fixes: Array.isArray(obj.fixes)
|
|
119
|
+
? obj.fixes
|
|
120
|
+
.filter((f) => typeof f === "string" && f.trim().length > 0)
|
|
121
|
+
.slice(0, 3)
|
|
122
|
+
: [],
|
|
123
|
+
reasoning: typeof obj.reasoning === "string" && obj.reasoning.trim()
|
|
124
|
+
? obj.reasoning.trim()
|
|
125
|
+
: "The model returned no reasoning.",
|
|
126
|
+
files,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* fall through */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
verdict: "inconclusive",
|
|
135
|
+
fixes: [],
|
|
136
|
+
reasoning: "The model's verdict could not be parsed.",
|
|
137
|
+
files,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verify one loss against the repository's source. Never throws — anything
|
|
142
|
+
* that prevents a real verdict (no source found, model unavailable) comes back
|
|
143
|
+
* as inconclusive so the caller can still file a plain, unverified report.
|
|
144
|
+
*/
|
|
145
|
+
async function investigateLoss(loss) {
|
|
146
|
+
const { settings } = await resolveSettings();
|
|
147
|
+
if (!settings.apiKey) {
|
|
148
|
+
return {
|
|
149
|
+
verdict: "inconclusive",
|
|
150
|
+
fixes: [],
|
|
151
|
+
reasoning: "No model is configured to verify against the source.",
|
|
152
|
+
files: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const files = await collectSource(loss);
|
|
156
|
+
if (files.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
verdict: "inconclusive",
|
|
159
|
+
fixes: [],
|
|
160
|
+
reasoning: "No source files mentioning the cited element were found in the repository.",
|
|
161
|
+
files: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const profile = await loadProjectProfile();
|
|
165
|
+
const sections = [
|
|
166
|
+
"SUSPECTED PROBLEM (from analytics):",
|
|
167
|
+
loss.issue,
|
|
168
|
+
"",
|
|
169
|
+
`EVIDENCE (identifier from the analytics data): ${loss.evidence}`,
|
|
170
|
+
];
|
|
171
|
+
if (profile) {
|
|
172
|
+
sections.push("", `PROJECT PROFILE (what this site and its components are):\n${profile.text}`);
|
|
173
|
+
}
|
|
174
|
+
sections.push("", "SOURCE FILES (from the GitHub repository):", ...files.map((f) => `--- ${f.path} ---\n${f.content}`));
|
|
175
|
+
try {
|
|
176
|
+
const reply = await callModel(settings, INVESTIGATE_PROMPT, sections.join("\n"));
|
|
177
|
+
if (!reply.text)
|
|
178
|
+
throw new Error("empty response");
|
|
179
|
+
return parseInvestigation(reply.text, files.map((f) => f.path));
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
return {
|
|
183
|
+
verdict: "inconclusive",
|
|
184
|
+
fixes: [],
|
|
185
|
+
reasoning: `Verification failed: ${err instanceof Error ? err.message : "model error"}.`,
|
|
186
|
+
files: files.map((f) => f.path),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ── Filing ───────────────────────────────────────────────────────────────────
|
|
191
|
+
/** Markdown body for a filed issue, sized to the verdict behind it. */
|
|
192
|
+
function issueBody(loss, inv, fileUrl) {
|
|
193
|
+
const lines = [loss.issue, "", `**Evidence (from analytics):** \`${loss.evidence}\``];
|
|
194
|
+
if (inv.verdict === "confirmed") {
|
|
195
|
+
if (inv.details)
|
|
196
|
+
lines.push("", "## Technical analysis", inv.details);
|
|
197
|
+
if (inv.files.length) {
|
|
198
|
+
lines.push("", "**Verified against:**", ...inv.files.map((p) => `- [\`${p}\`](${fileUrl(p)})`));
|
|
199
|
+
}
|
|
200
|
+
if (inv.fixes.length) {
|
|
201
|
+
lines.push("", "## Suggested fixes", ...inv.fixes.map((f) => `- ${f}`));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
lines.push("", `> **Note:** source verification was inconclusive — ${inv.reasoning}`);
|
|
206
|
+
if (loss.location)
|
|
207
|
+
lines.push("", `**Possible source:** [\`${loss.location}\`](${fileUrl(loss.location)})`);
|
|
208
|
+
}
|
|
209
|
+
lines.push("", "---", inv.verdict === "confirmed"
|
|
210
|
+
? "_Filed from the Polishd analytics dashboard — observed in real user-behavior data and confirmed against the source code._"
|
|
211
|
+
: "_Filed from the Polishd analytics dashboard — observed in real user-behavior data._");
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* File a bug from one loss — after verifying it against the source (see the
|
|
216
|
+
* module doc for the pipeline). Returns the issue on success, the stored issue
|
|
217
|
+
* when this evidence was already filed, or `not-a-bug` with the verdict's
|
|
218
|
+
* reasoning when the code disproves the report.
|
|
219
|
+
*/
|
|
220
|
+
export async function createIssueFromLoss(loss) {
|
|
221
|
+
if (!(await isGithubConnected())) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: "not-connected",
|
|
225
|
+
message: "Connect a GitHub repository in the settings first.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const key = issueKey(loss.evidence);
|
|
229
|
+
const existing = (await loadIssueLog())[key];
|
|
230
|
+
if (existing) {
|
|
231
|
+
if ("rejected" in existing) {
|
|
232
|
+
return { ok: false, error: "not-a-bug", message: existing.reason };
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, ...existing };
|
|
235
|
+
}
|
|
236
|
+
const inv = await investigateLoss(loss);
|
|
237
|
+
if (inv.verdict === "rejected") {
|
|
238
|
+
await recordLog(key, { rejected: true, reason: inv.reasoning });
|
|
239
|
+
return { ok: false, error: "not-a-bug", message: inv.reasoning };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const status = await getGithubStatus();
|
|
243
|
+
const fileUrl = (path) => status
|
|
244
|
+
? `https://github.com/${status.repo}/blob/${status.defaultBranch}/${path}`
|
|
245
|
+
: path;
|
|
246
|
+
const issue = await createGithubIssue({
|
|
247
|
+
title: (inv.verdict === "confirmed" && inv.title) || loss.issue,
|
|
248
|
+
body: issueBody(loss, inv, fileUrl),
|
|
249
|
+
labels: ["bug"],
|
|
250
|
+
});
|
|
251
|
+
await recordLog(key, issue);
|
|
252
|
+
return { ok: true, url: issue.url, number: issue.number };
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: "github-error",
|
|
258
|
+
message: err instanceof Error ? err.message : "Could not create the issue.",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Decorate a fresh summary's losses with their GitHub issues: attach the link
|
|
264
|
+
* for anything already filed and — when the owner enabled auto-filing — run
|
|
265
|
+
* the verify-and-file pipeline on the new ones. Losses whose report the code
|
|
266
|
+
* disproved are skipped (and remembered, so they aren't re-investigated).
|
|
267
|
+
* Never throws; a hiccup just leaves a loss undecorated, with the manual
|
|
268
|
+
* "file bug" button as the fallback.
|
|
269
|
+
*/
|
|
270
|
+
export async function attachGithubIssues(losses) {
|
|
271
|
+
if (losses.length === 0)
|
|
272
|
+
return losses;
|
|
273
|
+
const { settings } = await resolveSettings();
|
|
274
|
+
if (!(await isGithubConnected()))
|
|
275
|
+
return losses;
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const loss of losses) {
|
|
278
|
+
// Re-read per loss: auto-filing above may have just written new entries.
|
|
279
|
+
let entry = (await loadIssueLog())[issueKey(loss.evidence)];
|
|
280
|
+
if (!entry && settings.githubAutoIssues) {
|
|
281
|
+
const res = await createIssueFromLoss(loss);
|
|
282
|
+
if (res.ok)
|
|
283
|
+
entry = { url: res.url, number: res.number };
|
|
284
|
+
}
|
|
285
|
+
out.push(entry && "url" in entry
|
|
286
|
+
? { ...loss, issueUrl: entry.url, issueNumber: entry.number }
|
|
287
|
+
: loss);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=issues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issues.js","sourceRoot":"","sources":["../../src/ai/issues.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PolishdDashboardData } from "../server/queries";
|
|
2
|
+
import type { PolishdProjectProfile, GenerateProfileResult } from "./types";
|
|
3
|
+
/** The cached profile, or null. Read-only — never calls a model. */
|
|
4
|
+
export declare function loadProjectProfile(): Promise<PolishdProjectProfile | null>;
|
|
5
|
+
/** Component identifiers the analytics currently know about. */
|
|
6
|
+
export declare function componentIdentifiers(data: PolishdDashboardData): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Identifiers the analytics talk about that the profile does NOT cover — the
|
|
9
|
+
* trigger for a targeted source read at summary time (and a "re-scan" hint in
|
|
10
|
+
* the UI). An identifier counts as covered if the scan saw it in source or the
|
|
11
|
+
* profile prose mentions it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function coverageGaps(data: PolishdDashboardData, profile: PolishdProjectProfile | null): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Scan the codebase and (re)generate the project profile. Always calls the
|
|
16
|
+
* model — this is an explicit owner action, not something that runs on load.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateProjectProfile(data?: PolishdDashboardData): Promise<GenerateProfileResult>;
|
|
19
|
+
/** What the dashboard needs on first paint. Reads only — no model, no scan. */
|
|
20
|
+
export declare function loadProfileState(data: PolishdDashboardData): Promise<{
|
|
21
|
+
profile: PolishdProjectProfile | null;
|
|
22
|
+
gaps: string[];
|
|
23
|
+
/** Whether a scan could run here (source tree present on disk). */
|
|
24
|
+
sourceAvailable: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/ai/profile.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAK9D,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAI5E,oEAAoE;AACpE,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAQhF;AAED,gEAAgE;AAChE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAKzE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,oBAAoB,EAC1B,OAAO,EAAE,qBAAqB,GAAG,IAAI,GACpC,MAAM,EAAE,CAOV;AAkBD;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CA2EhC;AAED,+EAA+E;AAC/E,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC;IAC1E,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC,CAaD"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — project profile (server only).
|
|
3
|
+
*
|
|
4
|
+
* The one-time setup step: scan the host app's source, ask the model to write
|
|
5
|
+
* a compact "what this project is" brief — purpose, page map, every interactive
|
|
6
|
+
* component and its identifier, plus the owner's stated audience and ideology —
|
|
7
|
+
* and cache it in the store.
|
|
8
|
+
*
|
|
9
|
+
* From then on, every summary reuses this profile as authoritative context.
|
|
10
|
+
* The codebase is only read again when (a) the owner explicitly re-scans, or
|
|
11
|
+
* (b) analytics mention a component the profile doesn't cover, in which case
|
|
12
|
+
* the summary does a tiny targeted read of just the files naming it.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getMeta, setMeta } from "../server/store";
|
|
17
|
+
import { callModel } from "./providers";
|
|
18
|
+
import { collectSource } from "./scan";
|
|
19
|
+
import { resolveSettings } from "./settings";
|
|
20
|
+
const PROFILE_KEY = "project_profile";
|
|
21
|
+
/** The cached profile, or null. Read-only — never calls a model. */
|
|
22
|
+
export async function loadProjectProfile() {
|
|
23
|
+
const raw = await getMeta(PROFILE_KEY);
|
|
24
|
+
if (!raw)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Component identifiers the analytics currently know about. */
|
|
34
|
+
export function componentIdentifiers(data) {
|
|
35
|
+
const ids = new Set();
|
|
36
|
+
for (const m of data.monitored)
|
|
37
|
+
ids.add(m.name);
|
|
38
|
+
for (const t of data.topUsed)
|
|
39
|
+
if (t.isComponent)
|
|
40
|
+
ids.add(t.label);
|
|
41
|
+
return [...ids].sort();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Identifiers the analytics talk about that the profile does NOT cover — the
|
|
45
|
+
* trigger for a targeted source read at summary time (and a "re-scan" hint in
|
|
46
|
+
* the UI). An identifier counts as covered if the scan saw it in source or the
|
|
47
|
+
* profile prose mentions it.
|
|
48
|
+
*/
|
|
49
|
+
export function coverageGaps(data, profile) {
|
|
50
|
+
if (!profile)
|
|
51
|
+
return [];
|
|
52
|
+
const covered = new Set(profile.coveredIdentifiers);
|
|
53
|
+
const prose = profile.text.toLowerCase();
|
|
54
|
+
return componentIdentifiers(data).filter((id) => !covered.has(id) && !prose.includes(id.toLowerCase()));
|
|
55
|
+
}
|
|
56
|
+
const PROFILE_SYSTEM_PROMPT = "You are onboarding as the resident product analyst for a website. You are " +
|
|
57
|
+
"given excerpts of its source code and, when available, the owner's notes on " +
|
|
58
|
+
"audience and product values. Write a compact project profile that will be " +
|
|
59
|
+
"your ONLY code context in future analyses, so make it self-sufficient: " +
|
|
60
|
+
"(1) what the site is and its purpose; (2) a map of its pages/routes and what " +
|
|
61
|
+
"each is for; (3) every interactive component and tracked element — name each " +
|
|
62
|
+
"identifier exactly as it appears (PolishdMonitor names, data-component values, " +
|
|
63
|
+
"notable buttons/links) and say what it does and where it lives; (4) who the " +
|
|
64
|
+
"site is for and what success looks like, if stated. Plain prose with short " +
|
|
65
|
+
"paragraphs; no markdown headings, no code blocks. Be dense — under 450 words.";
|
|
66
|
+
// ~450 words of profile plus headroom for reasoning models' thinking tokens,
|
|
67
|
+
// which count against the output cap.
|
|
68
|
+
const PROFILE_MAX_TOKENS = 3000;
|
|
69
|
+
/**
|
|
70
|
+
* Scan the codebase and (re)generate the project profile. Always calls the
|
|
71
|
+
* model — this is an explicit owner action, not something that runs on load.
|
|
72
|
+
*/
|
|
73
|
+
export async function generateProjectProfile(data) {
|
|
74
|
+
const { settings } = await resolveSettings();
|
|
75
|
+
if (!settings.apiKey) {
|
|
76
|
+
return { ok: false, error: "no-key", message: "No API key configured." };
|
|
77
|
+
}
|
|
78
|
+
const knownIds = data ? componentIdentifiers(data) : [];
|
|
79
|
+
const scan = collectSource(settings.sourceDirs, knownIds);
|
|
80
|
+
if (!scan.available) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: "no-source",
|
|
84
|
+
message: "No source tree found on disk — run the scan where the app's source is available (local dev or CI).",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const parts = [];
|
|
88
|
+
if (settings.context)
|
|
89
|
+
parts.push(`OWNER'S DESCRIPTION: ${settings.context}`);
|
|
90
|
+
if (settings.audience)
|
|
91
|
+
parts.push(`TARGET AUDIENCE: ${settings.audience}`);
|
|
92
|
+
if (settings.ideology)
|
|
93
|
+
parts.push(`PRODUCT VALUES / IDEOLOGY: ${settings.ideology}`);
|
|
94
|
+
if (knownIds.length)
|
|
95
|
+
parts.push(`COMPONENT IDENTIFIERS SEEN IN ANALYTICS: ${knownIds.join(", ")}`);
|
|
96
|
+
parts.push("SOURCE FILES" + (scan.truncated ? " (large app — excerpts were clipped to budget)" : "") + ":");
|
|
97
|
+
for (const f of scan.files) {
|
|
98
|
+
parts.push(`--- ${f.path} ---\n${f.content}`);
|
|
99
|
+
}
|
|
100
|
+
let reply;
|
|
101
|
+
try {
|
|
102
|
+
reply = await callModel(settings, PROFILE_SYSTEM_PROMPT, parts.join("\n\n"), {
|
|
103
|
+
maxTokens: PROFILE_MAX_TOKENS,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: "provider-error",
|
|
110
|
+
message: err instanceof Error ? err.message : "The model request failed.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!reply.text) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: "provider-error",
|
|
117
|
+
message: "The model returned an empty response.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (reply.truncated) {
|
|
121
|
+
// Even the retry hit the cap — a partial profile would silently degrade
|
|
122
|
+
// every future summary, so refuse to store it.
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: "provider-error",
|
|
126
|
+
message: "The model's response was cut off at the output limit, twice. Try again or use a model that reasons less.",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const profile = {
|
|
130
|
+
text: reply.text,
|
|
131
|
+
provider: settings.provider,
|
|
132
|
+
model: settings.model,
|
|
133
|
+
generatedAt: Date.now(),
|
|
134
|
+
fingerprint: scan.fingerprint,
|
|
135
|
+
// Covered = names the scan found in source + names analytics knew at scan
|
|
136
|
+
// time (dynamic names — e.g. per-article slugs — never appear literally in
|
|
137
|
+
// source, so the analytics side is what keeps them from gapping forever).
|
|
138
|
+
coveredIdentifiers: [...new Set([...scan.identifiers, ...knownIds])].sort(),
|
|
139
|
+
sourceFiles: scan.files.length,
|
|
140
|
+
truncated: scan.truncated,
|
|
141
|
+
};
|
|
142
|
+
await setMeta(PROFILE_KEY, JSON.stringify(profile));
|
|
143
|
+
return { ok: true, profile };
|
|
144
|
+
}
|
|
145
|
+
/** What the dashboard needs on first paint. Reads only — no model, no scan. */
|
|
146
|
+
export async function loadProfileState(data) {
|
|
147
|
+
const [profile, { settings }] = await Promise.all([
|
|
148
|
+
loadProjectProfile(),
|
|
149
|
+
resolveSettings(),
|
|
150
|
+
]);
|
|
151
|
+
// Cheap availability probe — enough for the UI to enable/disable the button
|
|
152
|
+
// without walking the whole tree.
|
|
153
|
+
const configured = settings.sourceDirs?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
154
|
+
const roots = configured?.length
|
|
155
|
+
? configured
|
|
156
|
+
: ["src", "app", "components", "pages", "lib"];
|
|
157
|
+
const sourceAvailable = roots.some((r) => existsSync(join(process.cwd(), r)));
|
|
158
|
+
return { profile, gaps: coverageGaps(data, profile), sourceAvailable };
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/ai/profile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — model providers (server only).
|
|
3
|
+
*
|
|
4
|
+
* A deliberately tiny, dependency-free abstraction over the chat endpoints of
|
|
5
|
+
* the common vendors. We use raw `fetch` rather than any vendor SDK so the
|
|
6
|
+
* package keeps its zero-runtime-dependency footprint and so the owner can
|
|
7
|
+
* point Polishd at whichever model they already pay for.
|
|
8
|
+
*
|
|
9
|
+
* Each call is a single non-streaming request: one system prompt, one user
|
|
10
|
+
* message, a small token cap. That's all a one-paragraph summary needs, and it
|
|
11
|
+
* keeps the owner's spend to the absolute minimum.
|
|
12
|
+
*/
|
|
13
|
+
import type { PolishdAIProvider, PolishdAISettings } from "./types";
|
|
14
|
+
export interface ModelReply {
|
|
15
|
+
text: string;
|
|
16
|
+
/** True when the provider stopped at the output-token cap (clipped text). */
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
usage?: {
|
|
19
|
+
inputTokens?: number;
|
|
20
|
+
outputTokens?: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Sensible default model per provider, used when none is configured. */
|
|
24
|
+
export declare const DEFAULT_MODEL: Record<PolishdAIProvider, string>;
|
|
25
|
+
export interface CallOptions {
|
|
26
|
+
/** Output token cap for this call. */
|
|
27
|
+
maxTokens?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Dispatch to the configured provider. Throws on transport / API errors with a
|
|
31
|
+
* message safe to surface (no key material), which the caller turns into a
|
|
32
|
+
* `provider-error` result.
|
|
33
|
+
*
|
|
34
|
+
* If the reply hits the output cap (reasoning models can burn most of it on
|
|
35
|
+
* thinking), retry once at double the cap — a clipped reply is 100% wasted
|
|
36
|
+
* spend, so one bigger attempt is the cheaper path. Still-truncated replies
|
|
37
|
+
* are returned flagged; callers must not cache them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function callModel(settings: PolishdAISettings, system: string, user: string, opts?: CallOptions): Promise<ModelReply>;
|
|
40
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/ai/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEpE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACzD;AAED,yEAAyE;AACzE,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAM3D,CAAC;AAWF,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsKD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,UAAU,CAAC,CAQrB"}
|