nebulosity 1.0.0 → 1.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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +11 -11
- package/.next/diagnostics/route-bundle-stats.json +10 -10
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/server/app/_not-found/page/next-font-manifest.json +2 -3
- package/.next/server/app/_not-found/page.js +3 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +27 -20
- package/.next/server/app/_not-found.segments/_full.segment.rsc +27 -20
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +34 -26
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +29 -23
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +33 -24
- package/.next/server/app/index.segments/_full.segment.rsc +29 -23
- package/.next/server/app/index.segments/_tree.segment.rsc +4 -5
- package/.next/server/app/page/build-manifest.json +3 -3
- package/.next/server/app/page/next-font-manifest.json +2 -3
- package/.next/server/app/page.js +4 -4
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +3 -3
- package/.next/server/chunks/_0uqus0ehnijt-._.js +1 -1
- package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0_3l5tdzqy-td._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__10ockmlli-lun._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0dyligeswbb47._.js → [root-of-the-server]__16-gclvix9mjd._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1auodc3c0rppy._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1i9r59yk4i69p._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1l3qp8u9yc_ig._.js +3 -0
- package/.next/server/chunks/ssr/_1p-0wsx9qfm68._.js +13 -0
- package/.next/server/chunks/ssr/app_map_18iu6j19jsqmu._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_1r3iqtbm45lqp._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js +3 -0
- package/.next/server/middleware-build-manifest.js +11 -11
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +4 -6
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{1rljem7o1jtdo.js → 18kfexnrqek07.js} +1 -1
- package/.next/static/chunks/1hwbgd4nvvhg8.js +1 -0
- package/.next/static/chunks/25r4lkkf1shfo.css +3 -0
- package/.next/static/chunks/35ubvk2ivdjgs.js +1 -0
- package/.next/static/chunks/3ieqvmcg3hofj.js +31 -0
- package/.next/static/chunks/3w00e1w2125pp.js +11 -0
- package/.next/static/media/733da6bd8dac84c8-s.3hlisc-pwq13z.woff2 +0 -0
- package/.next/static/media/784318cea1dfcfa4-s.0-oo9_1x_xmvg.woff2 +0 -0
- package/.next/static/media/7e114457660b62ed-s.36fqqi66-3tjb.woff2 +0 -0
- package/.next/static/media/85e86a88f68a624a-s.357dd_6abeqge.woff2 +0 -0
- package/.next/static/media/9a321a2c78eff0b5-s.p.1bkzbscqrt8rl.woff2 +0 -0
- package/.next/static/media/9fecbd3e5ea9bccb-s.3_z45zcoc-xoz.woff2 +0 -0
- package/.next/static/media/c64053b510252f55-s.1kd-r_s9smv5m.woff2 +0 -0
- package/.next/static/media/c6a5c2fdc12da45c-s.p.17-aodiw50953.woff2 +0 -0
- package/.next/static/media/f263782c6a4bddee-s.1gyfqm5yhhzs7.woff2 +0 -0
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/README.md +13 -4
- package/app/globals.css +91 -54
- package/app/layout.tsx +20 -11
- package/app/map/BackButton.tsx +5 -5
- package/app/map/BoxActions.tsx +20 -12
- package/app/map/Breadcrumb.tsx +132 -46
- package/app/map/DetailPanel.tsx +408 -297
- package/app/map/Explorer.tsx +268 -0
- package/app/map/Header.tsx +105 -0
- package/app/map/HelpDialog.tsx +37 -34
- package/app/map/Legend.tsx +19 -75
- package/app/map/LineCard.tsx +187 -0
- package/app/map/MapBox.tsx +73 -38
- package/app/map/MapCanvas.tsx +344 -115
- package/app/map/MapToolbar.tsx +377 -0
- package/app/map/MapView.tsx +217 -213
- package/app/map/RepoMenu.tsx +13 -12
- package/app/map/RoutedEdge.tsx +48 -16
- package/app/map/SearchPalette.tsx +6 -6
- package/app/map/SourceViewer.tsx +19 -17
- package/app/map/StatusBar.tsx +166 -0
- package/app/map/embed.ts +22 -2
- package/app/map/emphasis.ts +94 -8
- package/app/map/explorerRows.ts +85 -0
- package/app/map/flowNodes.ts +12 -7
- package/app/map/format.ts +2 -2
- package/app/map/history.ts +4 -3
- package/app/map/inspector.ts +113 -0
- package/app/map/labels.ts +66 -7
- package/app/map/narrow.ts +165 -85
- package/app/map/opening.ts +16 -0
- package/app/map/portals.ts +2 -2
- package/app/map/routes.ts +1060 -55
- package/app/map/runs.ts +108 -0
- package/app/map/shell.ts +91 -0
- package/app/map/shortcuts.ts +2 -1
- package/app/map/summaries.ts +41 -5
- package/app/map/text.ts +9 -1
- package/app/map/theme.ts +197 -0
- package/app/map/useDismiss.ts +20 -6
- package/app/map/useEditorBridge.ts +12 -1
- package/app/map/useThemeKind.ts +28 -0
- package/app/map/useWindowSize.ts +37 -0
- package/app/map/viewport.ts +134 -16
- package/app/map/weights.ts +80 -0
- package/app/page.tsx +3 -5
- package/lib/background/child.ts +2 -0
- package/lib/background/service.ts +26 -10
- package/lib/contracts.ts +25 -2
- package/lib/db/migrations/0011_plain_summaries_prompt_version.sql +1 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1498 -0
- package/lib/db/migrations/meta/_journal.json +7 -0
- package/lib/db/schema.ts +2 -1
- package/lib/db/store/map.ts +6 -0
- package/lib/db/store/summaries.ts +14 -8
- package/lib/engine/describe.ts +2 -0
- package/lib/index/summarize/budget.ts +5 -2
- package/lib/index/summarize/claude.ts +19 -4
- package/lib/index/summarize/compare.ts +2 -2
- package/lib/index/summarize/plan.ts +73 -13
- package/lib/index/summarize/preview.ts +12 -9
- package/lib/index/summarize/prompts.ts +122 -25
- package/lib/index/summarize/run.ts +48 -18
- package/lib/index/summarize/snapshot.ts +7 -2
- package/lib/index/summarize/status.ts +3 -2
- package/lib/tools/text.ts +3 -2
- package/mcp/server.ts +1 -1
- package/package.json +4 -2
- package/scripts/commands/summarize-command.ts +27 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +0 -3
- package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +0 -3
- package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +0 -9
- package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +0 -3
- package/.next/static/chunks/02x9pxyl2d8om.css +0 -3
- package/.next/static/chunks/0p-0bv2dzer18.js +0 -7
- package/.next/static/chunks/10u7p2gj02qay.js +0 -1
- package/.next/static/chunks/31qp8jfkw0x2h.js +0 -31
- package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
- package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
- package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
- package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
- package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
- package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
- package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
- package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
- package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
- package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
- package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
- package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
- package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
- package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
- package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
- package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
- package/app/map/ActivityBar.tsx +0 -81
- package/app/map/StatusPill.tsx +0 -93
- package/app/map/TopBar.tsx +0 -84
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_buildManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_ssgManifest.js +0 -0
package/lib/db/schema.ts
CHANGED
|
@@ -156,9 +156,10 @@ export const summaries = sqliteTable("summaries", {
|
|
|
156
156
|
targetKind: text("target_kind", { enum: SUMMARY_TARGET_KINDS }).notNull(),
|
|
157
157
|
targetPath: text("target_path").notNull(), // "" for the repo itself
|
|
158
158
|
tagline: text("tagline"), // 3–6 words asked for, up to 8 kept whole; null until a summary exists
|
|
159
|
-
summary: text("summary"), //
|
|
159
|
+
summary: text("summary"), // short paragraphs, separated by blank lines
|
|
160
160
|
model: text("model"), // the model that wrote it, or "template"
|
|
161
161
|
inputHash: text("input_hash"), // hash(model, prompt version, input) of the text above
|
|
162
|
+
promptVersion: integer("prompt_version"), // the prompt version that wrote the text above, null for a template or for text from before v3 (ADR-034)
|
|
162
163
|
status: text("status", { enum: SUMMARY_STATUSES }).notNull(),
|
|
163
164
|
error: text("error"), // why the last attempt failed
|
|
164
165
|
// A Message Batch can take hours. These two say which batch carries this row's request and
|
package/lib/db/store/map.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type SummaryState = {
|
|
|
16
16
|
path: string;
|
|
17
17
|
tagline: string | null;
|
|
18
18
|
summary: string | null;
|
|
19
|
+
/** The model that wrote the text, or "template". */
|
|
20
|
+
model: string | null;
|
|
21
|
+
/** The prompt version that wrote the text, null for a template or for text from before v3 (ADR-034). */
|
|
22
|
+
promptVersion: number | null;
|
|
19
23
|
status: StoredSummaryStatus;
|
|
20
24
|
error: string | null;
|
|
21
25
|
};
|
|
@@ -40,6 +44,8 @@ const summaryColumns = {
|
|
|
40
44
|
path: summaries.targetPath,
|
|
41
45
|
tagline: summaries.tagline,
|
|
42
46
|
summary: summaries.summary,
|
|
47
|
+
model: summaries.model,
|
|
48
|
+
promptVersion: summaries.promptVersion,
|
|
43
49
|
status: summaries.status,
|
|
44
50
|
error: summaries.error,
|
|
45
51
|
};
|
|
@@ -20,6 +20,8 @@ export type StoredSummary = {
|
|
|
20
20
|
summary: string | null;
|
|
21
21
|
model: string | null;
|
|
22
22
|
inputHash: string | null;
|
|
23
|
+
/** The prompt version that wrote the text, null for a template or for text from before v3 (ADR-034). */
|
|
24
|
+
promptVersion: number | null;
|
|
23
25
|
status: StoredSummaryStatus;
|
|
24
26
|
batchId: string | null;
|
|
25
27
|
error: string | null;
|
|
@@ -53,7 +55,7 @@ export type SummaryResult = {
|
|
|
53
55
|
batchId: string | null;
|
|
54
56
|
/** The input the answer was written for. */
|
|
55
57
|
inputHash: string;
|
|
56
|
-
outcome: { ok: true; text: SummaryText; model: string } | { ok: false; error: string };
|
|
58
|
+
outcome: { ok: true; text: SummaryText; model: string; promptVersion: number } | { ok: false; error: string };
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
export type UsageRow = { model: string; transport: "direct" | "batch"; requests: number; inputTokens: number; outputTokens: number };
|
|
@@ -117,6 +119,7 @@ export function loadSummaries(db: Db, repoId: number): StoredSummary[] {
|
|
|
117
119
|
summary: summaries.summary,
|
|
118
120
|
model: summaries.model,
|
|
119
121
|
inputHash: summaries.inputHash,
|
|
122
|
+
promptVersion: summaries.promptVersion,
|
|
120
123
|
status: summaries.status,
|
|
121
124
|
batchId: summaries.batchId,
|
|
122
125
|
error: summaries.error,
|
|
@@ -165,7 +168,7 @@ export function applyTargetUpdates(db: Db, repoId: number, updates: readonly Tar
|
|
|
165
168
|
|
|
166
169
|
if (u.templateText){
|
|
167
170
|
const result = tx.update(summaries).set({
|
|
168
|
-
tagline: u.templateText.tagline, summary: u.templateText.summary, model: "template",
|
|
171
|
+
tagline: u.templateText.tagline, summary: u.templateText.summary, model: "template", promptVersion: null,
|
|
169
172
|
inputHash: u.inputHash, status: "skipped", error: null, updatedAt: now,
|
|
170
173
|
}).where(row).run();
|
|
171
174
|
if (result.changes > 0) changed = true;
|
|
@@ -217,11 +220,11 @@ export function loadBatchRows(db: Db, batchId: string): { id: number; kind: Summ
|
|
|
217
220
|
|
|
218
221
|
/**
|
|
219
222
|
* Writes answers back and returns how many rows changed.
|
|
220
|
-
* - A summary lands as "fresh" with its text and
|
|
221
|
-
* the embeddings stage redoes it.
|
|
222
|
-
* - A failure becomes "failed" and keeps any older text on show
|
|
223
|
-
* leaves a batch's claim on the row in place: that
|
|
224
|
-
* answer can still land.
|
|
223
|
+
* - A summary lands as "fresh" with its text, inputHash and prompt version. Its old vector is
|
|
224
|
+
* deleted, so the embeddings stage redoes it.
|
|
225
|
+
* - A failure becomes "failed" and keeps any older text on show, with the prompt version that
|
|
226
|
+
* wrote it. A failure from the regular API leaves a batch's claim on the row in place: that
|
|
227
|
+
* request is already paid for, and its answer can still land.
|
|
225
228
|
* - An answer from a batch that no longer owns the row (the row was redone some other way
|
|
226
229
|
* meanwhile) is dropped.
|
|
227
230
|
*/
|
|
@@ -234,7 +237,10 @@ export function writeResults(db: Db, results: readonly SummaryResult[]): number
|
|
|
234
237
|
for (const r of results){
|
|
235
238
|
const owner = r.batchId === null ? eq(summaries.id, r.id) : and(eq(summaries.id, r.id), eq(summaries.batchId, r.batchId));
|
|
236
239
|
const answer = r.outcome.ok
|
|
237
|
-
? {
|
|
240
|
+
? {
|
|
241
|
+
tagline: r.outcome.text.tagline, summary: r.outcome.text.summary, model: r.outcome.model, promptVersion: r.outcome.promptVersion,
|
|
242
|
+
inputHash: r.inputHash, status: "fresh" as const, error: null,
|
|
243
|
+
}
|
|
238
244
|
: { status: "failed" as const, error: r.outcome.error };
|
|
239
245
|
|
|
240
246
|
// Only a regular-API failure keeps the row's claim: a regular-API success supersedes the batch,
|
package/lib/engine/describe.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { requireRepository } from "../db/store/structure";
|
|
|
6
6
|
import { readIndexVersion } from "../db/store/version";
|
|
7
7
|
import { DEFAULT_MAX_FILE_BYTES } from "../index/discover";
|
|
8
8
|
import { repoKey } from "../index/paths";
|
|
9
|
+
import { writtenInOlderStyle } from "../index/summarize/prompts";
|
|
9
10
|
import { docsAbout, docsToReadFirst } from "./docs";
|
|
10
11
|
import { stalePaths } from "./freshness";
|
|
11
12
|
import { cachedAreaCounts, type TargetKind } from "./graph";
|
|
@@ -72,6 +73,7 @@ function describeUncached(db: Db, repo: string, repoId: number, version: number,
|
|
|
72
73
|
...(row?.tagline ? { tagline: row.tagline, summary: row.summary ?? "" } : {}),
|
|
73
74
|
summaryStatus,
|
|
74
75
|
...(summaryStatus === "failed" && row?.error ? { summaryError: row.error } : {}),
|
|
76
|
+
...(row && writtenInOlderStyle(row) ? { summaryOlderStyle: true as const } : {}),
|
|
75
77
|
...(docs.length > 0 ? { docs } : {}),
|
|
76
78
|
stats: folder
|
|
77
79
|
? { fileCount: folder.fileCount, lineCount: sumLinesInside(db, repoId, targetPath) }
|
|
@@ -16,8 +16,11 @@ export const ASSUMED_TOKENS: Readonly<Record<SummaryTargetKind, TokenUsage>> = {
|
|
|
16
16
|
repo: { inputTokens: 1_000, outputTokens: 250 },
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* One request the projection prices: the kind of target it's for, and the model that answers it. restyle: it's sent
|
|
21
|
+
* only because an older prompt version wrote its summary (PlannedTarget.restyle), so the estimate can say how many are.
|
|
22
|
+
*/
|
|
23
|
+
export type ProjectedRequest = { kind: SummaryTargetKind; model: string; restyle?: true };
|
|
21
24
|
|
|
22
25
|
type Billing = "direct" | "batch";
|
|
23
26
|
|
|
@@ -14,7 +14,7 @@ export const SUMMARY_SCHEMA = {
|
|
|
14
14
|
type: "object",
|
|
15
15
|
properties: {
|
|
16
16
|
tagline: { type: "string", description: "A noun phrase of 3 to 6 plain words." },
|
|
17
|
-
summary: { type: "string", description: "
|
|
17
|
+
summary: { type: "string", description: "Short paragraphs separated by a blank line, about the length the request gives." },
|
|
18
18
|
},
|
|
19
19
|
required: ["tagline", "summary"],
|
|
20
20
|
additionalProperties: false,
|
|
@@ -32,6 +32,20 @@ const SENTENCE_END = /[.!?]["'`)\]]*$/;
|
|
|
32
32
|
/** Two or more periods at the very end ("…in one place.."), which read as one. */
|
|
33
33
|
const TRAILING_PERIODS = /\.{2,}$/;
|
|
34
34
|
|
|
35
|
+
/** A paragraph break: two line breaks with only whitespace between them, CRLF and extra blank lines included. */
|
|
36
|
+
const PARAGRAPH_BREAK = /\n\s*\n/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A summary's paragraphs in the one shape every reader splits on (P5): exactly one blank line between paragraphs,
|
|
40
|
+
* each trimmed, and a single line break inside one read as a space.
|
|
41
|
+
*/
|
|
42
|
+
function normalizeParagraphs(summary: string): string {
|
|
43
|
+
return summary.split(PARAGRAPH_BREAK)
|
|
44
|
+
.map((paragraph) => paragraph.trim().replace(/\s*\n\s*/g, " "))
|
|
45
|
+
.filter((paragraph) => paragraph !== "")
|
|
46
|
+
.join("\n\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
/** Regular-API retries of a batch's cut-off answers in flight at once: as many as the runner's regular-API waves send. */
|
|
36
50
|
const RETRY_CONCURRENCY = 4;
|
|
37
51
|
|
|
@@ -170,10 +184,11 @@ export function parseSummary(reply: Pick<Reply, "stop_reason" | "content">): Sum
|
|
|
170
184
|
const tagline = cleanTagline(parsed.data.tagline);
|
|
171
185
|
if (!tagline) throw new Error("the reply didn't have a tagline and a summary");
|
|
172
186
|
|
|
173
|
-
// ".." is a slip, not an ellipsis, and so is "...": the run becomes one "." before the check below
|
|
174
|
-
|
|
187
|
+
// ".." is a slip, not an ellipsis, and so is "...": the run becomes one "." before the check below.
|
|
188
|
+
// Then the paragraphs take their one shape, whatever line breaks the model wrote between them.
|
|
189
|
+
const summary = normalizeParagraphs(parsed.data.summary.replace(TRAILING_PERIODS, "."));
|
|
175
190
|
// A normal stop doesn't prove the answer is whole: Summary Review #3 found well-formed JSON, stopped at end_turn,
|
|
176
|
-
// whose summary ended "…applyEdits. The". A summary
|
|
191
|
+
// whose summary ended "…applyEdits. The". A summary's paragraphs are whole sentences, so a whole one ends a sentence.
|
|
177
192
|
if (!SENTENCE_END.test(summary)) throw new Error(`${CUT_OFF} (stop reason: end_turn): ${summary === "" ? "it's empty" : "it ends mid-sentence"}`);
|
|
178
193
|
return { tagline, summary };
|
|
179
194
|
}
|
|
@@ -2,7 +2,7 @@ import type { FileFacts } from "../../db/store/summaries";
|
|
|
2
2
|
import { ASSUMED_TOKENS } from "./budget";
|
|
3
3
|
import { writtenByModel } from "./plan";
|
|
4
4
|
import { costUsd, formatUsd, pricePerMTok, type PriceTable } from "./pricing";
|
|
5
|
-
import { plural, PROMPT_VERSION } from "./prompts";
|
|
5
|
+
import { joinParagraphs, plural, PROMPT_VERSION } from "./prompts";
|
|
6
6
|
import type { SummaryOutcome } from "./types";
|
|
7
7
|
|
|
8
8
|
/** Folder inputs, assumed for the estimate as the spend cap assumes them (budget.ts): a folder lists its children's taglines and summaries. */
|
|
@@ -108,7 +108,7 @@ export function renderComparison(report: ComparisonReport): string {
|
|
|
108
108
|
for (const model of report.models){
|
|
109
109
|
const outcome = row.outcomes[model];
|
|
110
110
|
lines.push(outcome?.ok
|
|
111
|
-
? `- ${answerLabel(model, outcome.model)} *${outcome.text.tagline}* — ${outcome.text.summary}`
|
|
111
|
+
? `- ${answerLabel(model, outcome.model)} *${outcome.text.tagline}* — ${joinParagraphs(outcome.text.summary)}`
|
|
112
112
|
: `- **${model}:** failed: ${outcome ? outcome.error : "no answer"}`);
|
|
113
113
|
}
|
|
114
114
|
});
|
|
@@ -4,12 +4,14 @@ import { targetKey, type FileFacts, type FolderFacts, type StoredSummary, type S
|
|
|
4
4
|
import { DEFAULT_MAX_FILE_BYTES } from "../discover";
|
|
5
5
|
import { dirOf } from "../paths";
|
|
6
6
|
import {
|
|
7
|
-
importsOnlyFileSummary, PROMPT_VERSION, renderFolderPrompt, renderRepoPrompt, testFileSummary,
|
|
7
|
+
folderTier, importsOnlyFileSummary, MAX_FOLDER_ITEMS, PROMPT_VERSION, renderFolderPrompt, renderRepoPrompt, testFileSummary,
|
|
8
|
+
testFolderSummary, UNRECORDED_PROMPT_VERSION, withLengthLine, writtenInOlderStyle, type FolderItem,
|
|
8
9
|
} from "./prompts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* current: the text matches the current input
|
|
12
|
-
* template is missing or out of date, written right away ·
|
|
12
|
+
* current: the text matches the current input, or an older prompt wrote it for this same input (P1) ·
|
|
13
|
+
* template: a test file or test-only folder whose template is missing or out of date, written right away ·
|
|
14
|
+
* ready: needs a model and can go now ·
|
|
13
15
|
* waiting: needs a model, but a child isn't done yet · lazy: an out-of-date folder left
|
|
14
16
|
* for later (redone when someone opens it) · in-flight: a batch already carries it ·
|
|
15
17
|
* failed: failed earlier in this run; the next run retries it
|
|
@@ -26,10 +28,15 @@ export type PlannedTarget = {
|
|
|
26
28
|
/** The row's status from now on; null leaves it as it is. */
|
|
27
29
|
status: StoredSummaryStatus | null;
|
|
28
30
|
templateText?: SummaryText;
|
|
29
|
-
/** Folders and the repo. A file's prompt is built from disk when it's sent. */
|
|
31
|
+
/** Folders and the repo, ending with the length line. A file's prompt is built from disk when it's sent. */
|
|
30
32
|
prompt?: string;
|
|
33
|
+
/** planned to be written only because an older prompt version wrote its text (P1) */
|
|
34
|
+
restyle?: true;
|
|
31
35
|
};
|
|
32
36
|
|
|
37
|
+
/** Which summaries an older prompt wrote a plan restyles (PlanOptions.restyle). */
|
|
38
|
+
export type Restyle = "all" | "none" | { path: string };
|
|
39
|
+
|
|
33
40
|
export type PlanOptions = {
|
|
34
41
|
models: { readonly file: string; readonly folder: string; readonly repo: string };
|
|
35
42
|
/** true redoes out-of-date folders now (a full run); false leaves them lazy until opened (incremental runs). */
|
|
@@ -41,6 +48,19 @@ export type PlanOptions = {
|
|
|
41
48
|
* cap, DEFAULT_MAX_FILE_BYTES, the product's only one; tests pass another to match an index they built with it.
|
|
42
49
|
*/
|
|
43
50
|
maxFileBytes?: number;
|
|
51
|
+
/**
|
|
52
|
+
* which summaries an older prompt version wrote, with their input otherwise unchanged, are redone in the current
|
|
53
|
+
* style: every one (a full run and its estimate), the one a panel opened (summarizeNow), or none (the automatic jobs
|
|
54
|
+
* and the stage refresh). Default: "none".
|
|
55
|
+
*/
|
|
56
|
+
restyle?: Restyle;
|
|
57
|
+
/**
|
|
58
|
+
* true plans an out-of-date folder or the repo whose text an older prompt version wrote, and which this plan doesn't
|
|
59
|
+
* restyle, as lazy, so it counts as settled and its parent can go over its older text. Under Q2 B only opening it
|
|
60
|
+
* rewrites it, so without this its parent would wait for it for good (the Task 4 review's I1, ruled as option a).
|
|
61
|
+
* summarizeNow and the sweep pass it. A full run and the stage refresh don't. Default: false.
|
|
62
|
+
*/
|
|
63
|
+
olderFoldersWaitToBeOpened?: boolean;
|
|
44
64
|
};
|
|
45
65
|
|
|
46
66
|
/**
|
|
@@ -82,6 +102,10 @@ export function planSummaries(
|
|
|
82
102
|
return row?.tagline && row.summary !== null ? { tagline: row.tagline, summary: row.summary } : null;
|
|
83
103
|
};
|
|
84
104
|
|
|
105
|
+
// whether this plan restyles the target at `path`: a file and a folder never share a path, and only the repo's is ""
|
|
106
|
+
const restyled = (path: string): boolean =>
|
|
107
|
+
options.restyle === "all" || (typeof options.restyle === "object" && options.restyle.path === path);
|
|
108
|
+
|
|
85
109
|
// ---- files: independent of each other ---------------------------------------------
|
|
86
110
|
for (const file of files){
|
|
87
111
|
const key = targetKey("file", file.path);
|
|
@@ -99,9 +123,10 @@ export function planSummaries(
|
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
// A file's prompt is built from its bytes, so its content hash stands in for the prompt
|
|
102
|
-
// here: same bytes, same prompt. Planning then never has to read source files.
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
// here: same bytes, same prompt. Planning then never has to read source files. Older
|
|
127
|
+
// prompt versions hashed the same stand-in.
|
|
128
|
+
const input = `file\n${file.path}\n${file.contentHash}`;
|
|
129
|
+
planned.set(key, decide("file", file.path, options.models.file, { input, olderInput: () => input, restyled: restyled(file.path) }, row, true, options));
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
// ---- folders, deepest first, so every child is planned before its parent ------------
|
|
@@ -164,11 +189,21 @@ export function planSummaries(
|
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
const model = kind === "repo" ? options.models.repo : options.models.folder;
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
192
|
+
const repo = { name: folder.name, fileCount: folder.fileCount, folderCount: folders.length };
|
|
193
|
+
// The body is hashed without the length line (P2): the line follows from the file count, which the body
|
|
194
|
+
// holds, and from the prompt version, which the hash holds. The prompt sent ends with it.
|
|
195
|
+
const body = kind === "repo" ? renderRepoPrompt(repo, items) : renderFolderPrompt(folder, items);
|
|
196
|
+
// the body as prompt v2 rendered it, each child's summary verbatim, for the older check (P2): v2 is the only
|
|
197
|
+
// older version that stored summaries
|
|
198
|
+
const olderInput = (): string => (kind === "repo"
|
|
199
|
+
? renderRepoPrompt(repo, items, MAX_FOLDER_ITEMS, false)
|
|
200
|
+
: renderFolderPrompt(folder, items, MAX_FOLDER_ITEMS, false));
|
|
201
|
+
const prompt = withLengthLine(body, kind, kind === "repo" ? "large" : folderTier(folder.fileCount));
|
|
170
202
|
const key = targetKey(kind, folder.path);
|
|
171
|
-
planned.set(key, {
|
|
203
|
+
planned.set(key, {
|
|
204
|
+
...decide(kind, folder.path, model, { input: body, olderInput, restyled: restyled(folder.path) }, storedByKey.get(key), settled, options),
|
|
205
|
+
prompt,
|
|
206
|
+
});
|
|
172
207
|
}
|
|
173
208
|
|
|
174
209
|
// ---- queue order --------------------------------------------------------------------
|
|
@@ -216,28 +251,53 @@ export function foldersAbove(targets: readonly PlannedTarget[], below: readonly
|
|
|
216
251
|
return targets.filter((t) => above.has(targetKey(t.kind, t.path)));
|
|
217
252
|
}
|
|
218
253
|
|
|
219
|
-
/**
|
|
254
|
+
/** What decide() hashes for one model-written target, and whether the plan restyles it. */
|
|
255
|
+
type TargetInput = {
|
|
256
|
+
/** The input under the current prompt version: a file's stand-in, or a folder's or the repo's prompt body. */
|
|
257
|
+
input: string;
|
|
258
|
+
/** The input as an older prompt version rendered it (P2), rendered only when a row an older prompt wrote missed input's hash. */
|
|
259
|
+
olderInput: () => string;
|
|
260
|
+
/** Whether PlanOptions.restyle names this target. */
|
|
261
|
+
restyled: boolean;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The state of one model-written target, given its input, its stored row and whether its children are settled. A row
|
|
266
|
+
* an older prompt wrote for the same input is older style only (P1): current, unless the plan restyles it. One it wrote
|
|
267
|
+
* for another input is out of date, and with olderFoldersWaitToBeOpened a folder or the repo like that is lazy unless
|
|
268
|
+
* the plan restyles it.
|
|
269
|
+
*/
|
|
220
270
|
function decide(
|
|
221
271
|
kind: SummaryTargetKind,
|
|
222
272
|
path: string,
|
|
223
273
|
model: string,
|
|
224
|
-
|
|
274
|
+
{ input, olderInput, restyled }: TargetInput,
|
|
225
275
|
row: StoredSummary | undefined,
|
|
226
276
|
childrenSettled: boolean,
|
|
227
277
|
options: PlanOptions
|
|
228
278
|
): PlannedTarget {
|
|
279
|
+
const hash = inputHash(model, PROMPT_VERSION, input);
|
|
229
280
|
const base = { kind, path, model, inputHash: hash };
|
|
230
281
|
|
|
231
282
|
if (row?.batchId) return { ...base, state: "in-flight", status: null };
|
|
232
283
|
if (options.failedThisRun.has(targetKey(kind, path))) return { ...base, state: "failed", status: null };
|
|
233
284
|
// the text on the row was written for exactly this input (an undone edit lands here too)
|
|
234
285
|
if (row?.inputHash === hash) return { ...base, state: "current", status: row.status === "fresh" ? null : "fresh" };
|
|
286
|
+
// older style only (P1): a model wrote it with an older prompt, and its input hasn't changed since
|
|
287
|
+
if (row && writtenInOlderStyle(row) && row.inputHash === inputHash(model, row.promptVersion ?? UNRECORDED_PROMPT_VERSION, olderInput())){
|
|
288
|
+
const status = row.status === "fresh" ? null : "fresh";
|
|
289
|
+
if (!restyled) return { ...base, state: "current", status };
|
|
290
|
+
if (kind !== "file" && !options.refreshStaleFolders) return { ...base, state: "lazy", status, restyle: true };
|
|
291
|
+
return { ...base, state: childrenSettled ? "ready" : "waiting", status, restyle: true };
|
|
292
|
+
}
|
|
235
293
|
|
|
236
294
|
const hasText = Boolean(row?.tagline);
|
|
237
295
|
const status: StoredSummaryStatus = hasText ? "stale" : "pending";
|
|
238
296
|
const keep = (state: TargetState): PlannedTarget => ({ ...base, state, status: row?.status === status ? null : status });
|
|
239
297
|
|
|
240
298
|
if (kind !== "file" && hasText && !options.refreshStaleFolders) return keep("lazy");
|
|
299
|
+
// left to be opened (Q2 B, PlanOptions.olderFoldersWaitToBeOpened): only the target a plan restyles goes now
|
|
300
|
+
if (kind !== "file" && hasText && options.olderFoldersWaitToBeOpened && !restyled && row && writtenInOlderStyle(row)) return keep("lazy");
|
|
241
301
|
return keep(childrenSettled ? "ready" : "waiting");
|
|
242
302
|
}
|
|
243
303
|
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_SUMMARY_MODELS, type ResolvedSummaryModels } from "./models";
|
|
|
12
12
|
import { planSummaries, writtenByModel, type PlannedTarget } from "./plan";
|
|
13
13
|
import { formatUsd, pricePerMTok, type PriceTable } from "./pricing";
|
|
14
14
|
import {
|
|
15
|
-
FILE_SYSTEM_PROMPT, FOLDER_SYSTEM_PROMPT, importerLine, MAX_FOLDER_ITEMS, plural, PROMPT_VERSION, REPO_SYSTEM_PROMPT,
|
|
15
|
+
FILE_SYSTEM_PROMPT, FOLDER_SYSTEM_PROMPT, importerLine, joinParagraphs, MAX_FOLDER_ITEMS, plural, PROMPT_VERSION, REPO_SYSTEM_PROMPT,
|
|
16
16
|
} from "./prompts";
|
|
17
17
|
import { buildFilePrompt, requireStructureReady } from "./run";
|
|
18
18
|
import type { Summarizer, SummaryOutcome } from "./types";
|
|
@@ -95,10 +95,9 @@ export function nextWave(plan: readonly PlannedTarget[], scope: SampleScope, sen
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* An answer as the row the real run would write for it (writeResults): its text and model, the
|
|
99
|
-
* status "fresh". The planner
|
|
100
|
-
*
|
|
101
|
-
* it's never written. Pure.
|
|
98
|
+
* An answer as the row the real run would write for it (writeResults): its text and model, the current prompt version,
|
|
99
|
+
* the planned inputHash, and status "fresh". The planner then calls the target current and shows this text in its
|
|
100
|
+
* folder's prompt. A row the index already had keeps its id; a new one has none (0), since it's never written. Pure.
|
|
102
101
|
*/
|
|
103
102
|
export function answeredRow(target: PlannedTarget, outcome: Extract<SummaryOutcome, { ok: true }>, previous: StoredSummary | undefined): StoredSummary {
|
|
104
103
|
return {
|
|
@@ -109,6 +108,7 @@ export function answeredRow(target: PlannedTarget, outcome: Extract<SummaryOutco
|
|
|
109
108
|
summary: outcome.text.summary,
|
|
110
109
|
model: outcome.model,
|
|
111
110
|
inputHash: target.inputHash,
|
|
111
|
+
promptVersion: PROMPT_VERSION,
|
|
112
112
|
status: "fresh",
|
|
113
113
|
batchId: null,
|
|
114
114
|
error: null,
|
|
@@ -175,8 +175,11 @@ export async function runFolderPreview(input: PreviewInput): Promise<FolderPrevi
|
|
|
175
175
|
};
|
|
176
176
|
|
|
177
177
|
for (;;){
|
|
178
|
-
// refreshStaleFolders: true, as in a full run (npm run summarize): an out-of-date folder is
|
|
179
|
-
|
|
178
|
+
// refreshStaleFolders: true and restyle: "all", as in a full run (npm run summarize): an out-of-date folder is
|
|
179
|
+
// redone, not left lazy, and so is a summary an older prompt version wrote
|
|
180
|
+
const plan = planSummaries(input.files, input.folders, [...rows.values()], {
|
|
181
|
+
models: setting.models, refreshStaleFolders: true, failedThisRun, restyle: "all",
|
|
182
|
+
});
|
|
180
183
|
const wave = nextWave(plan, scope, sent);
|
|
181
184
|
|
|
182
185
|
if (wave.length === 0){
|
|
@@ -433,10 +436,10 @@ function shown(preview: FolderPreview, kind: SummaryTargetKind, targetPath: stri
|
|
|
433
436
|
if (!outcome.ok) return `failed: ${outcome.error}`;
|
|
434
437
|
// a server-side fallback's answer isn't the summary the model asked would have written, and the reader should know
|
|
435
438
|
const by = outcome.model === target?.model ? "" : ` (answered by ${outcome.model})`;
|
|
436
|
-
return `*${outcome.text.tagline}* — ${outcome.text.summary}${by}`;
|
|
439
|
+
return `*${outcome.text.tagline}* — ${joinParagraphs(outcome.text.summary)}${by}`;
|
|
437
440
|
}
|
|
438
441
|
|
|
439
442
|
const row = preview.rows.get(key);
|
|
440
|
-
if (target?.state === "current" && row?.tagline) return `*${row.tagline}* — ${row.summary} (already in the index, not sent again)`;
|
|
443
|
+
if (target?.state === "current" && row?.tagline) return `*${row.tagline}* — ${joinParagraphs(row.summary ?? "")} (already in the index, not sent again)`;
|
|
441
444
|
return `not summarized (${target?.state ?? "not planned"})`;
|
|
442
445
|
}
|