agent-simple-english 0.4.0 → 0.5.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/src/cli/hook.ts +3 -3
- package/src/cli/main.ts +13 -13
- package/src/cli/session-command.ts +10 -10
- package/src/config/schema.ts +37 -41
- package/src/dictionary/load.ts +12 -11
- package/src/dictionary/schema.ts +9 -12
- package/src/schema/parse-error.ts +32 -0
- package/src/schema/primitives.ts +7 -0
- package/src/tagger/wink.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "simple-english",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Apply technical and house-style writing rules to writes, edits, and git commit messages.",
|
|
6
6
|
"repository": "https://github.com/jyooi/agent-simple-english",
|
|
7
7
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-simple-english",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Technical and house-style English lint engine, CLI, and host adapters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@lezer/html": "^1.3.12",
|
|
48
48
|
"@lezer/markdown": "^1.7.2",
|
|
49
|
-
"effect": "
|
|
49
|
+
"effect": "4.0.0-rc.112",
|
|
50
50
|
"micromark": "^4.0.2",
|
|
51
51
|
"micromark-extension-frontmatter": "^2.0.0",
|
|
52
52
|
"micromark-extension-gfm-table": "^2.1.1",
|
package/src/cli/hook.ts
CHANGED
|
@@ -715,7 +715,7 @@ function recordEvaluation(event: HookEvent, evaluation: HookEvaluation): Effect.
|
|
|
715
715
|
}),
|
|
716
716
|
catch: () => undefined,
|
|
717
717
|
}).pipe(
|
|
718
|
-
Effect.
|
|
718
|
+
Effect.catch(() => Effect.void),
|
|
719
719
|
Effect.as(evaluation.output),
|
|
720
720
|
)
|
|
721
721
|
}
|
|
@@ -760,14 +760,14 @@ export function runHookMode(raw: string): Effect.Effect<HookOutput, never, Tagge
|
|
|
760
760
|
return yield* recordEvaluation(event, evaluation)
|
|
761
761
|
})
|
|
762
762
|
}),
|
|
763
|
-
Effect.
|
|
763
|
+
Effect.catch((error) =>
|
|
764
764
|
Effect.succeed(
|
|
765
765
|
event.hookEventName === "PreToolUse"
|
|
766
766
|
? nonBlockingWarning(error.message)
|
|
767
767
|
: nonBlockingError(error.message),
|
|
768
768
|
),
|
|
769
769
|
),
|
|
770
|
-
Effect.
|
|
770
|
+
Effect.catchCause((cause) =>
|
|
771
771
|
Effect.succeed(
|
|
772
772
|
event.hookEventName === "PreToolUse"
|
|
773
773
|
? hookInternalFailure(cause)
|
package/src/cli/main.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { readFile } from "node:fs/promises"
|
|
3
|
-
import { Effect,
|
|
3
|
+
import { Effect, Result } from "effect"
|
|
4
4
|
import packageManifest from "../../package.json" with { type: "json" }
|
|
5
5
|
import { loadConfig } from "../config/load.ts"
|
|
6
6
|
import { loadConfiguredDictionary } from "../dictionary/configured.ts"
|
|
@@ -164,7 +164,7 @@ const hookProgram = Effect.gen(function* () {
|
|
|
164
164
|
console.log(JSON.stringify(output))
|
|
165
165
|
return 0
|
|
166
166
|
}).pipe(
|
|
167
|
-
Effect.
|
|
167
|
+
Effect.catchCause((cause) =>
|
|
168
168
|
Effect.sync(() => {
|
|
169
169
|
console.log(JSON.stringify(hookInternalFailure(cause)))
|
|
170
170
|
return 0
|
|
@@ -220,20 +220,20 @@ const lintProgram = Effect.gen(function* () {
|
|
|
220
220
|
)
|
|
221
221
|
}
|
|
222
222
|
const config = yield* loadConfig(configPath)
|
|
223
|
-
const loadedDictionary = yield* Effect.
|
|
223
|
+
const loadedDictionary = yield* Effect.result(
|
|
224
224
|
loadConfiguredDictionary(config, process.cwd(), process.env.SIMPLE_ENGLISH_DICTIONARY),
|
|
225
225
|
)
|
|
226
|
-
const loadedRuleData = yield* Effect.
|
|
227
|
-
if (
|
|
228
|
-
return yield* Effect.fail(loadedDictionary.
|
|
226
|
+
const loadedRuleData = yield* Effect.result(loadRuleData(config.ruleDataExtensions))
|
|
227
|
+
if (Result.isFailure(loadedDictionary) && config.approvedWordsPath !== undefined) {
|
|
228
|
+
return yield* Effect.fail(loadedDictionary.failure)
|
|
229
229
|
}
|
|
230
|
-
const dictionary =
|
|
231
|
-
const ruleData =
|
|
232
|
-
if (
|
|
233
|
-
yield* Effect.sync(() => console.error(loadedDictionary.
|
|
230
|
+
const dictionary = Result.getOrUndefined(loadedDictionary)
|
|
231
|
+
const ruleData = Result.getOrUndefined(loadedRuleData)
|
|
232
|
+
if (Result.isFailure(loadedDictionary)) {
|
|
233
|
+
yield* Effect.sync(() => console.error(loadedDictionary.failure.message))
|
|
234
234
|
}
|
|
235
|
-
if (
|
|
236
|
-
yield* Effect.sync(() => console.error(loadedRuleData.
|
|
235
|
+
if (Result.isFailure(loadedRuleData)) {
|
|
236
|
+
yield* Effect.sync(() => console.error(loadedRuleData.failure.message))
|
|
237
237
|
}
|
|
238
238
|
const inputs =
|
|
239
239
|
paths.length === 0
|
|
@@ -282,7 +282,7 @@ const program: Effect.Effect<number, Error> =
|
|
|
282
282
|
: lintProgram.pipe(Effect.provide(WinkTaggerLive))
|
|
283
283
|
|
|
284
284
|
const handled = program.pipe(
|
|
285
|
-
Effect.
|
|
285
|
+
Effect.catch((error) =>
|
|
286
286
|
Effect.sync(() => {
|
|
287
287
|
console.error(error.message)
|
|
288
288
|
return 2
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path"
|
|
2
|
-
import { Effect,
|
|
2
|
+
import { Effect, Result } from "effect"
|
|
3
3
|
import { formatFailedStatusSummary, formatStatusSummary } from "../adapter/rule-summary.ts"
|
|
4
4
|
import { loadConfig } from "../config/load.ts"
|
|
5
5
|
import { loadConfiguredDictionary } from "../dictionary/configured.ts"
|
|
@@ -48,25 +48,25 @@ const updateStrict = (sessionId: string, strict: boolean) =>
|
|
|
48
48
|
function status(sessionId: string, cwd: string): Effect.Effect<string, Error> {
|
|
49
49
|
return Effect.gen(function* () {
|
|
50
50
|
const control = yield* readControl(sessionId)
|
|
51
|
-
const configResult = yield* Effect.
|
|
52
|
-
if (
|
|
53
|
-
return formatFailedStatusSummary(modeName(control), configResult.
|
|
51
|
+
const configResult = yield* Effect.result(loadConfig(undefined, cwd))
|
|
52
|
+
if (Result.isFailure(configResult)) {
|
|
53
|
+
return formatFailedStatusSummary(modeName(control), configResult.failure.message)
|
|
54
54
|
}
|
|
55
55
|
const dictionaryPath = process.env.SIMPLE_ENGLISH_DICTIONARY
|
|
56
|
-
const dictionaryResult = yield* Effect.
|
|
56
|
+
const dictionaryResult = yield* Effect.result(
|
|
57
57
|
Effect.all({
|
|
58
58
|
dictionary: loadConfiguredDictionary(
|
|
59
|
-
configResult.
|
|
59
|
+
configResult.success,
|
|
60
60
|
cwd,
|
|
61
61
|
dictionaryPath === undefined ? undefined : resolve(cwd, dictionaryPath),
|
|
62
62
|
),
|
|
63
|
-
ruleData: loadRuleData(configResult.
|
|
63
|
+
ruleData: loadRuleData(configResult.success.ruleDataExtensions, cwd),
|
|
64
64
|
}),
|
|
65
65
|
)
|
|
66
|
-
const dictionary: DictionaryState =
|
|
66
|
+
const dictionary: DictionaryState = Result.isSuccess(dictionaryResult)
|
|
67
67
|
? "loaded"
|
|
68
|
-
: `failed (${dictionaryResult.
|
|
69
|
-
return formatStatusSummary(configResult.
|
|
68
|
+
: `failed (${dictionaryResult.failure.message})`
|
|
69
|
+
return formatStatusSummary(configResult.success, modeName(control), dictionary)
|
|
70
70
|
})
|
|
71
71
|
}
|
|
72
72
|
|
package/src/config/schema.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
2
|
import type { RuleDataExtensions } from "../dictionary/rule-data.ts"
|
|
3
3
|
import { type RuleId, ruleIds } from "../engine/rules/registry.ts"
|
|
4
4
|
import type { RuleSetting } from "../engine/types.ts"
|
|
5
|
+
import {
|
|
6
|
+
formatParseErrorIssues,
|
|
7
|
+
formatParseErrorTree,
|
|
8
|
+
type ParseError,
|
|
9
|
+
} from "../schema/parse-error.ts"
|
|
10
|
+
import { NonEmptyTrimmedString } from "../schema/primitives.ts"
|
|
5
11
|
|
|
6
12
|
export interface SteConfig {
|
|
7
13
|
readonly rules?: Partial<Readonly<Record<RuleId, RuleSetting>>>
|
|
@@ -11,63 +17,53 @@ export interface SteConfig {
|
|
|
11
17
|
readonly approvedWordsPath?: string
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
const RuleSettingSchema = Schema.
|
|
15
|
-
|
|
16
|
-
message: `must be "hard", "soft", or "off", got ${JSON.stringify(issue.actual)}`,
|
|
17
|
-
override: true,
|
|
18
|
-
}),
|
|
20
|
+
const RuleSettingSchema = Schema.Literals(["hard", "soft", "off"]).annotate({
|
|
21
|
+
expected: '"hard", "soft", or "off"',
|
|
19
22
|
})
|
|
20
23
|
|
|
21
|
-
const RulesSchema = Schema.
|
|
22
|
-
|
|
24
|
+
const RulesSchema = Schema.Struct(
|
|
25
|
+
Object.fromEntries(ruleIds.map((id) => [id, Schema.optionalKey(RuleSettingSchema)])),
|
|
23
26
|
)
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
28
|
+
// One refinement over Unknown, rather than Number plus two checks, so that a
|
|
29
|
+
// wrong type and a wrong number both report the same text and the value.
|
|
30
|
+
const MaxSentenceWordsSchema = Schema.Unknown.pipe(
|
|
31
|
+
Schema.refine(
|
|
32
|
+
(value): value is number => typeof value === "number" && Number.isInteger(value) && value > 0,
|
|
33
|
+
{ expected: "a positive integer" },
|
|
34
|
+
),
|
|
35
|
+
)
|
|
31
36
|
|
|
32
|
-
const ExemptBlockQuotesSchema = Schema.Boolean
|
|
33
|
-
message: (issue) => ({
|
|
34
|
-
message: `must be a boolean, got ${JSON.stringify(issue.actual)}`,
|
|
35
|
-
override: true,
|
|
36
|
-
}),
|
|
37
|
-
})
|
|
37
|
+
const ExemptBlockQuotesSchema = Schema.Boolean
|
|
38
38
|
|
|
39
|
-
const RuleDataExtensionsSchema = Schema.
|
|
40
|
-
Schema.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}),
|
|
46
|
-
)
|
|
39
|
+
const RuleDataExtensionsSchema = Schema.Struct({
|
|
40
|
+
"phrasal-verb": Schema.optionalKey(Schema.Array(NonEmptyTrimmedString)),
|
|
41
|
+
hedging: Schema.optionalKey(Schema.Array(NonEmptyTrimmedString)),
|
|
42
|
+
marketing: Schema.optionalKey(Schema.Array(NonEmptyTrimmedString)),
|
|
43
|
+
"adjectival-participle": Schema.optionalKey(Schema.Array(NonEmptyTrimmedString)),
|
|
44
|
+
})
|
|
47
45
|
|
|
48
46
|
const SteConfigSchema = Schema.Struct({
|
|
49
|
-
rules: Schema.
|
|
50
|
-
maxSentenceWords: Schema.
|
|
51
|
-
exemptBlockQuotes: Schema.
|
|
52
|
-
ruleDataExtensions: Schema.
|
|
53
|
-
approvedWordsPath: Schema.
|
|
47
|
+
rules: Schema.optionalKey(RulesSchema),
|
|
48
|
+
maxSentenceWords: Schema.optionalKey(MaxSentenceWordsSchema),
|
|
49
|
+
exemptBlockQuotes: Schema.optionalKey(ExemptBlockQuotesSchema),
|
|
50
|
+
ruleDataExtensions: Schema.optionalKey(RuleDataExtensionsSchema),
|
|
51
|
+
approvedWordsPath: Schema.optionalKey(NonEmptyTrimmedString),
|
|
54
52
|
})
|
|
55
53
|
|
|
56
|
-
const decodeUnknown = Schema.
|
|
54
|
+
const decodeUnknown = Schema.decodeUnknownEffect(SteConfigSchema, {
|
|
57
55
|
onExcessProperty: "error",
|
|
58
56
|
errors: "all",
|
|
57
|
+
// v4 omits the rejected value from an issue unless this option is on.
|
|
58
|
+
reportInput: true,
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
export class ConfigError extends Error {
|
|
62
62
|
readonly _tag = "ConfigError"
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const formatError = (error:
|
|
66
|
-
|
|
67
|
-
// reports a useless "Expected undefined" branch; drop those.
|
|
68
|
-
const issues = ParseResult.ArrayFormatter.formatErrorSync(error).filter(
|
|
69
|
-
(issue) => !issue.message.startsWith("Expected undefined"),
|
|
70
|
-
)
|
|
65
|
+
const formatError = (error: ParseError, source: string): string => {
|
|
66
|
+
const issues = formatParseErrorIssues(error)
|
|
71
67
|
const lines = [
|
|
72
68
|
...new Set(
|
|
73
69
|
issues.map(
|
|
@@ -78,7 +74,7 @@ const formatError = (error: ParseResult.ParseError, source: string): string => {
|
|
|
78
74
|
const detail =
|
|
79
75
|
lines.length > 0
|
|
80
76
|
? lines.map((line) => ` ${line}`).join("\n")
|
|
81
|
-
: ` ${
|
|
77
|
+
: ` ${formatParseErrorTree(error)}`
|
|
82
78
|
return `invalid config in ${source}:\n${detail}`
|
|
83
79
|
}
|
|
84
80
|
|
package/src/dictionary/load.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises"
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
|
-
import { Effect,
|
|
4
|
+
import { Effect, Schema } from "effect"
|
|
5
|
+
import { formatParseErrorIssues, type ParseError } from "../schema/parse-error.ts"
|
|
5
6
|
import type { RuleData, RuleDataExtensions, RuleDataId } from "./rule-data.ts"
|
|
6
7
|
import {
|
|
7
8
|
type ApprovedWordList,
|
|
@@ -34,8 +35,8 @@ export class DictionaryLoadError extends Error {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const formatParseError = (error:
|
|
38
|
-
const issue =
|
|
38
|
+
const formatParseError = (error: ParseError): string => {
|
|
39
|
+
const issue = formatParseErrorIssues(error)[0]
|
|
39
40
|
if (issue === undefined) {
|
|
40
41
|
return "invalid dictionary data"
|
|
41
42
|
}
|
|
@@ -48,15 +49,15 @@ const formatParseError = (error: ParseResult.ParseError): string => {
|
|
|
48
49
|
return path === "" ? `invalid dictionary data: ${issue.message}` : `${path}: ${issue.message}`
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
errors: "all",
|
|
54
|
-
})
|
|
52
|
+
// v4 omits the rejected value from an issue unless reportInput is on.
|
|
53
|
+
const decodeOptions = { onExcessProperty: "error", errors: "all", reportInput: true } as const
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const decodeDictionary = Schema.decodeEffect(Schema.fromJsonString(DictionarySchema), decodeOptions)
|
|
56
|
+
|
|
57
|
+
const decodeApprovedWordList = Schema.decodeEffect(
|
|
58
|
+
Schema.fromJsonString(ApprovedWordListSchema),
|
|
59
|
+
decodeOptions,
|
|
60
|
+
)
|
|
60
61
|
|
|
61
62
|
const readDictionaryFile = (
|
|
62
63
|
path: string,
|
package/src/dictionary/schema.ts
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { Schema } from "effect"
|
|
2
|
+
import { NonEmptyTrimmedString } from "../schema/primitives.ts"
|
|
2
3
|
import { DICTIONARY_FORM_PATTERN, DICTIONARY_WORD_PATTERN } from "./form.ts"
|
|
3
4
|
|
|
4
5
|
const DictionarySourceSchema = Schema.Struct({
|
|
5
|
-
name:
|
|
6
|
-
repository:
|
|
7
|
-
commit:
|
|
8
|
-
path:
|
|
6
|
+
name: NonEmptyTrimmedString,
|
|
7
|
+
repository: NonEmptyTrimmedString,
|
|
8
|
+
commit: NonEmptyTrimmedString,
|
|
9
|
+
path: NonEmptyTrimmedString,
|
|
9
10
|
})
|
|
10
11
|
|
|
11
|
-
const DictionaryFormSchema =
|
|
12
|
-
Schema.pattern(DICTIONARY_FORM_PATTERN),
|
|
13
|
-
)
|
|
12
|
+
const DictionaryFormSchema = NonEmptyTrimmedString.check(Schema.isPattern(DICTIONARY_FORM_PATTERN))
|
|
14
13
|
|
|
15
|
-
const DictionaryWordSchema =
|
|
16
|
-
Schema.pattern(DICTIONARY_WORD_PATTERN),
|
|
17
|
-
)
|
|
14
|
+
const DictionaryWordSchema = NonEmptyTrimmedString.check(Schema.isPattern(DICTIONARY_WORD_PATTERN))
|
|
18
15
|
|
|
19
16
|
const DictionaryEntrySchema = Schema.Struct({
|
|
20
17
|
unapproved: Schema.NonEmptyArray(DictionaryFormSchema),
|
|
21
|
-
suggestions: Schema.NonEmptyArray(
|
|
22
|
-
partsOfSpeech: Schema.optional(Schema.NonEmptyArray(
|
|
18
|
+
suggestions: Schema.NonEmptyArray(NonEmptyTrimmedString),
|
|
19
|
+
partsOfSpeech: Schema.optional(Schema.NonEmptyArray(NonEmptyTrimmedString)),
|
|
23
20
|
})
|
|
24
21
|
|
|
25
22
|
export const DictionarySchema = Schema.Struct({
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Schema, SchemaIssue } from "effect"
|
|
2
|
+
|
|
3
|
+
// This isolates the Effect v4 `SchemaIssue` formatter calls, so a later
|
|
4
|
+
// Schema change touches only this file.
|
|
5
|
+
|
|
6
|
+
export type ParseError = Schema.SchemaError
|
|
7
|
+
|
|
8
|
+
export interface ParseErrorIssue {
|
|
9
|
+
readonly path: ReadonlyArray<string | number>
|
|
10
|
+
readonly message: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const formatIssues = SchemaIssue.makeFormatterStandardSchemaV1()
|
|
14
|
+
const formatTree = SchemaIssue.makeFormatterDefault()
|
|
15
|
+
|
|
16
|
+
// A Standard Schema path segment is either a raw key or a `{ key }` wrapper,
|
|
17
|
+
// and symbol keys never appear in this package's schemas.
|
|
18
|
+
const pathSegments = (
|
|
19
|
+
path: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined,
|
|
20
|
+
): ReadonlyArray<string | number> =>
|
|
21
|
+
(path ?? []).map((segment) => {
|
|
22
|
+
const key = typeof segment === "object" ? segment.key : segment
|
|
23
|
+
return typeof key === "number" ? key : String(key)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const formatParseErrorIssues = (error: ParseError): ParseErrorIssue[] =>
|
|
27
|
+
formatIssues(error.issue).issues.map((issue) => ({
|
|
28
|
+
path: pathSegments(issue.path),
|
|
29
|
+
message: issue.message,
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
export const formatParseErrorTree = (error: ParseError): string => formatTree(error.issue)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
// Effect v4 has no NonEmptyTrimmedString schema.
|
|
4
|
+
// Every call site imports this alias, so the definition stays in one file.
|
|
5
|
+
export const NonEmptyTrimmedString = Schema.Trimmed.check(
|
|
6
|
+
Schema.isNonEmpty({ expected: "a non-empty string" }),
|
|
7
|
+
)
|
package/src/tagger/wink.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function makeWinkTagger(): Tagger {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export class TaggerService extends Context.
|
|
34
|
+
export class TaggerService extends Context.Service<TaggerService, Tagger>()("TaggerService") {}
|
|
35
35
|
|
|
36
36
|
const makeLazyWinkTagger = (): Tagger => {
|
|
37
37
|
let tagger: Tagger | undefined
|