chaffjs 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/checks.d.ts +20 -0
  2. package/dist/checks.d.ts.map +1 -0
  3. package/dist/checks.js +58 -0
  4. package/dist/checks.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +63 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config/load.d.ts +4 -0
  9. package/dist/config/load.d.ts.map +1 -1
  10. package/dist/config/load.js +13 -1
  11. package/dist/config/load.js.map +1 -1
  12. package/dist/judge.d.ts +40 -0
  13. package/dist/judge.d.ts.map +1 -0
  14. package/dist/judge.js +0 -0
  15. package/dist/judge.js.map +1 -0
  16. package/dist/plugin.d.ts +2 -0
  17. package/dist/plugin.d.ts.map +1 -1
  18. package/dist/render/semantic.d.ts +7 -0
  19. package/dist/render/semantic.d.ts.map +1 -0
  20. package/dist/render/semantic.js +50 -0
  21. package/dist/render/semantic.js.map +1 -0
  22. package/dist/rule-load.d.ts.map +1 -1
  23. package/dist/rule-load.js +12 -3
  24. package/dist/rule-load.js.map +1 -1
  25. package/dist/run-semantic.d.ts +17 -0
  26. package/dist/run-semantic.d.ts.map +1 -0
  27. package/dist/run-semantic.js +71 -0
  28. package/dist/run-semantic.js.map +1 -0
  29. package/dist/run.d.ts.map +1 -1
  30. package/dist/run.js +4 -0
  31. package/dist/run.js.map +1 -1
  32. package/dist/semantic.d.ts +25 -0
  33. package/dist/semantic.d.ts.map +1 -0
  34. package/dist/semantic.js +55 -0
  35. package/dist/semantic.js.map +1 -0
  36. package/package.json +4 -3
  37. package/rules/empty-conclusion.yaml +37 -0
  38. package/rules/risk-disclosure.yaml +41 -0
  39. package/rules/unsourced-number.yaml +40 -0
  40. package/src/checks.ts +74 -0
  41. package/src/cli.ts +74 -0
  42. package/src/config/load.ts +16 -1
  43. package/src/judge.ts +0 -0
  44. package/src/plugin.ts +2 -0
  45. package/src/render/semantic.ts +60 -0
  46. package/src/rule-load.ts +14 -3
  47. package/src/run-semantic.ts +107 -0
  48. package/src/run.ts +4 -0
  49. package/src/semantic.ts +74 -0
