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.
Files changed (180) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +11 -11
  3. package/.next/diagnostics/route-bundle-stats.json +10 -10
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +5 -5
  6. package/.next/server/app/_global-error/page/build-manifest.json +3 -3
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page/build-manifest.json +3 -3
  14. package/.next/server/app/_not-found/page/next-font-manifest.json +2 -3
  15. package/.next/server/app/_not-found/page.js +3 -2
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +27 -20
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +27 -20
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +34 -26
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/index.html +1 -1
  23. package/.next/server/app/index.rsc +29 -23
  24. package/.next/server/app/index.segments/__PAGE__.segment.rsc +33 -24
  25. package/.next/server/app/index.segments/_full.segment.rsc +29 -23
  26. package/.next/server/app/index.segments/_tree.segment.rsc +4 -5
  27. package/.next/server/app/page/build-manifest.json +3 -3
  28. package/.next/server/app/page/next-font-manifest.json +2 -3
  29. package/.next/server/app/page.js +4 -4
  30. package/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +3 -3
  32. package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +3 -3
  33. package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +3 -3
  34. package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +3 -3
  35. package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +3 -3
  36. package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +3 -3
  37. package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +3 -3
  38. package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +3 -3
  39. package/.next/server/chunks/_0uqus0ehnijt-._.js +1 -1
  40. package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +1 -1
  41. package/.next/server/chunks/ssr/[root-of-the-server]__0_3l5tdzqy-td._.js +3 -0
  42. package/.next/server/chunks/ssr/[root-of-the-server]__10ockmlli-lun._.js +3 -0
  43. package/.next/server/chunks/ssr/{[root-of-the-server]__0dyligeswbb47._.js → [root-of-the-server]__16-gclvix9mjd._.js} +1 -1
  44. package/.next/server/chunks/ssr/[root-of-the-server]__1auodc3c0rppy._.js +3 -0
  45. package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +3 -3
  46. package/.next/server/chunks/ssr/[root-of-the-server]__1i9r59yk4i69p._.js +3 -0
  47. package/.next/server/chunks/ssr/[root-of-the-server]__1l3qp8u9yc_ig._.js +3 -0
  48. package/.next/server/chunks/ssr/_1p-0wsx9qfm68._.js +13 -0
  49. package/.next/server/chunks/ssr/app_map_18iu6j19jsqmu._.js +3 -0
  50. package/.next/server/chunks/ssr/node_modules_next_dist_1r3iqtbm45lqp._.js +3 -0
  51. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js +3 -0
  52. package/.next/server/middleware-build-manifest.js +11 -11
  53. package/.next/server/next-font-manifest.js +1 -1
  54. package/.next/server/next-font-manifest.json +4 -6
  55. package/.next/server/pages/404.html +1 -1
  56. package/.next/server/pages/500.html +1 -1
  57. package/.next/server/preview-props.json +3 -3
  58. package/.next/server/server-reference-manifest.js +1 -1
  59. package/.next/server/server-reference-manifest.json +1 -1
  60. package/.next/static/chunks/{1rljem7o1jtdo.js → 18kfexnrqek07.js} +1 -1
  61. package/.next/static/chunks/1hwbgd4nvvhg8.js +1 -0
  62. package/.next/static/chunks/25r4lkkf1shfo.css +3 -0
  63. package/.next/static/chunks/35ubvk2ivdjgs.js +1 -0
  64. package/.next/static/chunks/3ieqvmcg3hofj.js +31 -0
  65. package/.next/static/chunks/3w00e1w2125pp.js +11 -0
  66. package/.next/static/media/733da6bd8dac84c8-s.3hlisc-pwq13z.woff2 +0 -0
  67. package/.next/static/media/784318cea1dfcfa4-s.0-oo9_1x_xmvg.woff2 +0 -0
  68. package/.next/static/media/7e114457660b62ed-s.36fqqi66-3tjb.woff2 +0 -0
  69. package/.next/static/media/85e86a88f68a624a-s.357dd_6abeqge.woff2 +0 -0
  70. package/.next/static/media/9a321a2c78eff0b5-s.p.1bkzbscqrt8rl.woff2 +0 -0
  71. package/.next/static/media/9fecbd3e5ea9bccb-s.3_z45zcoc-xoz.woff2 +0 -0
  72. package/.next/static/media/c64053b510252f55-s.1kd-r_s9smv5m.woff2 +0 -0
  73. package/.next/static/media/c6a5c2fdc12da45c-s.p.17-aodiw50953.woff2 +0 -0
  74. package/.next/static/media/f263782c6a4bddee-s.1gyfqm5yhhzs7.woff2 +0 -0
  75. package/.next/trace +1 -1
  76. package/.next/trace-build +1 -1
  77. package/README.md +13 -4
  78. package/app/globals.css +91 -54
  79. package/app/layout.tsx +20 -11
  80. package/app/map/BackButton.tsx +5 -5
  81. package/app/map/BoxActions.tsx +20 -12
  82. package/app/map/Breadcrumb.tsx +132 -46
  83. package/app/map/DetailPanel.tsx +408 -297
  84. package/app/map/Explorer.tsx +268 -0
  85. package/app/map/Header.tsx +105 -0
  86. package/app/map/HelpDialog.tsx +37 -34
  87. package/app/map/Legend.tsx +19 -75
  88. package/app/map/LineCard.tsx +187 -0
  89. package/app/map/MapBox.tsx +73 -38
  90. package/app/map/MapCanvas.tsx +344 -115
  91. package/app/map/MapToolbar.tsx +377 -0
  92. package/app/map/MapView.tsx +217 -213
  93. package/app/map/RepoMenu.tsx +13 -12
  94. package/app/map/RoutedEdge.tsx +48 -16
  95. package/app/map/SearchPalette.tsx +6 -6
  96. package/app/map/SourceViewer.tsx +19 -17
  97. package/app/map/StatusBar.tsx +166 -0
  98. package/app/map/embed.ts +22 -2
  99. package/app/map/emphasis.ts +94 -8
  100. package/app/map/explorerRows.ts +85 -0
  101. package/app/map/flowNodes.ts +12 -7
  102. package/app/map/format.ts +2 -2
  103. package/app/map/history.ts +4 -3
  104. package/app/map/inspector.ts +113 -0
  105. package/app/map/labels.ts +66 -7
  106. package/app/map/narrow.ts +165 -85
  107. package/app/map/opening.ts +16 -0
  108. package/app/map/portals.ts +2 -2
  109. package/app/map/routes.ts +1060 -55
  110. package/app/map/runs.ts +108 -0
  111. package/app/map/shell.ts +91 -0
  112. package/app/map/shortcuts.ts +2 -1
  113. package/app/map/summaries.ts +41 -5
  114. package/app/map/text.ts +9 -1
  115. package/app/map/theme.ts +197 -0
  116. package/app/map/useDismiss.ts +20 -6
  117. package/app/map/useEditorBridge.ts +12 -1
  118. package/app/map/useThemeKind.ts +28 -0
  119. package/app/map/useWindowSize.ts +37 -0
  120. package/app/map/viewport.ts +134 -16
  121. package/app/map/weights.ts +80 -0
  122. package/app/page.tsx +3 -5
  123. package/lib/background/child.ts +2 -0
  124. package/lib/background/service.ts +26 -10
  125. package/lib/contracts.ts +25 -2
  126. package/lib/db/migrations/0011_plain_summaries_prompt_version.sql +1 -0
  127. package/lib/db/migrations/meta/0011_snapshot.json +1498 -0
  128. package/lib/db/migrations/meta/_journal.json +7 -0
  129. package/lib/db/schema.ts +2 -1
  130. package/lib/db/store/map.ts +6 -0
  131. package/lib/db/store/summaries.ts +14 -8
  132. package/lib/engine/describe.ts +2 -0
  133. package/lib/index/summarize/budget.ts +5 -2
  134. package/lib/index/summarize/claude.ts +19 -4
  135. package/lib/index/summarize/compare.ts +2 -2
  136. package/lib/index/summarize/plan.ts +73 -13
  137. package/lib/index/summarize/preview.ts +12 -9
  138. package/lib/index/summarize/prompts.ts +122 -25
  139. package/lib/index/summarize/run.ts +48 -18
  140. package/lib/index/summarize/snapshot.ts +7 -2
  141. package/lib/index/summarize/status.ts +3 -2
  142. package/lib/tools/text.ts +3 -2
  143. package/mcp/server.ts +1 -1
  144. package/package.json +4 -2
  145. package/scripts/commands/summarize-command.ts +27 -9
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +0 -3
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +0 -3
  148. package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +0 -3
  149. package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +0 -3
  150. package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +0 -3
  151. package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +0 -9
  152. package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +0 -3
  153. package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +0 -3
  154. package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +0 -3
  155. package/.next/static/chunks/02x9pxyl2d8om.css +0 -3
  156. package/.next/static/chunks/0p-0bv2dzer18.js +0 -7
  157. package/.next/static/chunks/10u7p2gj02qay.js +0 -1
  158. package/.next/static/chunks/31qp8jfkw0x2h.js +0 -31
  159. package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
  160. package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
  161. package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
  162. package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
  163. package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
  164. package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
  165. package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
  166. package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
  167. package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
  168. package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
  169. package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
  170. package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
  171. package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
  172. package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
  173. package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
  174. package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
  175. package/app/map/ActivityBar.tsx +0 -81
  176. package/app/map/StatusPill.tsx +0 -93
  177. package/app/map/TopBar.tsx +0 -84
  178. /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_buildManifest.js +0 -0
  179. /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_clientMiddlewareManifest.js +0 -0
  180. /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
