chaffjs 0.5.1 → 0.6.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 (96) hide show
  1. package/dist/adapter-load.d.ts.map +1 -1
  2. package/dist/adapter-load.js +18 -2
  3. package/dist/adapter-load.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +6 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/cite.d.ts +16 -0
  8. package/dist/commands/cite.d.ts.map +1 -0
  9. package/dist/commands/cite.js +69 -0
  10. package/dist/commands/cite.js.map +1 -0
  11. package/dist/commands/tree.d.ts +33 -0
  12. package/dist/commands/tree.d.ts.map +1 -0
  13. package/dist/commands/tree.js +76 -0
  14. package/dist/commands/tree.js.map +1 -0
  15. package/dist/detectors/index.d.ts.map +1 -1
  16. package/dist/detectors/index.js +4 -0
  17. package/dist/detectors/index.js.map +1 -1
  18. package/dist/detectors/structure-tree.d.ts +5 -0
  19. package/dist/detectors/structure-tree.d.ts.map +1 -0
  20. package/dist/detectors/structure-tree.js +25 -0
  21. package/dist/detectors/structure-tree.js.map +1 -0
  22. package/dist/document.d.ts +9 -0
  23. package/dist/document.d.ts.map +1 -1
  24. package/dist/document.js +21 -0
  25. package/dist/document.js.map +1 -1
  26. package/dist/eval.d.ts.map +1 -1
  27. package/dist/eval.js +13 -2
  28. package/dist/eval.js.map +1 -1
  29. package/dist/plugin.d.ts +71 -0
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/position.d.ts.map +1 -1
  32. package/dist/position.js +21 -1
  33. package/dist/position.js.map +1 -1
  34. package/dist/run.d.ts.map +1 -1
  35. package/dist/run.js +7 -2
  36. package/dist/run.js.map +1 -1
  37. package/dist/structure/build.d.ts +20 -0
  38. package/dist/structure/build.d.ts.map +1 -0
  39. package/dist/structure/build.js +162 -0
  40. package/dist/structure/build.js.map +1 -0
  41. package/dist/structure/cite.d.ts +21 -0
  42. package/dist/structure/cite.d.ts.map +1 -0
  43. package/dist/structure/cite.js +91 -0
  44. package/dist/structure/cite.js.map +1 -0
  45. package/dist/structure/issues.d.ts +15 -0
  46. package/dist/structure/issues.d.ts.map +1 -0
  47. package/dist/structure/issues.js +53 -0
  48. package/dist/structure/issues.js.map +1 -0
  49. package/dist/structure/lines.d.ts +15 -0
  50. package/dist/structure/lines.d.ts.map +1 -0
  51. package/dist/structure/lines.js +33 -0
  52. package/dist/structure/lines.js.map +1 -0
  53. package/dist/structure/markdown-path.d.ts +3 -0
  54. package/dist/structure/markdown-path.d.ts.map +1 -0
  55. package/dist/structure/markdown-path.js +5 -0
  56. package/dist/structure/markdown-path.js.map +1 -0
  57. package/dist/structure/numbering.d.ts +9 -0
  58. package/dist/structure/numbering.d.ts.map +1 -0
  59. package/dist/structure/numbering.js +38 -0
  60. package/dist/structure/numbering.js.map +1 -0
  61. package/dist/structure/of.d.ts +13 -0
  62. package/dist/structure/of.d.ts.map +1 -0
  63. package/dist/structure/of.js +8 -0
  64. package/dist/structure/of.js.map +1 -0
  65. package/dist/structure/sexp.d.ts +7 -0
  66. package/dist/structure/sexp.d.ts.map +1 -0
  67. package/dist/structure/sexp.js +25 -0
  68. package/dist/structure/sexp.js.map +1 -0
  69. package/dist/structure/universal.d.ts +7 -0
  70. package/dist/structure/universal.d.ts.map +1 -0
  71. package/dist/structure/universal.js +21 -0
  72. package/dist/structure/universal.js.map +1 -0
  73. package/package.json +3 -3
  74. package/rules/dangling-reference.yaml +27 -0
  75. package/rules/duplicate-definition.yaml +26 -0
  76. package/rules/numbering-gap.yaml +26 -0
  77. package/src/adapter-load.ts +19 -2
  78. package/src/cli.ts +6 -0
  79. package/src/commands/cite.ts +74 -0
  80. package/src/commands/tree.ts +94 -0
  81. package/src/detectors/index.ts +4 -0
  82. package/src/detectors/structure-tree.ts +32 -0
  83. package/src/document.ts +26 -2
  84. package/src/eval.ts +11 -2
  85. package/src/plugin.ts +75 -0
  86. package/src/position.ts +21 -1
  87. package/src/run.ts +7 -2
  88. package/src/structure/build.ts +203 -0
  89. package/src/structure/cite.ts +115 -0
  90. package/src/structure/issues.ts +63 -0
  91. package/src/structure/lines.ts +32 -0
  92. package/src/structure/markdown-path.ts +6 -0
  93. package/src/structure/numbering.ts +44 -0
  94. package/src/structure/of.ts +15 -0
  95. package/src/structure/sexp.ts +28 -0
  96. package/src/structure/universal.ts +21 -0
