agent-simple-english 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +435 -0
  5. package/THIRD_PARTY_NOTICES.md +13 -0
  6. package/commands/ste.md +13 -0
  7. package/hooks/hooks.json +58 -0
  8. package/package.json +64 -0
  9. package/src/adapter/commit-message.ts +472 -0
  10. package/src/adapter/feedback.ts +40 -0
  11. package/src/adapter/rule-summary.ts +79 -0
  12. package/src/cli/hook.ts +681 -0
  13. package/src/cli/main.ts +201 -0
  14. package/src/cli/session-command.ts +78 -0
  15. package/src/cli/session-state.ts +214 -0
  16. package/src/config/load.ts +85 -0
  17. package/src/config/merge.ts +20 -0
  18. package/src/config/schema.ts +69 -0
  19. package/src/dictionary/README.md +18 -0
  20. package/src/dictionary/data/pi-ste.json +200 -0
  21. package/src/dictionary/form.ts +6 -0
  22. package/src/dictionary/load.ts +54 -0
  23. package/src/dictionary/schema.ts +28 -0
  24. package/src/engine/comments.ts +386 -0
  25. package/src/engine/diff.ts +328 -0
  26. package/src/engine/identifiers.ts +20 -0
  27. package/src/engine/kinds.ts +43 -0
  28. package/src/engine/lint.ts +530 -0
  29. package/src/engine/markdown.ts +338 -0
  30. package/src/engine/paragraphs.ts +105 -0
  31. package/src/engine/rules/contraction.ts +19 -0
  32. package/src/engine/rules/dictionary.ts +281 -0
  33. package/src/engine/rules/hedging.ts +27 -0
  34. package/src/engine/rules/marketing.ts +71 -0
  35. package/src/engine/rules/paragraph-length.ts +23 -0
  36. package/src/engine/rules/phrasal-verb.ts +57 -0
  37. package/src/engine/rules/registry.ts +15 -0
  38. package/src/engine/rules/semicolon.ts +14 -0
  39. package/src/engine/rules/sentence-length.ts +24 -0
  40. package/src/engine/rules/verb-form.ts +76 -0
  41. package/src/engine/scan.ts +15 -0
  42. package/src/engine/sentences.ts +285 -0
  43. package/src/engine/tagger.ts +8 -0
  44. package/src/engine/tokens.ts +2 -0
  45. package/src/engine/types.ts +45 -0
  46. package/src/extension/index.ts +755 -0
  47. package/src/tagger/wink.ts +44 -0