@@ -0,0 +1,71 @@
1
+ import { severityOf } from "./checks.js";
2
+ import { ask } from "./judge.js";
3
+ import { FILTERS } from "./semantic.js";
4
+ import { localized } from "./render/text.js";
5
+ import { lineStarts, placeOf } from "./position.js";
6
+ const filterFor = (name) => FILTERS[name ?? "whole-document"] ?? FILTERS["whole-document"] ?? (() => []);
7
+ const SEVERITY_BY_VALUE = ["info", "info", "warning", "error"];
8
+ const severityFromLevel = (rule, level) => {
9
+ if (level === "off")
10
+ return "info";
11
+ const value = rule.levels[level] ?? rule.levels.normal ?? 2;
12
+ return SEVERITY_BY_VALUE[Math.min(3, Math.max(1, Math.round(value)))] ?? "warning";
13
+ };
14
+ const builtInJobs = (doc, rules, settings, genre) => rules
15
+ .filter((rule) => rule.layer === "L4" && rule.use_for.some((target) => genre.startsWith(target)) && settings[rule.id] !== "off")
16
+ .flatMap((rule) => {
17
+ const rubric = rule.what_to_check === undefined ? undefined : localized(rule.what_to_check, doc.language);
18
+ if (rubric === undefined || rubric.length === 0)
19
+ return [];
20
+ return [
21
+ {
22
+ rule: rule.id,
23
+ name: localized(rule.name, doc.language),
24
+ rubric,
25
+ howToFix: localized(rule.how_to_fix, doc.language),
26
+ severity: severityFromLevel(rule, settings[rule.id] ?? "normal"),
27
+ candidates: filterFor(rule.how_to_find)(doc),
28
+ },
29
+ ];
30
+ });
31
+ const userJobs = (doc, checks, genre) => checks
32
+ .filter((check) => check.level !== "off" && check.use_for.some((target) => genre.startsWith(target)))
33
+ .map((check) => ({
34
+ rule: check.id,
35
+ name: check.name,
36
+ // look_at は自然文なので、まだ絞り込みに変換できない。文書全体を渡す。spec §26-7。
37
+ rubric: check.look_at === undefined ? check.check : `${check.check}\n\n見るところ: ${check.look_at}`,
38
+ howToFix: check.how_to_fix,
39
+ severity: severityOf(check.level),
40
+ candidates: filterFor("whole-document")(doc),
41
+ }));
42
+ const findingOf = (job, candidate, reason, confidence, threshold, starts) => {
43
+ const at = placeOf(starts, candidate.offset);
44
+ // 確からしさが低いものは指摘として出すが、重さを下げる。spec §14。
45
+ const severity = confidence < threshold ? "info" : job.severity;
46
+ return {
47
+ rule: job.rule,
48
+ severity,
49
+ line: at.line,
50
+ column: at.column,
51
+ quote: candidate.text.slice(0, 200),
52
+ values: { reason, confidence: confidence.toFixed(2), offset: candidate.offset },
53
+ };
54
+ };
55
+ export const runSemantic = async (doc, rules, checks, settings, genre, options) => {
56
+ const jobs = [...builtInJobs(doc, rules, settings, genre), ...userJobs(doc, checks, genre)];
57
+ const starts = lineStarts(doc.source);
58
+ const skipped = jobs.filter((job) => job.candidates.length === 0).map((job) => ({ rule: job.rule, why: "見るところが無かったため" }));
59
+ const live = jobs.filter((job) => job.candidates.length > 0);
60
+ const answers = await Promise.all(live.flatMap((job) => job.candidates.map(async (candidate) => {
61
+ const verdict = await ask({ rule: job.rule, rubric: job.rubric, candidate: candidate.text, language: doc.language }, options);
62
+ return verdict.violated ? findingOf(job, candidate, verdict.reason, verdict.confidence, options.confidenceThreshold, starts) : undefined;
63
+ })));
64
+ return {
65
+ findings: answers.filter((finding) => finding !== undefined).sort((left, right) => left.line - right.line),
66
+ skipped,
67
+ asked: live.reduce((sum, job) => sum + job.candidates.length, 0),
68
+ sentencesSeen: doc.sentences.length,
69
+ };
70
+ };
71
+ //# sourceMappingURL=run-semantic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-semantic.js","sourceRoot":"","sources":["../src/run-semantic.ts"],"names":[],"mappings":"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"}
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGjF,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAgBvD,eAAO,MAAM,QAAQ,GAAI,KAAK,aAAa,EAAE,OAAO,SAAS,cAAc,EAAE,EAAE,UAAU,QAAQ,EAAE,cAAc,OAAO,EAAE,OAAO,MAAM,KAAG,SA8BzI,CAAC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGjF,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAgBvD,eAAO,MAAM,QAAQ,GAAI,KAAK,aAAa,EAAE,OAAO,SAAS,cAAc,EAAE,EAAE,UAAU,QAAQ,EAAE,cAAc,OAAO,EAAE,OAAO,MAAM,KAAG,SAkCzI,CAAC"}
package/dist/run.js CHANGED
@@ -22,6 +22,10 @@ export const runRules = (doc, rules, settings, experimental, genre) => {
22
22
  .filter((rule) => rule.status === "experimental" && settings[rule.id] !== undefined && settings[rule.id] !== "off")
23
23
  .map((rule) => rule.id);
24
24
  const outcome = applicable.reduce((acc, rule) => {
25
+ // L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
26
+ if (rule.layer === "L4") {
27
+ return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: "意味を読む検査のため(npx chaff test で動きます)" }] };
28
+ }
25
29
  const level = levelFor(rule, settings, experimental);