@@ -0,0 +1,15 @@
1
+ import type { StructureNode } from "../plugin.ts";
2
+ export type StructureIssue = {
3
+ readonly offset: number;
4
+ readonly values: Readonly<Record<string, string | number>>;
5
+ };
6
+ /** 木を上から順に平らにする。文書の中での順番と同じになる。再帰にしないのは、深い木でスタックを使い切らないため。 */
7
+ export declare const inDocumentOrder: (tree: StructureNode) => StructureNode[];
8
+ /**
9
+ * 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
10
+ * 番号付きのまとまりを 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
11
+ */
12
+ export declare const danglingReferences: (tree: StructureNode) => StructureIssue[];
13
+ /** 同じ語の二度目以降の定義。どちらが正しいかは決めず、両方の場所を示す。 */
14
+ export declare const duplicateDefinitions: (tree: StructureNode) => StructureIssue[];
15
+ //# sourceMappingURL=issues.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issues.d.ts","sourceRoot":"","sources":["../../src/structure/issues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlD,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC;AAIrH,8DAA8D;AAC9D,eAAO,MAAM,eAAe,GAAI,MAAM,aAAa,KAAG,aAAa,EAUlE,CAAC;AAcF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,aAAa,KAAG,cAAc,EAQtE,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,GAAI,MAAM,aAAa,KAAG,cAAc,EAaxE,CAAC"}
@@ -0,0 +1,53 @@
1
+ const NUMBERED = ["chapter", "article", "item"];
2
+ /** 木を上から順に平らにする。文書の中での順番と同じになる。再帰にしないのは、深い木でスタックを使い切らないため。 */
3
+ export const inDocumentOrder = (tree) => {
4
+ const order = [];
5
+ const pending = [tree];
6
+ while (pending.length > 0) {
7
+ const node = pending.pop();
8
+ if (node === undefined)
9
+ break;
10
+ order.push(node);
11
+ pending.push(...[...node.children].reverse());
12
+ }
13
+ return order;
14
+ };
15
+ const textOf = (node, key) => {
16
+ const value = node.attrs[key];
17
+ return value === undefined ? "" : String(value);
18
+ };
19
+ /**
20
+ * 参照がこの文書のどこかを指しているか。fallback は言語パッケージが付ける別の行き先で、
21
+ * 日本語の「第4条第1項」は、番号の無い第 1 項を持つ第4条を指しうる。
22
+ */
23
+ const resolves = (node, addresses) => addresses.has(textOf(node, "target")) || (node.attrs["fallback"] !== undefined && addresses.has(textOf(node, "fallback")));
24
+ /**
25
+ * 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
26
+ * 番号付きのまとまりを 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
27
+ */
28
+ export const danglingReferences = (tree) => {
29
+ const nodes = inDocumentOrder(tree);
30
+ const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).map((node) => node.address));
31
+ if (addresses.size === 0)
32
+ return [];
33
+ return nodes
34
+ .filter((node) => node.kind === "reference" && node.attrs["document"] === undefined)
35
+ .filter((node) => !resolves(node, addresses))
36
+ .map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
37
+ };
38
+ /** 同じ語の二度目以降の定義。どちらが正しいかは決めず、両方の場所を示す。 */
39
+ export const duplicateDefinitions = (tree) => {
40
+ const first = new Map();
41
+ return inDocumentOrder(tree)
42
+ .filter((node) => node.kind === "definition")
43
+ .flatMap((node) => {
44
+ const term = textOf(node, "term");
45
+ const earlier = first.get(term);
46
+ if (earlier === undefined) {
47
+ first.set(term, node);
48
+ return [];
49
+ }
50
+ return [{ offset: node.span.start, values: { term, first: earlier.line } }];
51
+ });
52
+ };
53
+ //# sourceMappingURL=issues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issues.js","sourceRoot":"","sources":["../../src/structure/issues.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /** 1 行。text は改行を含まず、\r\n の \r も落としてある。start は元の文書での位置。 */
2
+ export type Line = {
3
+ readonly text: string;
4
+ readonly start: number;
5
+ readonly number: number;
6
+ };
7
+ /**
8
+ * 行に分ける。位置は元の文字列のまま数える。
9
+ * Windows で書かれた契約書は \r\n で届くので、\r を本文に残すと行末の判定がずれる。
10
+ * 行ごとに配列を作り直さない。何万行もある契約書で二乗に遅くなるため。
11
+ */
12
+ export declare const linesOf: (source: string) => Line[];
13
+ /** offset を含む行の番号(1 始まり)。lines は start の昇順。二分探索で、行数に対して対数で引く。 */
14
+ export declare const lineNumberAt: (lines: readonly Line[], offset: number) => number | undefined;
15
+ //# sourceMappingURL=lines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lines.d.ts","sourceRoot":"","sources":["../../src/structure/lines.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAAI,QAAQ,MAAM,KAAG,IAAI,EAQ5C,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,YAAY,GAAI,OAAO,SAAS,IAAI,EAAE,EAAE,QAAQ,MAAM,KAAG,MAAM,GAAG,SAY9E,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 行に分ける。位置は元の文字列のまま数える。
3
+ * Windows で書かれた契約書は \r\n で届くので、\r を本文に残すと行末の判定がずれる。
4
+ * 行ごとに配列を作り直さない。何万行もある契約書で二乗に遅くなるため。
5
+ */
6
+ export const linesOf = (source) => {
7
+ const lines = [];
8
+ let at = 0;
9
+ source.split("\n").forEach((raw, index) => {
10
+ lines.push({ text: raw.endsWith("\r") ? raw.slice(0, -1) : raw, start: at, number: index + 1 });
11
+ at += raw.length + 1;
12
+ });
13
+ return lines;
14
+ };
15
+ /** offset を含む行の番号(1 始まり)。lines は start の昇順。二分探索で、行数に対して対数で引く。 */
16
+ export const lineNumberAt = (lines, offset) => {
17
+ let low = 0;
18
+ let high = lines.length - 1;
19
+ while (low <= high) {
20
+ const middle = (low + high) >> 1;
21
+ const line = lines[middle];
22
+ if (line === undefined)
23
+ return undefined;
24
+ if (offset < line.start)
25
+ high = middle - 1;
26
+ else if (offset > line.start + line.text.length)
27
+ low = middle + 1;
28
+ else
29
+ return line.number;
30
+ }
31
+ return undefined;
32
+ };
33
+ //# sourceMappingURL=lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lines.js","sourceRoot":"","sources":["../../src/structure/lines.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAc,EAAU,EAAE;IAChD,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QACxC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC;QAChG,EAAE,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAsB,EAAE,MAAc,EAAsB,EAAE;IACzF,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5B,OAAO,GAAG,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACzC,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK;YAAE,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC;aACtC,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC;;YAC7D,OAAO,IAAI,CAAC,MAAM,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** 見出しとコードの範囲を Markdown として読むか。.txt の契約書は行頭の番号だけで木にする。 */
2
+ export declare const isMarkdownPath: (path: string) => boolean;
3
+ //# sourceMappingURL=markdown-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-path.d.ts","sourceRoot":"","sources":["../../src/structure/markdown-path.ts"],"names":[],"mappings":"AAIA,0DAA0D;AAC1D,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAAyD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { extname } from "node:path";
2
+ const MARKDOWN = [".md", ".markdown", ".mdx"];
3
+ /** 見出しとコードの範囲を Markdown として読むか。.txt の契約書は行頭の番号だけで木にする。 */
4
+ export const isMarkdownPath = (path) => MARKDOWN.includes(extname(path).toLowerCase());
5
+ //# sourceMappingURL=markdown-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-path.js","sourceRoot":"","sources":["../../src/structure/markdown-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;AAE9C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { StructureNode } from "../plugin.ts";
2
+ import { type StructureIssue } from "./issues.ts";
3
+ /**
4
+ * 番号の抜けと重なり。比べるのは同じ親の子どうしだけ。英米の法令は章ごとに Section 101、201 と百の位を変えるので、
5
+ * 文書全体の通し番号として比べると抜けに見える。最初の番号は見ない。日本語の項は 2 から番号を振る。
6
+ * 1 に戻った番号は新しい並びの始まりとして扱うので、「(a) の後の (a)」の重なりは見逃す。
7
+ */
8
+ export declare const numberingBreaks: (tree: StructureNode) => StructureIssue[];
9
+ //# sourceMappingURL=numbering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbering.d.ts","sourceRoot":"","sources":["../../src/structure/numbering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAiCnE;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,aAAa,KAAG,cAAc,EAId,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { inDocumentOrder } from "./issues.js";
2
+ const labelOf = (node) => String(node.attrs["label"] ?? node.address);
3
+ /** 同じ親の子のうち、番号の付いたものを種類ごとに分ける。条と項は別の並び。 */
4
+ const sequencesOf = (parent) => {
5
+ const groups = new Map();
6
+ parent.children.forEach((node) => {
7
+ if (node.ordinal === undefined)
8
+ return;
9
+ // 配列を作り直さずに足す。条が何千もある法令で二乗にしない。
10
+ const group = groups.get(node.kind) ?? [];
11
+ group.push({ node, ordinal: node.ordinal });
12
+ groups.set(node.kind, group);
13
+ });
14
+ return [...groups.values()];
15
+ };
16
+ /** 隣り合う二つの番号が、1 つ進んでいなければ指摘する。同じ番号なら重なり、飛べば抜け。 */
17
+ const breaksIn = (sequence) => sequence.flatMap((current, index) => {
18
+ const previous = sequence[index - 1];
19
+ // 1 に戻ったら新しい並び。一つの条に (a)(b) の箇条書きが二つある、附則が第1条から振り直す、のどちらも誤りではない。
20
+ if (previous === undefined || current.ordinal === previous.ordinal + 1 || current.ordinal === 1)
21
+ return [];
22
+ return [
23
+ {
24
+ offset: current.node.span.start,
25
+ values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.ordinal + 1, found: current.ordinal },
26
+ },
27
+ ];
28
+ });
29
+ /**
30
+ * 番号の抜けと重なり。比べるのは同じ親の子どうしだけ。英米の法令は章ごとに Section 101、201 と百の位を変えるので、
31
+ * 文書全体の通し番号として比べると抜けに見える。最初の番号は見ない。日本語の項は 2 から番号を振る。
32
+ * 1 に戻った番号は新しい並びの始まりとして扱うので、「(a) の後の (a)」の重なりは見逃す。
33
+ */
34
+ export const numberingBreaks = (tree) => inDocumentOrder(tree)
35
+ .flatMap(sequencesOf)
36
+ .flatMap(breaksIn)
37
+ .sort((left, right) => left.offset - right.offset);
38
+ //# sourceMappingURL=numbering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbering.js","sourceRoot":"","sources":["../../src/structure/numbering.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAuB,MAAM,aAAa,CAAC;AAEnE,MAAM,OAAO,GAAG,CAAC,IAAmB,EAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;AAI7F,2CAA2C;AAC3C,MAAM,WAAW,GAAG,CAAC,MAAqB,EAAe,EAAE;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC5C,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;YAAE,OAAO;QACvC,gCAAgC;QAChC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF,kDAAkD;AAClD,MAAM,QAAQ,GAAG,CAAC,QAA4B,EAAoB,EAAE,CAClE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACrC,iEAAiE;IACjE,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC3G,OAAO;QACL;YACE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAC/B,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;SACnI;KACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEL;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAmB,EAAoB,EAAE,CACvE,eAAe,CAAC,IAAI,CAAC;KAClB,OAAO,CAAC,WAAW,CAAC;KACpB,OAAO,CAAC,QAAQ,CAAC;KACjB,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { StructureNode, StructurePatterns } from "../plugin.ts";
2
+ export type SourceInput = {
3
+ readonly path: string;
4
+ readonly source: string;
5
+ readonly language: string;
6
+ readonly markdown: boolean;
7
+ };
8
+ /**
9
+ * 文字列から木を作る。Markdown なら見出しとコードの範囲を先に取る。
10
+ * lint は文書モデルを作るときの解析を使い回すので、ここを通らずに buildTree を呼ぶ。
11
+ */
12
+ export declare const buildStructure: (input: SourceInput, patterns: StructurePatterns) => StructureNode;
13
+ //# sourceMappingURL=of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"of.d.ts","sourceRoot":"","sources":["../../src/structure/of.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGrE,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpI;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,EAAE,UAAU,iBAAiB,KAAG,aAI9E,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { markdownOutline } from "../document.js";
2
+ import { buildTree, NO_OUTLINE } from "./build.js";
3
+ /**
4
+ * 文字列から木を作る。Markdown なら見出しとコードの範囲を先に取る。
5
+ * lint は文書モデルを作るときの解析を使い回すので、ここを通らずに buildTree を呼ぶ。
6
+ */
7
+ export const buildStructure = (input, patterns) => buildTree({ path: input.path, source: input.source, language: input.language, outline: input.markdown ? markdownOutline(input.source) : NO_OUTLINE }, patterns);
8
+ //# sourceMappingURL=of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"of.js","sourceRoot":"","sources":["../../src/structure/of.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAInD;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAkB,EAAE,QAA2B,EAAiB,EAAE,CAC/F,SAAS,CACP,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,EAC1I,QAAQ,CACT,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { StructureNode } from "../plugin.ts";
2
+ /**
3
+ * 木を S 式にする。人と AI が読むための形で、JSON と同じ木から作る。
4
+ * `(kind "番地" :key value ... 子)`。番地を持たない葉は番地を書かない。
5
+ */
6
+ export declare const toSexp: (node: StructureNode, indent?: string) => string;
7
+ //# sourceMappingURL=sexp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sexp.d.ts","sourceRoot":"","sources":["../../src/structure/sexp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkBlD;;;GAGG;AACH,eAAO,MAAM,MAAM,GAAI,MAAM,aAAa,EAAE,eAAW,KAAG,MAKzD,CAAC"}
@@ -0,0 +1,25 @@
1
+ /** 文字列は二重引用符で囲み、中の \ と " だけを逃がす。改行は \n にして 1 行に保つ。 */
2
+ const quote = (text) => `"${text.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"').replace(/\r?\n/gu, "\\n")}"`;
3
+ const atom = (value) => (typeof value === "number" ? String(value) : quote(value));
4
+ /** 属性はキーの順に並べる。同じ木から毎回同じ文字列を出すため。 */
5
+ const keywords = (node) => [
6
+ ...Object.keys(node.attrs)
7
+ .sort((left, right) => left.localeCompare(right, "en"))
8
+ .flatMap((key) => {
9
+ const value = node.attrs[key];
10
+ return value === undefined ? [] : [`:${key} ${atom(value)}`];
11
+ }),
12
+ `:line ${String(node.line)}`,
13
+ ];
14
+ /**
15
+ * 木を S 式にする。人と AI が読むための形で、JSON と同じ木から作る。
16
+ * `(kind "番地" :key value ... 子)`。番地を持たない葉は番地を書かない。
17
+ */
18
+ export const toSexp = (node, indent = "") => {
19
+ const head = [node.kind, ...(node.address === "" ? [] : [quote(node.address)]), ...keywords(node)].join(" ");
20
+ if (node.children.length === 0)
21
+ return `${indent}(${head})`;
22
+ const inner = node.children.map((child) => toSexp(child, `${indent} `)).join("\n");
23
+ return `${indent}(${head}\n${inner})`;
24
+ };
25
+ //# sourceMappingURL=sexp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sexp.js","sourceRoot":"","sources":["../../src/structure/sexp.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC;AAE5H,MAAM,IAAI,GAAG,CAAC,KAAsB,EAAU,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAE5G,qCAAqC;AACrC,MAAM,QAAQ,GAAG,CAAC,IAAmB,EAAY,EAAE,CAAC;IAClD,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;SACvB,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;SACtD,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC,CAAC;IACJ,SAAS,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;CAC7B,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAmB,EAAE,MAAM,GAAG,EAAE,EAAU,EAAE;IACjE,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7G,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,MAAM,IAAI,IAAI,GAAG,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpF,OAAO,GAAG,MAAM,IAAI,IAAI,KAAK,KAAK,GAAG,CAAC;AACxC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { NumberedLine, NumberingContext } from "../plugin.ts";
2
+ /**
3
+ * 本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。点を含まない番号は、見出しの行でだけ読む。
4
+ * 「4.2 」のように点を含むものは、.txt の仕様書でも見出しとして書かれるので本文でも読む。
5
+ */
6
+ export declare const dottedNumber: (line: string, context: NumberingContext) => NumberedLine | undefined;
7
+ //# sourceMappingURL=universal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"universal.d.ts","sourceRoot":"","sources":["../../src/structure/universal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQnE;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,SAAS,gBAAgB,KAAG,YAAY,GAAG,SAQrF,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 「4.2 設定」「3.1.4 Scope」のような通し番号。数字と点だけで書くので言語を問わない。
3
+ * 各部分を 3 桁・深さを 6 までに抑え、後戻りが爆発しない形にしてある。
4
+ */
5
+ const DOTTED = /^[ \t]{0,3}(?<number>\d{1,3}(?:\.\d{1,3}){0,5})\.?[ \t\u3000]+(?<rest>\S.*)$/u;
6
+ /**
7
+ * 本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。点を含まない番号は、見出しの行でだけ読む。
8
+ * 「4.2 」のように点を含むものは、.txt の仕様書でも見出しとして書かれるので本文でも読む。
9
+ */
10
+ export const dottedNumber = (line, context) => {
11
+ const groups = DOTTED.exec(line)?.groups;
12
+ const number = groups?.["number"];
13
+ if (number === undefined)
14
+ return undefined;
15
+ const depth = number.split(".").length;
16
+ if (depth < 2 && !context.isHeading)
17
+ return undefined;
18
+ const rest = groups?.["rest"]?.trim() ?? "";
19
+ return { kind: "article", depth, number, absolute: true, label: number, heading: rest, rest, ordinal: Number(number.split(".").at(-1)) };
20
+ };
21
+ //# sourceMappingURL=universal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"universal.js","sourceRoot":"","sources":["../../src/structure/universal.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,GAAG,+EAA+E,CAAC;AAE/F;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,OAAyB,EAA4B,EAAE;IAChG,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3I,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaffjs",
3
- "version": "0.5.1",
3
+ "version": "0.6.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",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@anthropic-ai/sdk": "^0.125.0",
55
- "@chaffjs/lang-en": "0.5.0",
56
- "@chaffjs/lang-ja": "0.5.0",
55
+ "@chaffjs/lang-en": "0.6.0",
56
+ "@chaffjs/lang-ja": "0.6.0",
57
57
  "mdast-util-from-markdown": "^2.0.3",
