chaffjs 0.9.0 → 0.11.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 (180) hide show
  1. package/dist/cli-text.d.ts.map +1 -1
  2. package/dist/cli-text.js +2 -0
  3. package/dist/cli-text.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +5 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/detectors/char-shape.d.ts.map +1 -1
  8. package/dist/detectors/char-shape.js +34 -2
  9. package/dist/detectors/char-shape.js.map +1 -1
  10. package/dist/detectors/en-shape.d.ts.map +1 -1
  11. package/dist/detectors/en-shape.js +110 -9
  12. package/dist/detectors/en-shape.js.map +1 -1
  13. package/dist/detectors/gram-place.d.ts +14 -0
  14. package/dist/detectors/gram-place.d.ts.map +1 -0
  15. package/dist/detectors/gram-place.js +39 -0
  16. package/dist/detectors/gram-place.js.map +1 -0
  17. package/dist/detectors/index.d.ts.map +1 -1
  18. package/dist/detectors/index.js +4 -1
  19. package/dist/detectors/index.js.map +1 -1
  20. package/dist/detectors/lexicon-match.d.ts +9 -0
  21. package/dist/detectors/lexicon-match.d.ts.map +1 -0
  22. package/dist/detectors/lexicon-match.js +40 -0
  23. package/dist/detectors/lexicon-match.js.map +1 -0
  24. package/dist/detectors/lexicon.d.ts.map +1 -1
  25. package/dist/detectors/lexicon.js +20 -20
  26. package/dist/detectors/lexicon.js.map +1 -1
  27. package/dist/detectors/phrase-match.d.ts.map +1 -1
  28. package/dist/detectors/phrase-match.js +7 -6
  29. package/dist/detectors/phrase-match.js.map +1 -1
  30. package/dist/detectors/repeated-head.d.ts.map +1 -1
  31. package/dist/detectors/repeated-head.js +15 -5
  32. package/dist/detectors/repeated-head.js.map +1 -1
  33. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  34. package/dist/detectors/sentence-ending.js +7 -3
  35. package/dist/detectors/sentence-ending.js.map +1 -1
  36. package/dist/detectors/signals.d.ts.map +1 -1
  37. package/dist/detectors/signals.js +162 -22
  38. package/dist/detectors/signals.js.map +1 -1
  39. package/dist/detectors/structure-tree.d.ts +5 -0
  40. package/dist/detectors/structure-tree.d.ts.map +1 -1
  41. package/dist/detectors/structure-tree.js +50 -1
  42. package/dist/detectors/structure-tree.js.map +1 -1
  43. package/dist/detectors/team.d.ts.map +1 -1
  44. package/dist/detectors/team.js +7 -2
  45. package/dist/detectors/team.js.map +1 -1
  46. package/dist/detectors/word-list.d.ts +3 -0
  47. package/dist/detectors/word-list.d.ts.map +1 -0
  48. package/dist/detectors/word-list.js +3 -0
  49. package/dist/detectors/word-list.js.map +1 -0
  50. package/dist/document.d.ts.map +1 -1
  51. package/dist/document.js +22 -9
  52. package/dist/document.js.map +1 -1
  53. package/dist/lexicon-tokens.d.ts +4 -0
  54. package/dist/lexicon-tokens.d.ts.map +1 -0
  55. package/dist/lexicon-tokens.js +23 -0
  56. package/dist/lexicon-tokens.js.map +1 -0
  57. package/dist/page-furniture.d.ts +10 -0
  58. package/dist/page-furniture.d.ts.map +1 -0
  59. package/dist/page-furniture.js +35 -0
  60. package/dist/page-furniture.js.map +1 -0
  61. package/dist/plugin.d.ts +15 -1
  62. package/dist/plugin.d.ts.map +1 -1
  63. package/dist/profile/parse.d.ts.map +1 -1
  64. package/dist/profile/parse.js +15 -2
  65. package/dist/profile/parse.js.map +1 -1
  66. package/dist/render/compact.d.ts.map +1 -1
  67. package/dist/render/compact.js +13 -2
  68. package/dist/render/compact.js.map +1 -1
  69. package/dist/rule-load.d.ts.map +1 -1
  70. package/dist/rule-load.js +1 -0
  71. package/dist/rule-load.js.map +1 -1
  72. package/dist/run.d.ts.map +1 -1
  73. package/dist/run.js +22 -8
  74. package/dist/run.js.map +1 -1
  75. package/dist/sentence-shape.d.ts +6 -0
  76. package/dist/sentence-shape.d.ts.map +1 -1
  77. package/dist/sentence-shape.js +15 -0
  78. package/dist/sentence-shape.js.map +1 -1
  79. package/dist/structure/bare-numbers.d.ts +14 -0
  80. package/dist/structure/bare-numbers.d.ts.map +1 -0
  81. package/dist/structure/bare-numbers.js +53 -0
  82. package/dist/structure/bare-numbers.js.map +1 -0
  83. package/dist/structure/build.d.ts +2 -0
  84. package/dist/structure/build.d.ts.map +1 -1
  85. package/dist/structure/build.js +18 -8
  86. package/dist/structure/build.js.map +1 -1
  87. package/dist/structure/date-order.d.ts +12 -0
  88. package/dist/structure/date-order.d.ts.map +1 -0
  89. package/dist/structure/date-order.js +30 -0
  90. package/dist/structure/date-order.js.map +1 -0
  91. package/dist/structure/issues.d.ts.map +1 -1
  92. package/dist/structure/issues.js +11 -2
  93. package/dist/structure/issues.js.map +1 -1
  94. package/dist/structure/of.d.ts.map +1 -1
  95. package/dist/structure/of.js +4 -2
  96. package/dist/structure/of.js.map +1 -1
  97. package/dist/structure/relative-find.d.ts +2 -0
  98. package/dist/structure/relative-find.d.ts.map +1 -1
  99. package/dist/structure/relative-find.js +11 -0
  100. package/dist/structure/relative-find.js.map +1 -1
  101. package/dist/structure/runs.d.ts +20 -0
  102. package/dist/structure/runs.d.ts.map +1 -0
  103. package/dist/structure/runs.js +63 -0
  104. package/dist/structure/runs.js.map +1 -0
  105. package/dist/structure/total.d.ts +15 -0
  106. package/dist/structure/total.d.ts.map +1 -0
  107. package/dist/structure/total.js +143 -0
  108. package/dist/structure/total.js.map +1 -0
  109. package/dist/structure/universal.d.ts +2 -2
  110. package/dist/structure/universal.d.ts.map +1 -1
  111. package/dist/structure/universal.js +13 -4
  112. package/dist/structure/universal.js.map +1 -1
  113. package/dist/structure/unnumbered.d.ts +15 -0
  114. package/dist/structure/unnumbered.d.ts.map +1 -0
  115. package/dist/structure/unnumbered.js +28 -0
  116. package/dist/structure/unnumbered.js.map +1 -0
  117. package/dist/structure/weekday.d.ts +7 -0
  118. package/dist/structure/weekday.d.ts.map +1 -0
  119. package/dist/structure/weekday.js +21 -0
  120. package/dist/structure/weekday.js.map +1 -0
  121. package/dist/version.d.ts +5 -0
  122. package/dist/version.d.ts.map +1 -1
  123. package/dist/version.js +17 -2
  124. package/dist/version.js.map +1 -1
  125. package/package.json +3 -3
  126. package/profiles/statute.yaml +5 -1
  127. package/rules/ai-tell.yaml +1 -0
  128. package/rules/closing-cliche.yaml +1 -0
  129. package/rules/concrete-evidence-density.yaml +2 -0
  130. package/rules/cushion-phrase-density.yaml +1 -0
  131. package/rules/date-order.yaml +26 -0
  132. package/rules/date-weekday-mismatch.yaml +26 -0
  133. package/rules/double-keigo.yaml +1 -0
  134. package/rules/empty-intensifier.yaml +1 -0
  135. package/rules/excessive-hedging.yaml +1 -0
  136. package/rules/hiragana-fukushi.yaml +1 -0
  137. package/rules/internal-jargon.yaml +1 -1
  138. package/rules/max-kanji-continuous.yaml +2 -0
  139. package/rules/ngram-repetition.yaml +2 -0
  140. package/rules/padded-intro.yaml +1 -0
  141. package/rules/repeated-conjunction.yaml +1 -0
  142. package/rules/sasete-itadaku.yaml +1 -0
  143. package/rules/total-mismatch.yaml +26 -0
  144. package/rules/unqualified-superlative.yaml +1 -0
  145. package/src/cli-text.ts +2 -0
  146. package/src/cli.ts +5 -1
  147. package/src/detectors/char-shape.ts +38 -3
  148. package/src/detectors/en-shape.ts +138 -7
  149. package/src/detectors/gram-place.ts +41 -0
  150. package/src/detectors/index.ts +4 -1
  151. package/src/detectors/lexicon-match.ts +44 -0
  152. package/src/detectors/lexicon.ts +25 -22
  153. package/src/detectors/phrase-match.ts +8 -7
  154. package/src/detectors/repeated-head.ts +21 -7
  155. package/src/detectors/sentence-ending.ts +7 -3
  156. package/src/detectors/signals.ts +188 -21
  157. package/src/detectors/structure-tree.ts +67 -1
  158. package/src/detectors/team.ts +7 -2
  159. package/src/detectors/word-list.ts +2 -0
  160. package/src/document.ts +25 -9
  161. package/src/lexicon-tokens.ts +28 -0
  162. package/src/page-furniture.ts +45 -0
  163. package/src/plugin.ts +24 -3
  164. package/src/profile/parse.ts +15 -2
  165. package/src/render/compact.ts +15 -2
  166. package/src/rule-load.ts +1 -0
  167. package/src/run.ts +24 -8
  168. package/src/sentence-shape.ts +16 -0
  169. package/src/structure/bare-numbers.ts +78 -0
  170. package/src/structure/build.ts +27 -9
  171. package/src/structure/date-order.ts +44 -0
  172. package/src/structure/issues.ts +16 -2
  173. package/src/structure/of.ts +4 -2
  174. package/src/structure/relative-find.ts +13 -0
  175. package/src/structure/runs.ts +71 -0
  176. package/src/structure/total.ts +183 -0
  177. package/src/structure/universal.ts +14 -3
  178. package/src/structure/unnumbered.ts +33 -0
  179. package/src/structure/weekday.ts +25 -0
  180. package/src/version.ts +21 -3