26
30
  if (level === "off") {
27
31
  const why = rule.status === "experimental" && settings[rule.id] === undefined ? "まだ試験中のため" : "設定で止めているため";
package/dist/run.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { ProseDocument } from "./plugin.ts";
2
+ /**
3
+ * 二段構えの 1 段目。文書全体を LLM に投げない。spec §14。
4
+ *
5
+ * 絞り込みを持たない L4 rule は登録できない。持たせないと、1 記事ごとに全文を
6
+ * 読ませることになり、速度もコストも成り立たない。
7
+ */
8
+ export type Candidate = {
9
+ readonly text: string;
10
+ readonly offset: number;
11
+ };
12
+ export type Filter = (doc: ProseDocument) => Candidate[];
13
+ /** 提案文書にリスクが書かれているか。見出しにあれば見ない。 */
14
+ export declare const riskDisclosure: Filter;
15
+ /**
16
+ * 結びが本文の要約でしかないか。
17
+ * 最後の節だけを見て、そこに具体物が無いときにだけ問い合わせる。
18
+ */
19
+ export declare const emptyConclusion: Filter;
20
+ /** 効果を主張する数値に根拠が無い文だけを渡す。 */
21
+ export declare const unsourcedNumber: Filter;
22
+ /** 利用者が checks.yaml に書いた検査の既定。絞り込みを書かなければ文書全体。 */
23
+ export declare const wholeDocument: Filter;
24
+ export declare const FILTERS: Readonly<Record<string, Filter>>;
25
+ //# sourceMappingURL=semantic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../src/semantic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAW,MAAM,aAAa,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,SAAS,EAAE,CAAC;AAWzD,mCAAmC;AACnC,eAAO,MAAM,cAAc,EAAE,MAK5B,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAM7B,CAAC;AAeF,6BAA6B;AAC7B,eAAO,MAAM,eAAe,EAAE,MAGuD,CAAC;AAEtF,kDAAkD;AAClD,eAAO,MAAM,aAAa,EAAE,MAG3B,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKpD,CAAC"}
@@ -0,0 +1,55 @@
1
+ const heads = (doc) => doc.sections.map((section) => section.heading).join(" ");
2
+ const sectionText = (doc, section) => doc.source.slice(section.span.start, section.span.end).trim();
3
+ /** 見出しに答えが書いてあるなら、本文を読ませる必要はない。 */
4
+ const skipIfHeadingMatches = (doc, pattern) => pattern.test(heads(doc));
5
+ const RISK_HEADING = /リスク|懸念|デメリット|risk|tradeoff|downside/iu;
6
+ /** 提案文書にリスクが書かれているか。見出しにあれば見ない。 */
7
+ export const riskDisclosure = (doc) => {
8
+ if (skipIfHeadingMatches(doc, RISK_HEADING))
9
+ return [];
10
+ const body = doc.sentences.map((sentence) => sentence.text.trim()).join("");
11
+ if (body.length === 0)
12
+ return [];
13
+ return [{ text: body.slice(0, 4000), offset: doc.sentences[0]?.span.start ?? 0 }];
14
+ };
15
+ const EVIDENCE = /\d|`|https?:\/\//u;
16
+ /**
17
+ * 結びが本文の要約でしかないか。
18
+ * 最後の節だけを見て、そこに具体物が無いときにだけ問い合わせる。
19
+ */
20
+ export const emptyConclusion = (doc) => {
21
+ const last = doc.sections.at(-1);
22
+ if (last === undefined || last.sentences.length === 0)
23
+ return [];
24
+ const text = sectionText(doc, last);
25
+ if (EVIDENCE.test(text))
26
+ return [];
27
+ return [{ text: text.slice(0, 2000), offset: last.sentences[0]?.span.start ?? last.span.start }];
28
+ };
29
+ // \s* を挟むとバックトラックする。単位の前の空白は 1 つまでで足りる。
30
+ const NUMBER = /\d ?(?:%|%|倍|割|件|人|円|ドル|時間|分|秒)/u;
31
+ const EFFECT = /向上|改善|削減|短縮|増加|減少|成長|伸び/u;
32
+ /**
33
+ * 根拠と認めるもの。ここを狭く取ると、根拠つきの文まで LLM に送ってしまう。
34
+ * 二段構えの 1 段目が防ぐべきなのは、まさにその無駄。
35
+ */
36
+ const EVIDENCE_WORDS = ["出典", "調査", "自社調べ", "参考", "期間", "比較", "対比", "前年", "昨年", "同期", "前月", "前四半期", "vs"];
37
+ const EVIDENCE_SHAPE = /n ?=|\[\d+\]|\d{4} ?年|Q[1-4]/iu;
38
+ /** 語の照合は正規表現にしない。数が増えるほど式が読めなくなり、複雑度も上がる。 */
39
+ const hasEvidence = (text) => EVIDENCE_WORDS.some((word) => text.includes(word)) || EVIDENCE_SHAPE.test(text);
40
+ /** 効果を主張する数値に根拠が無い文だけを渡す。 */
41
+ export const unsourcedNumber = (doc) => doc.sentences
42
+ .filter((sentence) => NUMBER.test(sentence.text) && EFFECT.test(sentence.text) && !hasEvidence(sentence.text))
43
+ .map((sentence) => ({ text: sentence.text.trim(), offset: sentence.span.start }));
44
+ /** 利用者が checks.yaml に書いた検査の既定。絞り込みを書かなければ文書全体。 */
45
+ export const wholeDocument = (doc) => {
46
+ const body = doc.sentences.map((sentence) => sentence.text.trim()).join("");
47
+ return body.length === 0 ? [] : [{ text: body.slice(0, 4000), offset: doc.sentences[0]?.span.start ?? 0 }];
48
+ };
49
+ export const FILTERS = {
50
+ "risk-disclosure": riskDisclosure,
51
+ "empty-conclusion": emptyConclusion,
52
+ "unsourced-number": unsourcedNumber,
53
+ "whole-document": wholeDocument,
54
+ };
55
+ //# sourceMappingURL=semantic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.js","sourceRoot":"","sources":["../src/semantic.ts"],"names":[],"mappings":"AAYA,MAAM,KAAK,GAAG,CAAC,GAAkB,EAAU,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEvG,MAAM,WAAW,GAAG,CAAC,GAAkB,EAAE,OAAgB,EAAU,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAEpI,mCAAmC;AACnC,MAAM,oBAAoB,GAAG,CAAC,GAAkB,EAAE,OAAe,EAAW,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AAExG,MAAM,YAAY,GAAG,uCAAuC,CAAC;AAE7D,mCAAmC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAW,CAAC,GAAG,EAAE,EAAE;IAC5C,IAAI,oBAAoB,CAAC,GAAG,EAAE,YAAY,CAAC;QAAE,OAAO,EAAE,CAAC;IACvD,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;AACpF,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,mBAAmB,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW,CAAC,GAAG,EAAE,EAAE;IAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjE,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACpC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACnG,CAAC,CAAC;AAEF,wCAAwC;AACxC,MAAM,MAAM,GAAG,kCAAkC,CAAC;AAClD,MAAM,MAAM,GAAG,0BAA0B,CAAC;AAC1C;;;GAGG;AACH,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAC1G,MAAM,cAAc,GAAG,gCAAgC,CAAC;AAExD,6CAA6C;AAC7C,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAE/H,6BAA6B;AAC7B,MAAM,CAAC,MAAM,eAAe,GAAW,CAAC,GAAG,EAAE,EAAE,CAC7C,GAAG,CAAC,SAAS;KACV,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC7G,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAEtF,kDAAkD;AAClD,MAAM,CAAC,MAAM,aAAa,GAAW,CAAC,GAAG,EAAE,EAAE;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;AAC7G,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAqC;IACvD,iBAAiB,EAAE,cAAc;IACjC,kBAAkB,EAAE,eAAe;IACnC,kBAAkB,EAAE,eAAe;IACnC,gBAAgB,EAAE,aAAa;CAChC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaffjs",
3
- "version": "0.0.1",
3
+ "version": "0.1.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",
@@ -51,8 +51,9 @@
51
51
  "prepack": "yarn build"
52
52
  },
53
53
  "dependencies": {
54
- "@chaffjs/lang-en": "0.0.1",
55
- "@chaffjs/lang-ja": "0.0.1",
54
+ "@anthropic-ai/sdk": "^0.125.0",
55
+ "@chaffjs/lang-en": "0.1.0",
56
+ "@chaffjs/lang-ja": "0.1.0",
56
57
  "mdast-util-from-markdown": "^2.0.3",
57
58
  "mdast-util-frontmatter": "^2.0.1",
58
59
  "mdast-util-gfm-table": "^2.0.0",
@@ -0,0 +1,37 @@
1
+ id: empty-conclusion
2
+ layer: L4
3
+ status: stable
4
+ how_to_find: empty-conclusion
5
+ severity: warning
6
+
7
+ name:
8
+ ja: 結びに中身がない
9
+ en: Empty conclusion
10
+
11
+ why:
12
+ ja: 本文を繰り返すだけの結びは、読者に何も残しません。
13
+ en: A closing that only restates the body leaves the reader with nothing.
14
+
15
+ message:
16
+ ja: "{reason}"
17
+ en: "{reason}"
18
+
19
+ how_to_fix:
20
+ ja: 本文で分かったことのうち、いちばん持ち帰ってほしい 1 つを書いてください。
21
+ en: Name the one thing you want the reader to take away.
22
+
23
+ what_to_check:
24
+ ja: |
25
+ 文書の結びに、本文の要約以外のものがあること。
26
+ 新しい主張、判断、次にすべきこと、読者への問いのいずれかがあればよい。
27
+ 本文に書いたことを並べ直しているだけなら、決まりに反する。
28
+ en: |
29
+ The closing must contain something beyond a summary of the body — a new
30
+ claim, a judgement, a next step, or a question to the reader.
31
+
32
+ levels:
33
+ strict: error
34
+ normal: warning
35
+ relaxed: info
36
+
37
+ use_for: [blog]
@@ -0,0 +1,41 @@
1
+ id: risk-disclosure
2
+ layer: L4
3
+ status: stable
4
+ how_to_find: risk-disclosure
5
+ severity: warning
6
+
7
+ name:
8
+ ja: リスクが書かれていない
9
+ en: No risk disclosed
10
+
11
+ why:
12
+ ja: 提案を読む人は、うまくいく話だけでは判断できません。
13
+ en: A reader cannot decide from the upside alone.
14
+
15
+ message:
16
+ ja: "{reason}"
17
+ en: "{reason}"
18
+
19
+ how_to_fix:
20
+ ja: 「リスク」または「懸念」の節を足してください。
21
+ en: Add a section naming the risks.
22
+
23
+ what_to_check:
24
+ ja: |
25
+ 提案を行っている文書に、次のいずれかが書かれていること。
26
+ ・その施策のリスク
27
+ ・想定される副作用
28
+ ・実施しなかった場合の影響
29
+ いまの課題の記述は、リスクとしては数えない。
30
+ 提案をしていない文書(報告・議事録・説明)は、この決まりの対象外とする。
31
+ en: |
32
+ For a document making a proposal, at least one of: risks of the action,
33
+ expected side effects, or the cost of not acting. A description of the
34
+ current problem does not count. Documents that make no proposal are out of scope.
35
+
36
+ levels:
37
+ strict: error
38
+ normal: warning
39
+ relaxed: info
40
+
41
+ use_for: [business]
@@ -0,0 +1,40 @@
1
+ id: unsourced-number
2
+ layer: L4
3
+ status: stable
4
+ how_to_find: unsourced-number
5
+ severity: warning
6
+
7
+ name:
8
+ ja: 数字の根拠がない
9
+ en: Number without a source
10
+
11
+ why:
12
+ ja: 根拠のない数字は、読む人が確かめられません。
13
+ en: A number the reader cannot check is not evidence.
14
+
15
+ message:
16
+ ja: "{reason}"
17
+ en: "{reason}"
18
+
19
+ how_to_fix:
20
+ ja: 「(2026年4-6月、対前年同期、n=120)」のように括弧で添えてください。
21
+ en: Add the basis in parentheses — period, comparison, sample size.
22
+
23
+ what_to_check:
24
+ ja: |
25
+ 効果を主張する数値について、次のいずれかが同じ文か近くにあること。
26
+ ・どう計算したか
27
+ ・いつと比べたか
28
+ ・母数がいくつか
29
+ どれも無ければ、決まりに反する。
30
+ 一般に知られた事実の引用や、仕様として決まっている値は対象外とする。
31
+ en: |
32
+ A number claiming an effect must carry at least one of: how it was
33
+ computed, what it is compared against, or the sample size.
34
+
35
+ levels:
36
+ strict: error
37
+ normal: warning
38
+ relaxed: info
39
+
40
+ use_for: [business]
package/src/checks.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parse } from "yaml";
3
+ import type { Level, Severity } from "./plugin.ts";
4
+
5
+ export const CHECKS_FILE = "checks.yaml";
6
+
7
+ /**
8
+ * 利用者が自然文で足す検査。キーは英語、中身は書き手の言葉。
9
+ * プログラムは書かせない。spec §18.3。
10
+ */
11
+ export type UserCheck = {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly use_for: readonly string[];
15
+ readonly check: string;
16
+ readonly look_at: string | undefined;
17
+ readonly level: Level;
18
+ readonly how_to_fix: string;
19
+ readonly source: string;
20
+ };
21
+
22
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
23
+
24
+ const str = (value: unknown): string | undefined => (typeof value === "string" ? value.trim() : undefined);
25
+
26
+ const LEVELS: readonly Level[] = ["strict", "normal", "relaxed", "off"];
27
+ const asLevel = (value: unknown): Level => LEVELS.find((level) => level === value) ?? "normal";
28
+
29
+ /** id は名前から作る。利用者に id を書かせない。 */
30
+ const idOf = (name: string, index: number): string => {
31
+ const slug = name
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/gu, "-")
34
+ .replace(/^-|-$/gu, "");
35
+ return slug.length > 0 ? `check:${slug}` : `check:${index + 1}`;
36
+ };
37
+
38
+ /** use_for は 1 つでも配列でも書ける。書かなければ全ジャンル。 */
39
+ const useForOf = (value: unknown): string[] => {
40
+ if (typeof value === "string") return [value];
41
+ if (Array.isArray(value)) return value.map((entry) => String(entry));
42
+ return ["blog", "business"];
43
+ };
44
+
45
+ const toCheck = (raw: unknown, index: number, source: string): UserCheck | undefined => {
46
+ if (!isRecord(raw)) return undefined;
47
+ const name = str(raw["name"]);
48
+ const check = str(raw["check"]);
49
+ if (name === undefined || check === undefined) return undefined;
50
+ return {
51
+ id: idOf(name, index),
52
+ name,
53
+ use_for: useForOf(raw["use_for"]),
54
+ check,
55
+ look_at: str(raw["look_at"]),
56
+ level: asLevel(raw["level"]),
57
+ how_to_fix: str(raw["how_to_fix"]) ?? "",
58
+ source,
59
+ };
60
+ };
61
+
62
+ export const loadChecks = (path: string): UserCheck[] => {
63
+ if (!existsSync(path)) return [];
64
+ const raw: unknown = parse(readFileSync(path, "utf8"));
65
+ if (!isRecord(raw) || !Array.isArray(raw["checks"])) return [];
66
+ return raw["checks"].map((entry, index) => toCheck(entry, index, path)).filter((entry) => entry !== undefined);
67
+ };
68
+
69
+ /** 4 語を severity にする。L4 は件数ではなく深刻度を段で持つ。 */
70
+ export const severityOf = (level: Level): Severity => {
71
+ if (level === "strict") return "error";
72
+ if (level === "relaxed") return "info";
73
+ return "warning";
74
+ };
package/src/cli.ts CHANGED
@@ -11,6 +11,10 @@ import { BASELINE_FILE, fingerprint, readBaseline, splitByBaseline, writeBaselin
11
11
  import { applySuppressions } from "./stet.ts";
12
12
  import { renderSuppressions, type PerFile } from "./render/suppressions.ts";
13
13
  import { clock, describeChange, snapshotOf, watchPaths, type Snapshot } from "./watch.ts";
14
+ import { CHECKS_FILE, loadChecks, type UserCheck } from "./checks.ts";
15
+ import { CACHE_DIR, hasCredentials, isAuthFailure } from "./judge.ts";
16
+ import { runSemantic } from "./run-semantic.ts";
17
+ import { MACHINE_BANNER, renderSemantic } from "./render/semantic.ts";
14
18
  import { frontMatterGenre, guessGenre } from "./genre.ts";
15
19
  import { GENRES, runInit } from "./init.ts";
16
20
  import { loadRules } from "./rule-load.ts";
@@ -26,6 +30,7 @@ const USAGE = `chaff — 文章の読みにくいところを見つけます。
26
30
 
27
31
  chaff <file|dir|glob>... 検査する(設定も API key も要りません)
28
32
  chaff . この場所の Markdown を全部
33
+ chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
29
34
  chaff init chaff.yaml を作る
30
35
  chaff explain <rule> そのルールの意図と根拠を読む
31
36
  chaff genres ジャンルの一覧
@@ -158,6 +163,74 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
158
163
  return new Promise(() => undefined);
159
164
  };