@@ -0,0 +1,338 @@
1
+ const OPENING_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/
2
+ const CLOSING_FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/
3
+ const INDENTED = /^(?: {4,}|\t)/
4
+ const ATX_HEADING = /^ {0,3}#{1,6}(?:[ \t]+|$)/
5
+ const LIST_MARKER = /^( {0,3})(?:[-+*]|\d{1,9}[.)])(?:([ \t]{1,4})(?![ \t])|[ \t])/
6
+
7
+ const blankLine = (line: string): string => " ".repeat(line.length)
8
+
9
+ interface Container {
10
+ readonly quoteDepth: number
11
+ readonly listIndent: number
12
+ }
13
+
14
+ interface MarkdownContent {
15
+ readonly text: string
16
+ readonly start: number
17
+ readonly container: Container
18
+ }
19
+
20
+ const consumeBlockquotes = (
21
+ line: string,
22
+ initial: number,
23
+ requiredDepth?: number,
24
+ ): { index: number; depth: number } => {
25
+ let index = initial
26
+ let depth = 0
27
+ while (index < line.length && (requiredDepth === undefined || depth < requiredDepth)) {
28
+ let marker = index
29
+ let spaces = 0
30
+ while (spaces < 3 && line[marker] === " ") {
31
+ marker++
32
+ spaces++
33
+ }
34
+ if (line[marker] !== ">") break
35
+ depth++
36
+ index = marker + 1
37
+ if (line[index] === " " || line[index] === "\t") index++
38
+ }
39
+ return { index, depth }
40
+ }
41
+
42
+ const markdownContent = (line: string, contentStart: number): MarkdownContent => {
43
+ const base = Math.min(contentStart, line.length)
44
+ const blockquotes = consumeBlockquotes(line, base)
45
+ let index = blockquotes.index
46
+ let listIndent = 0
47
+
48
+ while (index < line.length) {
49
+ const match = line.slice(index).match(LIST_MARKER)
50
+ if (match === null) break
51
+ const width = match[0].length
52
+ listIndent += width
53
+ index += width
54
+ }
55
+
56
+ return {
57
+ text: line.slice(index),
58
+ start: index,
59
+ container: { quoteDepth: blockquotes.depth, listIndent },
60
+ }
61
+ }
62
+
63
+ const contentWithin = (
64
+ line: string,
65
+ contentStart: number,
66
+ container: Container,
67
+ ): MarkdownContent | null => {
68
+ const base = Math.min(contentStart, line.length)
69
+ if (container.quoteDepth === 0 && container.listIndent === 0) {
70
+ return { text: line.slice(base), start: base, container }
71
+ }
72
+
73
+ const blockquotes = consumeBlockquotes(line, base, container.quoteDepth)
74
+ if (blockquotes.depth !== container.quoteDepth) return null
75
+
76
+ let index = blockquotes.index
77
+ if (line.slice(index).trim() === "") {
78
+ return { text: "", start: line.length, container }
79
+ }
80
+ let remaining = container.listIndent
81
+ while (remaining > 0 && (line[index] === " " || line[index] === "\t")) {
82
+ index++
83
+ remaining--
84
+ }
85
+ if (remaining > 0) return null
86
+
87
+ return { text: line.slice(index), start: index, container }
88
+ }
89
+
90
+ const fenceLine = (line: string): string => (line.endsWith("\r") ? line.slice(0, -1) : line)
91
+
92
+ const openingFence = (line: string): string | null => {
93
+ const match = fenceLine(line).match(OPENING_FENCE)
94
+ if (match === null) return null
95
+ const marker = match[1] as string
96
+ const info = match[2] as string
97
+ return marker[0] === "`" && info.includes("`") ? null : marker
98
+ }
99
+
100
+ const closesFence = (line: string, fence: string): boolean => {
101
+ const marker = fenceLine(line).match(CLOSING_FENCE)?.[1]
102
+ return marker !== undefined && marker[0] === fence[0] && marker.length >= fence.length
103
+ }
104
+
105
+ interface BacktickRun {
106
+ readonly start: number
107
+ readonly end: number
108
+ readonly length: number
109
+ readonly escaped: boolean
110
+ }
111
+
112
+ const blankInlineCodeSpans = (text: string): string => {
113
+ const output = text.split("")
114
+ const runs: BacktickRun[] = []
115
+
116
+ for (let i = 0; i < text.length; ) {
117
+ if (text[i] !== "`") {
118
+ i++
119
+ continue
120
+ }
121
+ let end = i + 1
122
+ while (text[end] === "`") end++
123
+ let backslashes = 0
124
+ for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) backslashes++
125
+ runs.push({ start: i, end, length: end - i, escaped: backslashes % 2 !== 0 })
126
+ i = end
127
+ }
128
+
129
+ const nextMatchingRun = new Array<number | undefined>(runs.length)
130
+ const previousByLength = new Map<number, number>()
131
+ for (let i = 0; i < runs.length; i++) {
132
+ const run = runs[i] as BacktickRun
133
+ const previous = previousByLength.get(run.length)
134
+ if (previous !== undefined) nextMatchingRun[previous] = i
135
+ previousByLength.set(run.length, i)
136
+ }
137
+
138
+ for (let i = 0; i < runs.length; ) {
139
+ const opener = runs[i] as BacktickRun
140
+ const closerIndex = nextMatchingRun[i]
141
+ if (opener.escaped || closerIndex === undefined) {
142
+ i++
143
+ continue
144
+ }
145
+ const closer = runs[closerIndex] as BacktickRun
146
+ for (let j = opener.start; j < closer.end; j++) {
147
+ if (output[j] !== "\n") output[j] = " "
148
+ }
149
+ i = closerIndex + 1
150
+ }
151
+
152
+ return output.join("")
153
+ }
154
+
155
+ interface FenceState {
156
+ readonly marker: string
157
+ readonly container: Container
158
+ }
159
+
160
+ export interface MarkdownCodeResult {
161
+ readonly lines: string[]
162
+ readonly structuralLines: string[]
163
+ readonly structuralBlanks: boolean[]
164
+ }
165
+
166
+ export function blankMarkdownCodeWithStructure(
167
+ inputLines: readonly string[],
168
+ contentStarts: readonly number[] = inputLines.map(() => 0),
169
+ ): MarkdownCodeResult {
170
+ let fence: FenceState | null = null
171
+ let activeList: Container | null = null
172
+ let paragraphCanContinue = false
173
+ let inIndented = false
174
+ const inlineEligible: boolean[] = []
175
+ const structuralLines: string[] = []
176
+ const structuralBlanks: boolean[] = []
177
+ const lines = inputLines.map((line, index) => {
178
+ const contentStart = contentStarts[index] ?? 0
179
+
180
+ if (fence !== null) {
181
+ const contained = contentWithin(line, contentStart, fence.container)
182
+ if (contained !== null) {
183
+ if (closesFence(contained.text, fence.marker)) {
184
+ fence = null
185
+ paragraphCanContinue = false
186
+ inIndented = false
187
+ }
188
+ inlineEligible.push(false)
189
+ structuralLines.push(blankLine(line))
190
+ structuralBlanks.push(true)
191
+ return blankLine(line)
192
+ }
193
+ fence = null
194
+ paragraphCanContinue = false
195
+ inIndented = false
196
+ }
197
+
198
+ let content = markdownContent(line, contentStart)
199
+ if (content.container.listIndent > 0) {
200
+ activeList = content.container
201
+ } else if (content.text.trim() !== "" && activeList !== null) {
202
+ const continued = contentWithin(line, contentStart, activeList)
203
+ if (continued === null) {
204
+ activeList = null
205
+ } else {
206
+ content = continued
207
+ }
208
+ }
209
+
210
+ const visibleLine = `${" ".repeat(content.start)}${line.slice(content.start)}`
211
+ const marker = openingFence(content.text)
212
+ if (marker !== null) {
213
+ fence = { marker, container: content.container }
214
+ paragraphCanContinue = false
215
+ inIndented = false
216
+ inlineEligible.push(false)
217
+ structuralLines.push(blankLine(line))
218
+ structuralBlanks.push(true)
219
+ return blankLine(line)
220
+ }
221
+ if (content.text.trim() === "") {
222
+ paragraphCanContinue = false
223
+ inIndented = false
224
+ inlineEligible.push(false)
225
+ structuralLines.push(line)
226
+ structuralBlanks.push(true)
227
+ return visibleLine
228
+ }
229
+ if (INDENTED.test(content.text) && (!paragraphCanContinue || inIndented)) {
230
+ paragraphCanContinue = false
231
+ inIndented = true
232
+ inlineEligible.push(false)
233
+ structuralLines.push(blankLine(line))
234
+ structuralBlanks.push(true)
235
+ return blankLine(line)
236
+ }
237
+ paragraphCanContinue = !ATX_HEADING.test(content.text)
238
+ inIndented = false
239
+ inlineEligible.push(true)
240
+ structuralLines.push(line)
241
+ structuralBlanks.push(false)
242
+ return visibleLine
243
+ })
244
+
245
+ const blankEligibleInlineCode = (target: string[]) => {
246
+ let start = 0
247
+ while (start < target.length) {
248
+ if (!inlineEligible[start]) {
249
+ start++
250
+ continue
251
+ }
252
+ let end = start + 1
253
+ while (end < target.length && inlineEligible[end]) end++
254
+ const blanked = blankInlineCodeSpans(target.slice(start, end).join("\n")).split("\n")
255
+ for (let i = start; i < end; i++) target[i] = blanked[i - start] as string
256
+ start = end
257
+ }
258
+ }
259
+
260
+ blankEligibleInlineCode(lines)
261
+ blankEligibleInlineCode(structuralLines)
262
+
263
+ return { lines, structuralLines, structuralBlanks }
264
+ }
265
+
266
+ export function blankMarkdownCode(
267
+ inputLines: readonly string[],
268
+ contentStarts: readonly number[] = inputLines.map(() => 0),
269
+ ): string[] {
270
+ return blankMarkdownCodeWithStructure(inputLines, contentStarts).lines
271
+ }
272
+
273
+ export function maskMarkdownCode(text: string): string {
274
+ return blankMarkdownCode(text.split("\n")).join("\n")
275
+ }
276
+
277
+ interface InlineBacktickRun {
278
+ readonly start: number
279
+ readonly length: number
280
+ }
281
+
282
+ function blankInlineCodeLine(line: string): string {
283
+ const runs: InlineBacktickRun[] = []
284
+ for (let index = 0; index < line.length; index += 1) {
285
+ if (line[index] !== "`") continue
286
+ const start = index
287
+ while (line[index + 1] === "`") index += 1
288
+ runs.push({ start, length: index - start + 1 })
289
+ }
290
+
291
+ const nextMatchingRun = new Int32Array(runs.length).fill(-1)
292
+ const latestRunByLength = new Map<number, number>()
293
+ for (let index = runs.length - 1; index >= 0; index -= 1) {
294
+ const run = runs[index]
295
+ if (!run) continue
296
+ nextMatchingRun[index] = latestRunByLength.get(run.length) ?? -1
297
+ latestRunByLength.set(run.length, index)
298
+ }
299
+
300
+ const masked = line.split("")
301
+ for (let index = 0; index < runs.length; index += 1) {
302
+ const closingIndex = nextMatchingRun[index] ?? -1
303
+ if (closingIndex < 0) continue
304
+ const opening = runs[index]
305
+ const closing = runs[closingIndex]
306
+ if (!opening || !closing) continue
307
+
308
+ masked.fill(" ", opening.start, closing.start + closing.length)
309
+ index = closingIndex
310
+ }
311
+
312
+ return masked.join("")
313
+ }
314
+
315
+ export function blankInlineCode(lines: readonly string[]): string[] {
316
+ return lines.map(blankInlineCodeLine)
317
+ }
318
+
319
+ export function proseVisibility(text: string): Uint8Array {
320
+ const visibility = new Uint8Array(text.length)
321
+ const sourceLines = text.split("\n")
322
+ const proseLines = blankMarkdownCode(sourceLines)
323
+ let offset = 0
324
+
325
+ for (let index = 0; index < sourceLines.length; index++) {
326
+ const line = sourceLines[index] ?? ""
327
+ if (line === proseLines[index]) {
328
+ visibility.fill(1, offset, offset + line.length)
329
+ }
330
+ offset += line.length
331
+ if (offset < text.length) {
332
+ visibility[offset] = 1
333
+ offset++
334
+ }
335
+ }
336
+
337
+ return visibility
338
+ }
@@ -0,0 +1,105 @@
1
+ export interface Paragraph {
2
+ readonly lines: readonly string[]
3
+ readonly line: number
4
+ readonly column: number
5
+ }
6
+
7
+ const LIST_MARKER = /^(?:[-*+]|\d+[.)])\s+/
8
+
9
+ type LineKind = "blank" | "block-boundary" | "blockquote" | "list-item" | "prose"
10
+ type ParagraphKind = "blockquote" | "blockquote-list-item" | "list-item" | "prose"
11
+
12
+ const ATX_HEADING = /^ {0,3}#{1,6}(?:[ \t]+|$)/
13
+ const BLOCKQUOTE = /^ {0,3}>[ \t]?/
14
+
15
+ function listItemContent(line: string): string | undefined {
16
+ const trimmed = line.trimStart()
17
+ const marker = LIST_MARKER.exec(trimmed)
18
+ return marker ? trimmed.slice(marker[0].length) : undefined
19
+ }
20
+
21
+ function blockquoteContent(line: string): string | undefined {
22
+ const marker = BLOCKQUOTE.exec(line)
23
+ return marker ? line.slice(marker[0].length) : undefined
24
+ }
25
+
26
+ function classify(line: string): LineKind {
27
+ const trimmed = line.trim()
28
+ if (trimmed === "") return "blank"
29
+ if (trimmed.startsWith("|") || ATX_HEADING.test(line)) return "block-boundary"
30
+ if (blockquoteContent(line) !== undefined) return "blockquote"
31
+ if (listItemContent(line) !== undefined) return "list-item"
32
+ return "prose"
33
+ }
34
+
35
+ export function segmentParagraphs(
36
+ lines: readonly string[],
37
+ columns: readonly number[] = lines.map(() => 1),
38
+ ): Paragraph[] {
39
+ const paragraphs: Paragraph[] = []
40
+ let open: { line: number; column: number; lines: string[]; kind: ParagraphKind } | null = null
41
+
42
+ const close = () => {
43
+ if (open) {
44
+ paragraphs.push({ lines: open.lines, line: open.line, column: open.column })
45
+ open = null
46
+ }
47
+ }
48
+
49
+ lines.forEach((raw, index) => {
50
+ switch (classify(raw)) {
51
+ case "blank":
52
+ case "block-boundary":
53
+ close()
54
+ break
55
+ case "blockquote": {
56
+ const content = blockquoteContent(raw) ?? ""
57
+ const contentKind = classify(content)
58
+ if (contentKind === "blank" || contentKind === "block-boundary") {
59
+ close()
60
+ break
61
+ }
62
+ if (contentKind === "list-item") {
63
+ close()
64
+ open = {
65
+ line: index + 1,
66
+ column: columns[index] ?? 1,
67
+ lines: [listItemContent(content) ?? ""],
68
+ kind: "blockquote-list-item",
69
+ }
70
+ break
71
+ }
72
+ if (open?.kind !== "blockquote" && open?.kind !== "blockquote-list-item") close()
73
+ if (!open) {
74
+ open = { line: index + 1, column: columns[index] ?? 1, lines: [], kind: "blockquote" }
75
+ }
76
+ open.lines.push(open.kind === "blockquote-list-item" ? content.trimStart() : content)
77
+ break
78
+ }
79
+ case "list-item":
80
+ close()
81
+ open = {
82
+ line: index + 1,
83
+ column: columns[index] ?? 1,
84
+ lines: [listItemContent(raw) ?? ""],
85
+ kind: "list-item",
86
+ }
87
+ break
88
+ case "prose": {
89
+ if (!open) {
90
+ open = {
91
+ line: index + 1,
92
+ column: columns[index] ?? 1,
93
+ lines: [],
94
+ kind: "prose",
95
+ }
96
+ }
97
+ open.lines.push(open.kind === "list-item" ? raw.trimStart() : raw)
98
+ break
99
+ }
100
+ }
101
+ })
102
+ close()
103
+
104
+ return paragraphs
105
+ }
@@ -0,0 +1,19 @@
1
+ import { scanLines } from "../scan.ts"
2
+ import type { Violation } from "../types.ts"
3
+
4
+ const CONTRACTION = /\b\w+['’](?:t|re|ve|ll|d|m)\b/gi
5
+
6
+ // `'s` is the only ambiguous case: "repo's" is a possessive, not a
7
+ // contraction, so only this fixed set of stems forms a real `'s` contraction.
8
+ const CONTRACTION_S =
9
+ /\b(?:it|he|she|that|what|who|there|here|let|one|where|how|everyone|everybody|something|nothing|somebody|nobody)['’]s\b/gi
10
+
11
+ export function contraction(lines: readonly string[]): Violation[] {
12
+ return [...scanLines(lines, CONTRACTION), ...scanLines(lines, CONTRACTION_S)].map((match) => ({
13
+ ruleId: "contraction",
14
+ severity: "hard" as const,
15
+ message: `Do not use a contraction. Write the words in full. Found "${match.found}".`,
16
+ line: match.line,
17
+ column: match.column,
18
+ }))
19
+ }