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.
- package/.claude-plugin/marketplace.json +23 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/commands/ste.md +13 -0
- package/hooks/hooks.json +58 -0
- package/package.json +64 -0
- package/src/adapter/commit-message.ts +472 -0
- package/src/adapter/feedback.ts +40 -0
- package/src/adapter/rule-summary.ts +79 -0
- package/src/cli/hook.ts +681 -0
- package/src/cli/main.ts +201 -0
- package/src/cli/session-command.ts +78 -0
- package/src/cli/session-state.ts +214 -0
- package/src/config/load.ts +85 -0
- package/src/config/merge.ts +20 -0
- package/src/config/schema.ts +69 -0
- package/src/dictionary/README.md +18 -0
- package/src/dictionary/data/pi-ste.json +200 -0
- package/src/dictionary/form.ts +6 -0
- package/src/dictionary/load.ts +54 -0
- package/src/dictionary/schema.ts +28 -0
- package/src/engine/comments.ts +386 -0
- package/src/engine/diff.ts +328 -0
- package/src/engine/identifiers.ts +20 -0
- package/src/engine/kinds.ts +43 -0
- package/src/engine/lint.ts +530 -0
- package/src/engine/markdown.ts +338 -0
- package/src/engine/paragraphs.ts +105 -0
- package/src/engine/rules/contraction.ts +19 -0
- package/src/engine/rules/dictionary.ts +281 -0
- package/src/engine/rules/hedging.ts +27 -0
- package/src/engine/rules/marketing.ts +71 -0
- package/src/engine/rules/paragraph-length.ts +23 -0
- package/src/engine/rules/phrasal-verb.ts +57 -0
- package/src/engine/rules/registry.ts +15 -0
- package/src/engine/rules/semicolon.ts +14 -0
- package/src/engine/rules/sentence-length.ts +24 -0
- package/src/engine/rules/verb-form.ts +76 -0
- package/src/engine/scan.ts +15 -0
- package/src/engine/sentences.ts +285 -0
- package/src/engine/tagger.ts +8 -0
- package/src/engine/tokens.ts +2 -0
- package/src/engine/types.ts +45 -0
- package/src/extension/index.ts +755 -0
- 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,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
|
+
}
|