160
165
 
166
+ type Judged = { path: string; outcome: Awaited<ReturnType<typeof runSemantic>>; rules: RuleDefinition[]; language: string };
167
+
168
+ const judgeAll = async (
169
+ paths: readonly string[],
170
+ config: Config,
171
+ checks: readonly UserCheck[],
172
+ options: Parameters<typeof runSemantic>[5],
173
+ ): Promise<Judged[]> =>
174
+ Promise.all(
175
+ paths.map(async (path) => {
176
+ const source = await readFile(path, "utf8");
177
+ const language = config.language ?? guessLanguage(source).language;
178
+ const adapter = await loadAdapter(language);
179
+ const doc = buildDocument(path, source, adapter);
180
+ const { genre } = resolveGenre(path, source, config);
181
+ const rules = loadRules(language);
182
+ return { path, outcome: await runSemantic(doc, rules, checks, config.rules, genre, options), rules, language };
183
+ }),
184
+ );
185
+
186
+ /**
187
+ * 機械と AI を見出しで分けて出す。読む人が「これは揺れる判定か」を知らないと、
188
+ * AI の誤検知に振り回される。workflow spec §7、samples/README の軸 2。
189
+ */
190
+ const test = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
191
+ const paths = collectTargets(targets.length > 0 ? targets : ["."]);
192
+ if (paths.length === 0) {
193
+ console.error(`Markdown が 1 つも見つかりませんでした: ${targets.join(", ")}`);
194
+ return 1;
195
+ }
196
+ const config = readConfig();
197
+ const checks = loadChecks(join(process.cwd(), CHECKS_FILE));
198
+ const results = await Promise.all(paths.map((path) => inspect(path, config, argv)));
199
+ results.forEach((result) => {
200
+ console.log(result.text.replace(/\n/u, `\n${MACHINE_BANNER.join("\n")}`));
201
+ });
202
+ const machineOnly = results.some((result) => result.outcome.findings.some((finding) => finding.severity === "error")) ? 1 : 0;
203
+ const noCredentials = [
204
+ "",
205
+ " 意味を読む検査は動かしていません。Anthropic の認証情報がありません。",
206
+ " ANTHROPIC_API_KEY を設定するか、ant auth login を実行してください。",
207
+ " 機械による判定はすべて動いています。",
208
+ "",
209
+ ].join("\n");
210
+ // 呼んでから落ちるのを待たない。認証が無いときの SDK の例外は型で判別できない。
211
+ if (!hasCredentials()) {
212
+ console.log(noCredentials);
213
+ return machineOnly;
214
+ }
215
+ const options = { model: config.aiModel, cacheDir: join(process.cwd(), CACHE_DIR), confidenceThreshold: config.confidenceThreshold };
216
+ const semantic = await judgeAll(paths, config, checks, options).catch((error: unknown) => {
217
+ if (!isAuthFailure(error)) throw error;
218
+ console.log(noCredentials);
219
+ return undefined;
220
+ });
221
+ if (semantic === undefined) return machineOnly;
222
+ semantic.forEach((entry) => {
223
+ if (entry.outcome.findings.length === 0 && entry.outcome.asked === 0) return;
224
+ console.log([`${entry.path}`, ...renderSemantic(entry.outcome, entry.rules, checks, entry.language)].join("\n"));
225
+ });
226
+ const machine = results.flatMap((result) => result.outcome.findings);
227
+ const ai = semantic.flatMap((entry) => entry.outcome.findings);
228
+ console.log(
229
+ ["", "─".repeat(60), "", ` 機械 ${machine.length} 件 / AI ${ai.length} 件`, " 文章は書き換えていません。直すのは書いた人です。", ""].join("\n"),
230
+ );
231
+ return [...machine, ...ai].some((finding) => finding.severity === "error") ? 1 : 0;
232
+ };
233
+
161
234
  const explain = (ruleId: string | undefined): number => {
162
235
  const config = readConfig();
163
236
  const language = config.language ?? "ja";
@@ -239,6 +312,7 @@ const HANDLERS: Readonly<Record<string, Handler>> = {
239
312
  genres: showGenres,
240
313
  rules: showRules,
241
314
  explain: (argv) => explain(argv[1]),
315
+ test: (argv) => test(positional(argv), argv),
242
316
  baseline: (argv) => runBaseline(positional(argv), argv),
243
317
  suppressions: (argv) => runSuppressions(positional(argv), argv),
244
318
  relax: (argv) => changeSetting("relaxed", argv[1], flag(argv, "--why")),
@@ -11,9 +11,22 @@ export type Config = {
11
11
  readonly rules: Readonly<Record<string, Level>>;
12
12
  readonly experimental: boolean;
13
13
  readonly path: string | undefined;
14
+ readonly aiModel: string;
15
+ readonly confidenceThreshold: number;
14
16
  };
15
17
 
16
- export const EMPTY: Config = { genre: undefined, language: undefined, rules: {}, experimental: false, path: undefined };
18
+ /** 判定の質が誤検知に直結するので、既定は最上位のモデル。cost は絞り込みで削る。spec §14。 */
19
+ export const DEFAULT_MODEL = "claude-opus-5";
20
+
21
+ export const EMPTY: Config = {
22
+ genre: undefined,
23
+ language: undefined,
24
+ rules: {},
25
+ experimental: false,
26
+ path: undefined,
27
+ aiModel: DEFAULT_MODEL,
28
+ confidenceThreshold: 0.7,
29
+ };
17
30
 
18
31
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
19
32
 
@@ -34,5 +47,7 @@ export const loadConfig = (path: string): Config => {
34
47
  rules: rulesOf(raw["rules"]),
35
48
  experimental: raw["experimental"] === true,
36
49
  path,
50
+ aiModel: str(raw["ai_model"]) ?? DEFAULT_MODEL,
51
+ confidenceThreshold: typeof raw["confidence_threshold"] === "number" ? raw["confidence_threshold"] : 0.7,
37
52
  };
38
53
  };
package/src/judge.ts ADDED
Binary file
package/src/plugin.ts CHANGED
@@ -118,6 +118,8 @@ export type RuleDefinition = {
118
118
  readonly levels: LevelTable;
119
119
  readonly how_to_find: string;
120
120
  readonly word_list: string | undefined;
121
+ /** L4 のみ。LLM に渡す決まり。言語別。 */
122
+ readonly what_to_check: Localized | undefined;
121
123
  readonly where: string | undefined;
122
124
  readonly use_for: readonly string[];
123
125
  readonly severity: Severity;
@@ -0,0 +1,60 @@
1
+ import type { Finding, RuleDefinition } from "../plugin.ts";
2
+ import type { UserCheck } from "../checks.ts";
3
+ import type { SemanticResult } from "../run-semantic.ts";
4
+ import { MARK, localized } from "./text.ts";
5
+
6
+ const RULE = 60;
7
+
8
+ const indent = (text: string, pad: string): string[] => text.split("\n").map((line) => `${pad}${line}`);
9
+
10
+ export const MACHINE_BANNER = ["", `═══ 機械による判定 ${"═".repeat(RULE - 18)}`, " 同じ文章なら何度実行しても同じ結果になります", ""];
11
+
12
+ export const aiBanner = (result: SemanticResult): string[] => [
13
+ "",
14
+ `═══ AI による判定 ${"═".repeat(RULE - 17)}`,
15
+ " 文章の意味を読んでいます。実行するたび結果が変わることが",
16
+ " あります。おかしいと思ったら、そのまま無視して構いません。",
17
+ "",
18
+ ` ${result.sentencesSeen} 文のうち ${result.asked} 箇所を読みました(残りは機械が対象外と判断)`,
19
+ "",
20
+ ];
21
+
22
+ type Named = { readonly name: string; readonly howToFix: string };
23
+
24
+ const nameOf = (rule: string, rules: readonly RuleDefinition[], checks: readonly UserCheck[], language: string): Named => {
25
+ const built = rules.find((entry) => entry.id === rule);
26
+ if (built !== undefined) return { name: localized(built.name, language), howToFix: localized(built.how_to_fix, language) };
27
+ const user = checks.find((entry) => entry.id === rule);
28
+ return { name: user?.name ?? rule, howToFix: user?.how_to_fix ?? "" };
29
+ };
30
+
31
+ /**
32
+ * AI の指摘にだけ確からしさを出し、納得できないときの逃げ道を 2 つ添える。
33
+ * 揺れる判定だと読む人が知らないと、誤検知に振り回される。
34
+ */
35
+ const block = (finding: Finding, named: Named): string[] => {
36
+ const confidence = finding.values["confidence"];
37
+ const reason = String(finding.values["reason"] ?? "");
38
+ const sure = confidence === undefined ? "" : ` 確からしさ ${String(confidence)}`;
39
+ const fix = named.howToFix.length > 0 ? ["", ...indent(`→ ${named.howToFix}`, " ")] : [];
40
+ return [
41
+ "",
42
+ `─── ${finding.line} 行目 ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
43
+ "",
44
+ ` ${MARK[finding.severity] ?? "·"} ${named.name}${sure}`,
45
+ "",
46
+ ...indent(reason, " "),
47
+ ...fix,
48
+ "",
49
+ " この指摘が違うと思ったら:",
50
+ ` この箇所だけ黙らせる <!-- stet: ${finding.rule} — 理由 -->`,
51
+ ` ルールごとゆるめる npx chaff relax ${finding.rule}`,
52
+ "",
53
+ ];
54
+ };
55
+
56
+ export const renderSemantic = (result: SemanticResult, rules: readonly RuleDefinition[], checks: readonly UserCheck[], language: string): string[] => [
57
+ ...aiBanner(result),
58
+ ...result.findings.flatMap((finding) => block(finding, nameOf(finding.rule, rules, checks, language))),
59
+ ...(result.skipped.length > 0 ? [` ${result.skipped.length} 件の検査は、見るところが無いため動いていません。`, ""] : []),
60
+ ];