agent-simple-english 0.1.0 → 0.2.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 +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +174 -78
  4. package/commands/ste.md +1 -1
  5. package/hooks/hooks.json +1 -1
  6. package/package.json +13 -2
  7. package/src/adapter/feedback.ts +2 -1
  8. package/src/adapter/rule-summary.ts +46 -5
  9. package/src/cli/hook.ts +148 -52
  10. package/src/cli/main.ts +81 -9
  11. package/src/cli/observation-log.ts +257 -0
  12. package/src/cli/session-command.ts +14 -6
  13. package/src/cli/session-state.ts +3 -8
  14. package/src/cli/state-directory.ts +9 -0
  15. package/src/config/schema.ts +23 -0
  16. package/src/dictionary/README.md +61 -7
  17. package/src/dictionary/bundled-rule-data.ts +13 -0
  18. package/src/dictionary/configured.ts +16 -0
  19. package/src/dictionary/data/adjectival-participles.json +15 -0
  20. package/src/dictionary/data/hedging.json +22 -0
  21. package/src/dictionary/data/marketing.json +40 -0
  22. package/src/dictionary/data/phrasal-verbs.json +55 -0
  23. package/src/dictionary/form.ts +2 -0
  24. package/src/dictionary/load.ts +84 -6
  25. package/src/dictionary/rule-data.ts +12 -0
  26. package/src/dictionary/schema.ts +18 -1
  27. package/src/engine/case-fold.ts +17 -0
  28. package/src/engine/comments.ts +273 -23
  29. package/src/engine/diff-match.ts +189 -0
  30. package/src/engine/kinds.ts +19 -2
  31. package/src/engine/lint.ts +209 -238
  32. package/src/engine/markdown-syntax.ts +171 -0
  33. package/src/engine/markdown.ts +591 -245
  34. package/src/engine/paragraphs.ts +47 -6
  35. package/src/engine/phrase-matcher.ts +62 -0
  36. package/src/engine/rules/dictionary.ts +38 -3
  37. package/src/engine/rules/hedging.ts +19 -16
  38. package/src/engine/rules/marketing.ts +122 -56
  39. package/src/engine/rules/paragraph-length.ts +6 -1
  40. package/src/engine/rules/phrasal-verb.ts +36 -41
  41. package/src/engine/rules/registry.ts +1 -0
  42. package/src/engine/rules/verb-form.ts +26 -3
  43. package/src/engine/sentences.ts +359 -20
  44. package/src/engine/suppression.ts +174 -0
  45. package/src/engine/tokens.ts +2 -2
  46. package/src/engine/types.ts +19 -5
  47. package/src/extension/index.ts +63 -35
@@ -10,8 +10,8 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "simple-english",
13
- "description": "Apply Simplified Technical English rules to writes, edits, and git commit messages.",
14
- "version": "0.1.0",
13
+ "description": "Apply technical and house-style writing rules to writes, edits, and git commit messages.",
14
+ "version": "0.2.0",
15
15
  "author": {
16
16
  "name": "JIA YI"
17
17
  },
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "simple-english",
4
- "version": "0.1.0",
5
- "description": "Apply Simplified Technical English rules to writes, edits, and git commit messages.",
4
+ "version": "0.2.0",
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",
8
8
  "keywords": ["simplified-technical-english", "lint", "hooks"],
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # agent-simple-english
2
2
 
3
- `agent-simple-english` checks ASD-STE100 Simplified Technical English (STE).
3
+ `agent-simple-english` checks rules derived from ASD-STE100 Simplified Technical English (STE) and two enabled house-style rules.
4
4
  It supplies one Engine, one CLI, a pi Adapter, and a Claude Code Adapter.
5
5
  The Claude Code plugin uses CLI Hook mode to enforce the same rules.
6
6
 
@@ -9,7 +9,7 @@ It does not rewrite text because an automatic rewrite can change its meaning.
9
9
 
10
10
  ## What the pi Adapter does
11
11
 
12
- The pi Adapter adds its active STE rules to the model prompt before each agent turn.
12
+ The pi Adapter adds its active writing rules to the model prompt before each agent turn.
13
13
  It then applies the rules at three layers.
14
14
 
15
15
  1. **Write and edit gate.**
@@ -33,7 +33,7 @@ It then applies the rules at three layers.
33
33
 
