@polishd/next 0.2.1 → 0.2.2-dev.1
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/CHANGELOG.md +57 -0
- package/DATABASE.md +8 -1
- package/README.md +52 -0
- package/dist/ai/actions.d.ts +10 -1
- package/dist/ai/actions.d.ts.map +1 -1
- package/dist/ai/actions.js +21 -0
- package/dist/ai/actions.js.map +1 -1
- package/dist/ai/ignored.d.ts +26 -0
- package/dist/ai/ignored.d.ts.map +1 -0
- package/dist/ai/ignored.js +134 -0
- package/dist/ai/ignored.js.map +1 -0
- package/dist/ai/summary.d.ts +8 -1
- package/dist/ai/summary.d.ts.map +1 -1
- package/dist/ai/summary.js +59 -22
- package/dist/ai/summary.js.map +1 -1
- package/dist/ai/types.d.ts +31 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/dom.d.ts +23 -0
- package/dist/client/dom.d.ts.map +1 -0
- package/dist/client/dom.js +71 -0
- package/dist/client/dom.js.map +1 -0
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +3 -62
- package/dist/client/init.js.map +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +16 -10
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/summary.d.ts.map +1 -1
- package/dist/dashboard/summary.js +64 -4
- package/dist/dashboard/summary.js.map +1 -1
- package/dist/dashboard/telemetry-consent.d.ts +3 -0
- package/dist/dashboard/telemetry-consent.d.ts.map +1 -0
- package/dist/dashboard/telemetry-consent.js +20 -0
- package/dist/dashboard/telemetry-consent.js.map +1 -0
- package/dist/dashboard/telemetry.d.ts +21 -0
- package/dist/dashboard/telemetry.d.ts.map +1 -0
- package/dist/dashboard/telemetry.js +212 -0
- package/dist/dashboard/telemetry.js.map +1 -0
- package/dist/dashboard.css +52 -0
- package/dist/server/ingest.d.ts +20 -0
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +49 -0
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/store.d.ts +6 -2
- package/dist/server/store.d.ts.map +1 -1
- package/dist/server/store.js +34 -13
- package/dist/server/store.js.map +1 -1
- package/dist/server/telemetry.d.ts +28 -0
- package/dist/server/telemetry.d.ts.map +1 -0
- package/dist/server/telemetry.js +83 -0
- package/dist/server/telemetry.js.map +1 -0
- package/dist/shared/types.d.ts +16 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/telemetry.d.ts +43 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +81 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Cross-install telemetry ingest
|
|
6
|
+
|
|
7
|
+
A new `@polishd/next/telemetry` entry point exports a CORS-enabled ingest
|
|
8
|
+
route for collecting telemetry *from other polishd installations* — the
|
|
9
|
+
foundation for polishd dogfooding its own dashboards. Unlike the first-party
|
|
10
|
+
route, it answers preflights, accepts POSTs cross-origin, and takes identity
|
|
11
|
+
from the body (a client-minted anonymous session id plus a stable, anonymous
|
|
12
|
+
`installId`) since the httpOnly session cookie never crosses origins. Events
|
|
13
|
+
land tagged with an `install_id` — a new nullable column on `events`, migrated
|
|
14
|
+
automatically on both backends — so one shared database can tell installations
|
|
15
|
+
apart. Nothing sends to this endpoint yet; the dashboard-side emitter and its
|
|
16
|
+
opt-in consent flow ship separately.
|
|
17
|
+
|
|
18
|
+
### The dashboard dogfoods itself — with consent
|
|
19
|
+
|
|
20
|
+
The dashboard now asks, once, whether it may share anonymous usage of
|
|
21
|
+
*itself* with the polishd project. Opt-in and off by default: say no and it
|
|
22
|
+
never asks again; say yes and a small emitter records clicks (with the same
|
|
23
|
+
rage/dead classification the host capture layer uses, via newly shared DOM
|
|
24
|
+
helpers), tab views, and one viewport sample — namespaced under `/~polishd/…`
|
|
25
|
+
paths, tied to a random install id, and suspended the instant the URL leaves
|
|
26
|
+
the dashboard. Nothing from the host site is ever sent: not its analytics,
|
|
27
|
+
not its visitors, not its URLs. `POLISHD_TELEMETRY=off` kills the feature
|
|
28
|
+
(prompt included), `POLISHD_TELEMETRY_ENDPOINT` redirects it for testing, and
|
|
29
|
+
`navigator.doNotTrack` is honored regardless of stored consent. Batches
|
|
30
|
+
travel as `text/plain` so the cross-origin POST stays preflight-free and
|
|
31
|
+
beacon-compatible.
|
|
32
|
+
|
|
3
33
|
## 0.2.1
|
|
4
34
|
|
|
35
|
+
### Losses can be ignored, and the model remembers why
|
|
36
|
+
|
|
37
|
+
A loss had one answer next to it — **File bug** — and no way to say the other
|
|
38
|
+
thing: that it isn't a problem. Nothing recorded that judgement, so the next
|
|
39
|
+
regenerate found the deliberate two-step checkout again, and again.
|
|
40
|
+
|
|
41
|
+
**Ignore** now sits beside File bug on every loss. One click, no explanation
|
|
42
|
+
required: the loss leaves the card, leaves the cached summary immediately, and
|
|
43
|
+
is dropped from every later summary *before* the four-loss cap applies — so
|
|
44
|
+
dismissing one promotes the next real problem into its place rather than
|
|
45
|
+
leaving a gap.
|
|
46
|
+
|
|
47
|
+
A **reason** is optional and offered after the dismissal, so the fast path
|
|
48
|
+
stays one click. Give one and it's replayed to the model verbatim on every
|
|
49
|
+
later call, alongside the problem it dismissed — context the analytics digest
|
|
50
|
+
can't carry ("the second CTA is deliberate", "that page is staff-only"). The
|
|
51
|
+
model is told not to re-report a dismissed problem under any wording, and to
|
|
52
|
+
read the rest of the data taking the stated reason as true. Reasons can be
|
|
53
|
+
edited afterwards, and **Undo** puts a loss back where the model can find it
|
|
54
|
+
again.
|
|
55
|
+
|
|
56
|
+
Dismissals key off the same evidence citation the GitHub issue log uses, so a
|
|
57
|
+
problem refound under different wording is still recognised as the one you
|
|
58
|
+
already reviewed. They fold into the summary fingerprint too: ignoring
|
|
59
|
+
something marks the summary stale exactly like new analytics does, so the next
|
|
60
|
+
refresh really does re-ask the model.
|
|
61
|
+
|
|
5
62
|
### Setup moved out of the way, into a Settings tab
|
|
6
63
|
|
|
7
64
|
The first screen of the dashboard was also its setup form. Provider, key, site
|
package/DATABASE.md
CHANGED
|
@@ -88,13 +88,20 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
88
88
|
text TEXT,
|
|
89
89
|
value DOUBLE PRECISION,
|
|
90
90
|
meta JSONB,
|
|
91
|
-
received_at BIGINT NOT NULL
|
|
91
|
+
received_at BIGINT NOT NULL,
|
|
92
|
+
install_id TEXT
|
|
92
93
|
);
|
|
93
94
|
CREATE INDEX IF NOT EXISTS idx_events_path ON events(path);
|
|
94
95
|
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
95
96
|
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_events_install ON events(install_id);
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
(`install_id` is null on every first-party event; it is only set on rows that
|
|
101
|
+
arrived through a `@polishd/next/telemetry` endpoint, which tags each event
|
|
102
|
+
with the anonymous id of the remote installation that sent it. Databases
|
|
103
|
+
created before the column existed are migrated automatically at open.)
|
|
104
|
+
|
|
98
105
|
---
|
|
99
106
|
|
|
100
107
|
## Production — Option B: Turso (edge SQLite) — not yet wired up
|
package/README.md
CHANGED
|
@@ -363,6 +363,33 @@ authoritative context, so:
|
|
|
363
363
|
> provide. Your source is read only during an explicit scan (or a targeted
|
|
364
364
|
> gap-fill), and always under strict size budgets.
|
|
365
365
|
|
|
366
|
+
### Two verdicts on a loss: file it, or ignore it
|
|
367
|
+
|
|
368
|
+
Every loss carries both answers, side by side.
|
|
369
|
+
|
|
370
|
+
**File bug** (with GitHub connected) verifies the report against your repo's
|
|
371
|
+
source before anything is filed: confirmed reports become an issue with the
|
|
372
|
+
technical analysis and suggested fixes, and a report the code disproves comes
|
|
373
|
+
back as *"checked the source — not an actual bug"* with the reasoning, filing
|
|
374
|
+
nothing.
|
|
375
|
+
|
|
376
|
+
**Ignore** is the other answer: this isn't a problem, stop showing it to me. It
|
|
377
|
+
takes one click and no explanation. The loss drops out of the card and out of
|
|
378
|
+
every later summary — including the cached one, immediately — and the model is
|
|
379
|
+
told about the dismissal on every subsequent call, so it doesn't spend a loss
|
|
380
|
+
slot rediscovering it. Dismissing one loss doesn't shrink the list; the next
|
|
381
|
+
real problem takes its place.
|
|
382
|
+
|
|
383
|
+
A **reason** is optional and offered after the fact — *"the second CTA is
|
|
384
|
+
deliberate"*, *"that page is staff-only"*. It's worth adding: reasons are
|
|
385
|
+
replayed to the model verbatim as context it can't get from the numbers, which
|
|
386
|
+
is what stops the same finding coming back in different words. You can edit a
|
|
387
|
+
reason later, and **Undo** puts the loss back where the model can report it
|
|
388
|
+
again.
|
|
389
|
+
|
|
390
|
+
Ignoring a loss marks the summary stale, exactly like new analytics does, so
|
|
391
|
+
the next refresh actually re-asks the model with the dismissal in hand.
|
|
392
|
+
|
|
366
393
|
## Design review
|
|
367
394
|
|
|
368
395
|
The dashboard's **Design** tab (in the sidebar) is your site's brand guideline
|
|
@@ -415,6 +442,28 @@ import { PolishdMonitor } from "@polishd/next/client";
|
|
|
415
442
|
</PolishdMonitor>
|
|
416
443
|
```
|
|
417
444
|
|
|
445
|
+
## Dashboard telemetry
|
|
446
|
+
|
|
447
|
+
Polishd can report anonymous usage of its own dashboard back to the polishd
|
|
448
|
+
project — the same rage-click/dead-click/page-view signals it captures for
|
|
449
|
+
you, applied to itself, so the dashboard is improved by what it preaches.
|
|
450
|
+
|
|
451
|
+
**Opt-in, and off by default.** The first time you open the dashboard it asks
|
|
452
|
+
once; nothing is sent unless you say yes. What's shared when you do:
|
|
453
|
+
|
|
454
|
+
- Clicks and tab views **inside the dashboard only** (paths are namespaced
|
|
455
|
+
`/~polishd/…` and capture suspends the moment you navigate back to your
|
|
456
|
+
site), plus one viewport-size sample per session.
|
|
457
|
+
- A random install id (`crypto.randomUUID()`, minted on consent) and an
|
|
458
|
+
anonymous per-browser-session id. No cookies, no fingerprinting, no URLs,
|
|
459
|
+
page content, or user data from your site — your analytics never leave your
|
|
460
|
+
database.
|
|
461
|
+
|
|
462
|
+
Say no and it never asks again. Change your mind, or disable it fleet-wide,
|
|
463
|
+
with `POLISHD_TELEMETRY=off`; `navigator.doNotTrack` is honored regardless of
|
|
464
|
+
consent. The receiving end is an ordinary `@polishd/next/telemetry` route on
|
|
465
|
+
the polishd site's own installation — the same package, dogfooding itself.
|
|
466
|
+
|
|
418
467
|
## Configuration
|
|
419
468
|
|
|
420
469
|
Defaults suit a low-traffic site. Override via `polishd.config.ts`:
|
|
@@ -487,6 +536,8 @@ POLISHD_SESSION_COOKIE=my_session
|
|
|
487
536
|
| `POLISHD_GITHUB_REPO` | GitHub (optional) | Repository as `owner/repo` — enables filing bugs from AI-found losses |
|
|
488
537
|
| `POLISHD_GITHUB_TOKEN` | GitHub (optional) | Fine-grained PAT: Contents (read), Issues & Pull requests (write) |
|
|
489
538
|
| `POLISHD_GITHUB_AUTO_ISSUES` | GitHub (optional) | `true` — file a GitHub issue automatically for each new problem a summary finds |
|
|
539
|
+
| `POLISHD_TELEMETRY` | optional | `off` (or `0`/`false`/`no`) — disable [dashboard telemetry](#dashboard-telemetry) entirely, consent prompt included |
|
|
540
|
+
| `POLISHD_TELEMETRY_ENDPOINT` | optional | Where dashboard telemetry is sent, if not the polishd project's own endpoint |
|
|
490
541
|
|
|
491
542
|
AI settings are optional — the dashboard's **Settings** tab configures the same
|
|
492
543
|
fields, and what you save there overrides these env defaults.
|
|
@@ -519,6 +570,7 @@ Run `npx @polishd/next doctor` first — it catches most of these by name, and
|
|
|
519
570
|
| `@polishd/next/client` | `initPolishd`, `PolishdMonitor` |
|
|
520
571
|
| `@polishd/next/server` | store, ingest, queries, `withPolishdSession` (Node only) |
|
|
521
572
|
| `@polishd/next/route` | `POST`, `createPolishdRoute` |
|
|
573
|
+
| `@polishd/next/telemetry` | `POST`, `OPTIONS`, `createPolishdTelemetryRoute` — CORS-enabled ingest for telemetry from *other* polishd installs |
|
|
522
574
|
| `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher`, `composePolishd` |
|
|
523
575
|
| `@polishd/next/dashboard` | `createPolishdPage`, `polishdTokenAuth`, `PolishdDashboard`, `loadPolishdDashboardData` |
|
|
524
576
|
|
package/dist/ai/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ListModelsInput, type SaveAISettingsInput } from "./settings";
|
|
2
|
-
import type { PolishdAISettingsPublic, PolishdLossItem, CreateIssueResult, GenerateDesignReviewResult, GenerateProfileResult, GenerateSummaryResult, ListModelsResult, VerifyGithubResult } from "./types";
|
|
2
|
+
import type { PolishdAISettingsPublic, PolishdLossItem, CreateIssueResult, GenerateDesignReviewResult, GenerateProfileResult, GenerateSummaryResult, IgnoreLossResult, ListModelsResult, UnignoreLossResult, VerifyGithubResult } from "./types";
|
|
3
3
|
/** Generate or refresh the narrative. `force` re-asks the model even if data is unchanged. */
|
|
4
4
|
export declare function generateSummaryAction(force?: boolean): Promise<GenerateSummaryResult>;
|
|
5
5
|
/**
|
|
@@ -31,4 +31,13 @@ export declare function verifyGithubAction(): Promise<VerifyGithubResult>;
|
|
|
31
31
|
* disproved report returns `not-a-bug` with the reasoning instead of filing.
|
|
32
32
|
*/
|
|
33
33
|
export declare function createIssueFromLossAction(loss: Pick<PolishdLossItem, "issue" | "evidence" | "location">): Promise<CreateIssueResult>;
|
|
34
|
+
/**
|
|
35
|
+
* The other verdict on a loss: dismiss it, with an optional reason. The loss
|
|
36
|
+
* leaves every summary from here on, and the dismissal — reason included — is
|
|
37
|
+
* replayed into later model calls so the problem isn't reported again. Calling
|
|
38
|
+
* it on an already-dismissed loss just updates the reason.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ignoreLossAction(loss: Pick<PolishdLossItem, "issue" | "evidence">, reason?: string): Promise<IgnoreLossResult>;
|
|
41
|
+
/** Undo a dismissal: the loss can be found and reported again. */
|
|
42
|
+
export declare function unignoreLossAction(evidence: string): Promise<UnignoreLossResult>;
|
|
34
43
|
//# sourceMappingURL=actions.d.ts.map
|
package/dist/ai/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/ai/actions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/ai/actions.ts"],"names":[],"mappings":"AAsBA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EACV,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,8FAA8F;AAC9F,wBAAsB,qBAAqB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAGzF;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,UAAQ,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAGrC;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,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAGxF;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;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,UAAU,CAAC,EACjD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAED,kEAAkE;AAClE,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAKtF"}
|
package/dist/ai/actions.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { requirePolishdAuth } from "./guard";
|
|
16
16
|
import { generateDesignReview } from "./design";
|
|
17
17
|
import { verifyGithubConnection } from "./github";
|
|
18
|
+
import { ignoreLoss, unignoreLoss } from "./ignored";
|
|
18
19
|
import { createIssueFromLoss } from "./issues";
|
|
19
20
|
import { generateProjectProfile } from "./profile";
|
|
20
21
|
import { loadPolishdDashboardData } from "../server/queries";
|
|
@@ -73,4 +74,24 @@ export async function createIssueFromLossAction(loss) {
|
|
|
73
74
|
await requirePolishdAuth();
|
|
74
75
|
return createIssueFromLoss(loss);
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The other verdict on a loss: dismiss it, with an optional reason. The loss
|
|
79
|
+
* leaves every summary from here on, and the dismissal — reason included — is
|
|
80
|
+
* replayed into later model calls so the problem isn't reported again. Calling
|
|
81
|
+
* it on an already-dismissed loss just updates the reason.
|
|
82
|
+
*/
|
|
83
|
+
export async function ignoreLossAction(loss, reason) {
|
|
84
|
+
await requirePolishdAuth();
|
|
85
|
+
const ignored = await ignoreLoss(loss, reason);
|
|
86
|
+
return ignored
|
|
87
|
+
? { ok: true, ignored }
|
|
88
|
+
: { ok: false, message: "Couldn't save that — the analytics store didn't accept the write." };
|
|
89
|
+
}
|
|
90
|
+
/** Undo a dismissal: the loss can be found and reported again. */
|
|
91
|
+
export async function unignoreLossAction(evidence) {
|
|
92
|
+
await requirePolishdAuth();
|
|
93
|
+
return (await unignoreLoss(evidence))
|
|
94
|
+
? { ok: true }
|
|
95
|
+
: { ok: false, message: "Couldn't undo that — the analytics store didn't accept the write." };
|
|
96
|
+
}
|
|
76
97
|
//# sourceMappingURL=actions.js.map
|
package/dist/ai/actions.js.map
CHANGED
|
@@ -1 +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,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,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,EACL,qBAAqB,EACrB,cAAc,GAGf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;
|
|
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,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACrD,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,EACL,qBAAqB,EACrB,cAAc,GAGf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAc5C,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;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,KAAK,GAAG,KAAK;IAEb,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,oBAAoB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACzC,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,gBAAgB,CAAC,KAAsB;IAC3D,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,qBAAqB,CAAC,KAAK,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;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAiD,EACjD,MAAe;IAEf,MAAM,kBAAkB,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,OAAO;QACZ,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;QACvB,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,mEAAmE,EAAE,CAAC;AAClG,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IACvD,MAAM,kBAAkB,EAAE,CAAC;IAC3B,OAAO,CAAC,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE;QACd,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,mEAAmE,EAAE,CAAC;AAClG,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PolishdIgnoredLoss, PolishdLossItem, PolishdSummary } from "./types";
|
|
2
|
+
/** Same normalization as the issue log: one citation, one verdict. */
|
|
3
|
+
export declare function ignoreKey(evidence: string): string;
|
|
4
|
+
/** Every dismissal, newest first. */
|
|
5
|
+
export declare function loadIgnoredLosses(): Promise<PolishdIgnoredLoss[]>;
|
|
6
|
+
/** The evidence keys to filter losses by. */
|
|
7
|
+
export declare function ignoredKeys(list: PolishdIgnoredLoss[]): Set<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Dismiss one loss, with an optional reason. Re-ignoring an already-ignored
|
|
10
|
+
* loss updates its reason (that's the "add a reason afterwards" path), keeping
|
|
11
|
+
* the original timestamp so the log stays in the order things were reviewed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ignoreLoss(loss: Pick<PolishdLossItem, "issue" | "evidence">, reason?: string): Promise<PolishdIgnoredLoss | null>;
|
|
14
|
+
/** Undo a dismissal. Returns false only when the store refused the write. */
|
|
15
|
+
export declare function unignoreLoss(evidence: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* The dismissals as prompt context: what the owner already reviewed and
|
|
18
|
+
* rejected, so the model doesn't spend a loss slot re-reporting it. Reasons are
|
|
19
|
+
* included verbatim — they're the part the analytics can't tell the model.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ignoredSection(list: PolishdIgnoredLoss[]): string | null;
|
|
22
|
+
/** Hash of the dismissals, so a new one invalidates the cached summary. */
|
|
23
|
+
export declare function ignoredFingerprint(list: PolishdIgnoredLoss[]): string;
|
|
24
|
+
/** Drop dismissed losses from a summary about to be rendered. */
|
|
25
|
+
export declare function withoutIgnored(summary: PolishdSummary, keys: Set<string>): PolishdSummary;
|
|
26
|
+
//# sourceMappingURL=ignored.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignored.d.ts","sourceRoot":"","sources":["../../src/ai/ignored.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAUnF,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElD;AAeD,qCAAqC;AACrC,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAEvE;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAEnE;AAOD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,UAAU,CAAC,EACjD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA0BpC;AAED,6EAA6E;AAC7E,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMrE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,MAAM,GAAG,IAAI,CAcxE;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,MAAM,CASrE;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAChB,cAAc,CAIhB"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — ignored losses (server only).
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to filing a bug. A loss is a claim the model made from the
|
|
5
|
+
* analytics; "File bug" says *yes, and here's the tracker issue*, and this says
|
|
6
|
+
* *no — stop telling me about it*, with or without a reason.
|
|
7
|
+
*
|
|
8
|
+
* A dismissal is not just a UI toggle, or the next regenerate would hand the
|
|
9
|
+
* same problem straight back:
|
|
10
|
+
*
|
|
11
|
+
* 1. Ignored losses are dropped from every summary that renders — the fresh
|
|
12
|
+
* one and the cached one alike — before the four-loss cap applies, so
|
|
13
|
+
* dismissing one promotes a real problem into its place rather than
|
|
14
|
+
* leaving a gap.
|
|
15
|
+
* 2. The dismissals (and their reasons) are replayed into the prompt of every
|
|
16
|
+
* later model call, so the model knows what has already been reviewed and
|
|
17
|
+
* rejected, and why. "Checkout CTA is fine, it's a two-step flow on
|
|
18
|
+
* purpose" is context no digest can carry.
|
|
19
|
+
* 3. They fold into the summary fingerprint, so ignoring something marks the
|
|
20
|
+
* cached summary stale and a regenerate actually re-asks the model.
|
|
21
|
+
*
|
|
22
|
+
* Keyed by the evidence citation — the same key the GitHub issue log uses (see
|
|
23
|
+
* issues.ts) — so a problem refound under slightly different wording is still
|
|
24
|
+
* recognised as the one that was dismissed.
|
|
25
|
+
*/
|
|
26
|
+
import { getMeta, setMeta } from "../server/store";
|
|
27
|
+
import { fingerprintDigest } from "./digest";
|
|
28
|
+
const IGNORED_KEY = "ignored_losses";
|
|
29
|
+
/** Keep the log (and the prompt section built from it) bounded. */
|
|
30
|
+
const MAX_STORED = 50;
|
|
31
|
+
const MAX_IN_PROMPT = 20;
|
|
32
|
+
const MAX_ISSUE_CHARS = 160;
|
|
33
|
+
const MAX_REASON_CHARS = 240;
|
|
34
|
+
/** Same normalization as the issue log: one citation, one verdict. */
|
|
35
|
+
export function ignoreKey(evidence) {
|
|
36
|
+
return evidence.trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
async function loadLog() {
|
|
39
|
+
const raw = await getMeta(IGNORED_KEY);
|
|
40
|
+
if (!raw)
|
|
41
|
+
return {};
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Every dismissal, newest first. */
|
|
51
|
+
export async function loadIgnoredLosses() {
|
|
52
|
+
return Object.values(await loadLog()).sort((a, b) => b.ignoredAt - a.ignoredAt);
|
|
53
|
+
}
|
|
54
|
+
/** The evidence keys to filter losses by. */
|
|
55
|
+
export function ignoredKeys(list) {
|
|
56
|
+
return new Set(list.map((i) => ignoreKey(i.evidence)));
|
|
57
|
+
}
|
|
58
|
+
const clip = (s, max) => {
|
|
59
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
60
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Dismiss one loss, with an optional reason. Re-ignoring an already-ignored
|
|
64
|
+
* loss updates its reason (that's the "add a reason afterwards" path), keeping
|
|
65
|
+
* the original timestamp so the log stays in the order things were reviewed.
|
|
66
|
+
*/
|
|
67
|
+
export async function ignoreLoss(loss, reason) {
|
|
68
|
+
const evidence = loss.evidence.trim();
|
|
69
|
+
if (!evidence)
|
|
70
|
+
return null;
|
|
71
|
+
const key = ignoreKey(evidence);
|
|
72
|
+
const log = await loadLog();
|
|
73
|
+
const trimmed = reason?.trim();
|
|
74
|
+
const entry = {
|
|
75
|
+
issue: clip(loss.issue, MAX_ISSUE_CHARS),
|
|
76
|
+
evidence,
|
|
77
|
+
reason: trimmed ? clip(trimmed, MAX_REASON_CHARS) : undefined,
|
|
78
|
+
ignoredAt: log[key]?.ignoredAt ?? Date.now(),
|
|
79
|
+
};
|
|
80
|
+
log[key] = entry;
|
|
81
|
+
// Bound the log: oldest dismissals fall off first.
|
|
82
|
+
const keys = Object.keys(log);
|
|
83
|
+
if (keys.length > MAX_STORED) {
|
|
84
|
+
for (const k of keys
|
|
85
|
+
.sort((a, b) => log[a].ignoredAt - log[b].ignoredAt)
|
|
86
|
+
.slice(0, keys.length - MAX_STORED)) {
|
|
87
|
+
delete log[k];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return (await setMeta(IGNORED_KEY, JSON.stringify(log))) ? entry : null;
|
|
91
|
+
}
|
|
92
|
+
/** Undo a dismissal. Returns false only when the store refused the write. */
|
|
93
|
+
export async function unignoreLoss(evidence) {
|
|
94
|
+
const log = await loadLog();
|
|
95
|
+
const key = ignoreKey(evidence);
|
|
96
|
+
if (!(key in log))
|
|
97
|
+
return true; // already gone — nothing to undo
|
|
98
|
+
delete log[key];
|
|
99
|
+
return setMeta(IGNORED_KEY, JSON.stringify(log));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The dismissals as prompt context: what the owner already reviewed and
|
|
103
|
+
* rejected, so the model doesn't spend a loss slot re-reporting it. Reasons are
|
|
104
|
+
* included verbatim — they're the part the analytics can't tell the model.
|
|
105
|
+
*/
|
|
106
|
+
export function ignoredSection(list) {
|
|
107
|
+
if (list.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const lines = list
|
|
110
|
+
.slice(0, MAX_IN_PROMPT)
|
|
111
|
+
.map((i) => `- "${i.issue}" (cited: ${i.evidence}) — ` +
|
|
112
|
+
(i.reason ? `dismissed because: ${i.reason}` : "dismissed without a reason"));
|
|
113
|
+
return ("ALREADY REVIEWED AND DISMISSED BY THE SITE OWNER (do not report these " +
|
|
114
|
+
"again, in these or any other words — treat a stated reason as true):\n" +
|
|
115
|
+
lines.join("\n"));
|
|
116
|
+
}
|
|
117
|
+
/** Hash of the dismissals, so a new one invalidates the cached summary. */
|
|
118
|
+
export function ignoredFingerprint(list) {
|
|
119
|
+
if (list.length === 0)
|
|
120
|
+
return "";
|
|
121
|
+
return fingerprintDigest(list
|
|
122
|
+
.slice(0, MAX_IN_PROMPT)
|
|
123
|
+
.map((i) => `${ignoreKey(i.evidence)}:${i.reason ?? ""}`)
|
|
124
|
+
.sort()
|
|
125
|
+
.join("|"));
|
|
126
|
+
}
|
|
127
|
+
/** Drop dismissed losses from a summary about to be rendered. */
|
|
128
|
+
export function withoutIgnored(summary, keys) {
|
|
129
|
+
if (!summary.losses?.length || keys.size === 0)
|
|
130
|
+
return summary;
|
|
131
|
+
const losses = summary.losses.filter((l) => !keys.has(ignoreKey(l.evidence)));
|
|
132
|
+
return losses.length === summary.losses.length ? summary : { ...summary, losses };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=ignored.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ignored.js","sourceRoot":"","sources":["../../src/ai/ignored.ts"],"names":[],"mappings":"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"}
|
package/dist/ai/summary.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { type PolishdDashboardData } from "../server/queries";
|
|
2
2
|
import type { PolishdSummary, GenerateSummaryResult } from "./types";
|
|
3
3
|
export { getAISettingsPublic, saveAISettings, type SaveAISettingsInput, } from "./settings";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The last generated summary, or null. Read-only — never calls a model.
|
|
6
|
+
*
|
|
7
|
+
* Losses the owner has dismissed since it was generated are filtered out here,
|
|
8
|
+
* so an ignore takes effect on the cached summary immediately rather than at
|
|
9
|
+
* the next regenerate. The stored copy keeps them, which is what makes an undo
|
|
10
|
+
* bring the loss back.
|
|
11
|
+
*/
|
|
5
12
|
export declare function loadSummary(): Promise<PolishdSummary | null>;
|
|
6
13
|
/**
|
|
7
14
|
* Generate (or reuse) the narrative summary.
|
package/dist/ai/summary.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/ai/summary.ts"],"names":[],"mappings":"AAmBA,OAAO,EAA4B,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/ai/summary.ts"],"names":[],"mappings":"AAmBA,OAAO,EAA4B,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAexF,OAAO,KAAK,EAIV,cAAc,EACd,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AAIpB;;;;;;;GAOG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAUlE;AA2KD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAiFhC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,CAAC,EAAE,oBAAoB,GAC/B,OAAO,CAAC;IACT,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC,CAAC,CA0BD"}
|
package/dist/ai/summary.js
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* profile.ts), generated once — the summary call never re-reads source.
|
|
12
12
|
* Only when analytics mention a component the profile doesn't cover do we
|
|
13
13
|
* do a tiny targeted read of the files naming it.
|
|
14
|
-
* 3. We fingerprint the inputs (digest + profile) and cache the
|
|
15
|
-
* regenerate with no new data returns the cached text for free
|
|
14
|
+
* 3. We fingerprint the inputs (digest + profile + dismissals) and cache the
|
|
15
|
+
* summary. A regenerate with no new data returns the cached text for free
|
|
16
16
|
* (`regenerated: false`).
|
|
17
17
|
* 4. The system prompt is tight and the output is capped to a paragraph.
|
|
18
18
|
*/
|
|
19
19
|
import { getMeta, setMeta } from "../server/store";
|
|
20
20
|
import { loadPolishdDashboardData } from "../server/queries";
|
|
21
21
|
import { buildDigest, fingerprintDigest } from "./digest";
|
|
22
|
+
import { ignoreKey, ignoredFingerprint, ignoredKeys, ignoredSection, loadIgnoredLosses, withoutIgnored, } from "./ignored";
|
|
22
23
|
import { attachGithubIssues } from "./issues";
|
|
23
24
|
import { callModel } from "./providers";
|
|
24
25
|
import { coverageGaps, loadProjectProfile } from "./profile";
|
|
@@ -27,17 +28,26 @@ import { resolveSettings } from "./settings";
|
|
|
27
28
|
// Settings live in ./settings; re-exported here so existing imports keep working.
|
|
28
29
|
export { getAISettingsPublic, saveAISettings, } from "./settings";
|
|
29
30
|
const SUMMARY_KEY = "ai_summary";
|
|
30
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The last generated summary, or null. Read-only — never calls a model.
|
|
33
|
+
*
|
|
34
|
+
* Losses the owner has dismissed since it was generated are filtered out here,
|
|
35
|
+
* so an ignore takes effect on the cached summary immediately rather than at
|
|
36
|
+
* the next regenerate. The stored copy keeps them, which is what makes an undo
|
|
37
|
+
* bring the loss back.
|
|
38
|
+
*/
|
|
31
39
|
export async function loadSummary() {
|
|
32
40
|
const raw = await getMeta(SUMMARY_KEY);
|
|
33
41
|
if (!raw)
|
|
34
42
|
return null;
|
|
43
|
+
let summary;
|
|
35
44
|
try {
|
|
36
|
-
|
|
45
|
+
summary = JSON.parse(raw);
|
|
37
46
|
}
|
|
38
47
|
catch {
|
|
39
48
|
return null;
|
|
40
49
|
}
|
|
50
|
+
return withoutIgnored(summary, ignoredKeys(await loadIgnoredLosses()));
|
|
41
51
|
}
|
|
42
52
|
const SYSTEM_PROMPT = "You are a product analyst embedded in a website's analytics dashboard. " +
|
|
43
53
|
"You receive a compact digest of real user-behavior signals (clicks, rage " +
|
|
@@ -58,7 +68,10 @@ const SYSTEM_PROMPT = "You are a product analyst embedded in a website's analyti
|
|
|
58
68
|
"must be concrete (what is wrong, where, and what the numbers show), and " +
|
|
59
69
|
"evidence must be the exact page path, CSS selector, or component name " +
|
|
60
70
|
"copied verbatim from the digest that demonstrates it. Never invent a loss: " +
|
|
61
|
-
"if the data shows no real problems, return an empty array."
|
|
71
|
+
"if the data shows no real problems, return an empty array. If the prompt " +
|
|
72
|
+
"lists problems the site owner has already reviewed and dismissed, do not " +
|
|
73
|
+
"report them again — not under a different citation or a different wording — " +
|
|
74
|
+
"and take any reason they gave as true when reading the rest of the data.";
|
|
62
75
|
/**
|
|
63
76
|
* Parse the model's JSON reply, tolerating stray fences or prose around it.
|
|
64
77
|
* Falls back to treating the whole text as the story so a model that ignores
|
|
@@ -99,9 +112,11 @@ function citationTokens(s) {
|
|
|
99
112
|
/**
|
|
100
113
|
* Hold losses to their evidence bar. A loss survives only if its citation
|
|
101
114
|
* actually appears in the prompt we sent (so it can't be invented), and is
|
|
102
|
-
* then matched to a source file when the codebase is on disk.
|
|
115
|
+
* then matched to a source file when the codebase is on disk. Anything the
|
|
116
|
+
* owner has dismissed is dropped here, before the cap — so a dismissal frees
|
|
117
|
+
* its slot for the next real problem instead of leaving a gap.
|
|
103
118
|
*/
|
|
104
|
-
function verifyLosses(raw, promptText, sourceDirs) {
|
|
119
|
+
function verifyLosses(raw, promptText, sourceDirs, ignored) {
|
|
105
120
|
const hay = promptText.toLowerCase();
|
|
106
121
|
const out = [];
|
|
107
122
|
for (const l of raw) {
|
|
@@ -110,6 +125,8 @@ function verifyLosses(raw, promptText, sourceDirs) {
|
|
|
110
125
|
const evidence = typeof l.evidence === "string" ? l.evidence.trim() : "";
|
|
111
126
|
if (!evidence)
|
|
112
127
|
continue; // no citation, no loss
|
|
128
|
+
if (ignored.has(ignoreKey(evidence)))
|
|
129
|
+
continue; // reviewed and dismissed
|
|
113
130
|
const tokens = citationTokens(evidence);
|
|
114
131
|
if (!tokens.length || !tokens.some((t) => hay.includes(t)))
|
|
115
132
|
continue; // not in the data we sent
|
|
@@ -126,11 +143,29 @@ function verifyLosses(raw, promptText, sourceDirs) {
|
|
|
126
143
|
return out;
|
|
127
144
|
}
|
|
128
145
|
/**
|
|
129
|
-
* The
|
|
130
|
-
*
|
|
131
|
-
*
|
|
146
|
+
* The one place the cache fingerprint is defined. It covers the digest, the
|
|
147
|
+
* profile AND the dismissals, so a re-scan or a newly ignored loss marks the
|
|
148
|
+
* cached summary stale exactly like new analytics does.
|
|
149
|
+
*/
|
|
150
|
+
function fingerprintInputs(digest, profile, ignored) {
|
|
151
|
+
const ignoredHash = ignoredFingerprint(ignored);
|
|
152
|
+
return fingerprintDigest([
|
|
153
|
+
digest,
|
|
154
|
+
profile ? `PROFILE:${profile.fingerprint}` : "",
|
|
155
|
+
ignoredHash ? `IGNORED:${ignoredHash}` : "",
|
|
156
|
+
]
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join("\n"));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The exact user message for a summary call, plus its cache fingerprint and
|
|
162
|
+
* the slice of it a citation is allowed to be grounded in.
|
|
163
|
+
*
|
|
164
|
+
* `grounding` is the message minus the dismissals: past problems are quoted
|
|
165
|
+
* back to the model as context, and quoting them must not turn their citations
|
|
166
|
+
* into fresh evidence a new loss can lean on (see verifyLosses).
|
|
132
167
|
*/
|
|
133
|
-
function composePrompt(data, profile, context, sourceDirs) {
|
|
168
|
+
function composePrompt(data, profile, context, sourceDirs, ignored) {
|
|
134
169
|
const digest = buildDigest(data, context);
|
|
135
170
|
const sections = [];
|
|
136
171
|
if (profile) {
|
|
@@ -149,11 +184,12 @@ function composePrompt(data, profile, context, sourceDirs) {
|
|
|
149
184
|
}
|
|
150
185
|
}
|
|
151
186
|
}
|
|
152
|
-
|
|
153
|
-
const
|
|
187
|
+
const dismissed = ignoredSection(ignored);
|
|
188
|
+
const grounding = [...sections, digest].join("\n\n");
|
|
154
189
|
return {
|
|
155
|
-
user,
|
|
156
|
-
|
|
190
|
+
user: dismissed ? [...sections, dismissed, digest].join("\n\n") : grounding,
|
|
191
|
+
grounding,
|
|
192
|
+
fingerprint: fingerprintInputs(digest, profile, ignored),
|
|
157
193
|
};
|
|
158
194
|
}
|
|
159
195
|
/**
|
|
@@ -176,8 +212,8 @@ export async function generateSummary(opts = {}) {
|
|
|
176
212
|
message: "No analytics captured yet — browse the site, then generate.",
|
|
177
213
|
};
|
|
178
214
|
}
|
|
179
|
-
const profile = await loadProjectProfile();
|
|
180
|
-
const { user, fingerprint } = composePrompt(data, profile, settings.context, settings.sourceDirs);
|
|
215
|
+
const [profile, ignored] = await Promise.all([loadProjectProfile(), loadIgnoredLosses()]);
|
|
216
|
+
const { user, grounding, fingerprint } = composePrompt(data, profile, settings.context, settings.sourceDirs, ignored);
|
|
181
217
|
if (!opts.force) {
|
|
182
218
|
const cached = await loadSummary();
|
|
183
219
|
if (cached && cached.fingerprint === fingerprint) {
|
|
@@ -216,7 +252,7 @@ export async function generateSummary(opts = {}) {
|
|
|
216
252
|
const parsed = parseStructured(reply.text);
|
|
217
253
|
// Link losses to their GitHub issues — filing new ones when the owner
|
|
218
254
|
// enabled auto-filing, attaching already-filed ones either way.
|
|
219
|
-
const losses = await attachGithubIssues(verifyLosses(parsed.losses,
|
|
255
|
+
const losses = await attachGithubIssues(verifyLosses(parsed.losses, grounding, settings.sourceDirs, ignoredKeys(ignored)));
|
|
220
256
|
const summary = {
|
|
221
257
|
text: parsed.story,
|
|
222
258
|
wins: parsed.wins,
|
|
@@ -243,14 +279,15 @@ export async function loadSummaryState(preloaded) {
|
|
|
243
279
|
if (!data.overview.ready || data.overview.totalEvents === 0) {
|
|
244
280
|
return { summary, stale: false, currentFingerprint: null };
|
|
245
281
|
}
|
|
246
|
-
const [{ settings }, profile] = await Promise.all([
|
|
282
|
+
const [{ settings }, profile, ignored] = await Promise.all([
|
|
247
283
|
resolveSettings(),
|
|
248
284
|
loadProjectProfile(),
|
|
285
|
+
loadIgnoredLosses(),
|
|
249
286
|
]);
|
|
250
287
|
// Staleness only needs the fingerprint — skip the targeted source read by
|
|
251
|
-
// computing it the same
|
|
252
|
-
|
|
253
|
-
const currentFingerprint =
|
|
288
|
+
// computing it from the same inputs composePrompt does, without collecting
|
|
289
|
+
// excerpts.
|
|
290
|
+
const currentFingerprint = fingerprintInputs(buildDigest(data, settings.context), profile, ignored);
|
|
254
291
|
return {
|
|
255
292
|
summary,
|
|
256
293
|
stale: summary !== null && summary.fingerprint !== currentFingerprint,
|