@roughen/core 0.3.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/baselines.d.ts +55 -0
  4. package/dist/baselines.js +42 -0
  5. package/dist/baselines.js.map +1 -0
  6. package/dist/brief.d.ts +45 -0
  7. package/dist/brief.js +110 -0
  8. package/dist/brief.js.map +1 -0
  9. package/dist/document.d.ts +5 -0
  10. package/dist/document.js +531 -0
  11. package/dist/document.js.map +1 -0
  12. package/dist/editorial.d.ts +53 -0
  13. package/dist/editorial.js +252 -0
  14. package/dist/editorial.js.map +1 -0
  15. package/dist/fixes.d.ts +8 -0
  16. package/dist/fixes.js +53 -0
  17. package/dist/fixes.js.map +1 -0
  18. package/dist/index.d.ts +73 -0
  19. package/dist/index.js +232 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/markdown-links.d.ts +12 -0
  22. package/dist/markdown-links.js +130 -0
  23. package/dist/markdown-links.js.map +1 -0
  24. package/dist/preserve.d.ts +36 -0
  25. package/dist/preserve.js +83 -0
  26. package/dist/preserve.js.map +1 -0
  27. package/dist/protected-tokens.d.ts +7 -0
  28. package/dist/protected-tokens.js +103 -0
  29. package/dist/protected-tokens.js.map +1 -0
  30. package/dist/ranges.d.ts +13 -0
  31. package/dist/ranges.js +35 -0
  32. package/dist/ranges.js.map +1 -0
  33. package/dist/rules.d.ts +12 -0
  34. package/dist/rules.js +336 -0
  35. package/dist/rules.js.map +1 -0
  36. package/dist/sentences.d.ts +3 -0
  37. package/dist/sentences.js +95 -0
  38. package/dist/sentences.js.map +1 -0
  39. package/dist/types.d.ts +140 -0
  40. package/dist/types.js +2 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist-cjs/baselines.d.ts +55 -0
  43. package/dist-cjs/baselines.js +45 -0
  44. package/dist-cjs/baselines.js.map +1 -0
  45. package/dist-cjs/brief.d.ts +45 -0
  46. package/dist-cjs/brief.js +114 -0
  47. package/dist-cjs/brief.js.map +1 -0
  48. package/dist-cjs/document.d.ts +5 -0
  49. package/dist-cjs/document.js +535 -0
  50. package/dist-cjs/document.js.map +1 -0
  51. package/dist-cjs/editorial.d.ts +53 -0
  52. package/dist-cjs/editorial.js +263 -0
  53. package/dist-cjs/editorial.js.map +1 -0
  54. package/dist-cjs/fixes.d.ts +8 -0
  55. package/dist-cjs/fixes.js +57 -0
  56. package/dist-cjs/fixes.js.map +1 -0
  57. package/dist-cjs/index.d.ts +73 -0
  58. package/dist-cjs/index.js +258 -0
  59. package/dist-cjs/index.js.map +1 -0
  60. package/dist-cjs/markdown-links.d.ts +12 -0
  61. package/dist-cjs/markdown-links.js +134 -0
  62. package/dist-cjs/markdown-links.js.map +1 -0
  63. package/dist-cjs/package.json +1 -0
  64. package/dist-cjs/preserve.d.ts +36 -0
  65. package/dist-cjs/preserve.js +87 -0
  66. package/dist-cjs/preserve.js.map +1 -0
  67. package/dist-cjs/protected-tokens.d.ts +7 -0
  68. package/dist-cjs/protected-tokens.js +106 -0
  69. package/dist-cjs/protected-tokens.js.map +1 -0
  70. package/dist-cjs/ranges.d.ts +13 -0
  71. package/dist-cjs/ranges.js +41 -0
  72. package/dist-cjs/ranges.js.map +1 -0
  73. package/dist-cjs/rules.d.ts +12 -0
  74. package/dist-cjs/rules.js +340 -0
  75. package/dist-cjs/rules.js.map +1 -0
  76. package/dist-cjs/sentences.d.ts +3 -0
  77. package/dist-cjs/sentences.js +98 -0
  78. package/dist-cjs/sentences.js.map +1 -0
  79. package/dist-cjs/types.d.ts +140 -0
  80. package/dist-cjs/types.js +3 -0
  81. package/dist-cjs/types.js.map +1 -0
  82. package/package.json +45 -0