package/dist/version.js CHANGED
@@ -6,6 +6,21 @@ import { dirname, join } from "node:path";
6
6
  * SARIF の tool.version がそれで、指摘を出していない版に指摘が紐づく。
7
7
  */
8
8
  const MANIFEST = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
9
- const versionOf = (raw) => typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string" ? raw.version : "0.0.0";
10
- export const VERSION = versionOf(JSON.parse(readFileSync(MANIFEST, "utf8")));
9
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ const versionOf = (raw) => (isRecord(raw) && typeof raw["version"] === "string" ? raw["version"] : "0.0.0");
11
+ const BUNDLED = /^@chaffjs\/lang-/u;
12
+ /**
13
+ * `chaff --version` の行。chaffjs と、同梱の言語パッケージ。chaffjs は言語パッケージをちょうどの版で依存に書くので、manifest がそのまま答え。
14
+ */
15
+ export const versionLines = (raw) => {
16
+ const dependencies = isRecord(raw) && isRecord(raw["dependencies"]) ? raw["dependencies"] : {};
17
+ const bundled = Object.entries(dependencies)
18
+ .filter((entry) => BUNDLED.test(entry[0]) && typeof entry[1] === "string")
19
+ .sort(([left], [right]) => left.localeCompare(right))
20
+ .map(([name, version]) => `${name} ${version}`);
21
+ return [`chaffjs ${versionOf(raw)}`, ...bundled];
22
+ };
23
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf8"));
24
+ export const VERSION = versionOf(manifest);
25
+ export const VERSION_LINES = versionLines(manifest);
11
26
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C;;;GAGG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AAErF,MAAM,SAAS,GAAG,CAAC,GAAY,EAAU,EAAE,CACzC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,SAAS,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAEzH,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C;;;GAGG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AAErF,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAE5I,MAAM,SAAS,GAAG,CAAC,GAAY,EAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAE7H,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAEpC;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAY,EAAY,EAAE;IACrD,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/F,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC;SACzC,MAAM,CAAC,CAAC,KAAK,EAA6B,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;SACpG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC;IAClD,OAAO,CAAC,WAAW,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AAErE,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaffjs",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Prose validation harness. Finds what is hard to read, and never rewrites it for you.",
5
5
  "keywords": [
6
6
  "chaff",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@anthropic-ai/sdk": "^0.125.0",
57
- "@chaffjs/lang-en": "0.7.0",
58
- "@chaffjs/lang-ja": "0.8.0",
57
+ "@chaffjs/lang-en": "0.9.0",
58
+ "@chaffjs/lang-ja": "0.10.0",
59
59
  "mdast-util-from-markdown": "^2.0.3",
60
60
  "mdast-util-frontmatter": "^2.0.1",
61
61
  "mdast-util-gfm-table": "^2.0.0",
@@ -26,6 +26,9 @@ ja:
26
26
  address_end: "[^\\p{Script=Han}]|$"
27
27
  # 条の見出しは、条の前の行に括弧だけで書く(「(解雇の予告)」)。括弧の中が、すぐ次の条の見出しになる。
28
28
  caption: "^[ \\t\u3000]*[((]([^()()]{1,40})[))][ \\t\u3000]*$"
29
+ # 古い法令は第 2 項以降に番号を振らず、全角空白で字下げした行を新しい項(深さ 2)にする。
30
+ # 条の行に本文が続く条(「第三十四条 使用者は、…。」)の中だけ。契約書の「第1条(目的)」の次の字下げの行は、第 1 項の本文。
31
+ unnumbered: { indent: "\u3000", inside: article, depth: 2 }
29
32
  # 書いた場所から番地が決まる参照(前条・次項・同号・本条・前各項・前二項・前条第二項)。
30
33
  relative:
31
34
  before: [前]
@@ -42,7 +45,8 @@ ja:
42
45
  # 漢字の後ろの「前条」は語の一部(事前条件)。後ろの決まりは address_end と connectives を使う。
43
46
  not_after: "\\p{Script=Han}"
44
47
  # 読み替え(「『前条』とあるのは『…』と読み替える」)の括弧の中の前条・同項は、読み替える先の文の言葉。
45
- substitution: { open: 「, close: 」, after: [とあるのは], before: [とあるのは, とあるのは、] }
48
+ # 中の「第三条」も読み替える先の法令の番地なので、この文書の木では引かず、document の名前の文書を指す。
49
+ substitution: { open: 「, close: 」, after: [とあるのは], before: [とあるのは, とあるのは、], document: (読み替えの中) }
46
50
  # 条を書かない「第一項」「第二号」は、書いた条・項の中を指す。
47
51
  inside: [項, 号]
48
52
  # 並びをつなぐもの(正規表現)。「第三十三条第七項若しくは第九項」の「第九項」は、前の参照(第三十三条)の続き。
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: ai-tell
5
5
  word_list: ai-tell
6
+ uses: [pos]
6
7
  severity: info
7
8
 
8
9
  name:
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: stable
4
4
  how_to_find: phrase-match
5
5
  word_list: closing-cliche
6
+ uses: [pos]
6
7
  where: closing
7
8
  severity: warning
8
9
 
@@ -2,6 +2,8 @@ id: concrete-evidence-density
2
2
  layer: L1
3
3
  status: stable
4
4
  how_to_find: concrete-evidence
5
+ # 品詞があれば使う(数の語、言い回しの動詞)。無くても文字だけで動く。
6
+ uses: [pos]
5
7
  severity: info
6
8
 
7
9
  name:
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: cushion-density
5
5
  word_list: cushion-phrase
6
+ uses: [pos]
6
7
  severity: info
7
8
 
8
9
  name:
@@ -0,0 +1,26 @@
1
+ id: date-order
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: date-order
5
+ requires: [dates]
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 日程の順番が前後している
10
+ en: A date out of order in a schedule
11
+
12
+ why:
13
+ ja: 旅程やマイルストーンのように日付を並べた箇条書きや表で、ひとつだけ前の日付より早い日付があると、書き間違いか順番の入れ替わりかを読み手が確かめることになります。並びが古い順か新しい順かは多いほうで決め、それに逆らう日付だけを指します。
14
+ en: In a list or a table of dates, such as an itinerary or milestones, one date earlier than the one before it makes the reader check whether it is a typo or a swapped row. The direction of the list (oldest or newest first) is taken from most of its steps, and only a date against it is pointed out.
15
+
16
+ message:
17
+ ja: 「{date}」が、前後の日付の並びから外れています(前は「{previous}」)
18
+ en: "{date} breaks the order of the dates around it (after {previous})"
19
+
20
+ how_to_fix:
21
+ ja: 日付を確かめて、書き間違いなら直し、順番が違うなら並べ替えてください。
22
+ en: Check the date. Correct it if it is a typo, or move the row if the order is wrong.
23
+
24
+ levels: { strict: 1, normal: 1, relaxed: 1 }
25
+
26
+ use_for: [business, technical, blog]
@@ -0,0 +1,26 @@
1
+ id: date-weekday-mismatch
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: date-weekday-mismatch
5
+ requires: [dates]
6
+ severity: error
7
+
8
+ name:
9
+ ja: 日付と曜日の食い違い
10
+ en: Date and weekday disagree
11
+
12
+ why:
13
+ ja: 「2026年10月1日(金)」のように、日付と曜日が食い違っていると、読み手はどちらが正しいのか確かめに行くことになります。日程を書き写すときや、年をまたいで予定を使い回すときに起きます。年まで書いた日付の曜日だけを、暦と比べます。
14
+ en: When a date and its weekday disagree ("Friday, 1 October 2026"), the reader has to find out which one is right. It happens when a schedule is copied, or reused for another year. Only dates written with their year are checked against the calendar.
15
+
16
+ message:
17
+ ja: 「{date}」は{actual}です({written}と書いてあります)
18
+ en: "{date} is a {actual}, not a {written}"
19
+
20
+ how_to_fix:
21
+ ja: 日付と曜日のどちらが正しいかを確かめて、片方を直してください。
22
+ en: Check which is right, the date or the weekday, and correct the other.
23
+
24
+ levels: { strict: 1, normal: 1, relaxed: 1 }
25
+
26
+ use_for: [business, technical, blog]
@@ -3,6 +3,7 @@ layer: L3
3
3
  status: experimental
4
4
  how_to_find: phrase-match
5
5
  word_list: double-keigo
6
+ uses: [pos]
6
7
  languages: [ja]
7
8
  severity: warning
8
9
 
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: stable
4
4
  how_to_find: phrase-match
5
5
  word_list: empty-intensifier
6
+ uses: [pos]
6
7
  severity: warning
7
8
 
8
9
  name:
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: hedging-density
5
5
  word_list: excessive-hedging
6
+ uses: [pos]
6
7
  severity: warning
7
8
 
8
9
  name:
@@ -3,6 +3,7 @@ layer: L3
3
3
  status: experimental
4
4
  how_to_find: phrase-match
5
5
  word_list: hiragana-fukushi
6
+ uses: [pos]
6
7
  languages: [ja]
7
8
  severity: info
8
9
 
@@ -15,7 +15,7 @@ why:
15
15
 
16
16
  message:
17
17
  ja: 「{matched}」は社内でしか通じないかもしれません(そういう語が {count} 箇所)
18
- en: '"{matched}" may only work inside ({count} such words)'
18
+ en: '"{matched}" may only be understood inside your team ({count} such words)'
19
19
 
20
20
  how_to_fix:
21
21
  ja: 初出のところで普通の言葉に言い換えるか、括弧で説明を添えてください。jargon には辞書形(握る)でも語幹(巻き取)でも書けますが、複合動詞は語幹のほうが確実です。
@@ -2,6 +2,8 @@ id: max-kanji-continuous
2
2
  layer: L3
3
3
  status: experimental
4
4
  how_to_find: kanji-run
5
+ # 品詞があれば、住所(地名と数だけでできた連なり)を数えない。無くても文字だけで動く。
6
+ uses: [pos]
5
7
  languages: [ja]
6
8
  severity: warning
7
9
 
@@ -2,6 +2,8 @@ id: ngram-repetition
2
2
  layer: L1
3
3
  status: stable
4
4
  how_to_find: ngram-repetition
5
+ # 品詞があれば使う(数の語、言い回しの動詞)。無くても文字だけで動く。
6
+ uses: [pos]
5
7
  severity: info
6
8
 
7
9
  name:
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: stable
4
4
  how_to_find: phrase-match
5
5
  word_list: padded-intro
6
+ uses: [pos]
6
7
  where: opening
7
8
  severity: warning
8
9
 
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: repeated-conjunction
5
5
  word_list: paragraph-opener
6
+ uses: [pos]
6
7
  severity: warning
7
8
 
8
9
  name:
@@ -3,6 +3,7 @@ layer: L3
3
3
  status: experimental
4
4
  how_to_find: phrase-match
5
5
  word_list: sasete-itadaku
6
+ uses: [pos]
6
7
  languages: [ja]
7
8
  severity: info
8
9
 
@@ -0,0 +1,26 @@
1
+ id: total-mismatch
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: total-mismatch
5
+ requires: [quantities]
6
+ severity: error
7
+
8
+ name:
9
+ ja: 合計が内訳の和と合わない
10
+ en: A total that is not the sum of its items
11
+
12
+ why:
13
+ ja: 見積や請求の「合計」が、上に並べた金額の和と合わないと、読み手はどちらが正しいのかを計算し直すことになります。行を足したり消したりしたあとに、合計だけ直し忘れると起きます。箇条書きか表で、合計の語(合計・小計)で始まる行を、同じ単位で同じ列の金額の和と比べます。小計のあとの総計のように、足し方が何通りかあるときは、どの足し方でも合わないときだけ指します。
14
+ en: When the total of a quote or an invoice is not the sum of the amounts listed above it, the reader has to add them up again to find out which is right. It happens when a line is added or removed and the total is not updated. In a list or a table, a line starting with a total word (Total, Subtotal) is compared with the sum of the amounts in the same unit and column above it. When there is more than one way to add them up, such as a grand total after a subtotal and tax, it is pointed out only when none of them matches.
15
+
16
+ message:
17
+ ja: 合計「{written}」が、上の金額の和({sum})と合いません
18
+ en: "The total {written} is not the sum of the amounts above it ({sum})"
19
+
20
+ how_to_fix:
21
+ ja: 内訳の金額と合計のどちらが正しいかを確かめて、片方を直してください。
22
+ en: Check which is right, the items or the total, and correct the other.
23
+
24
+ levels: { strict: 1, normal: 1, relaxed: 1 }
25
+
26
+ use_for: [business, technical, blog]
@@ -3,6 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: unqualified-superlative
5
5
  word_list: superlative
6
+ uses: [pos]
6
7
  severity: warning
7
8
 
8
9
  name:
package/src/cli-text.ts CHANGED
@@ -12,6 +12,7 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
12
12
  chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
13
13
  判定役は chaff.yaml の ai_backend で選びます
14
14
  chaff init chaff.yaml を作る
15
+ chaff --version chaffjs と言語パッケージの版
15
16
  chaff eval <dir> 手元の文書で閾値を測り直す
16
17
  chaff explain <rule> そのルールの意図と根拠を読む
17
18
  chaff genres ジャンルの一覧
@@ -45,6 +46,7 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
45
46
  chaff test <file|dir>... also run the checks that read meaning (needs an API key)
46
47
  the judge is chosen by ai_backend in chaff.yaml
47
48
  chaff init create chaff.yaml
49
+ chaff --version the version of chaffjs and its language packages
48
50
  chaff eval <dir> re-measure the limits on your own documents
49
51
  chaff explain <rule> read what a rule is for and why
50
52
  chaff genres list the genres
package/src/cli.ts CHANGED
@@ -23,7 +23,7 @@ import { renderExplain } from "./render/explain.ts";
23
23
  import { renderFriendly } from "./render/friendly.ts";
24
24
  import { rulesJson } from "./render/rules-json.ts";
25
25
  import { renderSarif } from "./render/sarif.ts";
26
- import { VERSION } from "./version.ts";
26
+ import { VERSION, VERSION_LINES } from "./version.ts";
27
27
  import { runTree, treeTargets, type TreeContext } from "./commands/tree.ts";
28
28
  import { citeTargets, runCite } from "./commands/cite.ts";
29
29
  import { runSkill } from "./commands/skill.ts";
@@ -328,6 +328,10 @@ export const main = async (argv: readonly string[]): Promise<number> => {
328
328
  console.log(hostText(readConfig()).usage);
329
329
  return first === undefined ? 1 : 0;
330
330
  }
331
+ if (first === "--version" || first === "-v") {
332
+ console.log(VERSION_LINES.join("\n"));
333
+ return 0;
334
+ }
331
335
  const cliGenre = flag(argv, "--genre");
332
336
  if (cliGenre !== undefined && !GENRES.includes(cliGenre)) {
333
337
  console.error(hostText(readConfig()).unknownGenre(cliGenre));
@@ -1,15 +1,50 @@
1
- import type { Detector, DocumentProfile, Finding, Sentence } from "../plugin.ts";
1
+ import type { Detector, DocumentProfile, Finding, Sentence, Token } from "../plugin.ts";
2
+ import { compacted, placeOf } from "./gram-place.ts";
2
3
  import { maskAddresses } from "../address-chain.ts";
3
4
  import { proseText } from "../measure.ts";
4
5
 
5
6
  /**
6
- * 文字の並びだけを見る検出。品詞も語彙表も要らない。
7
+ * 文字の並びを見る検出。漢字の連なりが住所かどうかだけは、形態素解析の地名と数で決める。
7
8
  * 覆った箇所は空白になっているので、proseText で潰してから数える。
8
9
  */
9
10
  const KANJI_RUN = /[一-鿿]+/gu;
10
11
 
12
+ /** 地名(Geo)と、地名に付く単位(GeoUnit: 都・県・市・区・町)。 */
13
+ const isGeoName = (token: Token | undefined): boolean => token?.features?.["NameType"] === "Geo";
14
+ const isGeo = (token: Token | undefined): boolean => isGeoName(token) || token?.features?.["NameType"] === "GeoUnit";
15
+ const isNumber = (token: Token | undefined): boolean => token?.features?.["NumType"] === "Card";
16
+
17
+ /**
18
+ * 住所の語: 地名と地名の単位(NameType=Geo / GeoUnit)、数(NumType=Card)、数のすぐ後ろの助数詞(丁目、NounType=Class)。
19
+ * 辞書に無い町名(新橋)は、地名の後ろで数の前に来るときだけ住所の一部とする。
20
+ */
21
+ const isPlacePart = (tokens: readonly Token[], index: number): boolean =>
22
+ isGeo(tokens[index]) ||
23
+ isNumber(tokens[index]) ||
24
+ (isNumber(tokens[index - 1]) && tokens[index]?.features?.["NounType"] === "Class") ||
25
+ (isGeo(tokens[index - 1]) && isNumber(tokens[index + 1]));
26
+
27
+ /**
28
+ * 漢字の連なりが住所か(東京都港区新橋二丁目)。形態素解析が地名と読んだ語で始まり、住所の語だけでできている。
29
+ * 住所は決まった形で、ひらがなを挟んで書けない。組織名(日本経済団体連合会)は地名の後ろに普通の語が続くので住所ではない。品詞が無ければ判定しない。
30
+ */
31
+ const isPlaceName = (sentence: Sentence, run: string): boolean => {
32
+ const tokens = sentence.tokens;
33
+ const place = tokens === undefined ? undefined : placeOf(compacted(sentence.text, "word"), run);
34
+ if (tokens === undefined || place === undefined) return false;
35
+ const [start, end] = [sentence.span.start + place.start, sentence.span.start + place.end];
36
+ const covering = tokens.flatMap((token, index) => (token.span.start < end && start < token.span.end ? [index] : []));
37
+ const first = covering[0];
38
+ // 地名が単位を挟まずに続けば(東京大阪名古屋福岡)、住所ではなく地名の並び。
39
+ const listed = covering.some((index) => isGeoName(tokens[index]) && isGeoName(tokens[index + 1]));
40
+ return first !== undefined && isGeo(tokens[first]) && !listed && covering.every((index) => isPlacePart(tokens, index));
41
+ };
42
+
11
43
  const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined): string =>
12
- [...maskAddresses(proseText(sentence), profile).matchAll(KANJI_RUN)].reduce((longest, match) => (match[0].length > longest.length ? match[0] : longest), "");
44
+ [...maskAddresses(proseText(sentence), profile).matchAll(KANJI_RUN)]
45
+ .map((match) => match[0])
46
+ .filter((run) => !isPlaceName(sentence, run))
47
+ .reduce((longest, run) => (run.length > longest.length ? run : longest), "");
13
48
 
14
49
  /**
15
50
  * 漢字が続くと、どこで語が切れるのか読み手が探すことになる。
@@ -150,6 +150,139 @@ export const titleCaseMix: Detector = (doc, options): Finding[] => {
150
150
 
151
151
  const LIST_CONJUNCTION = new Set(["and", "or"]);
152
152
 
153
+ const isComma = (token: Token): boolean => token.surface === ",";
154
+
155
+ const commaBefore = (tokens: readonly Token[], at: number): boolean => tokens[at - 1]?.surface === ",";
156
+
157
+ /** 並列はこれをまたがない。セミコロンの前後は別の節。 */
158
+ const CLAUSE_BREAK = new Set([";", ":", "—"]);
159
+
160
+ /** 各 token の前で閉じていない括弧の数。括弧の中の読点(external users (e.g., guests), and ...)は外の並列を切らない。 */
161
+ const PAREN_STEP: Readonly<Record<string, number>> = { "(": 1, ")": -1 };
162
+
163
+ const depthsOf = (tokens: readonly Token[]): number[] =>
164
+ tokens.reduce<{ depths: number[]; open: number }>(
165
+ (acc, token) => ({ depths: [...acc.depths, acc.open], open: Math.max(0, acc.open + (PAREN_STEP[token.surface] ?? 0)) }),
166
+ { depths: [], open: 0 },
167
+ ).depths;
168
+
169
+ type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[] };
170
+
171
+ /** 冠詞や引用符を飛ばした、項目の頭の品詞。the parser と an exporter と samples を同じ形と見る。 */
172
+ const NOMINAL = new Set(["NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
173
+
174
+ const isContent = (token: Token): boolean => token.pos !== "DET" && token.pos !== "PUNCT" && token.pos !== "X";
175
+
176
+ const shapeOf = (item: readonly Token[]): string | undefined => {
177
+ const head = item.find(isContent);
178
+ if (head === undefined) return undefined;
179
+ return NOMINAL.has(head.pos) ? "NOMINAL" : head.pos;
180
+ };
181
+
182
+ const VERBAL = new Set(["VERB", "AUX"]);
183
+
184
+ /** 主語と述語のある項目。the team fixed the bug / these are crucial。gets us more は述語だけ。 */
185
+ const isClause = (item: readonly Token[]): boolean => {
186
+ const first = item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
187
+ if (first === undefined || !(first.pos === "DET" || NOMINAL.has(first.pos))) return false;
188
+ return item.some((token) => token !== first && VERBAL.has(token.pos));
189
+ };
190
+
191
+ /** 節の頭から and / or の手前までを、同じ深さの読点で項目に切る。Oxford comma の読点のあとは空なので項目にならない。 */
192
+ const itemsBefore = (clause: Clause, at: number): Token[][] => {
193
+ const level = clause.depths[at] ?? 0;
194
+ const start = clause.tokens.slice(0, at).findLastIndex((token) => CLAUSE_BREAK.has(token.surface)) + 1;
195
+ return clause.tokens
196
+ .slice(start, at)
197
+ .reduce<Token[][]>(
198
+ (items, token, offset) => {
199
+ if (isComma(token) && clause.depths[start + offset] === level) return [...items, []];
200
+ return [...items.slice(0, -1), [...(items.at(-1) ?? []), token]];
201
+ },
202
+ [[]],
203
+ )
204
+ .filter((item) => item.length > 0);
205
+ };
206
+
207
+ /** and / or の後ろの項目。次の読点か節の切れ目まで。 */
208
+ const itemAfter = (tokens: readonly Token[], at: number): Token[] => {
209
+ const end = tokens.findIndex((token, index) => index > at && (isComma(token) || CLAUSE_BREAK.has(token.surface)));
210
+ return tokens.slice(at + 1, end === -1 ? undefined : end);
211
+ };
212
+
213
+ /**
214
+ * 導入の句(After the review, / If it fails, / Finally, / To test it, / Based on the review,)は並列の項目ではない。
215
+ * 節の最初の項目が前置詞・接続詞・副詞・to・過去分詞で始まり、次の項目と頭の形が違えば外す。
216
+ * Quickly, quietly and carefully は残る。For managers, engineers and designers, も外れるが、前置詞のあとの名詞を
217
+ * 項目と読むと、Over this period, the subcommittees and the full committee ... を並びと誤る。こちらのほうが多い。
218
+ */
219
+ const LEAD_POS = new Set(["ADP", "SCONJ", "ADV", "PART"]);
220
+
221
+ const isParticiple = (token: Token): boolean => token.features?.["VerbForm"] === "Part";
222
+
223
+ const openingOf = (item: readonly Token[]): Token | undefined => item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
224
+
225
+ const openingKind = (item: readonly Token[]): string | undefined => {
226
+ const opening = openingOf(item);
227
+ return opening !== undefined && isParticiple(opening) ? "PARTICIPLE" : shapeOf(item);
228
+ };
229
+
230
+ const withoutLead = (items: readonly Token[][]): readonly Token[][] => {
231
+ const [first, second] = items;
232
+ const opening = first === undefined ? undefined : openingOf(first);
233
+ if (first === undefined || second === undefined || opening === undefined) return items;
234
+ if (!LEAD_POS.has(opening.pos) && !isParticiple(opening)) return items;
235
+ return openingKind(first) === openingKind(second) ? items : items.slice(1);
236
+ };
237
+
238
+ /**
239
+ * 形容詞のあとの読点は、名詞の前で形容詞を重ねているだけのことが多い(the long, winding bridge)。
240
+ * 次の項目が名詞で終わるならつなげ直す。形容詞そのものの並び(quick, cheap and reliable)は切ったまま。
241
+ */
242
+ const lastContent = (item: readonly Token[]): Token | undefined => item.findLast(isContent);
243
+
244
+ const joinAdjectives = (items: readonly Token[][]): Token[][] =>
245
+ items.reduce<Token[][]>((joined, item) => {
246
+ const previous = joined.at(-1);
247
+ const stacked = previous !== undefined && lastContent(previous)?.pos === "ADJ" && lastContent(item)?.pos !== "ADJ";
248
+ return stacked ? [...joined.slice(0, -1), [...previous, ...item]] : [...joined, item];
249
+ }, []);
250
+
251
+ /**
252
+ * 節を並べるなら、どの項目も節。Additionally, others can learn, and the mistake is rarer. の
253
+ * Additionally は項目ではなく、and の前の読点は節をつなぐ読点。名詞の並びは最初の項目に前置き
254
+ * (We shipped the parser)を抱えるので、この確かめは節の並びだけにする。
255
+ * and の後ろだけが節なのは主語の並び(The parser, the renderer and the exporter shipped.)なので外さない。
256
+ */
257
+ const clausesAgree = (items: readonly Token[][], after: readonly Token[]): boolean => {
258
+ const last = items.at(-1);
259
+ if (last === undefined || !isClause(last)) return true;
260
+ return isClause(after) && items.every(isClause);
261
+ };
262
+
263
+ /**
264
+ * 動詞で始まる項目を並べるなら、どの項目にも動詞がある。The scope, in contrast, is larger, and covers ... の
265
+ * The scope は項目ではなく、後ろの述語の主語。
266
+ */
267
+ const hasVerb = (item: readonly Token[]): boolean => item.some((token) => VERBAL.has(token.pos));
268
+
269
+ const predicatesAgree = (items: readonly Token[][], shape: string): boolean => !VERBAL.has(shape) || items.every(hasVerb);
270
+
271
+ /**
272
+ * 読点で区切った項目が and / or の前に 2 つ以上あり、最後の項目と and の後ろが同じ形のときだけ並列。
273
+ * 導入の読点(After the review, the team fixed the bug and shipped it.)や、節をつなぐ読点
274
+ * (We tested it, and the team shipped it.)は、Oxford comma を打つかどうかの選択を見せない。
275
+ */
276
+ const listAt = (clause: Clause, at: number): boolean | undefined => {
277
+ const items = withoutLead(joinAdjectives(itemsBefore(clause, at)));
278
+ const after = itemAfter(clause.tokens, at);
279
+ const last = items.at(-1);
280
+ const shape = last === undefined ? undefined : shapeOf(last);
281
+ if (items.length < 2 || shape === undefined || shape !== shapeOf(after)) return undefined;
282
+ if (!clausesAgree(items, after) || !predicatesAgree(items, shape)) return undefined;
283
+ return commaBefore(clause.tokens, at);
284
+ };
285
+
153
286
  /**
154
287
  * 3 つ以上の並列の最後の and / or の前に読点を打つか。Oxford comma。
155
288
  *
@@ -157,13 +290,11 @@ const LIST_CONJUNCTION = new Set(["and", "or"]);
157
290
  * 見るのは 1 つの文書で揃っているかだけ。spec §12.3。
158
291
  */
159
292
  const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
160
- const at = tokens.findIndex((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase()));
161
- if (at < 1) return undefined;
162
- const before = tokens.slice(0, at);
163
- // 並列が 3 つ以上あるときだけ判定できる。読点が 1 つも無ければ 2 つの並列。
164
- const commas = before.filter((token) => token.surface === ",").length;
165
- if (commas === 0) return undefined;
166
- return before.at(-1)?.surface === ",";
293
+ const clause = { tokens, depths: depthsOf(tokens) };
294
+ return tokens.reduce<boolean | undefined>(
295
+ (found, token, at) => found ?? (at > 0 && LIST_CONJUNCTION.has(token.surface.toLowerCase()) ? listAt(clause, at) : undefined),
296
+ undefined,
297
+ );
167
298
  };
168
299
 
169
300
  export const oxfordComma: Detector = (doc, options): Finding[] => {
@@ -0,0 +1,41 @@
1
+ import type { LengthUnit, Span } from "../plugin.ts";
2
+
3
+ /** 数えるために詰めた文と、詰めた文の各文字が元の文のどこにあったか。 */
4
+ export type Compacted = { readonly text: string; readonly offsets: readonly number[] };
5
+
6
+ /**
7
+ * n-gram を数えるときと同じ詰め方。word 単位は空白の並びを 1 つにして前後を削り、char 単位は空白を全部除く。
8
+ * 語句が元の文のどこにあるかを戻せるよう、残した文字ごとに元の位置を持つ。
9
+ */
10
+ export const compacted = (text: string, unit: LengthUnit): Compacted => {
11
+ const chars: string[] = [];
12
+ const offsets: number[] = [];
13
+ const state = { pendingSpace: -1, at: 0 };
14
+ Array.from(text).forEach((char) => {
15
+ const at = state.at;
16
+ state.at += char.length;
17
+ if (/\s/u.test(char)) {
18
+ if (unit === "word" && chars.length > 0 && state.pendingSpace === -1) state.pendingSpace = at;
19
+ return;
20
+ }
21
+ if (state.pendingSpace !== -1) {
22
+ chars.push(" ");
23
+ offsets.push(state.pendingSpace);
24
+ state.pendingSpace = -1;
25
+ }
26
+ chars.push(char);
27
+ // 位置は UTF-16 の単位で持つ。placeOf は indexOf の位置で引くので、絵文字(2 単位)には 2 つ置く。
28
+ Array.from({ length: char.length }, (_, unit) => offsets.push(at + unit));
29
+ });
30
+ return { text: chars.join(""), offsets };
31
+ };
32
+
33
+ /** 詰めた文の中の gram の最初の出現を、元の文の範囲に戻す。無ければ undefined。 */
34
+ export const placeOf = (source: Compacted, gram: string): Span | undefined => {
35
+ const at = source.text.indexOf(gram);
36
+ if (at === -1) return undefined;
37
+ const start = source.offsets[at];
38
+ const last = source.offsets[at + gram.length - 1];
39
+ if (start === undefined || last === undefined) return undefined;
40
+ return { start, end: last + 1 };
41
+ };
@@ -15,7 +15,7 @@ import { concreteEvidence, dashDensity, emojiDensity, ngramRepetition, undefined
15
15
  import { aiTell, contractionMix, cushionDensity, hedgingDensity, repeatedConjunction, unqualifiedSuperlative } from "./lexicon.ts";
16
16
  import { internalJargon, properNounDensity, requiredSections } from "./team.ts";
17
17
  import { latinSpacing, preferredTerm } from "./orthography.ts";
18
- import { danglingReference, duplicateDefinition, numberingGap } from "./structure-tree.ts";
18
+ import { danglingReference, dateOrder, dateWeekdayMismatch, duplicateDefinition, numberingGap, totalMismatch } from "./structure-tree.ts";
19
19
 
20
20
  /** rule 定義の how_to_find がここを引く。rule 側は実装を知らない。 */
21
21
  export const DETECTORS: Readonly<Record<string, Detector>> = {
@@ -58,6 +58,9 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
58
58
  "preferred-term": preferredTerm,
59
59
  "latin-spacing": latinSpacing,
60
60
  "dangling-reference": danglingReference,
61
+ "date-weekday-mismatch": dateWeekdayMismatch,
62
+ "date-order": dateOrder,
63
+ "total-mismatch": totalMismatch,
61
64
  "numbering-gap": numberingGap,
62
65
  "duplicate-definition": duplicateDefinition,
63
66
  };