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
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
import type { SummaryTargetKind } from "../../db/schema";
|
|
1
2
|
import type { Importer, SummaryText } from "../../db/store/summaries";
|
|
2
3
|
import { childToward, lcaFolder } from "../aggregate";
|
|
3
4
|
import { countLines } from "../languages/typescript";
|
|
4
5
|
import { dirOf } from "../paths";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
export const PROMPT_VERSION =
|
|
7
|
+
/** Version 2, ADR-034. Version 3, plain short paragraphs with a length line (ADR-034, amended 2026-10-29). */
|
|
8
|
+
export const PROMPT_VERSION = 3;
|
|
9
|
+
|
|
10
|
+
/** The prompt that wrote a row whose prompt_version is null (P3). Version 1 never wrote a stored summary. */
|
|
11
|
+
export const UNRECORDED_PROMPT_VERSION = 2;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether a row holds model text an older prompt wrote. A template isn't model text, and a row with no model has no
|
|
15
|
+
* text yet. A null prompt_version reads as UNRECORDED_PROMPT_VERSION.
|
|
16
|
+
*/
|
|
17
|
+
export function writtenInOlderStyle(row: { model: string | null; promptVersion: number | null }): boolean {
|
|
18
|
+
return !!row.model && row.model !== "template" && (row.promptVersion ?? UNRECORDED_PROMPT_VERSION) < PROMPT_VERSION;
|
|
19
|
+
}
|
|
8
20
|
|
|
9
21
|
/**
|
|
10
22
|
* Who writes what by default (ADR-028): Haiku for the many files, Sonnet for the folders and the repo people read first.
|
|
@@ -39,31 +51,102 @@ export const MAX_FOLDER_ITEMS = 50;
|
|
|
39
51
|
/** A file's prompt names this many of the areas its importers sit in (PIVOT_SPEC.md §4.2's groups); the rest fold into a count. */
|
|
40
52
|
export const MAX_IMPORTER_GROUPS = 3;
|
|
41
53
|
|
|
42
|
-
// Prompt
|
|
54
|
+
// Prompt v3.3 (ADR-034, amended 2026-10-29), v3.2 with a usage example that names a part instead of inviting a count,
|
|
55
|
+
// still PROMPT_VERSION 3 since no release wrote v3.2. The taglines' examples are deliberately not from VS Code, so the model can't copy one into a summary.
|
|
56
|
+
const PLAIN = "Use short sentences, with no semicolons, colons or dashes. When you need a technical term, say what it means the first time, or use a plainer word.";
|
|
57
|
+
|
|
43
58
|
export const FILE_SYSTEM_PROMPT = [
|
|
44
|
-
"You write the summary
|
|
45
|
-
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"
|
|
46
|
-
"The summary
|
|
47
|
-
"
|
|
48
|
-
"
|
|
59
|
+
"You write the summary people see for one source file while they find their way around a codebase that's new to them. Write for a smart reader who doesn't know this project's vocabulary.",
|
|
60
|
+
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words that says what the file is for, like \"Automatic retry for failed requests\". It doesn't count toward the summary's length.",
|
|
61
|
+
"The summary is short paragraphs with a blank line between them. The last line of the request says about how many words, paragraphs and code names to use. Stay close to it, and use fewer when the file is simple.",
|
|
62
|
+
"The first paragraph is 1 or 2 sentences on what the file is for and why the project needs it, in everyday words, with no code names.",
|
|
63
|
+
"Then say what's inside. Name only the most important exports, in backticks like `parseConfig`, and say in plain words what each one does.",
|
|
64
|
+
"End with one sentence on how widely the file is used and where, from the \"Imported by\" line, which comes from the repository's import graph. Say it in words that match how widely it really is used, like \"Almost every part of the app relies on it.\" or \"The settings screen uses it, and little else does.\" Name the parts that use it, never a number of files.",
|
|
65
|
+
PLAIN,
|
|
66
|
+
"Don't narrate the code line by line, don't guess beyond what you're shown, and don't mention truncation or code you weren't shown.",
|
|
49
67
|
].join("\n");
|
|
50
68
|
|
|
51
69
|
export const FOLDER_SYSTEM_PROMPT = [
|
|
52
|
-
"You write the summary
|
|
53
|
-
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"
|
|
54
|
-
"The summary
|
|
55
|
-
"
|
|
56
|
-
"
|
|
70
|
+
"You write the summary people see for one folder while they find their way around a codebase that's new to them. You're given the summaries of the files and subfolders inside it. Write for a smart reader who doesn't know this project's vocabulary.",
|
|
71
|
+
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words that says what the folder is for, like \"Online payment handling\". It doesn't count toward the summary's length.",
|
|
72
|
+
"The summary is short paragraphs with a blank line between them. The last line of the request says about how many words, paragraphs and code names to use. Stay close to it, and use fewer when the folder is simple.",
|
|
73
|
+
"The first paragraph is 1 or 2 sentences on what this part of the project does, in everyday words, with no code names.",
|
|
74
|
+
"Then say which parts matter most and how they fit together. Name only the most important files or subfolders, in backticks like `checkout/`, and say in plain words what each one does.",
|
|
75
|
+
PLAIN,
|
|
76
|
+
"Don't list every item, don't give numbers of files, and don't guess beyond the summaries you're given.",
|
|
57
77
|
].join("\n");
|
|
58
78
|
|
|
59
79
|
export const REPO_SYSTEM_PROMPT = [
|
|
60
|
-
"You write the overview
|
|
61
|
-
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"
|
|
62
|
-
"The summary
|
|
63
|
-
"
|
|
64
|
-
"
|
|
80
|
+
"You write the overview people see first when they open a repository that's new to them. You're given the summaries of its top-level folders and files. Write for a smart reader who doesn't know this project's vocabulary.",
|
|
81
|
+
"Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words that says what the project is, like \"Online store server\". It doesn't count toward the summary's length.",
|
|
82
|
+
"The summary is short paragraphs with a blank line between them. The last line of the request says about how many words, paragraphs and code names to use. Stay close to it.",
|
|
83
|
+
"The first paragraph is 1 or 2 sentences on what the project is and who it's for, in everyday words, with no code names.",
|
|
84
|
+
"Then name its main areas, in backticks like `src/server`, each with a few plain words on what it holds, and end by saying where a newcomer should start reading.",
|
|
85
|
+
PLAIN,
|
|
86
|
+
"Don't give numbers of files, and don't guess beyond the summaries you're given.",
|
|
65
87
|
].join("\n");
|
|
66
88
|
|
|
89
|
+
/** How long a summary is asked to be. Its line ends every request (lengthLine). */
|
|
90
|
+
export type LengthTier = "small" | "medium" | "large";
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* The tiers' thresholds (ADR-034, amended 2026-10-29). A file is large at LARGE_FILE_LINES lines or more, or at
|
|
94
|
+
* LARGE_FILE_IMPORTERS importers or more, and small under SMALL_FILE_LINES lines with SMALL_FILE_IMPORTERS importers or
|
|
95
|
+
* fewer. A folder is large at LARGE_FOLDER_FILES files or more, and small at SMALL_FOLDER_FILES or fewer. Everything
|
|
96
|
+
* else is medium, and the repo is always large. On VS Code's model-written summaries that makes files 38% small, 48%
|
|
97
|
+
* medium and 14% large, and folders 69%, 29% and 3%, as measured by evidence/probes/spread.cjs in the plainer
|
|
98
|
+
* summaries ledger (.superpowers/sdd/2026-10-29-plain-summaries).
|
|
99
|
+
*/
|
|
100
|
+
export const SMALL_FILE_LINES = 120;
|
|
101
|
+
export const SMALL_FILE_IMPORTERS = 3;
|
|
102
|
+
export const LARGE_FILE_LINES = 600;
|
|
103
|
+
export const LARGE_FILE_IMPORTERS = 30;
|
|
104
|
+
export const SMALL_FOLDER_FILES = 5;
|
|
105
|
+
export const LARGE_FOLDER_FILES = 100;
|
|
106
|
+
|
|
107
|
+
/** A file's tier, from its line count and every file that imports it (tests included), large first. */
|
|
108
|
+
export function fileTier(lineCount: number, importers: number): LengthTier {
|
|
109
|
+
if (lineCount >= LARGE_FILE_LINES || importers >= LARGE_FILE_IMPORTERS) return "large";
|
|
110
|
+
if (lineCount < SMALL_FILE_LINES && importers <= SMALL_FILE_IMPORTERS) return "small";
|
|
111
|
+
return "medium";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A folder's tier, from the source files under it. */
|
|
115
|
+
export function folderTier(fileCount: number): LengthTier {
|
|
116
|
+
if (fileCount >= LARGE_FOLDER_FILES) return "large";
|
|
117
|
+
if (fileCount <= SMALL_FOLDER_FILES) return "small";
|
|
118
|
+
return "medium";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What a length line counts, per kind of target. */
|
|
122
|
+
const NAMED: Record<SummaryTargetKind, string> = { file: "exports", folder: "files or subfolders", repo: "folders or files" };
|
|
123
|
+
|
|
124
|
+
/** Each tier's word target (design §4). Folders and the repo are told these, and kept within them in the samples. */
|
|
125
|
+
export const TIER_WORDS: Record<LengthTier, number> = { small: 50, medium: 90, large: 150 };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The words a file is told, below its tier's target for small and medium files. The final sample (Summary Review #6)
|
|
129
|
+
* measured Haiku writing 1.6 to 1.8 times what it was told on small files and 1.1 to 1.3 times on medium ones, so
|
|
130
|
+
* these land it near 50 and 90 (the owner, 2026-10-29, as design §4 allowed). Large files kept within 150.
|
|
131
|
+
*/
|
|
132
|
+
export const FILE_TOLD_WORDS: Record<LengthTier, number> = { small: 30, medium: 70, large: 150 };
|
|
133
|
+
|
|
134
|
+
/** The request's last line, saying about how many words, paragraphs and code names a summary of this tier uses. */
|
|
135
|
+
export function lengthLine(kind: SummaryTargetKind, tier: LengthTier): string {
|
|
136
|
+
const what = NAMED[kind];
|
|
137
|
+
const words = (kind === "file" ? FILE_TOLD_WORDS : TIER_WORDS)[tier];
|
|
138
|
+
switch (tier){
|
|
139
|
+
case "small": return `Write at most ${words} words, in 2 paragraphs, and name at most 2 ${what}.`;
|
|
140
|
+
case "medium": return `Write at most ${words} words, in 2 or 3 paragraphs, and name at most 3 ${what}.`;
|
|
141
|
+
case "large": return `Write at most ${words} words, in up to 5 paragraphs, as many as it needs, and name at most 5 ${what}.`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A request's body with its length line last, after a blank line, where sample #2 measured it. */
|
|
146
|
+
export function withLengthLine(body: string, kind: SummaryTargetKind, tier: LengthTier): string {
|
|
147
|
+
return `${body}\n\n${lengthLine(kind, tier)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
67
150
|
/** One child of a folder, as its prompt shows it. lineCount is a folder's total, used only for ordering. */
|
|
68
151
|
export type FolderItem = { name: string; kind: "file" | "folder"; fileCount: number; lineCount: number; text: SummaryText | null };
|
|
69
152
|
|
|
@@ -72,6 +155,11 @@ export function plural(n: number, word: string, pluralWord = `${word}s`): string
|
|
|
72
155
|
return `${n.toLocaleString("en-US")} ${n === 1 ? word : pluralWord}`;
|
|
73
156
|
}
|
|
74
157
|
|
|
158
|
+
/** A summary on one line, for the readers that show it so: its paragraphs, each trimmed, joined by a space. */
|
|
159
|
+
export function joinParagraphs(summary: string): string {
|
|
160
|
+
return summary.split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter((paragraph) => paragraph !== "").join(" ");
|
|
161
|
+
}
|
|
162
|
+
|
|
75
163
|
/**
|
|
76
164
|
* A file's prompt: the imports/exports listing chunks.ts generates (it opens with
|
|
77
165
|
* "File: <path>"), the line count, who imports the file (the line importerLine renders),
|
|
@@ -130,43 +218,52 @@ export function importerLine(filePath: string, importers: readonly Importer[], m
|
|
|
130
218
|
return line;
|
|
131
219
|
}
|
|
132
220
|
|
|
221
|
+
/** A folder's prompt, its body only (the length line is added by withLengthLine). oneLine is listItems'. */
|
|
133
222
|
export function renderFolderPrompt(
|
|
134
223
|
folder: { path: string; fileCount: number; directFileCount: number },
|
|
135
224
|
items: readonly FolderItem[],
|
|
136
|
-
maxItems: number = MAX_FOLDER_ITEMS
|
|
225
|
+
maxItems: number = MAX_FOLDER_ITEMS,
|
|
226
|
+
oneLine: boolean = true
|
|
137
227
|
): string {
|
|
138
228
|
return [
|
|
139
229
|
`Folder: ${folder.path}`,
|
|
140
230
|
`${plural(folder.fileCount, "source file")} in total, ${folder.directFileCount} directly inside.`,
|
|
141
231
|
"",
|
|
142
232
|
"Contents, largest first:",
|
|
143
|
-
...listItems(items, maxItems),
|
|
233
|
+
...listItems(items, maxItems, oneLine),
|
|
144
234
|
].join("\n");
|
|
145
235
|
}
|
|
146
236
|
|
|
237
|
+
/** The repo's prompt, its body only (the length line is added by withLengthLine). oneLine is listItems'. */
|
|
147
238
|
export function renderRepoPrompt(
|
|
148
239
|
repo: { name: string; fileCount: number; folderCount: number },
|
|
149
240
|
items: readonly FolderItem[],
|
|
150
|
-
maxItems: number = MAX_FOLDER_ITEMS
|
|
241
|
+
maxItems: number = MAX_FOLDER_ITEMS,
|
|
242
|
+
oneLine: boolean = true
|
|
151
243
|
): string {
|
|
152
244
|
return [
|
|
153
245
|
`Repository: ${repo.name}`,
|
|
154
246
|
`${plural(repo.fileCount, "source file")} in ${plural(repo.folderCount, "folder")}.`,
|
|
155
247
|
"",
|
|
156
248
|
"Top-level contents, largest first:",
|
|
157
|
-
...listItems(items, maxItems),
|
|
249
|
+
...listItems(items, maxItems, oneLine),
|
|
158
250
|
].join("\n");
|
|
159
251
|
}
|
|
160
252
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Largest first (by lines), ties by name; the overflow becomes one "…and N smaller items" line. Each child's summary
|
|
255
|
+
* is written on one line (joinParagraphs), so one child stays one line. With oneLine false it's written verbatim, as
|
|
256
|
+
* prompt v2 did, for P2's older check, which renders a folder's body the way the version that wrote it did.
|
|
257
|
+
*/
|
|
258
|
+
function listItems(items: readonly FolderItem[], maxItems: number, oneLine: boolean): string[] {
|
|
163
259
|
const sorted = [...items].sort((a, b) => b.lineCount - a.lineCount || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
164
260
|
|
|
165
261
|
const lines = sorted.slice(0, maxItems).map((item) => {
|
|
166
262
|
const label = item.kind === "folder"
|
|
167
263
|
? `${item.name}/ (${plural(item.fileCount, "file")})`
|
|
168
264
|
: `${item.name} (${plural(item.lineCount, "line")})`;
|
|
169
|
-
|
|
265
|
+
if (!item.text) return `- ${label}: (no summary yet)`;
|
|
266
|
+
return `- ${label}: ${item.text.tagline} — ${oneLine ? joinParagraphs(item.text.summary) : item.text.summary}`;
|
|
170
267
|
});
|
|
171
268
|
|
|
172
269
|
const hidden = sorted.length - maxItems;
|
|
@@ -16,9 +16,11 @@ import { repoKey } from "../paths";
|
|
|
16
16
|
import { projectSpend, type ProjectedRequest } from "./budget";
|
|
17
17
|
import { mapLimit } from "./concurrency";
|
|
18
18
|
import { DEFAULT_SUMMARY_MODELS, type ResolvedSummaryModels } from "./models";
|
|
19
|
-
import { foldersAbove, type PlannedTarget, type TargetState } from "./plan";
|
|
19
|
+
import { foldersAbove, type PlannedTarget, type Restyle, type TargetState } from "./plan";
|
|
20
20
|
import { formatUsd } from "./pricing";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
FILE_SYSTEM_PROMPT, fileTier, FOLDER_SYSTEM_PROMPT, importerLine, plural, PROMPT_VERSION, REPO_SYSTEM_PROMPT, renderFilePrompt, withLengthLine,
|
|
23
|
+
} from "./prompts";
|
|
22
24
|
import { planSnapshot, type Snapshot } from "./snapshot";
|
|
23
25
|
import { refreshSummaryStatus, stillAtAnthropic, summaryStageStatus } from "./status";
|
|
24
26
|
import { BatchGoneError, ModelUnavailableError, SummarizerAuthError, type Summarizer, type SummaryOutcome, type SummaryRequest } from "./types";
|
|
@@ -42,6 +44,16 @@ export type SummaryRunOptions = {
|
|
|
42
44
|
wait?: boolean;
|
|
43
45
|
/** Redo out-of-date folders now (default). Incremental runs pass false: those wait until someone opens them. */
|
|
44
46
|
refreshStaleFolders?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* true redoes the summaries an older prompt version wrote, as `nebulosity summarize` does after its estimate and yes.
|
|
49
|
+
* The worker's jobs leave it off.
|
|
50
|
+
*/
|
|
51
|
+
restyle?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* true leaves an out-of-date folder or the repo an older prompt version wrote to be opened, and lets the folder
|
|
54
|
+
* above it go without it (PlanOptions.olderFoldersWaitToBeOpened, Q2 B). The sweep passes it. Default: false.
|
|
55
|
+
*/
|
|
56
|
+
olderFoldersWaitToBeOpened?: boolean;
|
|
45
57
|
pollIntervalMs?: number;
|
|
46
58
|
/** How often a long wave refreshes the stage and the lock. */
|
|
47
59
|
heartbeatMs?: number;
|
|
@@ -220,6 +232,11 @@ type RunContext = {
|
|
|
220
232
|
* writes are part of the rest the spend cap projects (foldersAbove); otherwise they turn lazy, and nothing sends them.
|
|
221
233
|
*/
|
|
222
234
|
refreshStaleFolders: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Which summaries an older prompt version wrote every pass redoes (SummaryRunOptions.restyle): "all" when the run was
|
|
237
|
+
* told to restyle, else "none". Every wave plans the same way, so a later wave restyles what an earlier one left.
|
|
238
|
+
*/
|
|
239
|
+
restyle: Restyle;
|
|
223
240
|
/**
|
|
224
241
|
* The note of the spend-cap check that failed in this pass; every pass starts with none. While it's set nothing more
|
|
225
242
|
* is sent, but batches at Anthropic are still collected, and the run finishes with it when nothing more can happen.
|
|
@@ -235,10 +252,11 @@ type RunContext = {
|
|
|
235
252
|
|
|
236
253
|
function newContext(
|
|
237
254
|
db: Db, repoId: number, root: string, models: ResolvedSummaryModels, say: (message: string) => void, budget: Budget | null = null,
|
|
238
|
-
select: (target: PlannedTarget) => boolean = () => true, sendLeft: number = Infinity, refreshStaleFolders: boolean = true
|
|
255
|
+
select: (target: PlannedTarget) => boolean = () => true, sendLeft: number = Infinity, refreshStaleFolders: boolean = true,
|
|
256
|
+
restyle: Restyle = "none"
|
|
239
257
|
): RunContext {
|
|
240
258
|
return {
|
|
241
|
-
db, repoId, root, models, say, budget, select, sendLeft, refreshStaleFolders,
|
|
259
|
+
db, repoId, root, models, say, budget, select, sendLeft, refreshStaleFolders, restyle,
|
|
242
260
|
failedThisRun: new Set(),
|
|
243
261
|
tally: { summarized: 0, templated: 0, failed: 0, batches: 0, waves: 0 },
|
|
244
262
|
missedPasses: 0,
|
|
@@ -275,8 +293,9 @@ export async function runSummaries(repoRoot: string, options: SummaryRunOptions)
|
|
|
275
293
|
repoUsd: options.repoBudgetUsd ?? null,
|
|
276
294
|
};
|
|
277
295
|
const refreshStaleFolders = options.refreshStaleFolders ?? true;
|
|
296
|
+
const olderFoldersWaitToBeOpened = options.olderFoldersWaitToBeOpened ?? false;
|
|
278
297
|
const ctx = newContext(db, repoId, root, options.models ?? DEFAULT_SUMMARY_MODELS, options.onProgress ?? (() => {}), budget,
|
|
279
|
-
options.select, options.maxTargets, refreshStaleFolders);
|
|
298
|
+
options.select, options.maxTargets, refreshStaleFolders, options.restyle ? "all" : "none");
|
|
280
299
|
let last: PlannedTarget[] = [];
|
|
281
300
|
|
|
282
301
|
// a mistyped id must fail before anything is locked, released or rewritten
|
|
@@ -290,7 +309,7 @@ export async function runSummaries(repoRoot: string, options: SummaryRunOptions)
|
|
|
290
309
|
ctx.say(`released ${batchId}: ${plural(released, "request")} will be sent again`);
|
|
291
310
|
}
|
|
292
311
|
for (;;){
|
|
293
|
-
const snap = planSnapshot(db, repoId, refreshStaleFolders, ctx.failedThisRun, ctx.models);
|
|
312
|
+
const snap = planSnapshot(db, repoId, refreshStaleFolders, ctx.failedThisRun, ctx.models, ctx.restyle, olderFoldersWaitToBeOpened);
|
|
294
313
|
last = snap.targets;
|
|
295
314
|
const ids = applyTargetUpdates(db, repoId, snap.targets);
|
|
296
315
|
ctx.tally.templated += snap.targets.filter((t) => t.state === "template").length;
|
|
@@ -365,16 +384,19 @@ export async function runSummaries(repoRoot: string, options: SummaryRunOptions)
|
|
|
365
384
|
* What a full run would send to a model right now (S11): every target still to be written, as the kind and model of its
|
|
366
385
|
* request, planned the way runSummaries' first pass plans (stale folders redone), and then every folder and the repo
|
|
367
386
|
* above them, which their new text puts out of date (foldersAbove, R24), and above what's still at Anthropic. A template
|
|
368
|
-
* is no request.
|
|
387
|
+
* is no request. With restyle (the default, as a full run restyles), every summary an older prompt version wrote is one
|
|
388
|
+
* too, marked restyle; false plans as `--no-restyle` runs, leaving them as they are. Reads only.
|
|
369
389
|
*/
|
|
370
|
-
export function plannedRequests(
|
|
390
|
+
export function plannedRequests(
|
|
391
|
+
repoRoot: string, models: ResolvedSummaryModels = DEFAULT_SUMMARY_MODELS, restyle: boolean = true
|
|
392
|
+
): ProjectedRequest[] {
|
|
371
393
|
const db = getDb();
|
|
372
394
|
const repoId = requireRepository(db, repoKey(path.resolve(repoRoot)));
|
|
373
395
|
const toWrite: ReadonlySet<TargetState> = new Set(["ready", "waiting", "lazy", "failed"]);
|
|
374
|
-
const targets = planSnapshot(db, repoId, true, new Set(), models).targets;
|
|
396
|
+
const targets = planSnapshot(db, repoId, true, new Set(), models, restyle ? "all" : "none").targets;
|
|
375
397
|
const written = targets.filter((t) => toWrite.has(t.state) && t.model !== "template");
|
|
376
398
|
const above = foldersAbove(targets, [...written, ...targets.filter((t) => t.state === "in-flight")]);
|
|
377
|
-
return [...written, ...above].map((t) => ({ kind: t.kind, model: t.model }));
|
|
399
|
+
return [...written, ...above].map((t) => ({ kind: t.kind, model: t.model, ...(t.restyle ? { restyle: true as const } : {}) }));
|
|
378
400
|
}
|
|
379
401
|
|
|
380
402
|
/**
|
|
@@ -386,8 +408,10 @@ const summarizingNow = new Map<string, Promise<StoredSummary>>();
|
|
|
386
408
|
/**
|
|
387
409
|
* Summarizes one file or folder right now through the regular API, jumping the queue (PIVOT_SPEC.md §5.3: the Map calls
|
|
388
410
|
* this when someone opens a path that has no summary, or an out-of-date one). A current summary comes back as it is,
|
|
389
|
-
* without an API call
|
|
390
|
-
*
|
|
411
|
+
* without an API call, unless an older prompt version wrote it: that one target is rewritten in the current style (P8,
|
|
412
|
+
* Q2 B), and its children are left as they are. A folder whose children aren't all summarized is refused
|
|
413
|
+
* (TargetNotReadyError): its summary would be written over "(no summary yet)" lines. Concurrent calls for one target
|
|
414
|
+
* share one request (ADR-033, amended).
|
|
391
415
|
* A model the API no longer serves throws ModelUnavailableError, leaving the row as it was (ADR-066).
|
|
392
416
|
*/
|
|
393
417
|
export function summarizeNow(
|
|
@@ -407,7 +431,10 @@ async function summarizeOnce(root: string, targetPath: string, summarizer: Summa
|
|
|
407
431
|
const db = getDb();
|
|
408
432
|
const repoId = requireRepository(db, repoKey(root));
|
|
409
433
|
requireStructureReady(db, repoId);
|
|
410
|
-
|
|
434
|
+
// Only the opened target is restyled (P8): children an older prompt wrote count as current, so a folder whose
|
|
435
|
+
// children are all in the older style is ready at once instead of waiting for them. An out-of-date child folder an
|
|
436
|
+
// older prompt wrote waits to be opened itself (Q2 B), so it doesn't keep this one waiting either.
|
|
437
|
+
const snap = planSnapshot(db, repoId, true, new Set(), models, { path: targetPath }, true);
|
|
411
438
|
const target = snap.targets.find((t) => t.path === targetPath);
|
|
412
439
|
if (!target) throw new TargetNotFoundError(targetPath);
|
|
413
440
|
if (target.state === "waiting") throw new TargetNotReadyError(targetPath);
|
|
@@ -433,9 +460,9 @@ async function summarizeOnce(root: string, targetPath: string, summarizer: Summa
|
|
|
433
460
|
}
|
|
434
461
|
|
|
435
462
|
/**
|
|
436
|
-
* A file's prompt, built from exactly the bytes the index saw. Null when the
|
|
437
|
-
* on disk after indexing, or vanished: a summary of different bytes would contradict the
|
|
438
|
-
*
|
|
463
|
+
* A file's prompt, built from exactly the bytes the index saw, ending with its tier's length line (P6). Null when the
|
|
464
|
+
* file changed on disk after indexing, or vanished: a summary of different bytes would contradict the structure the
|
|
465
|
+
* index holds for it.
|
|
439
466
|
*/
|
|
440
467
|
export function buildFilePrompt(db: Db, repoId: number, root: string, file: Pick<FileFacts, "path" | "contentHash" | "lineCount">): string | null {
|
|
441
468
|
let source: string;
|
|
@@ -451,7 +478,10 @@ export function buildFilePrompt(db: Db, repoId: number, root: string, file: Pick
|
|
|
451
478
|
if (listing === null) return null;
|
|
452
479
|
|
|
453
480
|
// The importers are in the prompt but not in the input hash (path + content only, plan.ts), so a new importer doesn't re-summarize a central file on every pull.
|
|
454
|
-
|
|
481
|
+
// Their count, tests included (the same count as FileFacts.importedBy), also sets the tier, which is likewise left out of the hash.
|
|
482
|
+
const importers = loadImporters(db, repoId, file.path);
|
|
483
|
+
const body = renderFilePrompt(listing, file.lineCount, importerLine(file.path, importers), source);
|
|
484
|
+
return withLengthLine(body, "file", fileTier(file.lineCount, importers.length));
|
|
455
485
|
}
|
|
456
486
|
|
|
457
487
|
/**
|
|
@@ -873,7 +903,7 @@ function ingest(
|
|
|
873
903
|
id: destination.id,
|
|
874
904
|
batchId: destination.batchId,
|
|
875
905
|
inputHash: destination.inputHash,
|
|
876
|
-
outcome: outcome.ok ? { ok: true, text: outcome.text, model: outcome.model } : { ok: false, error: outcome.error },
|
|
906
|
+
outcome: outcome.ok ? { ok: true, text: outcome.text, model: outcome.model, promptVersion: PROMPT_VERSION } : { ok: false, error: outcome.error },
|
|
877
907
|
});
|
|
878
908
|
|
|
879
909
|
if (outcome.ok){
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Db } from "../../db/client";
|
|
2
2
|
import { loadFileFacts, loadFolderFacts, loadSummaries, type FileFacts } from "../../db/store/summaries";
|
|
3
3
|
import type { ResolvedSummaryModels } from "./models";
|
|
4
|
-
import { planSummaries, type PlannedTarget } from "./plan";
|
|
4
|
+
import { planSummaries, type PlannedTarget, type Restyle } from "./plan";
|
|
5
5
|
|
|
6
6
|
/** One planning pass: the plan, and the file facts it was made from. */
|
|
7
7
|
export type Snapshot = { targets: PlannedTarget[]; files: Map<string, FileFacts> };
|
|
@@ -10,9 +10,12 @@ export type Snapshot = { targets: PlannedTarget[]; files: Map<string, FileFacts>
|
|
|
10
10
|
* Plans every target of a repo from what the database holds right now. Summary runs, summarizeNow and the stage
|
|
11
11
|
* refresh (status.ts) all plan through here, so they can't disagree about a target's state. `models` is the setting
|
|
12
12
|
* as resolved for this job (ADR-066): the model is part of every inputHash, so every planner has to use the same one.
|
|
13
|
+
* `restyle` is PlanOptions.restyle: which summaries an older prompt wrote this plan redoes (P1).
|
|
14
|
+
* `olderFoldersWaitToBeOpened` is PlanOptions.olderFoldersWaitToBeOpened (Q2 B).
|
|
13
15
|
*/
|
|
14
16
|
export function planSnapshot(
|
|
15
|
-
db: Db, repoId: number, refreshStaleFolders: boolean, failedThisRun: ReadonlySet<string>, models: ResolvedSummaryModels
|
|
17
|
+
db: Db, repoId: number, refreshStaleFolders: boolean, failedThisRun: ReadonlySet<string>, models: ResolvedSummaryModels,
|
|
18
|
+
restyle: Restyle = "none", olderFoldersWaitToBeOpened: boolean = false
|
|
16
19
|
): Snapshot {
|
|
17
20
|
// One read transaction: a structure run committing meanwhile can't hand us half of its changes.
|
|
18
21
|
// (better-sqlite3 has one connection, so reads through `db` inside the callback belong to it.)
|
|
@@ -22,6 +25,8 @@ export function planSnapshot(
|
|
|
22
25
|
models: models.models,
|
|
23
26
|
refreshStaleFolders,
|
|
24
27
|
failedThisRun,
|
|
28
|
+
restyle,
|
|
29
|
+
olderFoldersWaitToBeOpened,
|
|
25
30
|
});
|
|
26
31
|
return { targets, files: new Map(files.map((f) => [f.path, f])) };
|
|
27
32
|
});
|
|
@@ -90,8 +90,9 @@ export function refreshSummaryStatus(
|
|
|
90
90
|
if (!models.ok && options.summarizerAvailable) return null;
|
|
91
91
|
const db = getDb();
|
|
92
92
|
const repoId = requireRepository(db, repoKey(path.resolve(repoRoot)));
|
|
93
|
-
// between runs, out-of-date folders are lazy: redone when opened, or by the background worker's sweep
|
|
94
|
-
|
|
93
|
+
// between runs, out-of-date folders are lazy: redone when opened, or by the background worker's sweep. Nothing is
|
|
94
|
+
// restyled: a summary an older prompt wrote for the same input counts as current, so it isn't waiting (P1).
|
|
95
|
+
const snap = planSnapshot(db, repoId, false, new Set(), models.ok ? models : DEFAULT_SUMMARY_MODELS, "none");
|
|
95
96
|
if (models.ok) applyTargetUpdates(db, repoId, snap.targets);
|
|
96
97
|
if (isSummaryLockHeld(db)) return null;
|
|
97
98
|
|
package/lib/tools/text.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SummaryStatus } from "../contracts";
|
|
2
2
|
import { freshnessNote } from "../engine/freshness";
|
|
3
|
+
import { joinParagraphs } from "../index/summarize/prompts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Answers stay under this many characters (ADR-047). Claude Code warns past 10,000 tokens and moves a text result of
|
|
@@ -18,10 +19,10 @@ export function shownPath(repoPath: string, kind: "folder" | "file"): string {
|
|
|
18
19
|
/** A count as answers write it: 4231 → "4,231". */
|
|
19
20
|
export const formatCount = (n: number): string => n.toLocaleString("en-US");
|
|
20
21
|
|
|
21
|
-
/** The line where a summary goes: its text, or what's in its place. */
|
|
22
|
+
/** The line where a summary goes: its text, its paragraphs joined by a space so it stays one line, or what's in its place. */
|
|
22
23
|
export function summaryLine(target: { tagline?: string; summary?: string; summaryStatus: SummaryStatus; summaryError?: string }): string {
|
|
23
24
|
if (target.tagline){
|
|
24
|
-
const text = target.summary ? `${target.tagline} — ${target.summary}` : target.tagline;
|
|
25
|
+
const text = target.summary ? `${target.tagline} — ${joinParagraphs(target.summary)}` : target.tagline;
|
|
25
26
|
return target.summaryStatus === "stale" ? `${text} (Out of date: changed since it was summarized.)` : text;
|
|
26
27
|
}
|
|
27
28
|
if (target.summaryStatus === "off") return SUMMARIES_OFF_LINE;
|
package/mcp/server.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { setupCall } from "../lib/tools/repo";
|
|
|
16
16
|
import { deliveryNote, type MapDelivery } from "../lib/tools/text";
|
|
17
17
|
|
|
18
18
|
/** package.json's version (a test keeps them equal). */
|
|
19
|
-
export const SERVER_VERSION = "1.
|
|
19
|
+
export const SERVER_VERSION = "1.1.0";
|
|
20
20
|
|
|
21
21
|
/** Sends one live event to the Map's server on `port`; true when it took it (lib/live/client.ts). */
|
|
22
22
|
export type LivePoster = (event: LiveEvent, port: number) => Promise<boolean>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nebulosity",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A live map of how a big codebase fits together, for Claude Code and for you
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A live map of how a big codebase fits together, for Claude Code and for you, with a local import index, an MCP server and the Map.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"mcp",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"!.next/**/*.map",
|
|
62
62
|
"!.next/**/*.nft.json",
|
|
63
63
|
"!**/*.test.ts",
|
|
64
|
+
"!**/*.test.tsx",
|
|
64
65
|
"LICENSE",
|
|
65
66
|
"README.md"
|
|
66
67
|
],
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"bench-calls": "tsx scripts/bench-calls.ts",
|
|
85
86
|
"bench-languages": "tsx scripts/bench-languages.ts",
|
|
86
87
|
"check-layout": "node scripts/check-layout.mjs",
|
|
88
|
+
"check-routes": "tsx scripts/check-routes.ts",
|
|
87
89
|
"compare-summaries": "tsx scripts/compare-summaries.ts",
|
|
88
90
|
"eval:run": "tsx scripts/eval/run.ts",
|
|
89
91
|
"eval:probe": "tsx scripts/eval/probe.ts",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* nebulosity summarize <path-to-repo> [--direct | --batch] [--no-wait] [--budget <usd>] [--path <repo-relative path>]
|
|
2
|
+
* nebulosity summarize <path-to-repo> [--direct | --batch] [--no-wait] [--no-restyle] [--budget <usd>] [--path <repo-relative path>]
|
|
3
3
|
* [--release-batch <batch id>]... [--yes]
|
|
4
4
|
* (in a checkout: npm run summarize -- <path-to-repo> …)
|
|
5
5
|
*
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* --no-wait never sleeps: it collects the batches that have already ended, submits what
|
|
9
9
|
* that makes ready, and exits. Running the command again (by hand, or from cron) carries
|
|
10
10
|
* on from there.
|
|
11
|
+
* - A full run also rewrites, in the current style, the summaries an older Nebulosity wrote (P7), and its estimate says
|
|
12
|
+
* how many requests do that. --no-restyle leaves them as they are, so a catch-up sends only what changed: the spend
|
|
13
|
+
* cap counts the repo's spend across runs, and a big repo's restyle could take it past the budget before anything else.
|
|
11
14
|
* - --budget caps this repo's recorded summary spend (summary_usage, across runs), in US
|
|
12
15
|
* dollars (default 15, S2's ceiling); ADR-035 lists the spend it can't see, such as the
|
|
13
16
|
* checkpoint reviews. Before each batch or regular-API wave the run projects spent + at
|
|
@@ -39,6 +42,7 @@ import { summarizerFromEnv } from "../../lib/index/summarize/claude";
|
|
|
39
42
|
import { estimateSpend } from "../../lib/index/summarize/estimate";
|
|
40
43
|
import { configuredSummaryModels, type ResolvedSummaryModels } from "../../lib/index/summarize/models";
|
|
41
44
|
import { costUsd, formatUsd, type PriceTable } from "../../lib/index/summarize/pricing";
|
|
45
|
+
import { joinParagraphs, plural } from "../../lib/index/summarize/prompts";
|
|
42
46
|
import {
|
|
43
47
|
BATCH_THRESHOLD, BatchesUnreachableError, DEFAULT_BUDGET_USD, plannedRequests, runSummaries, StructureNotReadyError, summarizeNow, TargetNotFoundError,
|
|
44
48
|
TargetNotReadyError, UnknownBatchError, type Transport,
|
|
@@ -47,7 +51,7 @@ import { ModelUnavailableError } from "../../lib/index/summarize/types";
|
|
|
47
51
|
import { keyHint } from "../../lib/hints";
|
|
48
52
|
import { loadLocalEnv } from "../env";
|
|
49
53
|
|
|
50
|
-
const USAGE = "usage: nebulosity summarize <path-to-repo> [--direct | --batch] [--no-wait] [--budget <usd>] [--path <repo-relative path>] [--release-batch <batch id>]... [--yes]"
|
|
54
|
+
const USAGE = "usage: nebulosity summarize <path-to-repo> [--direct | --batch] [--no-wait] [--no-restyle] [--budget <usd>] [--path <repo-relative path>] [--release-batch <batch id>]... [--yes]"
|
|
51
55
|
+ " (in a checkout: npm run summarize -- <path-to-repo> …)";
|
|
52
56
|
|
|
53
57
|
/** The arguments as a run uses them, or null when they can't be run as given. */
|
|
@@ -56,6 +60,8 @@ type SummarizeOptions = {
|
|
|
56
60
|
onePath: string | null;
|
|
57
61
|
transport: Transport;
|
|
58
62
|
wait: boolean;
|
|
63
|
+
/** A full run rewrites the summaries an older version wrote (P7), unless --no-restyle is given. */
|
|
64
|
+
restyle: boolean;
|
|
59
65
|
budgetUsd: number;
|
|
60
66
|
releaseBatches: string[];
|
|
61
67
|
/** --yes: spend without being asked (the only way without a terminal). */
|
|
@@ -65,7 +71,7 @@ type SummarizeOptions = {
|
|
|
65
71
|
/**
|
|
66
72
|
* Every argument after the repo path must be one this command knows, or it stops here. An argument it ignored would
|
|
67
73
|
* leave the defaults in charge: "--path=src/x.ts" used to start a full summary pass (up to the $15 budget) instead of
|
|
68
|
-
* the one request --path asks for. So, each at most once: the
|
|
74
|
+
* the one request --path asks for. So, each at most once: the five switches, and the two options whose value is the
|
|
69
75
|
* next argument, whatever it is ("--path ''" is the repo itself). Anything else, a "--flag=value" spelling or a second
|
|
70
76
|
* repo path included, gets the usage line.
|
|
71
77
|
*/
|
|
@@ -73,7 +79,7 @@ function parseSummarizeArgs(args: readonly string[]): SummarizeOptions | null {
|
|
|
73
79
|
const target = args[0];
|
|
74
80
|
if (!target || target.startsWith("--")) return null;
|
|
75
81
|
|
|
76
|
-
const SWITCHES = ["--direct", "--batch", "--no-wait", "--yes"];
|
|
82
|
+
const SWITCHES = ["--direct", "--batch", "--no-wait", "--no-restyle", "--yes"];
|
|
77
83
|
const OPTIONS = ["--budget", "--path"];
|
|
78
84
|
const REPEATABLE = ["--release-batch"];
|
|
79
85
|
const repeated = new Map<string, string[]>();
|
|
@@ -108,6 +114,7 @@ function parseSummarizeArgs(args: readonly string[]): SummarizeOptions | null {
|
|
|
108
114
|
onePath: values.get("--path") ?? null,
|
|
109
115
|
transport: switches.has("--direct") ? "direct" : switches.has("--batch") ? "batch" : "auto",
|
|
110
116
|
wait: !switches.has("--no-wait"),
|
|
117
|
+
restyle: !switches.has("--no-restyle"),
|
|
111
118
|
budgetUsd,
|
|
112
119
|
releaseBatches,
|
|
113
120
|
yes: switches.has("--yes"),
|
|
@@ -168,12 +175,17 @@ async function summarize(options: SummarizeOptions): Promise<number> {
|
|
|
168
175
|
}
|
|
169
176
|
const row = await summarizeNow(root, onePath, summarizer, models);
|
|
170
177
|
say(`${row.kind} ${row.path || "(repo)"}: ${row.status}${row.error ? ` (${row.error})` : ""}`);
|
|
171
|
-
|
|
178
|
+
// on one line (P9): a summary's paragraphs, joined by a space
|
|
179
|
+
if (row.tagline) say(`${row.tagline} — ${joinParagraphs(row.summary ?? "")}`);
|
|
172
180
|
} else{
|
|
173
181
|
if (!summarizer) say(`ANTHROPIC_API_KEY is not set: writing test-file templates only (summaries need a key: ${keyHint()}).`);
|
|
174
182
|
else if (!(await confirmSpend(options, models))) return 1;
|
|
175
183
|
const result = await runSummaries(root, {
|
|
176
|
-
summarizer, models, transport: options.transport, wait: options.wait,
|
|
184
|
+
summarizer, models, transport: options.transport, wait: options.wait,
|
|
185
|
+
// Without a key nothing can be sent, a restyle included, so the run plans as the stage refresh does: what an
|
|
186
|
+
// older version wrote counts as current (P1), instead of turning the stage "off" over rewrites it can't send.
|
|
187
|
+
restyle: summarizer !== null && options.restyle,
|
|
188
|
+
budgetUsd: options.budgetUsd, releaseBatches: options.releaseBatches, onProgress: say,
|
|
177
189
|
});
|
|
178
190
|
say(`up to date ${result.upToDate}/${result.total} · summarized ${result.summarized} · templated ${result.templated}`
|
|
179
191
|
+ ` · failed ${result.failed} · lazy ${result.lazy} · at Anthropic ${result.inFlight}`
|
|
@@ -195,11 +207,12 @@ async function summarize(options: SummarizeOptions): Promise<number> {
|
|
|
195
207
|
/**
|
|
196
208
|
* The estimate and the yes (S11): prints what a full run would send and cost, then asks. Without a terminal it needs --yes.
|
|
197
209
|
* A run with nothing to send needs no yes. A yes is recorded for the repo, with the run's budget. Returns false when the
|
|
198
|
-
* run must not start. The requests are planned, and priced, with the summary models setting (ADR-066)
|
|
210
|
+
* run must not start. The requests are planned, and priced, with the summary models setting (ADR-066), and restyled as the
|
|
211
|
+
* run restyles (P7).
|
|
199
212
|
*/
|
|
200
213
|
async function confirmSpend(options: SummarizeOptions, setting: ResolvedSummaryModels): Promise<boolean> {
|
|
201
214
|
const { root } = options;
|
|
202
|
-
const requests = plannedRequests(root, setting);
|
|
215
|
+
const requests = plannedRequests(root, setting, options.restyle);
|
|
203
216
|
const db = getDb();
|
|
204
217
|
const repoId = requireRepository(db, repoKey(root));
|
|
205
218
|
|
|
@@ -223,9 +236,14 @@ async function confirmSpend(options: SummarizeOptions, setting: ResolvedSummaryM
|
|
|
223
236
|
const folders = requests.filter((r) => r.kind === "folder").length.toLocaleString("en-US");
|
|
224
237
|
// the repo is named apart only when the setting gives it a model of its own
|
|
225
238
|
const above = models.repo === models.folder ? `${folders} folders and the repo to ${models.folder}` : `${folders} folders to ${models.folder} and the repo to ${models.repo}`;
|
|
239
|
+
// what an older version wrote, which this run rewrites unless it was given --no-restyle (P7)
|
|
240
|
+
const restyled = requests.filter((r) => r.restyle).length;
|
|
241
|
+
const restyle = restyled > 0
|
|
242
|
+
? ` This includes rewriting ${plural(restyled, "summary", "summaries")} an older version of Nebulosity wrote, in the new plain style. Add --no-restyle to leave them as they are.`
|
|
243
|
+
: "";
|
|
226
244
|
say(`${path.basename(root)}: about ${estimate.requests.toLocaleString("en-US")} requests (${files.toLocaleString("en-US")} files to ${models.file},`
|
|
227
245
|
+ ` ${above}), about ${formatUsd(estimate.usd)} at list prices`
|
|
228
|
-
+ `${batch ? ", by the Batch API" : ""}${noPrice}. Writing summaries sends each file's code to Anthropic
|
|
246
|
+
+ `${batch ? ", by the Batch API" : ""}${noPrice}. Writing summaries sends each file's code to Anthropic.${restyle}`);
|
|
229
247
|
|
|
230
248
|
if (!options.yes){
|
|
231
249
|
if (!process.stdin.isTTY){
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
module.exports=[[38872,a=>{let b={src:a.i(26758).default,width:256,height:256};a.s(["default",0,b])},48760,a=>{let b={src:a.i(71385).default,width:680,height:680};a.s(["default",0,b])},66114,a=>{var b=a.i(82509);a.n(b)},11857,(a,b,c)=>{b.exports=a.r(24951).vendored["react-rsc"].ReactServerDOMTurbopackServer},717,(a,b,c)=>{b.exports=a.r(24951).vendored["react-rsc"].React}],54799,(a,b,c)=>{b.exports=a.x("crypto",()=>require("crypto"))},70406,(a,b,c)=>{b.exports=a.x("next/dist/compiled/@opentelemetry/api",()=>require("next/dist/compiled/@opentelemetry/api"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},59043,(a,b,c)=>{b.exports=a.x("next/dist/server/runtime-reacts.external.js",()=>require("next/dist/server/runtime-reacts.external.js"))},93695,(a,b,c)=>{b.exports=a.x("next/dist/shared/lib/no-fallback-error.external.js",()=>require("next/dist/shared/lib/no-fallback-error.external.js"))},81111,(a,b,c)=>{b.exports=a.x("node:stream",()=>require("node:stream"))},14747,(a,b,c)=>{b.exports=a.x("path",()=>require("path"))},26758,a=>{a.v("/_next/static/media/favicon.2vob68tjqpejf.ico"+(globalThis.NEXT_CLIENT_ASSET_SUFFIX||""))},71385,a=>{a.v("/_next/static/media/icon.0absiuod-8x8n.svg"+(globalThis.NEXT_CLIENT_ASSET_SUFFIX||""))},82509,(a,b,c)=>{let{createClientModuleProxy:d}=a.r(11857);a.n(d("[project]/node_modules/next/dist/client/components/builtin/global-error.js"))},62212,function(a){a.n(a.i(66114))},14465,(a,b,c)=>{(()=>{"use strict";var a={86:a=>{a.exports=function(a,b){return"string"==typeof a?g(a):"number"==typeof a?f(a,b):null},a.exports.format=f,a.exports.parse=g;var b=/\B(?=(\d{3})+(?!\d))/g,c=/(?:\.0*|(\.[^0]+)0+)$/,d={b:1,kb:1024,mb:1048576,gb:0x40000000,tb:0x10000000000,pb:0x4000000000000},e=/^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;function f(a,e){if(!Number.isFinite(a))return null;var f=Math.abs(a),g=e&&e.thousandsSeparator||"",h=e&&e.unitSeparator||"",i=e&&void 0!==e.decimalPlaces?e.decimalPlaces:2,j=!!(e&&e.fixedDecimals),k=e&&e.unit||"";k&&d[k.toLowerCase()]||(k=f>=d.pb?"PB":f>=d.tb?"TB":f>=d.gb?"GB":f>=d.mb?"MB":f>=d.kb?"KB":"B");var l=(a/d[k.toLowerCase()]).toFixed(i);return j||(l=l.replace(c,"$1")),g&&(l=l.split(".").map(function(a,c){return 0===c?a.replace(b,g):a}).join(".")),l+h+k}function g(a){if("number"==typeof a&&!isNaN(a))return a;if("string"!=typeof a)return null;var b,c=e.exec(a),f="b";return c?(b=parseFloat(c[1]),f=c[4].toLowerCase()):(b=parseInt(a,10),f="b"),Math.floor(d[f]*b)}}},c={};function d(b){var e=c[b];if(void 0!==e)return e.exports;var f=c[b]={exports:{}},g=!0;try{a[b](f,f.exports,d),g=!1}finally{g&&delete c[b]}return f.exports}d.ab="/ROOT/node_modules/next/dist/compiled/bytes/",b.exports=d(86)})()}];
|
|
2
|
-
|
|
3
|
-
//# sourceMappingURL=%5Broot-of-the-server%5D__0ekw8p2ramybv._.js.map
|