@@ -0,0 +1,252 @@
1
+ import { baselines } from './baselines.js';
2
+ /**
3
+ * Density detectors for structural habits. One instance is ordinary writing;
4
+ * the tell is repetition above what human documents do. Patterns run over the
5
+ * parser's masked prose view, so code, quotations and syntax are already blank.
6
+ * Single source of truth for both the rules below and the baseline research script.
7
+ */
8
+ const A = "['’]";
9
+ const family = {
10
+ contrast: new RegExp([
11
+ `,\\s+not\\s+(?:a|an|the|just|only|because|by|for|from|to|in|on|about|as|what|how|who|your|their|our|more|less|every|all)\\b`,
12
+ `\\b(?:isn${A}t|is not|aren${A}t|are not|wasn${A}t|doesn${A}t|don${A}t|won${A}t)\\b[^.!?\\n]{0,80}[.;]\\s+(?:it${A}s|they${A}re|that${A}s|it is|this is|you${A}re)\\b`,
13
+ `\\b(?:it${A}s|this is|that${A}s|it is)\\s+not\\s+(?:just|only|merely|simply)\\b`,
14
+ `\\bnot\\s+only\\b[^.!?\\n]{1,80}\\bbut\\b`,
15
+ ].join('|'), 'giu'),
16
+ reveal: new RegExp([
17
+ `(?<![\\p{L}\\p{N}])the\\s+(?:real|key|biggest|main|hard|simple|honest|short)\\s+(?:question|issue|problem|difference|mistake|reason|risk|thing|answer|lesson|test|tradeoff|trade-off|point)\\b`,
18
+ `\\bthe\\s+(?:difference|catch|reality|truth|trick|takeaway)\\s+is\\b`,
19
+ `\\bhere${A}s\\s+(?:the thing|the kicker|the catch|the truth|what (?:matters|that means|you need to know))\\b`,
20
+ `,\\s+which\\s+is\\s+why\\b`,
21
+ `\\bin\\s+other\\s+words\\b`,
22
+ `\\blet${A}s\\s+(?:dive|delve|unpack|break (?:it|this|that) down)\\b`,
23
+ ].join('|'), 'giu'),
24
+ generalizer: /\b(?:usually|often|typically|generally|in most cases|most of the time|tends? to|commonly|frequently)\b/giu,
25
+ emDash: /—/gu,
26
+ semicolon: /;/gu,
27
+ // A second item opening with a function word is a clause ("development, on its own budget and…"), not an item.
28
+ series: /(?<![\p{L}\p{N}])[\p{L}\p{N}$’'-]+(?:\s[\p{L}\p{N}$’'-]+){0,3},\s+(?!(?:on|in|at|for|to|with|by|from|of|as|so|but|and|or|nor|yet|because|when|while|if|though|although|which|who|whom|whose|that|this|these|those|it|its|we|you|they|he|she|i|there|then|than|not)\b)[\p{L}\p{N}$’'-]+(?:\s[\p{L}\p{N}$’'-]+){0,3},?\s+(?:and|or)\s+[\p{L}\p{N}$’'-]+/giu,
29
+ };
30
+ /**
31
+ * Editable body prose: paragraphs and list items. Headings are deliberate
32
+ * (question headings match search queries), so they're never measured.
33
+ * Single source of truth for what the density rules and their baselines count.
34
+ */
35
+ export function bodyBlocks(document) {
36
+ return document.blocks.filter((block) => !block.skip && (block.kind === 'paragraph' || block.kind === 'listItem'));
37
+ }
38
+ /** Words are the body sentences' word counts, the same measure the metrics report. */
39
+ export function densityProfile(document, sentences) {
40
+ const instances = { series: [], contrast: [], generalizer: [], reveal: [], emDash: [], semicolon: [] };
41
+ for (const block of bodyBlocks(document)) {
42
+ for (const name of Object.keys(family)) {
43
+ // A bulleted item is already a structured inventory: where the series rule says lists belong.
44
+ if (name === 'series' && block.kind === 'listItem')
45
+ continue;
46
+ for (const match of block.text.matchAll(family[name])) {
47
+ const start = block.sourceRange[0] + match.index;
48
+ instances[name].push([start, start + match[0].length]);
49
+ }
50
+ }
51
+ }
52
+ return { words: sentences.reduce((sum, sentence) => sum + sentence.wordCount, 0), instances };
53
+ }
54
+ const describe = {
55
+ contrast: {
56
+ id: 'structure/contrast-frame', severity: 'warn', label: 'contrast frames ("X, not Y", "isn\'t X. It\'s Y")',
57
+ why: 'Setting every point against a straw alternative makes each sentence sound like a correction. Repeated, it reads as a formula rather than an argument.',
58
+ advice: 'State the point directly. Keep a contrast only where the reader genuinely holds the mistaken view.',
59
+ },
60
+ reveal: {
61
+ id: 'structure/reveal-frame', severity: 'info', label: 'reveal frames ("The real question is…", ", which is why")',
62
+ why: 'Announcing that the key point is coming delays the point. Repeated, the announcements become the voice.',
63
+ advice: 'Delete the announcement and lead with the claim itself.',
64
+ },
65
+ generalizer: {
66
+ id: 'hedge/generalizer', severity: 'warn', label: 'generalizing hedges (usually, often, typically)',
67
+ why: 'Qualifying most claims with "usually" or "typically" makes the writing sound unsure of its own experience and hides the specifics readers came for.',
68
+ advice: 'Replace the hedge with the actual condition, number or example. Keep it only where the exception matters.',
69
+ },
70
+ emDash: {
71
+ id: 'punctuation/em-dash', severity: 'warn', label: 'em dashes',
72
+ why: "Em dashes are a good tool, and plenty of human writers use them. When they carry most of a piece's asides and pivots, though, they become its rhythm, and readers notice the tic.",
73
+ advice: 'Keep the ones that mark a real interruption. Turn the rest into commas, parentheses, a colon, or two sentences.',
74
+ },
75
+ semicolon: {
76
+ id: 'punctuation/semicolon', severity: 'warn', label: 'semicolons',
77
+ why: 'Semicolons join clauses into long, formal sentences. In copy read on a screen they slow the reader down and sound academic.',
78
+ advice: 'Split the clauses into two sentences, or join them with a comma and "and", "but" or "so".',
79
+ },
80
+ series: {
81
+ id: 'structure/list-saturation', severity: 'warn', label: 'comma series ("A, B, and C")',
82
+ why: 'When most sentences enumerate three or more items, prose turns into inventory. Readers skim lists and remember none of the items.',
83
+ advice: 'Pick the one or two items that matter for this sentence and say something specific about them. Move genuine inventories into a bulleted list.',
84
+ },
85
+ };
86
+ /** Rule id for each density family, for callers that measure instances with profileText. */
87
+ export const densityRuleIds = Object.fromEntries(Object.entries(describe).map(([name, meta]) => [name, meta.id]));
88
+ /** Shorter bodies are too small to measure a rate. */
89
+ export const densityMinimumWords = 300;
90
+ /** One or two instances are ordinary writing whatever the rate; repetition is the tell. */
91
+ export const densityMinimumInstances = 3;
92
+ /**
93
+ * The human baseline a density family is measured against in a register:
94
+ * the register's own threshold where one was fit, otherwise the article one.
95
+ */
96
+ export function densityBaseline(name, register = 'article') {
97
+ if (register !== 'article') {
98
+ const fitted = baselines.registers[register];
99
+ const override = fitted.density[name];
100
+ if (override)
101
+ return { p95: override.p95, documents: fitted.corpus.measured, register };
102
+ }
103
+ return { p95: baselines.density[name].p95, documents: baselines.corpus.documents, register: 'article' };
104
+ }
105
+ /**
106
+ * Single source of truth for when a density rule fires, used by the rules and
107
+ * by the baseline report so the published exceedance matches what ships.
108
+ */
109
+ export function densityVerdict(profile, name, register = 'article') {
110
+ const count = profile.instances[name].length;
111
+ const per1k = profile.words ? (count / profile.words) * 1000 : 0;
112
+ const threshold = densityBaseline(name, register).p95;
113
+ const fires = profile.words >= densityMinimumWords && count >= densityMinimumInstances && per1k > threshold;
114
+ // Rewriting down to the baseline rate, but never below the instance floor, clears the finding.
115
+ const allowed = Math.max(densityMinimumInstances - 1, Math.floor((threshold * profile.words) / 1000));
116
+ return { count, per1k, threshold, fires, rewriteAtLeast: Math.max(0, count - allowed) };
117
+ }
118
+ // A banned character is reported at every occurrence, so its density rule would only repeat the ban.
119
+ const familyCharacter = { emDash: '—', semicolon: ';' };
120
+ /** Characters the project bans outright, normalized from the voice config. Single source of truth for the ban rule and the density rules it supersedes. */
121
+ export function bannedCharacters(config) {
122
+ return (config.voice?.bannedCharacters ?? []).map((entry) => typeof entry === 'string' ? { character: entry } : entry).filter((entry) => entry.character.length > 0);
123
+ }
124
+ function densityRule(name) {
125
+ const meta = describe[name];
126
+ return {
127
+ id: meta.id,
128
+ severity: meta.severity,
129
+ description: `Flag ${meta.label} above the human-writing baseline.`,
130
+ check({ profile, config, register }) {
131
+ const character = familyCharacter[name];
132
+ if (character && bannedCharacters(config).some((entry) => entry.character === character))
133
+ return [];
134
+ const verdict = densityVerdict(profile, name, register);
135
+ if (!verdict.fires)
136
+ return [];
137
+ const found = profile.instances[name];
138
+ const per1k = verdict.per1k;
139
+ const baseline = densityBaseline(name, register);
140
+ const population = baseline.register === 'marketing' ? `human marketing pages in Roughen's marketing baseline` : `human documents in Roughen's baseline`;
141
+ return found.map((range) => ({
142
+ range,
143
+ message: `One of ${found.length} ${meta.label} (${per1k.toFixed(1)} per 1,000 words).`,
144
+ why: `${meta.why} ${baseline.p95 ? `${population[0].toUpperCase()}${population.slice(1)} stay at or below ${baseline.p95.toFixed(1)} per 1,000 words` : `Most ${population} use none`} (95th percentile, ${baseline.documents} ${baseline.register === 'marketing' ? 'pages' : 'documents'}). ${meta.advice}`,
145
+ fixSafety: 'manual',
146
+ confidence: baseline.p95 ? Math.min(1, 0.5 + (per1k - baseline.p95) / (2 * baseline.p95)) : 0.75,
147
+ evidence: { count: found.length, per1k: Number(per1k.toFixed(2)), baselineP95: baseline.p95, rewriteAtLeast: verdict.rewriteAtLeast, words: profile.words, ...(baseline.register === 'article' ? {} : { register: baseline.register }) },
148
+ }));
149
+ },
150
+ };
151
+ }
152
+ /** Longest run of consecutive sentences within ±25% of the run's mean: the metronome. */
153
+ function uniformRun(lengths) {
154
+ let best = [0, 0];
155
+ for (let i = 0; i < lengths.length; i++) {
156
+ if (lengths.length - 1 - i <= best[1] - best[0])
157
+ break; // no longer run can start here
158
+ let lo = lengths[i];
159
+ let hi = lengths[i];
160
+ for (let j = i + 1; j < lengths.length; j++) {
161
+ lo = Math.min(lo, lengths[j]);
162
+ hi = Math.max(hi, lengths[j]);
163
+ const mean = (lo + hi) / 2;
164
+ if (hi - mean > 0.25 * mean)
165
+ break;
166
+ if (j - i > best[1] - best[0])
167
+ best = [i, j];
168
+ }
169
+ }
170
+ return best;
171
+ }
172
+ const lowVariance = {
173
+ id: 'rhythm/low-variance',
174
+ severity: 'info',
175
+ description: 'Flag sentence lengths that vary less than human writing does.',
176
+ check({ sentences }) {
177
+ const lengths = sentences.map((sentence) => sentence.wordCount);
178
+ if (lengths.length < 8)
179
+ return [];
180
+ const mean = lengths.reduce((sum, value) => sum + value, 0) / lengths.length;
181
+ const cv = Math.sqrt(lengths.reduce((sum, value) => sum + (value - mean) ** 2, 0) / lengths.length) / mean;
182
+ const threshold = baselines.rhythm.cvP5;
183
+ if (cv >= threshold)
184
+ return [];
185
+ const [first, last] = uniformRun(lengths);
186
+ const range = [sentences[first].range[0], sentences[last].range[1]];
187
+ return [{
188
+ range,
189
+ message: `Sentence lengths barely vary (coefficient of variation ${cv.toFixed(2)}).`,
190
+ why: `Sentences ${first + 1} through ${last + 1} run ${Math.min(...lengths.slice(first, last + 1))} to ${Math.max(...lengths.slice(first, last + 1))} words each. Human documents in Roughen's baseline vary more than ${threshold.toFixed(2)} in 95% of cases, and the evenness reads as a metronome. Combine two short related sentences, or break a long one where the thought turns.`,
191
+ fixSafety: 'manual', confidence: Math.min(1, 0.5 + (threshold - cv) / threshold),
192
+ evidence: { cv: Number(cv.toFixed(3)), baselineP5: threshold, sentences: lengths.length, meanWords: Number(mean.toFixed(1)) },
193
+ }];
194
+ },
195
+ };
196
+ /** Sentences longer than 90% of human sentences, the ones a split helps most. */
197
+ export function longSentences(sentences) {
198
+ return sentences.filter((sentence) => sentence.wordCount > baselines.sentences.longWords);
199
+ }
200
+ /** Single source of truth for when the long-sentence rule fires, shared with the baseline report. */
201
+ export function longSentenceVerdict(words, sentences) {
202
+ const { meanWordsP95 } = baselines.sentences;
203
+ const long = longSentences(sentences);
204
+ const mean = sentences.length ? words / sentences.length : 0;
205
+ const fires = words >= densityMinimumWords && sentences.length >= 8 && mean > meanWordsP95 && long.length >= densityMinimumInstances;
206
+ // Splitting a sentence adds one; this many splits brings the mean back to the baseline.
207
+ const rewriteAtLeast = Math.min(long.length, Math.max(1, Math.ceil(words / meanWordsP95 - sentences.length)));
208
+ return { mean, long, fires, rewriteAtLeast };
209
+ }
210
+ const longSentenceRule = {
211
+ id: 'rhythm/long-sentences',
212
+ severity: 'warn',
213
+ description: 'Flag documents whose sentences run longer than human writing does.',
214
+ check({ sentences, profile }) {
215
+ const { mean, long, fires, rewriteAtLeast } = longSentenceVerdict(profile.words, sentences);
216
+ if (!fires)
217
+ return [];
218
+ const { meanWordsP95, longWords } = baselines.sentences;
219
+ return long.map((sentence) => ({
220
+ range: sentence.range,
221
+ message: `One of ${long.length} long sentences (${sentence.wordCount} words; this document averages ${mean.toFixed(1)}).`,
222
+ why: `Long sentences make readers hold several ideas at once. Human documents in Roughen's baseline average ${meanWordsP95.toFixed(1)} words a sentence or fewer (95th percentile, ${baselines.corpus.documents} documents), and only 1 human sentence in 10 runs past ${longWords} words. Split this one where the thought turns.`,
223
+ fixSafety: 'manual', confidence: Math.min(1, 0.5 + (mean - meanWordsP95) / meanWordsP95),
224
+ evidence: { count: long.length, meanWords: Number(mean.toFixed(1)), baselineMeanP95: meanWordsP95, longWords, words: sentence.wordCount, rewriteAtLeast },
225
+ }));
226
+ },
227
+ };
228
+ /**
229
+ * Instances of the given rules' habits in a text, whether or not they fire.
230
+ * Single source of truth for "did a rewrite reduce what was flagged", used by
231
+ * Signal's revision gate and anything else that compares a before and after.
232
+ */
233
+ export function flaggedInstances(profile, sentences, ruleIds) {
234
+ let total = 0;
235
+ for (const [name, meta] of Object.entries(describe))
236
+ if (ruleIds.includes(meta.id))
237
+ total += profile.instances[name].length;
238
+ if (ruleIds.includes('rhythm/long-sentences'))
239
+ total += longSentences(sentences).length;
240
+ return total;
241
+ }
242
+ export const editorialRules = [
243
+ densityRule('series'),
244
+ densityRule('contrast'),
245
+ densityRule('generalizer'),
246
+ densityRule('reveal'),
247
+ densityRule('emDash'),
248
+ densityRule('semicolon'),
249
+ longSentenceRule,
250
+ lowVariance,
251
+ ];
252
+ //# sourceMappingURL=editorial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorial.js","sourceRoot":"","sources":["../src/editorial.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,GAAG,MAAM,CAAC;AACjB,MAAM,MAAM,GAAkC;IAC5C,QAAQ,EAAE,IAAI,MAAM,CAAC;QACnB,6HAA6H;QAC7H,YAAY,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,oCAAoC,CAAC,SAAS,CAAC,UAAU,CAAC,sBAAsB,CAAC,QAAQ;QACtK,WAAW,CAAC,iBAAiB,CAAC,mDAAmD;QACjF,2CAA2C;KAC5C,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;IACnB,MAAM,EAAE,IAAI,MAAM,CAAC;QACjB,gMAAgM;QAChM,sEAAsE;QACtE,UAAU,CAAC,mGAAmG;QAC9G,4BAA4B;QAC5B,4BAA4B;QAC5B,SAAS,CAAC,2DAA2D;KACtE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;IACnB,WAAW,EAAE,2GAA2G;IACxH,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,KAAK;IAChB,+GAA+G;IAC/G,MAAM,EAAE,0VAA0V;CACnW,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,QAAkB;IAC3C,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC;AACrH,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAE,SAA8B;IAC/E,MAAM,SAAS,GAAmC,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACvI,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAoB,EAAE,CAAC;YAC1D,8FAA8F;YAC9F,IAAI,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;gBAAE,SAAS;YAC7D,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBACtD,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAM,CAAC;gBAClD,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,GAAG,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC;AAChG,CAAC;AAED,MAAM,QAAQ,GAAiH;IAC7H,QAAQ,EAAE;QACR,EAAE,EAAE,0BAA0B,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,mDAAmD;QAC5G,GAAG,EAAE,uJAAuJ;QAC5J,MAAM,EAAE,oGAAoG;KAC7G;IACD,MAAM,EAAE;QACN,EAAE,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,2DAA2D;QAClH,GAAG,EAAE,yGAAyG;QAC9G,MAAM,EAAE,yDAAyD;KAClE;IACD,WAAW,EAAE;QACX,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,iDAAiD;QACnG,GAAG,EAAE,qJAAqJ;QAC1J,MAAM,EAAE,2GAA2G;KACpH;IACD,MAAM,EAAE;QACN,EAAE,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW;QAC/D,GAAG,EAAE,mLAAmL;QACxL,MAAM,EAAE,iHAAiH;KAC1H;IACD,SAAS,EAAE;QACT,EAAE,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY;QAClE,GAAG,EAAE,6HAA6H;QAClI,MAAM,EAAE,2FAA2F;KACpG;IACD,MAAM,EAAE;QACN,EAAE,EAAE,2BAA2B,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,8BAA8B;QACxF,GAAG,EAAE,mIAAmI;QACxI,MAAM,EAAE,+IAA+I;KACxJ;CACF,CAAC;AAEF,4FAA4F;AAC5F,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAkC,CAAC;AAEnJ,sDAAsD;AACtD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,2FAA2F;AAC3F,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB,EAAE,WAAqB,SAAS;IACjF,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAI,MAAM,CAAC,OAA2D,CAAC,IAAI,CAAC,CAAC;QAC3F,IAAI,QAAQ;YAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC;IAC1F,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAC1G,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAuB,EAAE,IAAmB,EAAE,WAAqB,SAAS;IACzG,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC;IACtD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,mBAAmB,IAAI,KAAK,IAAI,uBAAuB,IAAI,KAAK,GAAG,SAAS,CAAC;IAC5G,+FAA+F;IAC/F,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,uBAAuB,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACtG,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,qGAAqG;AACrG,MAAM,eAAe,GAA2C,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAEhG,2JAA2J;AAC3J,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACvK,CAAC;AAED,SAAS,WAAW,CAAC,IAAmB;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,QAAQ,IAAI,CAAC,KAAK,oCAAoC;QACnE,KAAK,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE;YACjC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,SAAS,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC;gBAAE,OAAO,EAAE,CAAC;YACpG,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YACxD,IAAI,CAAC,OAAO,CAAC,KAAK;gBAAE,OAAO,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;YAC5B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACjD,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,uDAAuD,CAAC,CAAC,CAAC,uCAAuC,CAAC;YACzJ,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAe,EAAE,CAAC,CAAC;gBACxC,KAAK;gBACL,OAAO,EAAE,UAAU,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB;gBACtF,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,QAAQ,UAAU,WAAW,sBAAsB,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,MAAM,IAAI,CAAC,MAAM,EAAE;gBAC9S,SAAS,EAAE,QAAQ;gBACnB,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;gBAChG,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE;aACzO,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,SAAS,UAAU,CAAC,OAAiB;IACnC,IAAI,IAAI,GAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,+BAA+B;QACvF,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QAAC,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC;YAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC;YAC/D,MAAM,IAAI,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI;gBAAE,MAAM;YACnC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;gBAAE,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,WAAW,GAAS;IACxB,EAAE,EAAE,qBAAqB;IACzB,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,+DAA+D;IAC5E,KAAK,CAAC,EAAE,SAAS,EAAE;QACjB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAChE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7E,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QAC3G,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;QACxC,IAAI,EAAE,IAAI,SAAS;YAAE,OAAO,EAAE,CAAC;QAC/B,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAU,CAAC,SAAS,CAAC,KAAK,CAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,IAAI,CAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7E,OAAO,CAAC;gBACN,KAAK;gBACL,OAAO,EAAE,0DAA0D,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;gBACpF,GAAG,EAAE,aAAa,KAAK,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,QAAQ,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,qEAAqE,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,4IAA4I;gBACzX,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,SAAS,GAAG,EAAE,CAAC,GAAG,SAAS,CAAC;gBAChF,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;aAC9H,CAAC,CAAC;IACL,CAAC;CACF,CAAC;AAEF,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,SAA8B;IAC1D,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AAC5F,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,SAA8B;IAC/E,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC,SAAS,CAAC;IAC7C,MAAM,IAAI,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,KAAK,IAAI,mBAAmB,IAAI,SAAS,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,GAAG,YAAY,IAAI,IAAI,CAAC,MAAM,IAAI,uBAAuB,CAAC;IACrI,wFAAwF;IACxF,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,YAAY,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC9G,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,gBAAgB,GAAS;IAC7B,EAAE,EAAE,uBAAuB;IAC3B,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,oEAAoE;IACjF,KAAK,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE;QAC1B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAC5F,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,SAAS,CAAC,SAAS,CAAC;QACxD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAe,EAAE,CAAC,CAAC;YAC1C,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,OAAO,EAAE,UAAU,IAAI,CAAC,MAAM,oBAAoB,QAAQ,CAAC,SAAS,kCAAkC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;YACzH,GAAG,EAAE,yGAAyG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,gDAAgD,SAAS,CAAC,MAAM,CAAC,SAAS,0DAA0D,SAAS,iDAAiD;YACnU,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,IAAI,GAAG,YAAY,CAAC,GAAG,YAAY,CAAC;YACxF,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE;SAC1J,CAAC,CAAC,CAAC;IACN,CAAC;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAuB,EAAE,SAA8B,EAAE,OAA0B;IAClH,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAsC;QAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,KAAK,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IACjK,IAAI,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAC;QAAE,KAAK,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAW;IACpC,WAAW,CAAC,QAAQ,CAAC;IACrB,WAAW,CAAC,UAAU,CAAC;IACvB,WAAW,CAAC,aAAa,CAAC;IAC1B,WAAW,CAAC,QAAQ,CAAC;IACrB,WAAW,CAAC,QAAQ,CAAC;IACrB,WAAW,CAAC,WAAW,CAAC;IACxB,gBAAgB;IAChB,WAAW;CACZ,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Finding } from './types.js';
2
+ export interface FixOptions {
3
+ careful?: boolean;
4
+ }
5
+ /** Single source of truth for fix safety and overlap selection on every surface. */
6
+ export declare function selectFixes(source: string, findings: readonly Finding[], options?: FixOptions): Finding[];
7
+ /** Splices original-source ranges in one pass; never rewrites a parsed document. */
8
+ export declare function applyFixes(source: string, findings: readonly Finding[], options?: FixOptions): string;
package/dist/fixes.js ADDED
@@ -0,0 +1,53 @@
1
+ import { rules } from './rules.js';
2
+ import { firstEndingAfter, fixRangesConflict } from './ranges.js';
3
+ const fixRange = (finding) => finding.fix.range;
4
+ function validRange(range, length) {
5
+ return Number.isInteger(range[0]) && Number.isInteger(range[1]) &&
6
+ range[0] >= 0 && range[0] <= range[1] && range[1] <= length;
7
+ }
8
+ /** Single source of truth for fix safety and overlap selection on every surface. */
9
+ export function selectFixes(source, findings, options = {}) {
10
+ const rulePriority = new Map(rules.map((rule, index) => [rule.id, index]));
11
+ const candidates = findings.map((finding, index) => ({ finding, priority: rulePriority.get(finding.ruleId) ?? rules.length + index })).filter(({ finding }) => {
12
+ if (!finding.fix || finding.fixSafety === 'manual')
13
+ return false;
14
+ if (finding.fixSafety === 'careful' && !options.careful)
15
+ return false;
16
+ if (!validRange(finding.fix.range, source.length))
17
+ throw new RangeError(`Invalid fix range for ${finding.ruleId}`);
18
+ if (!Number.isFinite(finding.confidence) || finding.confidence < 0 || finding.confidence > 1) {
19
+ throw new RangeError(`Invalid fix confidence for ${finding.ruleId}`);
20
+ }
21
+ return source.slice(...finding.fix.range) !== finding.fix.text;
22
+ });
23
+ candidates.sort((a, b) => b.finding.confidence - a.finding.confidence || a.priority - b.priority);
24
+ // Selections never conflict, so ordered by start they are ordered by end too:
25
+ // a candidate can only conflict with the selections around its own position.
26
+ const selected = [];
27
+ for (const { finding } of candidates) {
28
+ const range = fixRange(finding);
29
+ let index = firstEndingAfter(selected, range[0] - 1, fixRange);
30
+ let conflict = false;
31
+ for (let i = index; !conflict && i < selected.length && fixRange(selected[i])[0] <= range[1]; i++) {
32
+ conflict = fixRangesConflict(fixRange(selected[i]), range);
33
+ }
34
+ if (conflict)
35
+ continue;
36
+ while (index < selected.length && fixRange(selected[index])[0] < range[0])
37
+ index++;
38
+ selected.splice(index, 0, finding);
39
+ }
40
+ return selected;
41
+ }
42
+ /** Splices original-source ranges in one pass; never rewrites a parsed document. */
43
+ export function applyFixes(source, findings, options = {}) {
44
+ const parts = [];
45
+ let last = 0;
46
+ for (const { fix } of selectFixes(source, findings, options)) {
47
+ parts.push(source.slice(last, fix.range[0]), fix.text);
48
+ last = fix.range[1];
49
+ }
50
+ parts.push(source.slice(last));
51
+ return parts.join('');
52
+ }
53
+ //# sourceMappingURL=fixes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixes.js","sourceRoot":"","sources":["../src/fixes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { parseDocument } from './document.js';
2
+ import { segmentSentences } from './sentences.js';
3
+ import { applyFixes } from './fixes.js';
4
+ export { bodyBlocks, densityBaseline, densityMinimumWords, densityMinimumInstances, densityVerdict, densityRuleIds, longSentenceVerdict } from './editorial.js';
5
+ import type { Config, Finding, Metrics, Register, Result, Sentence, Severity } from './types.js';
6
+ export { parseDocument, segmentSentences, applyFixes };
7
+ export { collectMarkdownLinkTargets } from './document.js';
8
+ export { revisionBrief, scopeFindings } from './brief.js';
9
+ export type { BriefItem, BriefOptions, RevisionBrief } from './brief.js';
10
+ export { comparePreservation, revisionLengthTolerance } from './preserve.js';
11
+ import { type PreservationReport } from './preserve.js';
12
+ export type { PreservationOptions, PreservationReport } from './preserve.js';
13
+ export { baselines } from './baselines.js';
14
+ export type { FixOptions } from './fixes.js';
15
+ export type * from './types.js';
16
+ export declare const version = "0.3.0";
17
+ /** Rule descriptions only. Consumers cannot mutate the engine's registry. */
18
+ export declare function listRules(): {
19
+ id: string;
20
+ severity: Severity;
21
+ description: string;
22
+ }[];
23
+ /** Profile of a text as Roughen measures it: body words and density-rule instances. For baselines and reports. */
24
+ export declare function profileText(source: string, format?: Config['format']): {
25
+ sentences: Sentence[];
26
+ metrics: Metrics;
27
+ words: number;
28
+ instances: Record<import("./types.js").DensityFamily, import("./types.js").Range[]>;
29
+ };
30
+ /** Occurrences of what the project bans outright (voice.bannedCharacters, voice.bannedPatterns). */
31
+ export declare function countBanned(source: string, voice: Config['voice'], format?: Config['format']): number;
32
+ /**
33
+ * How many instances of the given rules' habits `source` contains, whether or
34
+ * not the rules fire. Banned characters and patterns count when `voice` names them.
35
+ */
36
+ export declare function countFlagged(source: string, ruleIds: readonly string[], format?: Config['format'], voice?: Config['voice']): number;
37
+ /** Runs locally and deterministically. No network, model, locale, clock, or randomness. */
38
+ export declare function lint(source: string, config?: Config | null): Result;
39
+ export interface JudgeOptions {
40
+ format?: Config['format'];
41
+ /** Rules whose instances must decrease. Default: the rules that asked for a rewrite of the original. */
42
+ rules?: readonly string[];
43
+ /** The baseline those default rules are judged against. */
44
+ register?: Register;
45
+ /** Banned characters and patterns: a revision may not add any. */
46
+ voice?: Config['voice'];
47
+ /** Allowed body-length change, as a fraction. Default revisionLengthTolerance. */
48
+ lengthTolerance?: number;
49
+ }
50
+ export interface RevisionJudgment {
51
+ /** Use the revision: it kept every fact and reduced the flagged habits. */
52
+ accepted: boolean;
53
+ rules: string[];
54
+ flaggedBefore: number;
55
+ flaggedAfter: number;
56
+ /** Banned characters and patterns in each version (zero when the voice bans nothing). */
57
+ bannedBefore: number;
58
+ bannedAfter: number;
59
+ reasons: string[];
60
+ preservation: PreservationReport;
61
+ }
62
+ /** A finding that asks for a rewrite: manual, warn or error, with a rewrite target. Info findings stay in the review. */
63
+ export declare function isRevisable(finding: Finding): boolean;
64
+ /** Rules whose findings ask for a rewrite. */
65
+ export declare function revisableRules(result: Pick<Result, 'findings'>): string[];
66
+ /**
67
+ * Single source of truth for whether a rewrite should replace the original,
68
+ * used by Signal's revision gate and the MCP server. The revision must keep
69
+ * every figure, link, code sample, quotation and heading, add none, stay in
70
+ * length bounds, and reduce the flagged habits. `rules` defaults to the rules
71
+ * that asked for a rewrite of the original.
72
+ */
73
+ export declare function judgeRevision(original: string, revised: string, options?: JudgeOptions): RevisionJudgment;