@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.
Files changed (127) hide show
  1. package/AGENTS.md +262 -0
  2. package/DATABASE.md +147 -0
  3. package/LICENSE +21 -0
  4. package/README.md +310 -0
  5. package/bin/init.mjs +224 -0
  6. package/dist/ai/actions.d.ts +23 -0
  7. package/dist/ai/actions.d.ts.map +1 -0
  8. package/dist/ai/actions.js +58 -0
  9. package/dist/ai/actions.js.map +1 -0
  10. package/dist/ai/digest.d.ts +9 -0
  11. package/dist/ai/digest.d.ts.map +1 -0
  12. package/dist/ai/digest.js +108 -0
  13. package/dist/ai/digest.js.map +1 -0
  14. package/dist/ai/github.d.ts +51 -0
  15. package/dist/ai/github.d.ts.map +1 -0
  16. package/dist/ai/github.js +175 -0
  17. package/dist/ai/github.js.map +1 -0
  18. package/dist/ai/guard.d.ts +43 -0
  19. package/dist/ai/guard.d.ts.map +1 -0
  20. package/dist/ai/guard.js +57 -0
  21. package/dist/ai/guard.js.map +1 -0
  22. package/dist/ai/issues.d.ts +21 -0
  23. package/dist/ai/issues.d.ts.map +1 -0
  24. package/dist/ai/issues.js +291 -0
  25. package/dist/ai/issues.js.map +1 -0
  26. package/dist/ai/profile.d.ts +26 -0
  27. package/dist/ai/profile.d.ts.map +1 -0
  28. package/dist/ai/profile.js +160 -0
  29. package/dist/ai/profile.js.map +1 -0
  30. package/dist/ai/providers.d.ts +40 -0
  31. package/dist/ai/providers.d.ts.map +1 -0
  32. package/dist/ai/providers.js +142 -0
  33. package/dist/ai/providers.js.map +1 -0
  34. package/dist/ai/scan.d.ts +38 -0
  35. package/dist/ai/scan.d.ts.map +1 -0
  36. package/dist/ai/scan.js +274 -0
  37. package/dist/ai/scan.js.map +1 -0
  38. package/dist/ai/settings.d.ts +31 -0
  39. package/dist/ai/settings.d.ts.map +1 -0
  40. package/dist/ai/settings.js +182 -0
  41. package/dist/ai/settings.js.map +1 -0
  42. package/dist/ai/summary.d.ts +26 -0
  43. package/dist/ai/summary.d.ts.map +1 -0
  44. package/dist/ai/summary.js +260 -0
  45. package/dist/ai/summary.js.map +1 -0
  46. package/dist/ai/types.d.ts +188 -0
  47. package/dist/ai/types.d.ts.map +1 -0
  48. package/dist/ai/types.js +13 -0
  49. package/dist/ai/types.js.map +1 -0
  50. package/dist/client/init.d.ts +14 -0
  51. package/dist/client/init.d.ts.map +1 -0
  52. package/dist/client/init.js +274 -0
  53. package/dist/client/init.js.map +1 -0
  54. package/dist/client/monitor.d.ts +23 -0
  55. package/dist/client/monitor.d.ts.map +1 -0
  56. package/dist/client/monitor.js +176 -0
  57. package/dist/client/monitor.js.map +1 -0
  58. package/dist/client.d.ts +11 -0
  59. package/dist/client.d.ts.map +1 -0
  60. package/dist/client.js +10 -0
  61. package/dist/client.js.map +1 -0
  62. package/dist/config.d.ts +36 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +26 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/dashboard/elements.d.ts +6 -0
  67. package/dist/dashboard/elements.d.ts.map +1 -0
  68. package/dist/dashboard/elements.js +35 -0
  69. package/dist/dashboard/elements.js.map +1 -0
  70. package/dist/dashboard/features.d.ts +6 -0
  71. package/dist/dashboard/features.d.ts.map +1 -0
  72. package/dist/dashboard/features.js +28 -0
  73. package/dist/dashboard/features.js.map +1 -0
  74. package/dist/dashboard/index.d.ts +54 -0
  75. package/dist/dashboard/index.d.ts.map +1 -0
  76. package/dist/dashboard/index.js +140 -0
  77. package/dist/dashboard/index.js.map +1 -0
  78. package/dist/dashboard/journeys.d.ts +5 -0
  79. package/dist/dashboard/journeys.d.ts.map +1 -0
  80. package/dist/dashboard/journeys.js +158 -0
  81. package/dist/dashboard/journeys.js.map +1 -0
  82. package/dist/dashboard/pages.d.ts +5 -0
  83. package/dist/dashboard/pages.d.ts.map +1 -0
  84. package/dist/dashboard/pages.js +104 -0
  85. package/dist/dashboard/pages.js.map +1 -0
  86. package/dist/dashboard/summary.d.ts +13 -0
  87. package/dist/dashboard/summary.d.ts.map +1 -0
  88. package/dist/dashboard/summary.js +333 -0
  89. package/dist/dashboard/summary.js.map +1 -0
  90. package/dist/index.d.ts +10 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +10 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/proxy.d.ts +40 -0
  95. package/dist/proxy.d.ts.map +1 -0
  96. package/dist/proxy.js +29 -0
  97. package/dist/proxy.js.map +1 -0
  98. package/dist/route.d.ts +24 -0
  99. package/dist/route.d.ts.map +1 -0
  100. package/dist/route.js +37 -0
  101. package/dist/route.js.map +1 -0
  102. package/dist/server/ingest.d.ts +14 -0
  103. package/dist/server/ingest.d.ts.map +1 -0
  104. package/dist/server/ingest.js +73 -0
  105. package/dist/server/ingest.js.map +1 -0
  106. package/dist/server/queries.d.ts +245 -0
  107. package/dist/server/queries.d.ts.map +1 -0
  108. package/dist/server/queries.js +547 -0
  109. package/dist/server/queries.js.map +1 -0
  110. package/dist/server/store.d.ts +29 -0
  111. package/dist/server/store.d.ts.map +1 -0
  112. package/dist/server/store.js +359 -0
  113. package/dist/server/store.js.map +1 -0
  114. package/dist/server.d.ts +15 -0
  115. package/dist/server.d.ts.map +1 -0
  116. package/dist/server.js +15 -0
  117. package/dist/server.js.map +1 -0
  118. package/dist/session.d.ts +16 -0
  119. package/dist/session.d.ts.map +1 -0
  120. package/dist/session.js +29 -0
  121. package/dist/session.js.map +1 -0
  122. package/dist/shared/types.d.ts +56 -0
  123. package/dist/shared/types.d.ts.map +1 -0
  124. package/dist/shared/types.js +27 -0
  125. package/dist/shared/types.js.map +1 -0
  126. package/docs/SETUP.md +315 -0
  127. 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"}