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,328 @@
1
+ export interface ChangedRange {
2
+ readonly start: number
3
+ readonly end: number
4
+ }
5
+
6
+ export interface Deletion {
7
+ readonly previousStart: number
8
+ readonly previousEnd: number
9
+ readonly currentOffset: number
10
+ }
11
+
12
+ export interface RetainedRange {
13
+ readonly previousStart: number
14
+ readonly currentStart: number
15
+ readonly length: number
16
+ }
17
+
18
+ export interface TextChanges {
19
+ readonly ranges: readonly ChangedRange[]
20
+ readonly deletions: readonly Deletion[]
21
+ readonly retained: readonly RetainedRange[]
22
+ }
23
+
24
+ const DIFF_CELL_LIMIT = 1_000_000
25
+
26
+ interface DiffBudget {
27
+ remaining: number
28
+ }
29
+
30
+ function reserveCells(budget: DiffBudget, previousLength: number, currentLength: number): boolean {
31
+ const cells = previousLength * currentLength
32
+ if (cells > budget.remaining) return false
33
+ budget.remaining -= cells
34
+ return true
35
+ }
36
+
37
+ function lineTokens(text: string): string[] {
38
+ const tokens: string[] = []
39
+ let start = 0
40
+ for (let index = 0; index < text.length; index++) {
41
+ if (text[index] === "\n") {
42
+ tokens.push(text.slice(start, index + 1))
43
+ start = index + 1
44
+ }
45
+ }
46
+ if (start < text.length) {
47
+ tokens.push(text.slice(start))
48
+ }
49
+ return tokens
50
+ }
51
+
52
+ function addRange(ranges: ChangedRange[], start: number, end: number): void {
53
+ if (start === end) return
54
+ const last = ranges.at(-1)
55
+ if (last && start <= last.end) {
56
+ ranges[ranges.length - 1] = { start: last.start, end: Math.max(last.end, end) }
57
+ return
58
+ }
59
+ ranges.push({ start, end })
60
+ }
61
+
62
+ function addRetained(
63
+ retained: RetainedRange[],
64
+ previousStart: number,
65
+ currentStart: number,
66
+ length: number,
67
+ ): void {
68
+ if (length === 0) return
69
+ const last = retained.at(-1)
70
+ if (
71
+ last &&
72
+ last.previousStart + last.length === previousStart &&
73
+ last.currentStart + last.length === currentStart
74
+ ) {
75
+ retained[retained.length - 1] = { ...last, length: last.length + length }
76
+ return
77
+ }
78
+ retained.push({ previousStart, currentStart, length })
79
+ }
80
+
81
+ function diffCharacters(
82
+ previous: string,
83
+ current: string,
84
+ previousOffset: number,
85
+ currentOffset: number,
86
+ ranges: ChangedRange[],
87
+ deletions: Deletion[],
88
+ retained: RetainedRange[],
89
+ budget: DiffBudget,
90
+ ): void {
91
+ let prefix = 0
92
+ while (
93
+ prefix < previous.length &&
94
+ prefix < current.length &&
95
+ previous[prefix] === current[prefix]
96
+ ) {
97
+ prefix++
98
+ }
99
+
100
+ let previousEnd = previous.length
101
+ let currentEnd = current.length
102
+ while (
103
+ previousEnd > prefix &&
104
+ currentEnd > prefix &&
105
+ previous[previousEnd - 1] === current[currentEnd - 1]
106
+ ) {
107
+ previousEnd--
108
+ currentEnd--
109
+ }
110
+
111
+ const oldText = previous.slice(prefix, previousEnd)
112
+ const newText = current.slice(prefix, currentEnd)
113
+ const oldBase = previousOffset + prefix
114
+ const newBase = currentOffset + prefix
115
+ const suffixLength = previous.length - previousEnd
116
+ const retainEdges = () => {
117
+ addRetained(retained, previousOffset, currentOffset, prefix)
118
+ addRetained(retained, previousOffset + previousEnd, currentOffset + currentEnd, suffixLength)
119
+ }
120
+
121
+ if (oldText.length === 0) {
122
+ addRetained(retained, previousOffset, currentOffset, prefix)
123
+ addRange(ranges, newBase, newBase + newText.length)
124
+ addRetained(retained, previousOffset + previousEnd, currentOffset + currentEnd, suffixLength)
125
+ return
126
+ }
127
+ if (newText.length === 0) {
128
+ addRetained(retained, previousOffset, currentOffset, prefix)
129
+ deletions.push({
130
+ previousStart: oldBase,
131
+ previousEnd: oldBase + oldText.length,
132
+ currentOffset: newBase,
133
+ })
134
+ addRetained(retained, previousOffset + previousEnd, currentOffset + currentEnd, suffixLength)
135
+ return
136
+ }
137
+ if (!reserveCells(budget, oldText.length, newText.length)) {
138
+ retainEdges()
139
+ addRange(ranges, newBase, newBase + newText.length)
140
+ deletions.push({
141
+ previousStart: oldBase,
142
+ previousEnd: oldBase + oldText.length,
143
+ currentOffset: newBase,
144
+ })
145
+ return
146
+ }
147
+
148
+ addRetained(retained, previousOffset, currentOffset, prefix)
149
+
150
+ const width = newText.length + 1
151
+ const table = new Int32Array((oldText.length + 1) * width)
152
+ const lcs = (oldIndex: number, newIndex: number) => table[oldIndex * width + newIndex] ?? 0
153
+ for (let oldIndex = oldText.length - 1; oldIndex >= 0; oldIndex--) {
154
+ for (let newIndex = newText.length - 1; newIndex >= 0; newIndex--) {
155
+ table[oldIndex * width + newIndex] =
156
+ oldText[oldIndex] === newText[newIndex]
157
+ ? lcs(oldIndex + 1, newIndex + 1) + 1
158
+ : Math.max(lcs(oldIndex + 1, newIndex), lcs(oldIndex, newIndex + 1))
159
+ }
160
+ }
161
+
162
+ let oldIndex = 0
163
+ let newIndex = 0
164
+ let deletionStart: number | undefined
165
+ let deletionPoint = 0
166
+ const flushDeletion = () => {
167
+ if (deletionStart === undefined) return
168
+ deletions.push({
169
+ previousStart: oldBase + deletionStart,
170
+ previousEnd: oldBase + oldIndex,
171
+ currentOffset: newBase + deletionPoint,
172
+ })
173
+ deletionStart = undefined
174
+ }
175
+
176
+ while (oldIndex < oldText.length && newIndex < newText.length) {
177
+ if (oldText[oldIndex] === newText[newIndex]) {
178
+ flushDeletion()
179
+ addRetained(retained, oldBase + oldIndex, newBase + newIndex, 1)
180
+ oldIndex++
181
+ newIndex++
182
+ } else if (lcs(oldIndex + 1, newIndex) >= lcs(oldIndex, newIndex + 1)) {
183
+ if (deletionStart === undefined) {
184
+ deletionStart = oldIndex
185
+ deletionPoint = newIndex
186
+ }
187
+ oldIndex++
188
+ } else {
189
+ flushDeletion()
190
+ addRange(ranges, newBase + newIndex, newBase + newIndex + 1)
191
+ newIndex++
192
+ }
193
+ }
194
+ while (oldIndex < oldText.length) {
195
+ if (deletionStart === undefined) {
196
+ deletionStart = oldIndex
197
+ deletionPoint = newIndex
198
+ }
199
+ oldIndex++
200
+ }
201
+ flushDeletion()
202
+ addRange(ranges, newBase + newIndex, newBase + newText.length)
203
+ addRetained(retained, previousOffset + previousEnd, currentOffset + currentEnd, suffixLength)
204
+ }
205
+
206
+ export function changedText(previousText: string, currentText: string): TextChanges {
207
+ const previous = lineTokens(previousText)
208
+ const current = lineTokens(currentText)
209
+
210
+ let start = 0
211
+ let previousOffset = 0
212
+ let currentOffset = 0
213
+ while (start < previous.length && start < current.length && previous[start] === current[start]) {
214
+ previousOffset += previous[start]?.length ?? 0
215
+ currentOffset += current[start]?.length ?? 0
216
+ start++
217
+ }
218
+
219
+ let previousEnd = previous.length
220
+ let currentEnd = current.length
221
+ while (
222
+ previousEnd > start &&
223
+ currentEnd > start &&
224
+ previous[previousEnd - 1] === current[currentEnd - 1]
225
+ ) {
226
+ previousEnd--
227
+ currentEnd--
228
+ }
229
+
230
+ const oldLines = previous.slice(start, previousEnd)
231
+ const newLines = current.slice(start, currentEnd)
232
+ const ranges: ChangedRange[] = []
233
+ const deletions: Deletion[] = []
234
+ const retained: RetainedRange[] = []
235
+ const budget: DiffBudget = { remaining: DIFF_CELL_LIMIT }
236
+ addRetained(retained, 0, 0, currentOffset)
237
+
238
+ if (!reserveCells(budget, oldLines.length, newLines.length)) {
239
+ const oldText = oldLines.join("")
240
+ const newText = newLines.join("")
241
+ if (newText.length > 0) {
242
+ addRange(ranges, currentOffset, currentOffset + newText.length)
243
+ } else if (oldText.length > 0) {
244
+ deletions.push({
245
+ previousStart: previousOffset,
246
+ previousEnd: previousOffset + oldText.length,
247
+ currentOffset,
248
+ })
249
+ }
250
+ addRetained(
251
+ retained,
252
+ previousOffset + oldText.length,
253
+ currentOffset + newText.length,
254
+ previousText.length - previousOffset - oldText.length,
255
+ )
256
+ return { ranges, deletions, retained }
257
+ }
258
+
259
+ const width = newLines.length + 1
260
+ const table = new Int32Array((oldLines.length + 1) * width)
261
+ const lcs = (oldIndex: number, newIndex: number) => table[oldIndex * width + newIndex] ?? 0
262
+ for (let oldIndex = oldLines.length - 1; oldIndex >= 0; oldIndex--) {
263
+ for (let newIndex = newLines.length - 1; newIndex >= 0; newIndex--) {
264
+ table[oldIndex * width + newIndex] =
265
+ oldLines[oldIndex] === newLines[newIndex]
266
+ ? lcs(oldIndex + 1, newIndex + 1) + 1
267
+ : Math.max(lcs(oldIndex + 1, newIndex), lcs(oldIndex, newIndex + 1))
268
+ }
269
+ }
270
+
271
+ let oldIndex = 0
272
+ let newIndex = 0
273
+ let oldChunk = ""
274
+ let newChunk = ""
275
+ let oldChunkOffset = previousOffset
276
+ let newChunkOffset = currentOffset
277
+ const flushChunk = () => {
278
+ if (oldChunk === "" && newChunk === "") return
279
+ diffCharacters(
280
+ oldChunk,
281
+ newChunk,
282
+ oldChunkOffset,
283
+ newChunkOffset,
284
+ ranges,
285
+ deletions,
286
+ retained,
287
+ budget,
288
+ )
289
+ oldChunk = ""
290
+ newChunk = ""
291
+ }
292
+
293
+ while (oldIndex < oldLines.length && newIndex < newLines.length) {
294
+ if (oldLines[oldIndex] === newLines[newIndex]) {
295
+ flushChunk()
296
+ const lineLength = oldLines[oldIndex]?.length ?? 0
297
+ addRetained(retained, previousOffset, currentOffset, lineLength)
298
+ previousOffset += lineLength
299
+ currentOffset += lineLength
300
+ oldIndex++
301
+ newIndex++
302
+ oldChunkOffset = previousOffset
303
+ newChunkOffset = currentOffset
304
+ } else if (lcs(oldIndex + 1, newIndex) >= lcs(oldIndex, newIndex + 1)) {
305
+ oldChunk += oldLines[oldIndex] ?? ""
306
+ previousOffset += oldLines[oldIndex]?.length ?? 0
307
+ oldIndex++
308
+ } else {
309
+ newChunk += newLines[newIndex] ?? ""
310
+ currentOffset += newLines[newIndex]?.length ?? 0
311
+ newIndex++
312
+ }
313
+ }
314
+ while (oldIndex < oldLines.length) {
315
+ oldChunk += oldLines[oldIndex] ?? ""
316
+ previousOffset += oldLines[oldIndex]?.length ?? 0
317
+ oldIndex++
318
+ }
319
+ while (newIndex < newLines.length) {
320
+ newChunk += newLines[newIndex] ?? ""
321
+ currentOffset += newLines[newIndex]?.length ?? 0
322
+ newIndex++
323
+ }
324
+ flushChunk()
325
+ addRetained(retained, previousOffset, currentOffset, previousText.length - previousOffset)
326
+
327
+ return { ranges, deletions, retained }
328
+ }
@@ -0,0 +1,20 @@
1
+ // A token is exempt as an identifier when it has an interior signal prose
2
+ // words lack: a camelCase hump, an underscore, a dotted or :: path, or a
3
+ // call suffix. Mixed-case prose words (brand names) are an accepted loss.
4
+ const IDENTIFIER_CANDIDATE =
5
+ /[A-Za-z_$][\w$]*(?:(?:\.|::)(?:[A-Za-z$][\w$]*|_+[A-Za-z0-9$][\w$]*))*(?:\(\))?/g
6
+
7
+ const isIdentifier = (token: string): boolean =>
8
+ token.endsWith("()") ||
9
+ token.includes(".") ||
10
+ token.includes("::") ||
11
+ /[a-z][A-Z]/.test(token) ||
12
+ /[A-Za-z0-9$]_[A-Za-z0-9_$]/.test(token)
13
+
14
+ export function blankIdentifiers(lines: readonly string[]): string[] {
15
+ return lines.map((line) =>
16
+ line.replace(IDENTIFIER_CANDIDATE, (match) =>
17
+ isIdentifier(match) ? " ".repeat(match.length) : match,
18
+ ),
19
+ )
20
+ }
@@ -0,0 +1,43 @@
1
+ import type { LintKind, SourceDialect } from "./types.ts"
2
+
3
+ const SLASH_EXTENSIONS = new Set([
4
+ "ts",
5
+ "tsx",
6
+ "js",
7
+ "jsx",
8
+ "mjs",
9
+ "cjs",
10
+ "go",
11
+ "rs",
12
+ "java",
13
+ "c",
14
+ "h",
15
+ "cpp",
16
+ "hpp",
17
+ "cc",
18
+ "cs",
19
+ "swift",
20
+ "kt",
21
+ "scala",
22
+ ])
23
+
24
+ const HASH_EXTENSIONS = new Set(["sh", "bash", "zsh", "py", "rb", "yaml", "yml", "toml", "pl"])
25
+
26
+ export interface PathClassification {
27
+ readonly kind: LintKind
28
+ readonly sourceDialect: SourceDialect
29
+ }
30
+
31
+ export const classifyPath = (path: string): PathClassification => {
32
+ const dot = path.lastIndexOf(".")
33
+ if (dot <= Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))) {
34
+ return { kind: "prose-file", sourceDialect: "general" }
35
+ }
36
+ const extension = path.slice(dot + 1).toLowerCase()
37
+ if (SLASH_EXTENSIONS.has(extension)) return { kind: "slash-source", sourceDialect: "general" }
38
+ if (HASH_EXTENSIONS.has(extension)) {
39
+ const sourceDialect = ["sh", "bash", "zsh"].includes(extension) ? "shell" : "general"
40
+ return { kind: "hash-source", sourceDialect }
41
+ }
42
+ return { kind: "prose-file", sourceDialect: "general" }
43
+ }