supipowers 1.5.2 → 2.0.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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +149 -45
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +19 -9
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +298 -127
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +115 -14
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +11 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1401 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -0,0 +1,258 @@
1
+ // src/context/savings.ts
2
+ //
3
+ // Pure rendering for the L1 "Savings" panel and its drilldown report. The
4
+ // command (`/supi:context`) wires the rendered lines into the existing TUI;
5
+ // this module never touches `ctx.ui`, never reads files, and never opens DBs
6
+ // itself — it queries the `MetricsStore` accessors that Chunk 1 added.
7
+ //
8
+ // Ownership rule: the `Metrics DB: <abs-path>` footer is **not** part of any
9
+ // function exported here. The consumer (`handleContext`) appends it once,
10
+ // immediately after the savings lines, so no caller can produce a duplicate.
11
+
12
+ import { canonicalToolName } from "../context-mode/tool-name.js";
13
+ import type { MetricsStore } from "../context-mode/metrics-store.js";
14
+ import { estimateTokens, formatSize } from "./analyzer.js";
15
+
16
+ /** Format token counts with a k-suffix, mirroring `optimize-context.ts:13`. */
17
+ function formatTokens(tokens: number): string {
18
+ if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`;
19
+ return String(tokens);
20
+ }
21
+
22
+ /** Format a millisecond delta as a coarse "Xh ago" / "Ym ago" string. */
23
+ function relativeTime(startedAtMs: number | null, now = Date.now()): string {
24
+ if (startedAtMs === null) return "unknown";
25
+ const delta = Math.max(0, now - startedAtMs);
26
+ const seconds = Math.floor(delta / 1000);
27
+ if (seconds < 60) return `${seconds}s ago`;
28
+ const minutes = Math.floor(seconds / 60);
29
+ if (minutes < 60) return `${minutes}m ago`;
30
+ const hours = Math.floor(minutes / 60);
31
+ if (hours < 24) return `${hours}h ago`;
32
+ const days = Math.floor(hours / 24);
33
+ return `${days}d ago`;
34
+ }
35
+
36
+ const FALLBACK_LINE = "Measurement unavailable — see /supi:doctor";
37
+
38
+ const FIRST_RUN_NOTICE_PREFIX = "Measurement enabled. Data lives at ";
39
+ const FIRST_RUN_NOTICE_SUFFIX = ". Use /supi:clear to reset.";
40
+
41
+ /**
42
+ * Single-line first-run notice. Returns `null` once the marker has been
43
+ * persisted, or when `store` is null (degraded mode — no marker to update).
44
+ */
45
+ export function getFirstRunNotice(
46
+ store: MetricsStore | null,
47
+ projectSlug: string,
48
+ dbAbsPath: string,
49
+ ): string | null {
50
+ if (!store) return null;
51
+
52
+ let meta;
53
+ try {
54
+ meta = store.getProjectMeta(projectSlug);
55
+ } catch {
56
+ return null;
57
+ }
58
+
59
+ if (meta?.first_run_notice_shown_at != null) return null;
60
+
61
+ try {
62
+ store.setFirstRunNoticeShown(projectSlug);
63
+ } catch {
64
+ // If we can't persist, surfacing once vs forever is acceptable trade-off;
65
+ // failing closed (no notice) is the safer default.
66
+ return null;
67
+ }
68
+
69
+ return `${FIRST_RUN_NOTICE_PREFIX}${dbAbsPath}${FIRST_RUN_NOTICE_SUFFIX}`;
70
+ }
71
+
72
+ /** Inputs to `buildSavingsLines`. Pure data; no store handles. */
73
+ export interface SavingsPanelInput {
74
+ session: { id: string; startedAt: number | null; rowCount: number };
75
+ totals: {
76
+ beforeBytes: number;
77
+ afterBytes: number;
78
+ saved: number;
79
+ tokensEstimated: number;
80
+ };
81
+ perCompressor: Array<{ compressor: string; saved: number; calls: number }>;
82
+ uniqueSourceShare: number;
83
+ }
84
+
85
+ function shortSessionId(id: string): string {
86
+ return id.length <= 12 ? id : `${id.slice(0, 8)}\u2026`;
87
+ }
88
+
89
+ /**
90
+ * Render the four savings lines for the `/supi:context` panel:
91
+ * 1. Session: <id> | Started: <relative> | Compressors tracked: <n>
92
+ * 2. Saved this session: <bytes> (~<tokens> tokens estimated)
93
+ * 3. Top compressors: <compressor1> -<bytes1> · <compressor2> -<bytes2> · <compressor3> -<bytes3>
94
+ * 4. Unique-source share: <pct>% (lower = more re-reads)
95
+ *
96
+ * The consumer is responsible for appending the `Metrics DB: <abs-path>`
97
+ * footer; this function deliberately does not emit it.
98
+ */
99
+ export function buildSavingsLines(input: SavingsPanelInput): string[] {
100
+ const tracked = new Set(input.perCompressor.map((t) => t.compressor)).size;
101
+ const sessionLine =
102
+ `Session: ${shortSessionId(input.session.id)}` +
103
+ ` | Started: ${relativeTime(input.session.startedAt)}` +
104
+ ` | Compressors tracked: ${tracked}`;
105
+
106
+ const savedLine =
107
+ `Saved this session: ${formatSize(input.totals.saved)}` +
108
+ ` (~${formatTokens(input.totals.tokensEstimated)} tokens estimated)`;
109
+
110
+ let topLine: string;
111
+ if (input.perCompressor.length === 0) {
112
+ topLine = "Top compressors: (none)";
113
+ } else {
114
+ const top3 = input.perCompressor.slice(0, 3).map((t) => {
115
+ const display = canonicalToolName(t.compressor);
116
+ return `${display} -${formatSize(t.saved)}`;
117
+ });
118
+ topLine = `Top compressors: ${top3.join(" \u00b7 ")}`;
119
+ }
120
+
121
+ const sharePct = Math.round(input.uniqueSourceShare * 100);
122
+ const shareLine = `Unique-source share: ${sharePct}% (lower = more re-reads)`;
123
+
124
+ return [sessionLine, savedLine, topLine, shareLine];
125
+ }
126
+
127
+ /**
128
+ * Convenience renderer when the consumer has direct access to a store. When
129
+ * `store` is null, returns just the session line and the fallback line so the
130
+ * panel still surfaces *something* the user can act on. The `Metrics DB`
131
+ * footer is always appended by the consumer regardless of which branch fires.
132
+ */
133
+ export function buildSavingsLinesFromStore(
134
+ store: MetricsStore | null,
135
+ sessionId: string,
136
+ sessionStartedAtMs: number | null,
137
+ _dbAbsPath: string,
138
+ ): string[] {
139
+ if (!store) {
140
+ const sessionLine =
141
+ `Session: ${shortSessionId(sessionId)}` +
142
+ ` | Started: ${relativeTime(sessionStartedAtMs)}` +
143
+ ` | Compressors tracked: 0`;
144
+ return [sessionLine, FALLBACK_LINE];
145
+ }
146
+
147
+ const totals = store.getSessionTotals(sessionId);
148
+ const perCompressor = store.getTopProcessors(sessionId, 5).map((entry) => ({
149
+ compressor: entry.processor,
150
+ saved: entry.saved,
151
+ calls: entry.calls,
152
+ }));
153
+ const uniqueSourceShare = store.getUniqueSourceShare(sessionId);
154
+ const tokensEstimated = estimateTokens("x".repeat(Math.max(0, totals.saved)));
155
+
156
+ return buildSavingsLines({
157
+ session: {
158
+ id: sessionId,
159
+ startedAt: sessionStartedAtMs,
160
+ rowCount: totals.rowCount,
161
+ },
162
+ totals: {
163
+ beforeBytes: totals.beforeBytes,
164
+ afterBytes: totals.afterBytes,
165
+ saved: totals.saved,
166
+ tokensEstimated,
167
+ },
168
+ perCompressor,
169
+ uniqueSourceShare,
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Markdown drilldown for a single savings line. Reused by the
175
+ * `writeReport`/`openInEditor` pipeline already in `/supi:context`.
176
+ */
177
+ export function formatSavingsReport(input: SavingsPanelInput): string {
178
+ const tracked = new Set(input.perCompressor.map((t) => t.compressor)).size;
179
+ const lines: string[] = [];
180
+ lines.push("# Session savings");
181
+ lines.push("");
182
+ lines.push(`- Session: ${input.session.id}`);
183
+ lines.push(`- Started: ${relativeTime(input.session.startedAt)}`);
184
+ lines.push(`- Rows recorded: ${input.session.rowCount}`);
185
+ lines.push(`- Compressors tracked: ${tracked}`);
186
+ lines.push("");
187
+
188
+ lines.push("## Totals");
189
+ lines.push("");
190
+ lines.push(`- Before: ${formatSize(input.totals.beforeBytes)}`);
191
+ lines.push(`- After: ${formatSize(input.totals.afterBytes)}`);
192
+ lines.push(`- Saved: ${formatSize(input.totals.saved)}`);
193
+ lines.push(`- Tokens estimated: ~${formatTokens(input.totals.tokensEstimated)}`);
194
+ lines.push("");
195
+
196
+ lines.push("## Top compressors");
197
+ lines.push("");
198
+ if (input.perCompressor.length === 0) {
199
+ lines.push("(no compressors tracked yet)");
200
+ } else {
201
+ for (const t of input.perCompressor) {
202
+ lines.push(
203
+ `- ${canonicalToolName(t.compressor)} — ${formatSize(t.saved)} saved across ${t.calls} call${t.calls === 1 ? "" : "s"}`,
204
+ );
205
+ }
206
+ }
207
+ lines.push("");
208
+
209
+ lines.push("## Unique-source share");
210
+ lines.push("");
211
+ const sharePct = Math.round(input.uniqueSourceShare * 100);
212
+ lines.push(`${sharePct}% — lower values mean the agent re-read the same source repeatedly.`);
213
+ lines.push("");
214
+
215
+ return lines.join("\n");
216
+ }
217
+
218
+ /** Build a full drilldown report from the live store. Returns null when the
219
+ * store is unavailable. */
220
+ export function formatSavingsReportFromStore(
221
+ store: MetricsStore | null,
222
+ sessionId: string,
223
+ sessionStartedAtMs: number | null,
224
+ ): string | null {
225
+ if (!store) return null;
226
+
227
+ const totals = store.getSessionTotals(sessionId);
228
+ const perCompressor = store.getTopProcessors(sessionId, 10).map((entry) => ({
229
+ compressor: entry.processor,
230
+ saved: entry.saved,
231
+ calls: entry.calls,
232
+ }));
233
+ const uniqueSourceShare = store.getUniqueSourceShare(sessionId);
234
+ const tokensEstimated = estimateTokens("x".repeat(Math.max(0, totals.saved)));
235
+
236
+ return formatSavingsReport({
237
+ session: {
238
+ id: sessionId,
239
+ startedAt: sessionStartedAtMs,
240
+ rowCount: totals.rowCount,
241
+ },
242
+ totals: {
243
+ beforeBytes: totals.beforeBytes,
244
+ afterBytes: totals.afterBytes,
245
+ saved: totals.saved,
246
+ tokensEstimated,
247
+ },
248
+ perCompressor,
249
+ uniqueSourceShare,
250
+ });
251
+ }
252
+
253
+ /** Exported test seam so the panel test can assert exact copy. */
254
+ export const _internals = {
255
+ FALLBACK_LINE,
256
+ FIRST_RUN_NOTICE_PREFIX,
257
+ FIRST_RUN_NOTICE_SUFFIX,
258
+ };
@@ -0,0 +1,380 @@
1
+ import type { ParsedSkill, PromptSection } from "./analyzer.js";
2
+ import { parseManagedRule } from "./rule-renderer.js";
3
+ import {
4
+ hashOptimizationSource,
5
+ type ManualOptimizationAction,
6
+ type RuleMode,
7
+ } from "./startup-optimizer.js";
8
+ import { parseManagedTokenignore } from "./tokenignore.js";
9
+
10
+ export interface StartupOptimizerManifestRule {
11
+ path: string;
12
+ mode: RuleMode;
13
+ sourceId: string;
14
+ sourceName: string;
15
+ sourceHash: string;
16
+ slug: string;
17
+ sourceBytes: number;
18
+ condition?: string;
19
+ description?: string;
20
+ }
21
+
22
+ export interface StartupOptimizerManifest {
23
+ version: 1;
24
+ targetBytes: number;
25
+ sourceSetHash: string;
26
+ beforeBytes: number;
27
+ estimatedAfterBytes: number;
28
+ estimatedSavedBytes: number;
29
+ rules: StartupOptimizerManifestRule[];
30
+ tokenignore: {
31
+ path: string;
32
+ entries: string[];
33
+ hash: string;
34
+ };
35
+ manualActions: ManualOptimizationAction[];
36
+ }
37
+
38
+ export type StartupCheckStatus = "pass" | "warn" | "fail";
39
+
40
+ export type StartupCheckReason =
41
+ | "missing-manifest"
42
+ | "malformed-manifest"
43
+ | "missing-rule"
44
+ | "unmanaged-rule"
45
+ | "malformed-rule"
46
+ | "rule-drift"
47
+ | "rule-body-drift"
48
+ | "tokenignore-drift"
49
+ | "still-loaded-source"
50
+ | "unresolved-manual-action"
51
+ | "prompt-over-target"
52
+ | "prompt-unavailable";
53
+
54
+ export interface StartupCheckIssue {
55
+ severity: "fail" | "warn";
56
+ reason: StartupCheckReason;
57
+ path?: string;
58
+ sourceId?: string;
59
+ message: string;
60
+ remediation: string;
61
+ }
62
+
63
+ export interface StartupCheckInput {
64
+ manifestPath: string;
65
+ manifestText: string | null | undefined;
66
+ ruleFiles: Record<string, string | null | undefined>;
67
+ tokenignorePath: string;
68
+ tokenignoreText: string | null | undefined;
69
+ currentPrompt: string | null | undefined;
70
+ currentSkills: ParsedSkill[];
71
+ currentSections: PromptSection[];
72
+ }
73
+
74
+ export interface StartupCheckReport {
75
+ status: StartupCheckStatus;
76
+ issues: StartupCheckIssue[];
77
+ manifest: StartupOptimizerManifest | null;
78
+ manifestPath: string;
79
+ currentBytes: number | null;
80
+ targetBytes: number | null;
81
+ beforeBytes: number | null;
82
+ afterBytes: number | null;
83
+ sourceSetHash: string | null;
84
+ }
85
+
86
+ export function runStartupCheck(input: StartupCheckInput): StartupCheckReport {
87
+ if (input.currentPrompt == null) {
88
+ return {
89
+ status: "fail",
90
+ issues: [issue("prompt-unavailable", {
91
+ path: input.manifestPath,
92
+ message: "Current system prompt is unavailable, so L6 savings cannot be proven.",
93
+ remediation: "Run /supi:optimize-context --check inside an OMP session that exposes ctx.getSystemPrompt().",
94
+ })],
95
+ manifest: null,
96
+ manifestPath: input.manifestPath,
97
+ currentBytes: null,
98
+ targetBytes: null,
99
+ beforeBytes: null,
100
+ afterBytes: null,
101
+ sourceSetHash: null,
102
+ };
103
+ }
104
+
105
+ const currentBytes = byteLength(input.currentPrompt);
106
+ const manifestResult = parseStartupOptimizerManifest(input.manifestText, input.manifestPath);
107
+ if (typeof manifestResult === "string") {
108
+ const reason: StartupCheckReason = input.manifestText == null ? "missing-manifest" : "malformed-manifest";
109
+ return reportFromIssues(input.manifestPath, null, currentBytes, null, [issue(reason, {
110
+ path: input.manifestPath,
111
+ message: manifestResult,
112
+ remediation: reason === "missing-manifest"
113
+ ? "Run /supi:optimize-context --apply to create the startup optimizer manifest."
114
+ : "Remove or repair the manifest, then rerun /supi:optimize-context --apply.",
115
+ })]);
116
+ }
117
+
118
+ const manifest = manifestResult;
119
+ const issues: StartupCheckIssue[] = [];
120
+
121
+ for (const rule of manifest.rules) {
122
+ const text = input.ruleFiles[rule.path];
123
+ if (text == null) {
124
+ issues.push(issue("missing-rule", {
125
+ path: rule.path,
126
+ sourceId: rule.sourceId,
127
+ message: `Managed rule file is missing for ${rule.sourceId}.`,
128
+ remediation: "Rerun /supi:optimize-context --apply to regenerate managed rules.",
129
+ }));
130
+ continue;
131
+ }
132
+
133
+ const parsed = parseManagedRule(text);
134
+ if (parsed.status === "unmanaged") {
135
+ issues.push(issue("unmanaged-rule", {
136
+ path: rule.path,
137
+ sourceId: rule.sourceId,
138
+ message: `Rule file ${rule.path} is not managed by supipowers.`,
139
+ remediation: "Move the user-authored rule aside or choose a different slug before applying again.",
140
+ }));
141
+ continue;
142
+ }
143
+
144
+ if (parsed.status === "malformed") {
145
+ issues.push(issue("malformed-rule", {
146
+ path: rule.path,
147
+ sourceId: rule.sourceId,
148
+ message: `Managed rule ${rule.path} is malformed: ${parsed.error}.`,
149
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed rule.",
150
+ }));
151
+ continue;
152
+ }
153
+
154
+ const metadata = parsed.metadata;
155
+ const frontmatterDrift = rule.mode === "ttsr"
156
+ ? parsed.frontmatter.condition !== rule.condition
157
+ : (typeof rule.description === "string" && parsed.frontmatter.description !== rule.description);
158
+
159
+ if (
160
+ metadata.sourceId !== rule.sourceId ||
161
+ metadata.sourceHash !== rule.sourceHash ||
162
+ metadata.slug !== rule.slug ||
163
+ metadata.mode !== rule.mode ||
164
+ metadata.sourceBytes !== rule.sourceBytes ||
165
+ frontmatterDrift
166
+ ) {
167
+ issues.push(issue("rule-drift", {
168
+ path: rule.path,
169
+ sourceId: rule.sourceId,
170
+ message: `Managed rule ${rule.path} no longer matches the startup optimizer manifest.`,
171
+ remediation: "Rerun /supi:optimize-context --apply to refresh managed artifacts.",
172
+ }));
173
+ continue;
174
+ }
175
+
176
+ // Body verification — proves the rule body still matches what was migrated,
177
+ // not just that the supipowers-managed header is intact.
178
+ const actualBodyHash = hashOptimizationSource(parsed.body);
179
+ const actualBodyBytes = byteLength(parsed.body);
180
+ if (actualBodyHash !== rule.sourceHash || actualBodyBytes !== rule.sourceBytes) {
181
+ issues.push(issue("rule-body-drift", {
182
+ path: rule.path,
183
+ sourceId: rule.sourceId,
184
+ message: `Managed rule ${rule.path} body has been modified (hash/size no longer matches the manifest).`,
185
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed rule from the current prompt source.",
186
+ }));
187
+ }
188
+ }
189
+
190
+ const tokenignore = parseManagedTokenignore(input.tokenignoreText);
191
+ if (
192
+ tokenignore.status !== "managed" ||
193
+ !tokenignore.hashMatches ||
194
+ tokenignore.hash !== manifest.tokenignore.hash ||
195
+ !sameStringSet(tokenignore.entries, manifest.tokenignore.entries)
196
+ ) {
197
+ issues.push(issue("tokenignore-drift", {
198
+ path: input.tokenignorePath,
199
+ message: "Managed .tokenignore block is missing, malformed, or does not match the manifest.",
200
+ remediation: "Rerun /supi:optimize-context --apply to refresh the managed .tokenignore block.",
201
+ }));
202
+ }
203
+
204
+ // Skill/section presence proof obligations come from BOTH `manifest.rules`
205
+ // (write-rule sources expected to be disabled) and `manifest.manualActions`
206
+ // (tech-stack-irrelevant disables and AGENTS.md split, neither of which has a
207
+ // generated rule file). Without checking manualActions the previous
208
+ // implementation could return `pass` while required manual steps were never
209
+ // performed.
210
+ const skillSourceIdsToVerify = new Set<string>();
211
+ const sectionSourceIdsToVerify = new Set<string>();
212
+ for (const rule of manifest.rules) {
213
+ if (rule.sourceId.startsWith("skill:")) skillSourceIdsToVerify.add(rule.sourceId);
214
+ else if (rule.sourceId.startsWith("section:")) sectionSourceIdsToVerify.add(rule.sourceId);
215
+ }
216
+ for (const action of manifest.manualActions) {
217
+ if (action.kind !== "manual-disable") continue;
218
+ if (action.sourceId.startsWith("skill:")) skillSourceIdsToVerify.add(action.sourceId);
219
+ else if (action.sourceId.startsWith("section:")) sectionSourceIdsToVerify.add(action.sourceId);
220
+ }
221
+
222
+ const reportedSourceIds = new Set<string>();
223
+ for (const skill of input.currentSkills) {
224
+ const sourceId = `skill:${skill.name.trim().toLowerCase()}`;
225
+ if (!skillSourceIdsToVerify.has(sourceId)) continue;
226
+ if (reportedSourceIds.has(sourceId)) continue;
227
+ reportedSourceIds.add(sourceId);
228
+ issues.push(issue("still-loaded-source", {
229
+ sourceId,
230
+ message: `Migrated source ${sourceId} is still loaded in the startup prompt.`,
231
+ remediation: "Disable the original skill/source and restart OMP so the new managed rule is picked up.",
232
+ }));
233
+ }
234
+ for (const section of input.currentSections) {
235
+ const sourceId = `section:${section.label}`;
236
+ if (!sectionSourceIdsToVerify.has(sourceId)) continue;
237
+ if (reportedSourceIds.has(sourceId)) continue;
238
+ reportedSourceIds.add(sourceId);
239
+ issues.push(issue("still-loaded-source", {
240
+ sourceId,
241
+ message: `Migrated section ${sourceId} is still present in the startup prompt.`,
242
+ remediation: "Remove or split the section and restart OMP so the prompt actually shrinks.",
243
+ }));
244
+ }
245
+
246
+ // manual-agents-split proof: the AGENTS.md (or named) section must be at or
247
+ // below the threshold the planner recorded.
248
+ for (const action of manifest.manualActions) {
249
+ if (action.kind !== "manual-agents-split") continue;
250
+ const matching = input.currentSections.find((section) => section.label === action.sourceName);
251
+ if (!matching) continue;
252
+ if (matching.bytes > action.thresholdBytes) {
253
+ issues.push(issue("unresolved-manual-action", {
254
+ sourceId: action.sourceId,
255
+ message: `${action.sourceName} is still ${matching.bytes} bytes, above threshold ${action.thresholdBytes}.`,
256
+ remediation: action.remediation,
257
+ }));
258
+ }
259
+ }
260
+
261
+ if (currentBytes > manifest.targetBytes) {
262
+ issues.push(issue("prompt-over-target", {
263
+ message: `Current startup prompt is ${currentBytes} bytes, above target ${manifest.targetBytes} bytes.`,
264
+ remediation: "Disable migrated sources, split oversized startup files, then restart OMP before rerunning --check.",
265
+ }));
266
+ }
267
+
268
+ return reportFromIssues(input.manifestPath, manifest, currentBytes, manifest.targetBytes, issues);
269
+ }
270
+
271
+ /**
272
+ * Parse and validate a `manifest.json` from disk. Exported so the command and
273
+ * the check share one parser; previously the command had its own ad-hoc
274
+ * partial parser that could accept shapes the real validator would reject.
275
+ *
276
+ * Returns the validated manifest or a human-readable error string.
277
+ */
278
+ export function parseStartupOptimizerManifest(
279
+ text: string | null | undefined,
280
+ manifestPath: string,
281
+ ): StartupOptimizerManifest | string {
282
+ if (text == null) return `Startup optimizer manifest is missing at ${manifestPath}.`;
283
+
284
+ let parsed: unknown;
285
+ try {
286
+ parsed = JSON.parse(text);
287
+ } catch (error) {
288
+ return `Startup optimizer manifest is malformed: ${(error as Error).message}.`;
289
+ }
290
+
291
+ if (!isRecord(parsed)) return "Startup optimizer manifest must be an object.";
292
+ if (parsed.version !== 1) return "Startup optimizer manifest has unsupported version.";
293
+ if (!isFiniteNumber(parsed.targetBytes)) return "Startup optimizer manifest is missing targetBytes.";
294
+ if (typeof parsed.sourceSetHash !== "string") return "Startup optimizer manifest is missing sourceSetHash.";
295
+ if (!isFiniteNumber(parsed.beforeBytes)) return "Startup optimizer manifest is missing beforeBytes.";
296
+ if (!isFiniteNumber(parsed.estimatedAfterBytes)) return "Startup optimizer manifest is missing estimatedAfterBytes.";
297
+ if (!isFiniteNumber(parsed.estimatedSavedBytes)) return "Startup optimizer manifest is missing estimatedSavedBytes.";
298
+ if (!Array.isArray(parsed.rules)) return "Startup optimizer manifest is missing rules.";
299
+ if (!isRecord(parsed.tokenignore)) return "Startup optimizer manifest is missing tokenignore.";
300
+ if (!Array.isArray(parsed.manualActions)) return "Startup optimizer manifest is missing manualActions.";
301
+
302
+ const rules: StartupOptimizerManifestRule[] = [];
303
+ for (const candidate of parsed.rules) {
304
+ if (!isRecord(candidate)) return "Startup optimizer manifest has invalid rule entry.";
305
+ if (candidate.mode !== "ttsr" && candidate.mode !== "rulebook") return "Startup optimizer manifest has invalid rule mode.";
306
+ for (const key of ["path", "sourceId", "sourceName", "sourceHash", "slug"] as const) {
307
+ if (typeof candidate[key] !== "string") return `Startup optimizer manifest rule is missing ${key}.`;
308
+ }
309
+ if (!isFiniteNumber(candidate.sourceBytes)) return "Startup optimizer manifest rule is missing sourceBytes.";
310
+ rules.push(candidate as unknown as StartupOptimizerManifestRule);
311
+ }
312
+
313
+ if (typeof parsed.tokenignore.path !== "string") return "Startup optimizer manifest tokenignore is missing path.";
314
+ if (!Array.isArray(parsed.tokenignore.entries) || !parsed.tokenignore.entries.every((entry) => typeof entry === "string")) {
315
+ return "Startup optimizer manifest tokenignore has invalid entries.";
316
+ }
317
+ if (typeof parsed.tokenignore.hash !== "string") return "Startup optimizer manifest tokenignore is missing hash.";
318
+
319
+ return {
320
+ version: 1,
321
+ targetBytes: parsed.targetBytes,
322
+ sourceSetHash: parsed.sourceSetHash,
323
+ beforeBytes: parsed.beforeBytes,
324
+ estimatedAfterBytes: parsed.estimatedAfterBytes,
325
+ estimatedSavedBytes: parsed.estimatedSavedBytes,
326
+ rules,
327
+ tokenignore: {
328
+ path: parsed.tokenignore.path,
329
+ entries: parsed.tokenignore.entries,
330
+ hash: parsed.tokenignore.hash,
331
+ },
332
+ manualActions: parsed.manualActions as ManualOptimizationAction[],
333
+ };
334
+ }
335
+
336
+ function reportFromIssues(
337
+ manifestPath: string,
338
+ manifest: StartupOptimizerManifest | null,
339
+ currentBytes: number | null,
340
+ targetBytes: number | null,
341
+ issues: StartupCheckIssue[],
342
+ ): StartupCheckReport {
343
+ const hasFailure = issues.some((candidate) => candidate.severity === "fail");
344
+ const hasWarning = issues.some((candidate) => candidate.severity === "warn");
345
+
346
+ return {
347
+ status: hasFailure ? "fail" : hasWarning ? "warn" : "pass",
348
+ issues,
349
+ manifest,
350
+ manifestPath,
351
+ currentBytes,
352
+ targetBytes: targetBytes ?? manifest?.targetBytes ?? null,
353
+ beforeBytes: manifest?.beforeBytes ?? null,
354
+ afterBytes: currentBytes,
355
+ sourceSetHash: manifest?.sourceSetHash ?? null,
356
+ };
357
+ }
358
+
359
+ function issue(reason: StartupCheckReason, input: Omit<StartupCheckIssue, "severity" | "reason">): StartupCheckIssue {
360
+ return { severity: "fail", reason, ...input };
361
+ }
362
+
363
+ function byteLength(value: string): number {
364
+ return new TextEncoder().encode(value).length;
365
+ }
366
+
367
+ function isRecord(value: unknown): value is Record<string, any> {
368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
369
+ }
370
+
371
+ function isFiniteNumber(value: unknown): value is number {
372
+ return typeof value === "number" && Number.isFinite(value);
373
+ }
374
+
375
+ function sameStringSet(a: string[], b: string[]): boolean {
376
+ if (a.length !== b.length) return false;
377
+ const left = [...a].sort();
378
+ const right = [...b].sort();
379
+ return left.every((value, index) => value === right[index]);
380
+ }