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.
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "name": "simple-english",
13
13
  "description": "Apply technical and house-style writing rules to writes, edits, and git commit messages.",
14
- "version": "0.4.0",
14
+ "version": "0.5.0",
15
15
  "author": {
16
16
  "name": "JIA YI"
17
17
  },
@@ -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.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.4.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": "^3.14.0",
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.catchAll(() => Effect.void),
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.catchAll((error) =>
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.catchAllCause((cause) =>
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, Either } from "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.catchAllCause((cause) =>
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.either(
223
+ const loadedDictionary = yield* Effect.result(
224
224
  loadConfiguredDictionary(config, process.cwd(), process.env.SIMPLE_ENGLISH_DICTIONARY),
225
225
  )
226
- const loadedRuleData = yield* Effect.either(loadRuleData(config.ruleDataExtensions))
227
- if (Either.isLeft(loadedDictionary) && config.approvedWordsPath !== undefined) {
228
- return yield* Effect.fail(loadedDictionary.left)
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 = Either.getOrUndefined(loadedDictionary)
231
- const ruleData = Either.getOrUndefined(loadedRuleData)
232
- if (Either.isLeft(loadedDictionary)) {
233
- yield* Effect.sync(() => console.error(loadedDictionary.left.message))
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 (Either.isLeft(loadedRuleData)) {
236
- yield* Effect.sync(() => console.error(loadedRuleData.left.message))
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.catchAll((error) =>
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, Either } from "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.either(loadConfig(undefined, cwd))
52
- if (Either.isLeft(configResult)) {
53
- return formatFailedStatusSummary(modeName(control), configResult.left.message)
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.either(
56
+ const dictionaryResult = yield* Effect.result(
57
57
  Effect.all({
58
58
  dictionary: loadConfiguredDictionary(
59
- configResult.right,
59
+ configResult.success,
60
60
  cwd,
61
61
  dictionaryPath === undefined ? undefined : resolve(cwd, dictionaryPath),
62
62
  ),
63
- ruleData: loadRuleData(configResult.right.ruleDataExtensions, cwd),
63
+ ruleData: loadRuleData(configResult.success.ruleDataExtensions, cwd),
64
64
  }),
65
65
  )
66
- const dictionary: DictionaryState = Either.isRight(dictionaryResult)
66
+ const dictionary: DictionaryState = Result.isSuccess(dictionaryResult)
67
67
  ? "loaded"
68
- : `failed (${dictionaryResult.left.message})`
69
- return formatStatusSummary(configResult.right, modeName(control), dictionary)
68
+ : `failed (${dictionaryResult.failure.message})`
69
+ return formatStatusSummary(configResult.success, modeName(control), dictionary)
70
70
  })
71
71
  }
72
72
 
@@ -1,7 +1,13 @@
1
- import { Effect, ParseResult, Schema } from "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.Literal("hard", "soft", "off").annotations({
15
- message: (issue) => ({
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.partial(
22
- Schema.Struct(Object.fromEntries(ruleIds.map((id) => [id, RuleSettingSchema]))),
24
+ const RulesSchema = Schema.Struct(
25
+ Object.fromEntries(ruleIds.map((id) => [id, Schema.optionalKey(RuleSettingSchema)])),
23
26
  )
24
27
 
25
- const MaxSentenceWordsSchema = Schema.Int.pipe(Schema.positive()).annotations({
26
- message: (issue) => ({
27
- message: `must be a positive integer, got ${JSON.stringify(issue.actual)}`,
28
- override: true,
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.annotations({
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.partial(
40
- Schema.Struct({
41
- "phrasal-verb": Schema.Array(Schema.NonEmptyTrimmedString),
42
- hedging: Schema.Array(Schema.NonEmptyTrimmedString),
43
- marketing: Schema.Array(Schema.NonEmptyTrimmedString),
44
- "adjectival-participle": Schema.Array(Schema.NonEmptyTrimmedString),
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.optional(RulesSchema),
50
- maxSentenceWords: Schema.optional(MaxSentenceWordsSchema),
51
- exemptBlockQuotes: Schema.optional(ExemptBlockQuotesSchema),
52
- ruleDataExtensions: Schema.optional(RuleDataExtensionsSchema),
53
- approvedWordsPath: Schema.optional(Schema.NonEmptyTrimmedString),
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.decodeUnknown(SteConfigSchema, {
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: ParseResult.ParseError, source: string): string => {
66
- // Optional fields decode as `T | undefined` unions, so every failure also
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
- : ` ${ParseResult.TreeFormatter.formatErrorSync(error)}`
77
+ : ` ${formatParseErrorTree(error)}`
82
78
  return `invalid config in ${source}:\n${detail}`
83
79
  }
84
80
 
@@ -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, ParseResult, Schema } from "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: ParseResult.ParseError): string => {
38
- const issue = ParseResult.ArrayFormatter.formatErrorSync(error)[0]
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
- const decodeDictionary = Schema.decode(Schema.parseJson(DictionarySchema), {
52
- onExcessProperty: "error",
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 decodeApprovedWordList = Schema.decode(Schema.parseJson(ApprovedWordListSchema), {
57
- onExcessProperty: "error",
58
- errors: "all",
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,
@@ -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: Schema.NonEmptyTrimmedString,
6
- repository: Schema.NonEmptyTrimmedString,
7
- commit: Schema.NonEmptyTrimmedString,
8
- path: Schema.NonEmptyTrimmedString,
6
+ name: NonEmptyTrimmedString,
7
+ repository: NonEmptyTrimmedString,
8
+ commit: NonEmptyTrimmedString,
9
+ path: NonEmptyTrimmedString,
9
10
  })
10
11
 
11
- const DictionaryFormSchema = Schema.NonEmptyTrimmedString.pipe(
12
- Schema.pattern(DICTIONARY_FORM_PATTERN),
13
- )
12
+ const DictionaryFormSchema = NonEmptyTrimmedString.check(Schema.isPattern(DICTIONARY_FORM_PATTERN))
14
13
 
15
- const DictionaryWordSchema = Schema.NonEmptyTrimmedString.pipe(
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(Schema.NonEmptyTrimmedString),
22
- partsOfSpeech: Schema.optional(Schema.NonEmptyArray(Schema.NonEmptyTrimmedString)),
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
+ )
@@ -31,7 +31,7 @@ export function makeWinkTagger(): Tagger {
31
31
  }
32
32
  }
33
33
 
34
- export class TaggerService extends Context.Tag("TaggerService")<TaggerService, Tagger>() {}
34
+ export class TaggerService extends Context.Service<TaggerService, Tagger>()("TaggerService") {}
35
35
 
36
36
  const makeLazyWinkTagger = (): Tagger => {
37
37
  let tagger: Tagger | undefined