- /** Part of every inputHash. Bump it to redo every summary on purpose (a better prompt, new settings). Version 2: ADR-034. */
7
- export const PROMPT_VERSION = 2;
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 v2 (ADR-034). The taglines' examples are deliberately not from VS Code, so the model can't copy one into a summary.
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 a developer sees for one source file while exploring an unfamiliar codebase.",
45
- "Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"Retry policy with exponential backoff\". The summary is 2 to 4 sentences.",
46
- "The summary says what the file is responsible for, names its most important exports, and says how other code uses it. The \"Imported by\" line comes from the repository's import graph; use it to say how widely and where the file is used.",
47
- "In the summary, write identifiers (functions, classes, types, variables and file names) in backticks, like `parseConfig`.",
48
- "Use real identifiers from the file. Don't narrate the code line by line, and don't guess beyond what you're shown. Don't mention truncation or code you weren't shown.",
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 a developer sees for one folder while exploring an unfamiliar codebase. You're given the summaries of the files and subfolders inside it.",
53
- "Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"Payment provider integrations\". The summary is 2 to 4 sentences.",
54
- "The summary says what the folder is responsible for as a whole, which parts matter most, and how they fit together.",
55
- "In the summary, write file, folder, function and class names in backticks, like `retry.ts`.",
56
- "Don't list every item, and don't guess beyond the summaries you're given.",
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 a developer sees first when opening an unfamiliar repository. You're given the summaries of its top-level folders and files.",
61
- "Give a tagline and a summary. The tagline is a noun phrase of 3 to 6 plain words, like \"Distributed task scheduling service\". The summary is 2 to 4 sentences.",
62
- "The summary says what the project is, names its main areas, and says where a newcomer should start reading.",
63
- "In the summary, write folder and file names in backticks, like `src/server`.",
64
- "Don't guess beyond the summaries you're given.",
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
- /** Largest first (by lines), ties by name; the overflow becomes one "…and N smaller items" line. */
162
- function listItems(items: readonly FolderItem[], maxItems: number): string[] {
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
- return `- ${label}: ${item.text ? `${item.text.tagline} — ${item.text.summary}` : "(no summary yet)"}`;
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 { FILE_SYSTEM_PROMPT, FOLDER_SYSTEM_PROMPT, importerLine, plural, REPO_SYSTEM_PROMPT, renderFilePrompt } from "./prompts";
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. Reads only.
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(repoRoot: string, models: ResolvedSummaryModels = DEFAULT_SUMMARY_MODELS): ProjectedRequest[] {
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. A folder whose children aren't all summarized is refused (TargetNotReadyError): its summary would
390
- * be written over "(no summary yet)" lines. Concurrent calls for one target share one request (ADR-033, amended).
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
- const snap = planSnapshot(db, repoId, true, new Set(), models);
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 file changed
437
- * on disk after indexing, or vanished: a summary of different bytes would contradict the
438
- * structure the index holds for it.
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
- return renderFilePrompt(listing, file.lineCount, importerLine(file.path, loadImporters(db, repoId, file.path)), source);
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
- const snap = planSnapshot(db, repoId, false, new Set(), models.ok ? models : DEFAULT_SUMMARY_MODELS);
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.0.0";
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.0.0",
4
- "description": "A live map of how a big codebase fits together, for Claude Code and for you: a local import index, an MCP server and the Map.",
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 three switches, and the two options whose value is 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
- if (row.tagline) say(`${row.tagline} — ${row.summary}`);
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, budgetUsd: options.budgetUsd, releaseBatches: options.releaseBatches, onProgress: say,
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