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,285 @@
1
+ export interface Sentence {
2
+ readonly text: string
3
+ readonly line: number
4
+ readonly column: number
5
+ readonly endLine: number
6
+ readonly startOffset: number
7
+ readonly endOffset: number
8
+ readonly contentRanges: readonly {
9
+ readonly start: number
10
+ readonly end: number
11
+ }[]
12
+ }
13
+
14
+ const CLOSING_DELIMITERS = new Set([
15
+ '"',
16
+ "'",
17
+ "’",
18
+ "”",
19
+ "»",
20
+ "›",
21
+ ")",
22
+ "]",
23
+ "}",
24
+ "*",
25
+ "_",
26
+ "~",
27
+ "`",
28
+ ])
29
+
30
+ function valueAt(values: Int32Array | Uint32Array, index: number): number {
31
+ const value = values[index]
32
+ if (value === undefined) throw new RangeError(`Array index ${index} is out of bounds`)
33
+ return value
34
+ }
35
+
36
+ function sentinelAt(values: Int32Array, index: number): number {
37
+ return values[index] ?? -1
38
+ }
39
+
40
+ function markdownDelimiterEnds(text: string): {
41
+ readonly parentheses: Int32Array
42
+ readonly brackets: Int32Array
43
+ readonly linkSuffixes: Int32Array
44
+ } {
45
+ const parentheses = new Int32Array(text.length).fill(-1)
46
+ const brackets = new Int32Array(text.length).fill(-1)
47
+ const linkSuffixes = new Int32Array(text.length).fill(-1)
48
+ const parenthesisStack: number[] = []
49
+ const bracketStack: number[] = []
50
+ let opaqueDelimiter: string | undefined
51
+
52
+ for (let index = 0; index < text.length; index += 1) {
53
+ const character = text[index]
54
+ if (opaqueDelimiter !== undefined) {
55
+ if (character === "\\") {
56
+ index += 1
57
+ } else if (character === opaqueDelimiter) {
58
+ opaqueDelimiter = undefined
59
+ }
60
+ continue
61
+ }
62
+
63
+ switch (character) {
64
+ case "\\":
65
+ index += 1
66
+ break
67
+ case '"':
68
+ case "'": {
69
+ const opening = parenthesisStack[parenthesisStack.length - 1]
70
+ const isLinkTitle =
71
+ opening !== undefined && text[opening - 1] === "]" && /\s/.test(text[index - 1] ?? "")
72
+ if (isLinkTitle) opaqueDelimiter = character
73
+ break
74
+ }
75
+ case "<": {
76
+ const opening = parenthesisStack[parenthesisStack.length - 1]
77
+ const isAngleDestination =
78
+ opening !== undefined && text[opening - 1] === "]" && index === opening + 1
79
+ if (isAngleDestination) opaqueDelimiter = ">"
80
+ break
81
+ }
82
+ case "(":
83
+ parenthesisStack.push(index)
84
+ break
85
+ case ")": {
86
+ const opening = parenthesisStack.pop()
87
+ if (opening !== undefined) parentheses[opening] = index + 1
88
+ break
89
+ }
90
+ case "[":
91
+ bracketStack.push(index)
92
+ break
93
+ case "]": {
94
+ const opening = bracketStack.pop()
95
+ if (opening !== undefined) brackets[opening] = index + 1
96
+ break
97
+ }
98
+ }
99
+ }
100
+
101
+ for (let index = 0; index < text.length - 1; index += 1) {
102
+ if (text[index] !== "]") continue
103
+ const suffixStart = index + 1
104
+ const parenthesisEnd = valueAt(parentheses, suffixStart)
105
+ const bracketEnd = valueAt(brackets, suffixStart)
106
+ if (text[suffixStart] === "(" && parenthesisEnd >= 0) {
107
+ linkSuffixes[suffixStart] = parenthesisEnd
108
+ } else if (text[suffixStart] === "[" && bracketEnd >= 0) {
109
+ linkSuffixes[suffixStart] = bracketEnd
110
+ }
111
+ }
112
+
113
+ return { parentheses, brackets, linkSuffixes }
114
+ }
115
+
116
+ function sentenceTerminatorEnds(text: string): number[] {
117
+ const { parentheses, brackets, linkSuffixes } = markdownDelimiterEnds(text)
118
+ const closingRuns = new Uint32Array(text.length + 1)
119
+ const closingBracketCounts = new Uint32Array(text.length + 1)
120
+ const referenceRuns = new Int32Array(text.length).fill(-1)
121
+ const ends: number[] = []
122
+ closingRuns[text.length] = text.length
123
+
124
+ for (let index = text.length - 1; index >= 0; index -= 1) {
125
+ closingRuns[index] = CLOSING_DELIMITERS.has(text[index] ?? "")
126
+ ? valueAt(closingRuns, index + 1)
127
+ : index
128
+ closingBracketCounts[index] =
129
+ valueAt(closingBracketCounts, index + 1) + (text[index] === "]" ? 1 : 0)
130
+ }
131
+
132
+ for (let index = text.length - 1; index >= 0; index -= 1) {
133
+ const bracketEnd = valueAt(brackets, index)
134
+ if (text[index] !== "[" || bracketEnd < 0) continue
135
+ let end = valueAt(closingRuns, bracketEnd)
136
+ const referenceEnd = sentinelAt(referenceRuns, end)
137
+ if (text[end] === "[" && referenceEnd >= 0) end = referenceEnd
138
+ referenceRuns[index] = end
139
+ }
140
+
141
+ for (let index = 0; index < text.length; index += 1) {
142
+ const linkSuffixEnd = valueAt(linkSuffixes, index)
143
+ if (linkSuffixEnd >= 0) {
144
+ index = linkSuffixEnd - 1
145
+ continue
146
+ }
147
+ if (text[index] !== "." && text[index] !== "!" && text[index] !== "?") continue
148
+
149
+ let punctuationEnd = index + 1
150
+ while (
151
+ text[punctuationEnd] === "." ||
152
+ text[punctuationEnd] === "!" ||
153
+ text[punctuationEnd] === "?"
154
+ ) {
155
+ punctuationEnd += 1
156
+ }
157
+
158
+ let end = valueAt(closingRuns, punctuationEnd)
159
+ const closedLinkLabel =
160
+ valueAt(closingBracketCounts, punctuationEnd) > valueAt(closingBracketCounts, end)
161
+
162
+ if (closedLinkLabel && text[end] === "(") {
163
+ const parenthesisEnd = valueAt(parentheses, end)
164
+ if (parenthesisEnd < 0) {
165
+ index = punctuationEnd - 1
166
+ continue
167
+ }
168
+ end = valueAt(closingRuns, parenthesisEnd)
169
+ }
170
+
171
+ const referenceEnd = sentinelAt(referenceRuns, end)
172
+ if (text[end] === "[" && referenceEnd >= 0) end = referenceEnd
173
+ if (end === text.length || /\s/.test(text[end] ?? "")) {
174
+ ends.push(end)
175
+ index = end - 1
176
+ } else {
177
+ index = punctuationEnd - 1
178
+ }
179
+ }
180
+
181
+ return ends
182
+ }
183
+
184
+ // A sentence starts at the first non-whitespace character and ends at
185
+ // terminal punctuation and closing delimiters, at a blank line, or at EOF.
186
+ // Sentences may span lines; position is where the sentence starts (1-based).
187
+ export function segmentSentences(
188
+ lines: readonly string[],
189
+ sourceText: string = lines.join("\n"),
190
+ structuralBlanks: readonly boolean[] = lines.map((line) => line.trim() === ""),
191
+ ): Sentence[] {
192
+ const sentences: Sentence[] = []
193
+ const lineOffsets = [0]
194
+ for (let index = 0; index < sourceText.length; index++) {
195
+ if (sourceText[index] === "\n") lineOffsets.push(index + 1)
196
+ }
197
+ let open: {
198
+ line: number
199
+ column: number
200
+ endLine: number
201
+ startOffset: number
202
+ endOffset: number
203
+ parts: string[]
204
+ contentRanges: Array<{ start: number; end: number }>
205
+ } | null = null
206
+
207
+ const appendPart = (part: string, startOffset: number) => {
208
+ if (!open) return
209
+ const leadingWhitespace = part.length - part.trimStart().length
210
+ const text = part.trim()
211
+ open.parts.push(text)
212
+ if (text === "") return
213
+ const start = startOffset + leadingWhitespace
214
+ const end = start + text.length
215
+ open.contentRanges.push({ start, end })
216
+ open.endOffset = end
217
+ }
218
+
219
+ const close = () => {
220
+ if (!open) return
221
+ const text = open.parts.join(" ").trim()
222
+ if (text !== "") {
223
+ sentences.push({
224
+ text,
225
+ line: open.line,
226
+ column: open.column,
227
+ endLine: open.endLine,
228
+ startOffset: open.startOffset,
229
+ endOffset: open.endOffset,
230
+ contentRanges: open.contentRanges,
231
+ })
232
+ }
233
+ open = null
234
+ }
235
+
236
+ lines.forEach((raw, index) => {
237
+ if (raw.trim() === "") {
238
+ if (structuralBlanks[index] ?? true) close()
239
+ return
240
+ }
241
+ let offset = 0
242
+ for (const end of sentenceTerminatorEnds(raw)) {
243
+ const part = raw.slice(offset, end)
244
+ if (!open) {
245
+ const indent = part.length - part.trimStart().length
246
+ const startOffset = (lineOffsets[index] ?? 0) + offset + indent
247
+ open = {
248
+ line: index + 1,
249
+ column: offset + indent + 1,
250
+ endLine: index + 1,
251
+ startOffset,
252
+ endOffset: startOffset,
253
+ parts: [],
254
+ contentRanges: [],
255
+ }
256
+ }
257
+ open.endLine = index + 1
258
+ appendPart(part, (lineOffsets[index] ?? 0) + offset)
259
+ close()
260
+ offset = end
261
+ }
262
+
263
+ const rest = raw.slice(offset)
264
+ if (rest.trim() !== "") {
265
+ if (!open) {
266
+ const indent = rest.length - rest.trimStart().length
267
+ const startOffset = (lineOffsets[index] ?? 0) + offset + indent
268
+ open = {
269
+ line: index + 1,
270
+ column: offset + indent + 1,
271
+ endLine: index + 1,
272
+ startOffset,
273
+ endOffset: startOffset,
274
+ parts: [],
275
+ contentRanges: [],
276
+ }
277
+ }
278
+ open.endLine = index + 1
279
+ appendPart(rest, (lineOffsets[index] ?? 0) + offset)
280
+ }
281
+ })
282
+ close()
283
+
284
+ return sentences
285
+ }
@@ -0,0 +1,8 @@
1
+ export interface TaggedToken {
2
+ readonly text: string
3
+ readonly pos: string
4
+ readonly lemma: string
5
+ readonly offset: number
6
+ }
7
+
8
+ export type Tagger = (text: string) => readonly TaggedToken[]
@@ -0,0 +1,2 @@
1
+ export const TOKEN_CHARACTER_PATTERN = "[A-Za-z0-9_'’-]"
2
+ export const TOKEN_RUN_PATTERN = new RegExp(`${TOKEN_CHARACTER_PATTERN}+`, "g")
@@ -0,0 +1,45 @@
1
+ import type { Dictionary } from "../dictionary/schema.ts"
2
+ import type { RuleId } from "./rules/registry.ts"
3
+ import type { Tagger } from "./tagger.ts"
4
+
5
+ export type LintKind = "prose-file" | "slash-source" | "hash-source" | "commit-message"
6
+
7
+ export type Severity = "hard" | "soft"
8
+
9
+ export type RuleSetting = Severity | "off"
10
+
11
+ export interface Violation {
12
+ readonly ruleId: RuleId
13
+ readonly severity: Severity
14
+ readonly message: string
15
+ readonly suggestions?: readonly string[]
16
+ readonly line: number
17
+ readonly column: number
18
+ readonly suggestion?: string
19
+ }
20
+
21
+ export type SourceDialect = "general" | "shell"
22
+
23
+ export interface LintOptions {
24
+ readonly rules?: Partial<Record<RuleId, RuleSetting>>
25
+ readonly maxSentenceWords?: number
26
+ readonly dictionary?: Dictionary
27
+ // POS tagger for the verb-form rules and POS-aware dictionary entries.
28
+ readonly tagger?: Tagger
29
+ readonly sourceDialect?: SourceDialect
30
+ /**
31
+ * The prior document text used to report only new violations.
32
+ * The engine compares sentence-scoped and paragraph-scoped violations structurally.
33
+ * Omit this value to lint the current document in full.
34
+ * Violation positions refer to the current text.
35
+ */
36
+ readonly previousText?: string
37
+ }
38
+
39
+ export interface LintReport {
40
+ readonly violations: readonly Violation[]
41
+ readonly summary: {
42
+ readonly total: number
43
+ readonly hard: number
44
+ }
45
+ }