34
34
  The pi Adapter checks Markdown prose and source comments according to the [content kinds](#content-kinds).
35
35
  It gives the line, column, rule ID, and suggested correction for a blocked tool call.
36
- A config or dictionary load error makes enabled write, edit, and commit gates fail closed.
36
+ A config, dictionary, or rule-data load error makes enabled write, edit, and commit gates fail closed.
37
37
 
38
38
  ## Install the Claude Code Adapter
39
39
 
@@ -52,7 +52,7 @@ The repository supplies the local marketplace manifest.
52
52
  Marketplace publication is outside this package release.
53
53
 
54
54
  At `SessionStart`, the Adapter loads the merged config for an enabled session.
55
- It reads the config from the session working directory and adds the active STE rule summary to context.
55
+ It reads the config from the session working directory and adds the active writing-rule summary to context.
56
56
  The summary honors `hard`, `soft`, and `off` rule settings plus `maxSentenceWords`.
57
57
 
58
58
  The `PreToolUse` gate checks `Write`, `Edit`, and `Bash` events.
@@ -166,8 +166,44 @@ In strict mode, it blocks a reply that has hard violations.
166
166
  A `UserPromptSubmit` event adds pending feedback to context and clears that feedback.
167
167
  Every hook reads the current session mode before it applies a gate.
168
168
  Malformed JSON returns a non-blocking error so Claude Code can continue.
169
- The hook also allows the event when configuration, dictionary, tagger, transcript, state, or file processing fails.
170
- It adds warning text.
169
+ The hook also allows the event when configuration, dictionary, rule-data, tagger, transcript, state, or file processing fails.
170
+ It adds warning text for these operational failures.
171
+ Observation-write failures are silent and do not change the hook output or decision.
172
+
173
+ ### Review hook observations
174
+
175
+ Enabled Hook mode logs every write, edit, static commit-message, and reply lint decision to the local XDG state directory.
176
+ Observation logging is on by default, and the log includes clean allows and soft Findings.
177
+ Plain lint runs, disabled hook sessions, and the pi Adapter do not write Observations.
178
+ Set `SIMPLE_ENGLISH_OBSERVE=0` to stop observation logging.
179
+
180
+ Monthly Observation files use `$XDG_STATE_HOME/simple-english/observations/YYYY-MM.jsonl`.
181
+ The default base directory is `~/.local/state`.
182
+ Each Finding stores its offending snippet for later review.
183
+ Verdicts use the separate `$XDG_STATE_HOME/simple-english/verdicts.jsonl` file.
184
+ These global, host-local records can contain snippets, working directories, and file paths.
185
+ New state directories use mode `0700`, and new JSONL files use mode `0600`.
186
+ Each record uses one append-mode write without a lock, so concurrent hooks can safely add complete lines.
187
+
188
+ Review each unjudged Finding:
189
+
190
+ ```sh
191
+ simple-english observe review
192
+ ```
193
+
194
+ Press `t` for a true positive or `f` for a false positive.
195
+ Press `s` to leave a Finding unjudged or `q` to quit.
196
+ A Verdict can include an optional note.
197
+ A false positive exists only after a human records that Verdict.
198
+ The latest Verdict for a Finding wins.
199
+
200
+ Show fire counts, judged counts, and false-positive rates for each rule:
201
+
202
+ ```sh
203
+ simple-english observe stats
204
+ ```
205
+
206
+ The report also shows total Observations and clean allows.
171
207
 
172
208
  ### Lint files and standard input
173
209
 
@@ -214,6 +250,7 @@ Soft violations can appear with exit code 0.
214
250
  ### CLI flags
215
251
 
216
252
  - `--json` writes one JSON report with `violations` and `summary` fields.
253
+ Each violation includes its offending sentence or paragraph as `snippet`.
217
254
 
218
255
  - `--config <path>` uses only that config file and disables config discovery.
219
256
 
@@ -221,6 +258,12 @@ Soft violations can appear with exit code 0.
221
258
  Valid values are `prose-file`, `slash-source`, `hash-source`, and `commit-message`.
222
259
  The form `--kind=<kind>` also works.
223
260
 
261
+ - `--help` writes the command usage.
262
+
263
+ - `--version` writes the package version.
264
+
265
+ - The command rejects an unknown flag as an argument error.
266
+
224
267
  - A path of `-` reads standard input.
225
268
  With no paths, the command also reads standard input.
226
269
 
@@ -238,10 +281,39 @@ It is the default for standard input, extensionless paths, and file types that h
238
281
  `commit-message` checks the complete input as a commit message.
239
282
 
240
283
  File extension matching does not depend on letter case.
241
- Source kinds ignore comment markers inside string literals.
284
+ Source kinds ignore comment markers inside supported string literal forms.
242
285
  All kinds preserve the original line and column.
243
- They ignore identifiers plus fenced and indented Markdown code.
244
- Inline Markdown code is outside every rule except `semicolon`.
286
+ They ignore identifiers, YAML frontmatter, valid GFM tables, and fenced, indented, and inline Markdown code.
287
+
288
+ ### Inline suppression
289
+
290
+ A suppression directive names one or more registered rule IDs and applies only to the next physical line.
291
+ Use this Markdown comment form in prose files:
292
+
293
+ ```md
294
+ <!-- ste-disable-next-line marketing -->
295
+ The robust estimator uses this sample.
296
+ ```
297
+
298
+ Use the matching plain comment form in slash and hash source files:
299
+
300
+ ```ts
301
+ // ste-disable-next-line marketing
302
+ // The robust estimator uses this sample.
303
+ ```
304
+
305
+ ```py
306
+ # ste-disable-next-line marketing
307
+ # The robust estimator uses this sample.
308
+ ```
309
+
310
+ Separate multiple rule IDs with whitespace.
311
+ Commas are also accepted as separators.
312
+ A missing or unknown rule ID produces an `invalid-suppression` violation, which is hard by default.
313
+
314
+ Directive recognition uses lightweight per-language comment extraction rather than full language parsers.
315
+ Known accepted losses are JavaScript regular expressions inside template expressions, Ruby and Perl regular-expression literals containing directive text, Perl `<<~` heredocs, adjacent multiline YAML flow scalars, and compact nested YAML block scalars with explicit indentation.
316
+ Engine fixtures pin these limitations pending per-language lexers.
245
317
 
246
318
  ## Configuration
247
319
 
@@ -261,7 +333,6 @@ The global fallback is `simple-english.json` in the pi agent config directory.
261
333
  The default pi agent config directory is `~/.pi/agent`.
262
334
  `PI_CODING_AGENT_DIR` can change that directory.
263
335
 
264
- The loader resolves a relative value from the working directory that requested the config.
265
336
  The loader reads a fallback file only when the new file at the same level is absent.
266
337
 
267
338
  This example contains every config key and every rule:
@@ -269,10 +340,19 @@ This example contains every config key and every rule:
269
340
  ```json
270
341
  {
271
342
  "maxSentenceWords": 25,
343
+ "exemptBlockQuotes": true,
344
+ "approvedWordsPath": "./approved-words.json",
345
+ "ruleDataExtensions": {
346
+ "phrasal-verb": ["config/phrasal-verbs.json"],
347
+ "hedging": ["config/hedging.json"],
348
+ "marketing": ["config/marketing.json"],
349
+ "adjectival-participle": ["config/adjectival-participles.json"]
350
+ },
272
351
  "rules": {
273
352
  "contraction": "hard",
274
353
  "dictionary-not-approved-word": "hard",
275
354
  "hedging": "soft",
355
+ "invalid-suppression": "hard",
276
356
  "marketing": "soft",
277
357
  "paragraph-length": "hard",
278
358
  "phrasal-verb": "hard",
@@ -291,117 +371,122 @@ A soft violation produces a report or warning but does not block the action.
291
371
  The `off` value disables that rule.
292
372
 
293
373
  `maxSentenceWords` must be a positive integer and has a default value of 25.
374
+ `exemptBlockQuotes` must be a boolean and has a default value of `false`.
375
+ When it is omitted or `false`, block quotes receive the same checks as other prose.
376
+ When it is `true`, CommonMark block quote content is exempt from `dictionary-not-approved-word`, `contraction`, `phrasal-verb`, `hedging`, and `marketing` checks.
377
+ All other rules continue to check block quote content.
378
+ `approvedWordsPath` selects a user-owned approved-word list.
379
+ The loader resolves a relative path from the working directory that requested the config.
380
+ This list replaces the bundled not-approved sample and any `SIMPLE_ENGLISH_DICTIONARY` replacement.
381
+ A missing, unreadable, or invalid list causes lint exit code 2.
382
+ The enabled pi Adapter gates fail closed after this error.
383
+ Claude Code Hook mode reports a warning and allows the event, as it does for other load errors.
294
384
  Unknown keys, unknown rule IDs, and invalid values cause a config error.
295
385
 
386
+ `ruleDataExtensions` maps each list-backed data set to more JSON data files.
387
+ The loader adds entries from these files after the bundled entries.
388
+ The loader resolves relative paths from the current working directory.
389
+ Each file must use the [package dictionary data format](src/dictionary/README.md).
390
+ An `adjectival-participle` extension adds exact tagged-token forms that the progressive and passive rules allow after a form of `be`.
391
+ A lint command reports an extension load error and continues with the bundled rule data.
392
+ The enabled pi Adapter fails closed after that error, while Claude Code Hook mode allows the event and adds a warning.
393
+
296
394
  ## Rule reference
297
395
 
298
- ### `contraction`
396
+ ### Rules derived from ASD-STE100
397
+
398
+ #### `contraction`
299
399
 
300
400
  Default: hard.
301
401
  Reports apostrophe contractions such as forms that end in `n't`, `'re`, `'ve`, `'ll`, `'d`, or `'m`.
302
402
  It also reports unambiguous forms that end in `'s`.
303
403
 
304
- ### `dictionary-not-approved-word`
404
+ #### `dictionary-not-approved-word`
305
405
 
306
406
  Default: hard.
307
- Reports an unapproved word or phrase from the bundled dictionary and supplies approved alternatives.
308
- Part-of-speech data limits applicable entries when that data exists.
309
- Matching does not depend on letter case.
310
-
311
- ### `hedging`
312
-
313
- Default: soft.
314
- Reports these phrases: `it is important to note`, `it should be noted`, `it is worth noting`, `please note that`, `as mentioned`, `as noted above`.
315
- A phrase match stays on one source line.
407
+ Without an approved-word list, this rule reports forms from the bundled not-approved sample and supplies approved alternatives.
408
+ Part-of-speech data limits applicable sample entries when that data exists.
409
+ With an approved-word list, this rule reports each prose token that the list does not contain.
410
+ Matching uses exact surface forms without regard to case.
411
+ See [Configuration](#configuration) for list selection, precedence, and load errors.
316
412
 
317
- ### `marketing`
318
-
319
- Default: soft.
320
- Reports the first listed term in each token.
321
- Matching does not depend on letter case, and it also examines components of hyphenated tokens.
322
-
323
- - `seamless`.
324
- - `seamlessly`.
325
- - `robust`.
326
- - `powerful`.
327
- - `cutting-edge`.
328
- - `effortless`.
329
- - `effortlessly`.
330
- - `world-class`.
331
- - `next-generation`.
332
- - `revolutionary`.
333
- - `blazing`.
334
- - `lightning-fast`.
335
- - `elegant`.
336
- - `delightful`.
337
- - `turnkey`.
338
- - `best-in-class`.
339
- - `state-of-the-art`.
340
- - `game-changing`.
341
- - `battle-tested`.
342
- - `enterprise-grade`.
343
- - `supercharge`.
344
- - `unleash`.
345
- - `empower`.
346
- - `empowers`.
347
-
348
- ### `paragraph-length`
413
+ #### `paragraph-length`
349
414
 
350
415
  Default: hard.
351
416
  Reports a prose paragraph that has more than six sentences.
352
417
  Markdown block boundaries and list items start separate paragraphs.
418
+ Sentence counts use the [`sentence-length`](#sentence-length) segmentation rules.
353
419
 
354
- ### `phrasal-verb`
420
+ #### `phrasal-verb`
355
421
 
356
422
  Default: hard.
357
- Reports these forms and supplies the listed suggestion:
358
-
359
- | Forms | Suggestion |
360
- | --- | --- |
361
- | `carry out`, `carries out`, `carried out`, `carrying out` | `do`. |
362
- | `spin up`, `spins up`, `spun up`, `spinning up` | `start`. |
363
- | `spin down`, `spins down`, `spun down`, `spinning down` | `stop`. |
364
- | `tear down`, `tears down`, `tore down`, `torn down`, `tearing down` | `remove`. |
365
- | `reach out`, `reaches out`, `reached out`, `reaching out` | `ask`. |
366
- | `dive into`, `dives into`, `dived into`, `dove into`, `diving into` | `examine`. |
367
- | `kick off`, `kicks off`, `kicked off`, `kicking off` | `start`. |
368
- | `roll out`, `rolls out`, `rolled out`, `rolling out` | `release`. |
369
- | `ramp up`, `ramps up`, `ramped up`, `ramping up` | `increase`. |
370
- | `circle back`, `circles back`, `circled back`, `circling back` | `return`. |
371
- | `drill down`, `drills down`, `drilled down`, `drilling down` | `examine`. |
423
+ Reports listed forms and supplies their suggestion.
424
+ The bundled forms and suggestions are in [`src/dictionary/data/phrasal-verbs.json`](src/dictionary/data/phrasal-verbs.json).
372
425
 
373
426
  Matching does not depend on letter case.
374
427
  A phrase match stays on one source line.
375
428
 
376
- ### `semicolon`
429
+ #### `semicolon`
377
430
 
378
431
  Default: hard.
379
- Reports each semicolon and asks for two sentences.
380
- This rule also checks semicolons inside inline Markdown code.
432
+ Reports each prose semicolon and asks for two sentences.
381
433
 
382
- ### `sentence-length`
434
+ #### `sentence-length`
383
435
 
384
436
  Default: hard.
385
437
  Reports a sentence above `maxSentenceWords`.
386
438
  The default maximum is 25 words.
439
+ Sentence segmentation recognizes only `e.g.`, `i.e.`, `etc.`, `vs.`, `Fig.`, `No.`, and a single capital letter followed by a period as abbreviation forms.
440
+ It keeps an abbreviation in the current sentence when lowercase prose follows it.
441
+ It preserves a sentence boundary before capitalized prose, except after a single capital letter or before a short `Fig.` or `No.` designator such as `Fig. A` or `No. 7`.
442
+ Markdown block boundaries stop abbreviation lookahead.
387
443
 
388
- ### `verb-progressive`
444
+ #### `verb-progressive`
389
445
 
390
446
  Default: hard.
391
447
  Reports a form of `be` followed by an `-ing` verb, with optional adverbs or `not` between them.
392
448
 
393
- ### `verb-passive`
449
+ #### `verb-passive`
394
450
 
395
451
  Default: soft.
396
452
  Reports a form of `be` followed by a past participle, with optional adverbs or `not` between them.
397
453
 
398
- ### `verb-perfect`
454
+ Before either rule reports a finding, it checks the exact tagged token against the [bundled adjectival-participle allowlist](src/dictionary/data/adjectival-participles.json).
455
+ The `ruleDataExtensions.adjectival-participle` configuration extends that allowlist.
456
+
457
+ #### `verb-perfect`
399
458
 
400
459
  Default: hard.
401
460
  Reports auxiliary `have` followed by a past participle, with optional adverbs or `not` between them.
402
461
 
403
462
  The three verb rules and applicable dictionary entries use the bundled English part-of-speech tagger.
404
463
 
464
+ ### Directive validation
465
+
466
+ #### `invalid-suppression`
467
+
468
+ Default: hard.
469
+ Reports a suppression directive that has no rule ID or names an unknown rule ID.
470
+
471
+ ### House-style rules
472
+
473
+ These rules define package house style.
474
+ They do not come from ASD-STE100.
475
+ The default config enables both rules.
476
+
477
+ #### `hedging`
478
+
479
+ Default: soft.
480
+ Reports the phrases in [`src/dictionary/data/hedging.json`](src/dictionary/data/hedging.json).
481
+ A phrase match stays on one source line.
482
+
483
+ #### `marketing`
484
+
485
+ Default: soft.
486
+ Reports complete listed forms from [`src/dictionary/data/marketing.json`](src/dictionary/data/marketing.json).
487
+ A multi-word form stays on one source line.
488
+ Matching does not depend on letter case, and the rule also reports the first listed single-token component of a hyphenated token.
489
+
405
490
  ## Dictionary and attribution
406
491
 
407
492
  The package vendors dictionary data converted from Cameron Moore's MIT-licensed [`ctotheameron/pi-ste`](https://github.com/ctotheameron/pi-ste).
@@ -412,8 +497,18 @@ This package does not include the official specification or the complete ASD dic
412
497
  Get the current official specification from the [ASD-STE100 site](https://www.asd-ste100.org/).
413
498
  This project has no affiliation with or endorsement from ASD.
414
499
 
415
- The package dictionary format and match rules are in [`src/dictionary/README.md`](src/dictionary/README.md).
416
- Set `SIMPLE_ENGLISH_DICTIONARY` to a replacement dictionary file if necessary.
500
+ The package dictionary format and token rules are in [`src/dictionary/README.md`](src/dictionary/README.md).
501
+
502
+ To create an approved-word list, use your own licensed copy of the current ASD-STE100 specification.
503
+ Extract the approved words into the [package approved-word list format](src/dictionary/README.md#approved-word-list).
504
+ Follow that format's exact surface-form requirements.
505
+ Add source metadata that identifies your licensed revision and extraction record.
506
+ Repeat the extraction and update the metadata when you adopt a new licensed revision.
507
+ Keep the list private unless your license lets you distribute it.
508
+ This package does not extract, include, or distribute that data.
509
+ See [Configuration](#configuration) to select the list.
510
+
511
+ Set `SIMPLE_ENGLISH_DICTIONARY` to replace only the bundled not-approved sample.
417
512
  Hook mode resolves a relative replacement path from the session working directory.
418
513
  A lint command reports a replacement dictionary load error and continues with all other rules.
419
514
  The enabled pi Adapter fails closed after that error.
@@ -423,6 +518,7 @@ The enabled pi Adapter fails closed after that error.
423
518
  ```sh
424
519
  bun install
425
520
  bun run test
521
+ bun run bench:markdown
426
522
  bun run lint
427
523
  bun run typecheck
428
524
  npm publish --dry-run
package/commands/ste.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Control STE for this Claude Code session
2
+ description: Control writing-rule enforcement for this Claude Code session
3
3
  argument-hint: on|off|status|strict|strict off
4
4
  allowed-tools: Bash
5
5
  disable-model-invocation: true
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Inject active STE rules, gate changes, and return reply feedback on the next prompt.",
2
+ "description": "Inject active writing rules, gate changes, and return reply feedback on the next prompt.",
3
3
 
4
4
  "hooks": {
5
5
  "SessionStart": [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-simple-english",
3
- "version": "0.1.0",
4
- "description": "ASD-STE100 Simplified Technical English lint engine, CLI, and host adapters",
3
+ "version": "0.2.0",
4
+ "description": "Technical and house-style English lint engine, CLI, and host adapters",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "author": {
@@ -33,11 +33,22 @@
33
33
  },
34
34
  "scripts": {
35
35
  "test": "vitest run",
36
+ "bench:markdown": "vitest bench test/engine/markdown.bench.ts",
36
37
  "lint": "biome check .",
37
38
  "typecheck": "tsc --noEmit"
38
39
  },
39
40
  "dependencies": {
41
+ "@lezer/html": "^1.3.12",
42
+ "@lezer/markdown": "^1.7.2",
40
43
  "effect": "^3.14.0",
44
+ "micromark": "^4.0.2",
45
+ "micromark-extension-frontmatter": "^2.0.0",
46
+ "micromark-extension-gfm-table": "^2.1.1",
47
+ "micromark-util-character": "^2.1.1",
48
+ "micromark-util-html-tag-name": "^2.0.1",
49
+ "micromark-util-normalize-identifier": "^2.0.1",
50
+ "micromark-util-types": "^2.0.2",
51
+ "unicode-case-folding": "^1.1.1",
41
52
  "wink-eng-lite-web-model": "^1.8.1",
42
53
  "wink-nlp": "^2.4.0"
43
54
  },
@@ -8,8 +8,9 @@ function suggestedFix(violation: Violation): string {
8
8
  }
9
9
  const fixes: Readonly<Record<RuleId, string>> = {
10
10
  contraction: "Write the contracted words in full.",
11
- "dictionary-not-approved-word": "Replace the unapproved word with an approved alternative.",
11
+ "dictionary-not-approved-word": "Use a word from the approved-word list.",
12
12
  hedging: "Delete the hedging phrase.",
13
+ "invalid-suppression": "Name one or more registered rule IDs.",
13
14
  marketing: "Replace the phrase with factual language.",
14
15
  "paragraph-length": "Split the paragraph into shorter paragraphs.",
15
16
  "phrasal-verb": "Replace the phrasal verb with one approved verb.",
@@ -7,6 +7,7 @@ const DEFAULT_RULE_SETTINGS: Readonly<Record<RuleId, RuleSetting>> = {
7
7
  contraction: "hard",
8
8
  "dictionary-not-approved-word": "hard",
9
9
  hedging: "soft",
10
+ "invalid-suppression": "hard",
10
11
  marketing: "soft",
11
12
  "paragraph-length": "hard",
12
13
  "phrasal-verb": "hard",
@@ -21,6 +22,7 @@ export const RULE_SUMMARIES: Readonly<Record<RuleId, string>> = {
21
22
  contraction: "Do not use contractions. Write the words in full.",
22
23
  "dictionary-not-approved-word": "Use approved words from the STE dictionary.",
23
24
  hedging: "Remove hedging phrases.",
25
+ "invalid-suppression": "Name registered rule IDs in suppression directives.",
24
26
  marketing: "Use factual language instead of marketing language.",
25
27
  "paragraph-length": "Use no more than six sentences in one paragraph.",
26
28
  "phrasal-verb": "Use an approved single-word verb instead of a phrasal verb.",
@@ -63,9 +65,16 @@ export function formatStatusSummary(
63
65
  ].join("\n")
64
66
  }
65
67
 
66
- export function ruleSummary(config: SteConfig): string {
67
- const maxSentenceWords = config.maxSentenceWords ?? DEFAULT_MAX_SENTENCE_WORDS
68
- const rules = (Object.keys(RULE_SUMMARIES) as RuleId[])
68
+ const HOUSE_STYLE_RULE_IDS: ReadonlySet<RuleId> = new Set(["hedging", "marketing"])
69
+ const DIRECTIVE_RULE_IDS: ReadonlySet<RuleId> = new Set(["invalid-suppression"])
70
+
71
+ function formatRuleGroup(
72
+ heading: string,
73
+ ruleIds: readonly RuleId[],
74
+ config: SteConfig,
75
+ maxSentenceWords: number,
76
+ ): string | undefined {
77
+ const rules = ruleIds
69
78
  .filter((ruleId) => resolvedRuleSetting(config, ruleId) !== "off")
70
79
  .map((ruleId) => {
71
80
  const summary =
@@ -74,6 +83,38 @@ export function ruleSummary(config: SteConfig): string {
74
83
  : RULE_SUMMARIES[ruleId]
75
84
  return `- [${resolvedRuleSetting(config, ruleId)}] ${summary}`
76
85
  })
77
- .join("\n")
78
- return `## Simplified Technical English\n\nFollow these STE rules in prose that you write or edit:\n${rules}\n\nWrites, edits, and git commit messages reject hard violations. Correct the reported text and retry. Soft violations produce warnings.`
86
+ if (rules.length === 0) return undefined
87
+ return `### ${heading}\n\n${rules.join("\n")}`
88
+ }
89
+
90
+ export function ruleSummary(config: SteConfig): string {
91
+ const maxSentenceWords = config.maxSentenceWords ?? DEFAULT_MAX_SENTENCE_WORDS
92
+ const ruleIds = Object.keys(RULE_SUMMARIES) as RuleId[]
93
+ const sections = [
94
+ formatRuleGroup(
95
+ "Rules derived from ASD-STE100 Simplified Technical English",
96
+ ruleIds.filter(
97
+ (ruleId) => !HOUSE_STYLE_RULE_IDS.has(ruleId) && !DIRECTIVE_RULE_IDS.has(ruleId),
98
+ ),
99
+ config,
100
+ maxSentenceWords,
101
+ ),
102
+ formatRuleGroup(
103
+ "Directive validation",
104
+ ruleIds.filter((ruleId) => DIRECTIVE_RULE_IDS.has(ruleId)),
105
+ config,
106
+ maxSentenceWords,
107
+ ),
108
+ formatRuleGroup(
109
+ "House-style rules",
110
+ ruleIds.filter((ruleId) => HOUSE_STYLE_RULE_IDS.has(ruleId)),
111
+ config,
112
+ maxSentenceWords,
113
+ ),
114
+ ].filter((section): section is string => section !== undefined)
115
+ const rules =
116
+ sections.length === 0
117
+ ? "No writing rules are enabled."
118
+ : `Apply these enabled rules to prose that you write or edit:\n\n${sections.join("\n\n")}`
119
+ return `## Writing rules\n\n${rules}\n\nWrites, edits, and git commit messages reject hard violations. Correct the reported text and retry. Soft violations produce warnings.`
79
120
  }