58
58
  "mdast-util-frontmatter": "^2.0.1",
59
59
  "mdast-util-gfm-table": "^2.0.0",
@@ -0,0 +1,27 @@
1
+ id: dangling-reference
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: dangling-reference
5
+ requires: [structure]
6
+ severity: error
7
+
8
+ name:
9
+ ja: 参照先が無い
10
+ en: Reference to a missing provision
11
+
12
+ why:
13
+ ja: 「第12条に定める」のような参照は、その条が文書に無ければ読み手を行き止まりにします。条を消したり番号を振り直したりしたときに、参照だけが古い番号のまま残ることがよくあります。chaff は文書を番地の付いた木にして(chaff tree)、参照の番地が木にあるかを見ます。
14
+ en: A reference such as "as set out in Section 12" leaves the reader at a dead end when that section is not in the document. It usually happens when a provision is deleted or renumbered and the reference keeps the old number. chaff builds the document's tree of addresses (chaff tree) and checks that each reference's address is in it.
15
+
16
+ message:
17
+ ja: 「{label}」(番地 {target})はこの文書にありません
18
+ en: '"{label}" (address {target}) is not in this document'
19
+
20
+ how_to_fix:
21
+ ja: 正しい番号に直すか、参照を消してください。他の文書を指しているなら、「民法第3条」のように文書名を添えると読み手に伝わります。
22
+ en: Fix the number or remove the reference. If it points into another document, name that document so the reader knows where to look.
23
+
24
+ # 何件あれば指摘するか。1 は「1 件でもあれば」。
25
+ levels: { strict: 1, normal: 1, relaxed: 1 }
26
+
27
+ use_for: [business, technical, blog]
@@ -0,0 +1,26 @@
1
+ id: duplicate-definition
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: duplicate-definition
5
+ requires: [structure]
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 同じ語の二重定義
10
+ en: Term defined twice
11
+
12
+ why:
13
+ ja: 同じ語を二か所で定義すると、二つの定義が食い違っていないかを読み手が確かめなければなりません。長い契約書では、後から足した定義が先の定義と少しずれることがよくあります。中身が食い違っているかは機械では決まらないので、chaff は二度目の定義の場所だけを示します。
14
+ en: A term defined in two places makes the reader check whether the two definitions agree. In long contracts a definition added later often drifts from the first. Whether they conflict is not something a machine can decide, so chaff only points at the second one.
15
+
16
+ message:
17
+ ja: 「{term}」は {first} 行目でも定義されています
18
+ en: '"{term}" is also defined on line {first}'
19
+
20
+ how_to_fix:
21
+ ja: 定義を一か所にまとめ、もう一方は「第2条に定める{term}」のように参照にしてください。意図して意味を変えているなら、別の語にします。
22
+ en: Keep one definition and refer to it from the other place. If the meaning changes on purpose, use a different term.
23
+
24
+ levels: { strict: 1, normal: 1, relaxed: 1 }
25
+
26
+ use_for: [business, technical, blog]
@@ -0,0 +1,26 @@
1
+ id: numbering-gap
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: numbering-gap
5
+ requires: [structure]
6
+ severity: error
7
+
8
+ name:
9
+ ja: 番号の抜け・重なり
10
+ en: Skipped or repeated number
11
+
12
+ why:
13
+ ja: 第3条の次が第5条、(a) の次が (c)、同じ第2項が二つ。番号が飛ぶか重なると、読み手は何かが消えたのか、参照がどちらを指すのかを判断できません。同じ親の中で並ぶ番号だけを比べます。枝番号(第3条の2)は並びの外として扱います。
14
+ en: Article 5 right after Article 3, (c) right after (a), two paragraphs numbered 2. A reader cannot tell whether something was removed, or which one a reference means. Only numbers side by side under the same parent are compared.
15
+
16
+ message:
17
+ ja: 「{previous}」の次が「{label}」です({expected} 番目のはず)
18
+ en: '"{label}" follows "{previous}" (expected number {expected})'
19
+
20
+ how_to_fix:
21
+ ja: 番号を振り直してください。削った条を欠番として残すなら、「第4条(削除)」のように番号だけを残すと読み手に伝わります。
22
+ en: Renumber. If a provision was removed on purpose, keep its number with a note such as "Section 4 [Deleted]".
23
+
24
+ levels: { strict: 1, normal: 1, relaxed: 1 }
25
+
26
+ use_for: [business, technical, blog]
@@ -50,7 +50,24 @@ const pickExport = (module: object): unknown => {
50
50
  return isAdapter(record["adapter"]) ? record["adapter"] : record["default"];
51
51
  };
