parseman 0.46.0 → 0.47.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 (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +9929 -7465
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +9929 -7465
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10865 -8535
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10865 -8535
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +40 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9224 -7203
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9219 -7198
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +311 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +311 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11146 -9593
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11146 -9593
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2898 -1623
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2898 -1623
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11375 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11352 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +446 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +493 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +25 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +322 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +221 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +821 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +2994 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1766 -0
  286. package/src/table/emit.ts +418 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1672 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +459 -0
  295. package/src/table/program.ts +855 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,217 @@
1
+ /**
2
+ * The human layer over `FixReport`.
3
+ *
4
+ * The claim this rendering has to carry is unusual, so it is made explicitly and never
5
+ * by implication: a fix shown here was APPLIED to the grammar, the grammar was
6
+ * RECOMPILED, the corpus was RE-PARSED, and the output did not move. Every offered
7
+ * rewrite prints that evidence next to it, with the sample and byte counts, because
8
+ * "proven" without a corpus size is a word, not a fact.
9
+ *
10
+ * The diff is the primary interface. `--apply` writing to a file is a second, explicit
11
+ * step; what a reader gets by default is what would change, in the form they would read
12
+ * it in a review.
13
+ *
14
+ * TWO STATES, MADE VISUALLY DISTINCT
15
+ * ----------------------------------
16
+ * ACTIONABLE and LOCATED are the whole model, and in the first cut they were two words
17
+ * in the same colour — which is to say, not a distinction at all until you read them.
18
+ * They now differ in glyph, colour and weight, so a reader triages the report by
19
+ * scanning the left margin rather than by parsing it. LOCATED prints the REASON and
20
+ * never advice: a site that cannot be rewritten is still worth knowing about, and that
21
+ * is the difference between "here is exactly where the problem is" and silence.
22
+ *
23
+ * Like `diagnose-render.ts`, this emits LINES OF SPANS and never an escape byte;
24
+ * `terminal.ts` owns every interaction with a terminal.
25
+ */
26
+ import type { FixReport, VerifiedFix, LocatedFinding } from './fix.ts'
27
+ import {
28
+ DEFAULT_WIDTH, TONE, blank, codeFrame, groupDigits, render, rule, t, wrap,
29
+ type Line, type RenderTarget,
30
+ } from './terminal.ts'
31
+
32
+ export type FixRenderOptions = RenderTarget & {
33
+ name?: string
34
+ /**
35
+ * How many edits were WRITTEN to disk. A count, not a flag: `applyFixEdits` skips an
36
+ * edit whose span moved under it, so a run can write some of `r.verified` and skip the
37
+ * rest. A boolean here made the report say every verified change had been written
38
+ * whenever any one of them had been — the one number a reader checks after `--apply`,
39
+ * wrong in exactly the case that matters. Absent (or 0) means nothing was written.
40
+ */
41
+ applied?: number
42
+ /** Absolute path of the edited source, used ONLY for the frame's terminal hyperlink. */
43
+ sourceRoot?: string
44
+ }
45
+
46
+ export function fixReportLines(r: FixReport, opts: FixRenderOptions = {}): Line[] {
47
+ const name = opts.name ?? 'grammar'
48
+ const width = opts.width ?? DEFAULT_WIDTH
49
+ const out: Line[] = []
50
+
51
+ if (!r.ok) {
52
+ out.push([t('✗ ', TONE.loud), t(name, TONE.strong),
53
+ t(' — nothing can be offered, because nothing could be checked', TONE.bad)])
54
+ for (const l of wrap(r.blocked ?? 'the verification loop could not run', width - 4, ' ')) out.push([t(l)])
55
+ out.push([t(' parseman only offers a change after it has applied it, rebuilt the parser and', TONE.quiet)])
56
+ out.push([t(' confirmed your files still parse to exactly the same thing. It could not do that', TONE.quiet)])
57
+ out.push([t(' here, so it is offering nothing rather than guessing.', TONE.quiet)])
58
+ return out
59
+ }
60
+
61
+ const engines = r.engines.join(' + ')
62
+ if (r.verified.length === 0 && r.located.length === 0) {
63
+ out.push([t('✓ ', TONE.good), t(name, TONE.strong), t(' — nothing here can be rewritten', TONE.good)])
64
+ out.push([t(` Checked against ${groupDigits(r.corpus.samples)} of your files `
65
+ + `(${groupDigits(r.corpus.bytes)} bytes). No change parseman knows how to make applies here.`,
66
+ TONE.quiet)])
67
+ return out
68
+ }
69
+
70
+ // A count of edits actually written, and whether that is all of them. `skipped > 0` is
71
+ // the partial case: `applyFixEdits` declined an edit whose span no longer matched.
72
+ const written = opts.applied ?? 0
73
+ const wrote = written > 0
74
+ const skipped = wrote ? r.verified.length - written : 0
75
+ out.push([
76
+ t(wrote ? '✓ ' : '● ', wrote ? TONE.good : TONE.warn),
77
+ t(name, TONE.strong),
78
+ t(' — ', TONE.quiet),
79
+ t(`${groupDigits(r.verified.length)} change${r.verified.length === 1 ? '' : 's'} that `
80
+ + `${r.verified.length === 1 ? 'is' : 'are'} safe to make`, TONE.good),
81
+ t(r.located.length > 0
82
+ ? `, ${groupDigits(r.located.length)} place${r.located.length === 1 ? '' : 's'} that `
83
+ + `${r.located.length === 1 ? 'needs' : 'need'} you`
84
+ : '', TONE.warn),
85
+ ])
86
+ for (const l of wrap(
87
+ `Every change below was applied, the parser rebuilt, and your ${groupDigits(r.corpus.samples)} `
88
+ + `file${r.corpus.samples === 1 ? '' : 's'} (${groupDigits(r.corpus.bytes)} bytes) parsed again with `
89
+ + `${engines === 'interpreted + compiled' ? 'both engines' : engines} — the result was identical `
90
+ + 'every time. A change that altered the result was thrown away and is not shown.',
91
+ width - 2, ' ')) out.push([t(l, TONE.faint)])
92
+ if (!wrote && r.verified.length > 0) {
93
+ out.push([t(' Nothing has been written. Add ', TONE.quiet),
94
+ t('--apply', TONE.strong), t(' to make these edits.', TONE.quiet)])
95
+ }
96
+
97
+ for (const f of r.verified) out.push(...verifiedLines(f, opts))
98
+ for (const l of r.located) out.push(...locatedLines(l, opts))
99
+
100
+ if (r.frozen.length > 0) {
101
+ out.push(blank())
102
+ for (const l of wrap(
103
+ `${groupDigits(r.frozen.length)} part(s) of the grammar were left untouched because parseman `
104
+ + 'cannot rebuild them exactly and will not guess: '
105
+ + r.frozen.slice(0, 6).map(f => `${f.rule} (${f.tag})`).join(', ')
106
+ + (r.frozen.length > 6 ? ', …' : ''), width - 2, ' ')) out.push([t(l, TONE.faint)])
107
+ }
108
+ out.push(blank())
109
+ out.push([
110
+ t(wrote ? '✓ ' : '🔧 ', TONE.good),
111
+ t(`${groupDigits(r.verified.length)} safe to apply`, TONE.strong),
112
+ t(r.located.length > 0 ? `, ${groupDigits(r.located.length)} need you` : '', TONE.strong),
113
+ // Name the skipped edits. Silently reporting "written to disk" over a partial write
114
+ // is the failure this count exists to prevent: the reader would go on believing the
115
+ // file holds all of them.
116
+ t(wrote
117
+ ? (skipped > 0
118
+ ? ` · ${groupDigits(written)} written to disk, ${groupDigits(skipped)} skipped (the source moved under the edit)`
119
+ : ' · written to disk')
120
+ : ' · add --apply to make them · exiting 0 (nothing written)', TONE.faint),
121
+ ])
122
+ return out
123
+ }
124
+
125
+ export function renderFixReport(r: FixReport, opts: FixRenderOptions = {}): string {
126
+ return render(fixReportLines(r, opts), opts)
127
+ }
128
+
129
+ function verifiedLines(f: VerifiedFix, opts: FixRenderOptions): Line[] {
130
+ const out: Line[] = []
131
+ const width = opts.width ?? DEFAULT_WIDTH
132
+ const b = f.benefit
133
+ const effects: string[] = []
134
+ if (b.antiPatternsAfter !== b.antiPatternsBefore) {
135
+ // The measured delta, not a hard-coded 1: the rewrite replaces a COMBINATOR, and a
136
+ // combinator shared by several arms removes a finding at each of them.
137
+ const removed = b.antiPatternsBefore - b.antiPatternsAfter
138
+ effects.push(`removes ${groupDigits(removed)} of the ${b.antiPatternsBefore} arms that hide their first character`)
139
+ }
140
+ if (b.ungatedChoicesAfter !== b.ungatedChoicesBefore) {
141
+ effects.push(`${b.ungatedChoicesBefore - b.ungatedChoicesAfter} fewer choice(s) the parser must guess at`)
142
+ }
143
+ if (b.gatedChoicesAfter !== b.gatedChoicesBefore) {
144
+ effects.push(`${b.gatedChoicesAfter - b.gatedChoicesBefore} more choice(s) decided from one character`)
145
+ }
146
+ // The first-set line earns its place only when the set MOVED. Printing `'i' → 'i'`
147
+ // under a heading called "why" is how a diagnostic loses trust.
148
+ if (f.armFirstSetBefore !== f.armFirstSetAfter) {
149
+ effects.push(`arm ${f.armIndex} now starts with a known character (${f.armFirstSetAfter})`)
150
+ }
151
+ if (f.choiceGatesBefore !== f.choiceGatesAfter) {
152
+ effects.push(`${f.choiceId} can now be decided from the next character alone`)
153
+ }
154
+ const effect = effects.join(' · ')
155
+
156
+ out.push(blank())
157
+ out.push(rule(width, TONE.frame))
158
+ out.push([
159
+ t(' 🔧 ', TONE.good),
160
+ t('SAFE TO APPLY', { color: 'green', bold: true }),
161
+ t(' '),
162
+ t(f.id, TONE.ident),
163
+ ])
164
+ // The grammar SOURCE is the world this fix lives in, so it is a frame with the caret
165
+ // under the term being replaced rather than a bare `path:line:col`. The frame's own
166
+ // message carries the measured effect, so no line repeats the heading above it.
167
+ if (f.edit !== undefined) {
168
+ out.push(...codeFrame({
169
+ path: f.edit.path,
170
+ fullPath: opts.sourceRoot ?? f.edit.path,
171
+ line: f.edit.line,
172
+ column: f.edit.column,
173
+ // INCLUSIVE, per linecraft's CodeDebug (`highlightEnd - highlightStart + 1`), so the
174
+ // last underlined column is the last column of `oldText` — not the one after it.
175
+ endColumn: f.edit.column + Math.max(f.edit.oldText.length - 1, 0),
176
+ lineText: f.edit.lineText.replace(/\t/g, ' '),
177
+ message: effect,
178
+ shortMessage: `→ ${f.edit.newText}`,
179
+ type: 'info',
180
+ }, opts, ' '))
181
+ }
182
+ else {
183
+ out.push([t(' - ', TONE.bad), t(f.before, TONE.bad)])
184
+ out.push([t(' + ', TONE.good), t(f.after, TONE.good)])
185
+ out.push([t(' effect ', TONE.quiet), t(effect, TONE.strong)])
186
+ }
187
+ if (b.artifactBytesBefore !== null && b.artifactBytesAfter !== null && b.artifactBytesAfter !== b.artifactBytesBefore) {
188
+ const d = b.artifactBytesAfter - b.artifactBytesBefore
189
+ out.push([t(' size ', TONE.faint),
190
+ t(`the generated parser ${d > 0 ? 'grows' : 'shrinks'} by ${groupDigits(Math.abs(d))} bytes`, TONE.faint)])
191
+ }
192
+ const checked = `this exact change was made, the parser rebuilt, and your `
193
+ + `${groupDigits(f.evidence.samples)} file${f.evidence.samples === 1 ? '' : 's'} parsed again — `
194
+ + 'identical result'
195
+ const cl = wrap(checked, width - 12, '')
196
+ out.push([t(' checked ', TONE.quiet), t(cl[0] ?? '', TONE.good)])
197
+ for (const x of cl.slice(1)) out.push([t(' '), t(x, TONE.good)])
198
+ return out
199
+ }
200
+
201
+ function locatedLines(l: LocatedFinding, opts: FixRenderOptions): Line[] {
202
+ const out: Line[] = []
203
+ const width = opts.width ?? DEFAULT_WIDTH
204
+ out.push(blank())
205
+ out.push(rule(width, TONE.frame))
206
+ out.push([
207
+ t(' ✋ ', TONE.warn),
208
+ t('NEEDS YOU', { color: 'yellow', bold: true }),
209
+ t(' '),
210
+ t(l.id, TONE.ident),
211
+ ])
212
+ out.push([t(' here ', TONE.faint), t(l.site, TONE.quiet)])
213
+ const lines = wrap(`No change can be offered here: ${l.reason}`, width - 12, '')
214
+ out.push([t(' why ', TONE.quiet), t(lines[0] ?? '', TONE.warn)])
215
+ for (const x of lines.slice(1)) out.push([t(' '), t(x, TONE.warn)])
216
+ return out
217
+ }