52
52
 
53
- export const packageFor = (language: string): string | undefined => ADAPTER_PACKAGE[language];
53
+ /**
54
+ * 同梱の言語は表から、それ以外は `@chaffjs/lang-<言語>` を探す。
55
+ * 利用者が lang-zh のようなパッケージを書いて入れれば、core を変えずにその言語で動く。
56
+ */
57
+ const LANGUAGE_ID = /^[a-z]{2,3}$/u;
58
+
59
+ export const packageFor = (language: string): string | undefined =>
60
+ ADAPTER_PACKAGE[language] ?? (LANGUAGE_ID.test(language) ? `@chaffjs/lang-${language}` : undefined);
61
+
62
+ const importAdapter = async (specifier: string, language: string): Promise<unknown> => {
63
+ try {
64
+ return await import(specifier);
65
+ } catch (err) {
66
+ const missing = err instanceof Error && "code" in err && err.code === "ERR_MODULE_NOT_FOUND";
67
+ if (missing) throw new Error(`言語 ${language} のパッケージ ${specifier} が入っていません(npm i -D ${specifier})`, { cause: err });
68
+ throw err;
69
+ }
70
+ };
54
71
 
55
72
  /**
56
73
  * アダプタは「必要になったものだけ」を実行時に解決する。spec §17.2。
@@ -60,7 +77,7 @@ export const packageFor = (language: string): string | undefined => ADAPTER_PACK
60
77
  export const loadAdapter = async (language: string): Promise<LanguageAdapter> => {
61
78
  const specifier = packageFor(language);
62
79
  if (specifier === undefined) throw new Error(`no adapter for language "${language}"`);
63
- const module: unknown = await import(specifier);
80
+ const module = await importAdapter(specifier, language);
64
81
  if (typeof module !== "object" || module === null) throw new Error(`${specifier} did not export a module`);
65
82
  const candidate = pickExport(module);
66
83
  const broken = checkAdapter(candidate);
package/src/cli.ts CHANGED
@@ -23,6 +23,8 @@ import { renderFriendly } from "./render/friendly.ts";
23
23
  import { rulesJson } from "./render/rules-json.ts";
24
24
  import { renderSarif } from "./render/sarif.ts";
25
25
  import { VERSION } from "./version.ts";
26
+ import { runTree, treeTargets } from "./commands/tree.ts";
27
+ import { citeTargets, runCite } from "./commands/cite.ts";
26
28
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
27
29
  import { neededBy, runRules } from "./run.ts";
28
30
  import type { Level, RuleDefinition } from "./plugin.ts";
@@ -37,6 +39,8 @@ const USAGE = `chaff — 文章の読みにくいところを見つけます。
37
39
  chaff eval <dir> 手元の文書で閾値を測り直す
38
40
  chaff explain <rule> そのルールの意図と根拠を読む
39
41
  chaff genres ジャンルの一覧
42
+ chaff tree <file> [--format sexp|json] 文書を番地の付いた木にする(条・項・定義・参照)
43
+ chaff cite <原文> <引用.json> 回答の引用(番地と引用文)が原文にあるかを確かめる
40
44
  chaff rules --json いまの設定を JSON で出す(AI に渡す用)
41
45
  chaff baseline <dir> いまある指摘を棚上げする(既存の repo に入れるとき)
42
46
  chaff suppressions <dir> stet で黙らせている指摘を数える
@@ -283,6 +287,8 @@ const HANDLERS: Readonly<Record<string, Handler>> = {
283
287
  rules: showRules,
284
288
  explain: (argv) => explain(argv[1]),
285
289
  eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre, flag }),
290
+ tree: (argv) => runTree(treeTargets(argv), argv, { config: readConfig(), flag }),
291
+ cite: (argv) => runCite(citeTargets(argv), argv, { config: readConfig(), flag }),
286
292
  test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre, inspect }),
287
293
  baseline: (argv) => runBaseline(positional(argv), argv),
288
294
  suppressions: (argv) => runSuppressions(positional(argv), argv),
@@ -0,0 +1,74 @@
1
+ import { checkCitations, type Citation, type CitationResult } from "../structure/cite.ts";
2
+ import { readSource, readTree, type TreeContext } from "./tree.ts";
3
+
4
+ const USAGE = "使い方: chaff cite <原文> <引用.json> [--format text|json] [--language ja|en|…]";
5
+ const FORMATS = ["text", "json"];
6
+ const QUOTE_WIDTH = 40;
7
+ const VALUED = ["--format", "--language"];
8
+
9
+ export const citeTargets = (argv: readonly string[]): string[] =>
10
+ argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.includes(all[index - 1] ?? ""));
11
+
12
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
13
+
14
+ const isCitation = (value: unknown): value is Citation => isRecord(value) && typeof value["address"] === "string" && typeof value["quote"] === "string";
15
+
16
+ export type ParsedCitations = { readonly citations: readonly Citation[] } | { readonly error: string };
17
+
18
+ const parseJson = (text: string): { readonly value: unknown } | { readonly error: string } => {
19
+ try {
20
+ return { value: JSON.parse(text) };
21
+ } catch (err) {
22
+ return { error: `JSON として読めません: ${err instanceof Error ? err.message : String(err)}` };
23
+ }
24
+ };
25
+
26
+ /** 引用の一覧。[{ address, quote }] の配列だけを受け取る。形が違えば理由を返す。 */
27
+ export const parseCitations = (text: string): ParsedCitations => {
28
+ const parsed = parseJson(text);
29
+ if ("error" in parsed) return parsed;
30
+ if (!Array.isArray(parsed.value)) return { error: '引用は配列で渡してください: [{ "address": "3.2", "quote": "…" }]' };
31
+ const entries: readonly unknown[] = parsed.value;
32
+ const bad = entries.findIndex((entry) => !isCitation(entry));
33
+ if (bad !== -1) return { error: `${String(bad + 1)} 件目に address(文字列)と quote(文字列)がありません` };
34
+ return { citations: entries.filter(isCitation).map((entry) => ({ address: entry.address, quote: entry.quote })) };
35
+ };
36
+
37
+ const MESSAGES: Readonly<Record<CitationResult["status"], (result: CitationResult) => string>> = {
38
+ ok: () => "一致",
39
+ "missing-address": (result) => `番地 ${result.citation.address} は原文にありません`,
40
+ "quote-elsewhere": (result) => `引用文は ${result.citation.address} ではなく ${result.foundAt ?? "番地の外"}(${String(result.line ?? "?")} 行目)にあります`,
41
+ "quote-not-found": () => "引用文が原文のどこにもありません",
42
+ };
43
+
44
+ const describe = (result: CitationResult): string => {
45
+ const mark = result.status === "ok" ? "✓" : "✗";
46
+ const oneLine = result.citation.quote.replace(/\s+/gu, " ").trim();
47
+ const quote = oneLine.length > QUOTE_WIDTH ? `${oneLine.slice(0, QUOTE_WIDTH)}…` : oneLine;
48
+ return `${mark} ${result.citation.address}「${quote}」: ${MESSAGES[result.status](result)}`;
49
+ };
50
+
51
+ /**
52
+ * 回答の引用が原文にあるかを確かめる。書き換えはしない。
53
+ * 一つでも外れていれば 1 で終わるので、AI の回答を単体試験のように検査できる。
54
+ */
55
+ export const runCite = async (targets: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
56
+ const [sourcePath, citationsPath] = targets;
57
+ const format = context.flag(argv, "--format") ?? "text";
58
+ if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.includes(format)) {
59
+ console.error(USAGE);
60
+ return 1;
61
+ }
62
+ const citationsText = await readSource(citationsPath);
63
+ if (citationsText === undefined) return 1;
64
+ const parsed = parseCitations(citationsText);
65
+ if ("error" in parsed) {
66
+ console.error(`${citationsPath}: ${parsed.error}`);
67
+ return 1;
68
+ }
69
+ const read = await readTree(sourcePath, argv, context);
70
+ if (read === undefined) return 1;
71
+ const results = checkCitations(read.source, read.tree, parsed.citations);
72
+ console.log(format === "json" ? JSON.stringify(results, null, 2) : results.map(describe).join("\n"));
73
+ return results.every((result) => result.status === "ok") ? 0 : 1;
74
+ };