@uehaj/sys1grep 0.5.0-next.0 → 0.5.0-next.2

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.
package/README.ja.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  背景と設計の解説: [Jevのキラーアプリ、「意味で探す grep」を作った(Zenn)](https://zenn.dev/uehaj/articles/jev-semgrep-grep-by-meaning)
6
6
 
7
+ > **semgrep から改名しました(0.5.0)。** 理由は 2 つです。1 つは、静的解析ツール [Semgrep](https://semgrep.dev/) の商標と名前が衝突すること。もう 1 つは、このツールがもともと Jev 専用ではなく、System 1 モデル全般を対象にしていることです。TypeSafe AI が Jev の後に Jev 以外の名前で出すかもしれないモデルにも対応したいです :)。そこで 0.5.0 から **sys1grep**(System 1 + grep)になりました。npm パッケージは `@uehaj/sys1grep`(旧 `@uehaj/semgrep`)、コマンドは `sys1grep` / `git sys1grep`、環境変数は `SYS1GREP_*`、設定ファイルは `~/.config/sys1grep/.env` です。旧 `SEMGREP_*` と `~/.config/semgrep/.env` は 1 マイナーリリースの間は読み、使うたびに非推奨の 1 行を出します(1.0.0 で廃止)。詳細は [CHANGELOG](CHANGELOG.md#renamed)。Zenn 記事の URL は旧 slug のままです。
8
+
7
9
  正規表現ではなく **意味** で行を探す grep です。
8
10
  判定には [TypeSafe AI](https://typesafe.ai/) の System One モデル **Jev** を使います。
9
11
  Jev は文章を生成せず、typed な質問に確率だけを返すモデルなので、1 行ごとに
@@ -13,9 +15,9 @@ Jev は文章を生成せず、typed な質問に確率だけを返すモデル
13
15
  ./sys1grep -n -e "顧客が怒っている、または不満を持っている" tickets.txt
14
16
  ```
15
17
 
16
- [![sys1grep のデモ: 日本語の意味で 6 言語の返金要求を探す / 「返金について」と「返金を求めている」の違い / -Q で答えを探す](docs/demo.svg)](https://uehaj.github.io/sys1grep/)
18
+ [![sys1grep のデモ: 日本語の意味で 6 言語の返金要求を探す / 「返金について」と「返金を求めている」の違い / -Q で答えを探す](docs/demo.svg)](https://sys1grep.js.org/)
17
19
 
18
- <sub>▶ デモをクリックするか <a href="https://uehaj.github.io/sys1grep/">uehaj.github.io/sys1grep</a> を開くと、ランディングページで全編のデモが見られます。</sub>
20
+ <sub>▶ デモをクリックするか <a href="https://sys1grep.js.org/">uehaj.github.io/sys1grep</a> を開くと、ランディングページで全編のデモが見られます。</sub>
19
21
 
20
22
  - 依存ゼロ。1 ファイル、Node.js 20.16 以降と `fetch` だけで動きます。
21
23
  - 速い。30 行を 1 リクエストにまとめ、8 本並列で投げます。210 行のファイルが 1 秒弱で終わります。
@@ -122,8 +124,7 @@ top-k か質問ごとの閾値調整が要ります。また索引を作らず
122
124
  その項の意味を尋ねられるので、意味の前に安い正規表現を置くと、費用も待ち時間も下がります。行が送られるのは、
123
125
  どれかの項の正規表現がすべて当たったときだけです(正規表現の無い項はどの行にも当たる扱い)。そのため
124
126
  `-e '/re/' -a A -e B` では、`re` を含まない行も送られ、`B` だけを尋ねられます。正規表現項だけの式は何も
125
- 送りません。ただし `--sentence`(既定の `=jev`)は折り返しの切れ目を Jev に尋ねるので、送らずに済ませたい
126
- ときは `--sentence=rules` を使います。`/…/flags` の形をしていないものは今まで通り意味です。`-e '/etc 以下のファイルを変更している'`
127
+ 送りません。`--unit=sentence-by-jev` でも、そのときは折り返しのつなぎ方を規則だけで決めます。`/…/flags` の形をしていないものは今まで通り意味です。`-e '/etc 以下のファイルを変更している'`
127
128
  (閉じる `/` が無い)は影響を受けません。本当に `/` で始まり `/` で終わる意味は、正規表現と誤認されない
128
129
  よう先頭にスペースを置けます。
129
130
 
@@ -159,23 +160,51 @@ $ ./sys1grep -o -n -e '/[A-Z]+-\d+/' -a 'チケットがまだ閉じていない
159
160
  Jev に送る行が増えるほど、費用も時間もかかります。いちばん安いのは、送らずに済ませた行です。絞り方を、
160
161
  大きく削れるものから順に並べます。
161
162
 
162
- - **どのファイルを探すか。** `-r` は `.git`、`node_modules`、バイナリ、秘密情報らしいファイル、git が無視する
163
+ - **どのファイルを探すか。** `-r` は `.git`、`node_modules`、バイナリ、秘密情報らしいファイル、生成されたファイル
164
+ (ソースマップ、minify 済み JS/CSS、ロックファイル)、git が無視する
163
165
  ものを飛ばします。[`git sys1grep`](#git-のサブコマンドとして-git-sys1grep) は追跡しているファイルだけを探します。
164
166
  `--include` / `--exclude`(ファイル名のグロブ)と `--changed-within`(`30m`、`7d`、`today`、`this-week`、日付)で
165
167
  さらに絞れます。言語や変更時期を指定する意味は、それだけでファイルを絞ります
166
168
  ([意味からの絞り込み](#意味からの絞り込み))。
167
169
  - **どの行を送るか。** [正規表現項](#正規表現項)はローカルで判定し、当たった行だけが同じ AND 項の意味を
168
- 尋ねられます。空行は送りません。
170
+ 尋ねられます。空行は送りません。`-M`/`--max-columns`(既定 2000、`-z` なら 8000)を超える行は、
171
+ 先頭からその文字数までだけを送ります。判定はそれでも行われ、超えた先にしか無い一致だけが見つかりません。
169
172
  - **何回判定するか。** [`--dedup`](#テンプレートごとに-1-行だけ判定する---dedup) は、ID・数値・時刻・パスだけが
170
- 違う行をまとめて、テンプレートごとに 1 行だけ判定します。
171
- - **払う前に確かめる。** `--dry-run` は何も送らず、検索するファイル、ファイルごとの送る行数、各リクエストと
172
- その質問を表示します。最後の行には入力トークン数と、TypeSafe 本体なら料金の見積もりが出ます
173
- (`~3178 input tokens, ~$0.000133`。誤差 1 割程度)。`-i` は同じ集計を端末に出し、`y` と答えたときだけ送ります。
173
+ 違う行をまとめて、テンプレートごとに 1 行だけ判定します。`auto`/`always`/`never`、当面は既定で off。
174
+ `--dedup=auto` が割に合うときはヒントを出します。
175
+ - **払う前に確かめる。** `--dry-run` は何も送らず、この検索が使う設定、検索するファイル、ファイルごとの
176
+ 送る行数、各リクエストとその質問を表示します。最後の行には入力トークン数と、TypeSafe 本体なら料金の
177
+ 見積もりが出ます(`~3178 input tokens, ~$0.000133`。誤差 1 割程度)。`-i` は同じ集計を端末に出し、
178
+ `y` と答えたときだけ送ります。
179
+ リクエストの本体を送る前に、まず対象のサイズ(`--max-filesize`、既定 10M)を計測し、超えるものは
180
+ rg の `--max-filesize` と同じく無条件に飛ばして stderr に名前を出します(`-y` は効きません)。`.gz` は
181
+ 展開後の大きさで計測します(上限で展開を止めるので、大きすぎるものを全部展開することはありません)。標準入力は
182
+ 読んでから(すでにメモリ上にあるので)計測し、超えていれば同じく飛ばします。`--cached` / `<tree>:` の
183
+ 対象はファイルではなく blob なので、その内容(`git cat-file --batch` で一括して読んだもの。blob ごとに
184
+ git を起動し直しません)から計測します。`-g` のコミットはここに含まれません。送るときにすでに `-M` で
185
+ 上限があるからです。
186
+ 送る予定のトークンの値段(`--max-cost`、既定 1 USD)は別に見積もり、超えていれば端末で続けるか聞きます。
187
+ `-y` は聞かずに yes と答え、端末が無く超えていれば終了コード 2 です(`-q` でも同じで、スクリプトからは
188
+ `-y` を渡します)。
174
189
 
175
190
  ```sh
176
191
  $ sys1grep --dry-run -r --include='*.log' --changed-within=today -e '/ERROR|FATAL/' -a '顧客に影響が出ている' logs/
177
192
  ```
178
193
 
194
+ `--verbose`(や `--dry-run`)は、コマンドラインで指定しなかった設定が*どこから*来たか
195
+ (`SYS1GREP_OPTS`、環境変数、`~/.config/sys1grep/.env`、プリセットの既定値のいずれか)も表示するので、
196
+ 想定と違う結果をその設定まで辿れます。キーの値は表示せず、どのオプションや変数が渡したかだけを表示します。
197
+
198
+ ```sh
199
+ $ SYS1GREP_OPTS='--level strict' sys1grep --verbose -e "APIキーがファイルから読まれている" .
200
+ sys1grep: endpoint api.typesafe.ai/v1/systemone (default), model jev-latest (default)
201
+ sys1grep: key: SYS1GREP_API_KEY (~/.config/sys1grep/.env)
202
+ sys1grep: SYS1GREP_OPTS: --level strict
203
+ sys1grep: options: --level strict (SYS1GREP_OPTS) = -t 0.7 -T 0.3, --chunk 30, -j 8, scope on, -M 2000, --max-filesize 10M, --max-cost 1
204
+ sys1grep: file ./a.py: 120 lines, 120 to send
205
+ …
206
+ ```
207
+
179
208
  ## インストール
180
209
 
181
210
  使い方は 2 通りあります。コマンドラインツールとして使う(この節)か、Claude Code のスキルとして使う
@@ -369,12 +398,17 @@ tests/tickets/sub/b.txt
369
398
  `-r` はディレクトリを名前順にたどり、`.git`、`node_modules`、`.ssh`、`.aws`、`.gnupg`、`.kube`、`.docker`、
370
399
  バイナリ(先頭 8 KB に NUL がある、または PDF。BOM 付きの UTF-16 はテキストとして読む)、
371
400
  秘密情報になりがちなファイル(`.env*`、`.netrc`、`.npmrc`、`.pypirc`、`.pgpass`、`.git-credentials`、`*.pem`、
372
- `*.key`、`*.p12`、`*.pfx`、`*.jks`、`*.keystore`、`id_rsa*` など。大文字小文字は区別しない)を飛ばします。
401
+ `*.key`、`*.p12`、`*.pfx`、`*.jks`、`*.keystore`、`id_rsa*` など。大文字小文字は区別しない)、
402
+ 生成されたファイル(`*.map`、`*.min.js`、`*.min.css`、`package-lock.json`、`yarn.lock`、`pnpm-lock.yaml`、
403
+ `Cargo.lock`、`poetry.lock`、`composer.lock`、`Gemfile.lock`、`go.sum`)を飛ばします。
373
404
  **検索対象の行はすべて TypeSafe の API に送られる**ので、スキャンするつもりのディレクトリだけを指定してください。
374
405
  git リポジトリの中では、git が無視するもの(`.gitignore`、`.git/info/exclude`、グローバルの除外ファイル)も `-r` で
375
406
  飛ばすので、ビルド成果物や手元だけのファイルは送られません。追跡中のファイルは、無視パターンに当たっても検索します。
376
407
  コマンドラインで明示したファイルは、除外リストに該当しても、git が無視していても検索します。git が無視している
377
408
  ディレクトリも、名前を指定すれば検索します(`sys1grep -r -e ... dist`)。
409
+ `*.gz`(ローテートしたログ `app.log.1.gz`)は `zgrep` のように展開して読み、ディスク上の名前で表示します
410
+ (`app.log.1.gz:12:...`)。`--include='*.gz'` で選べ、バイナリ判定は展開後のバイトで行い、壊れた `.gz` は
411
+ 読めないファイルとして扱います。圧縮した秘密情報らしいファイル(`private.key.gz`)は元と同じく飛ばします。gzip だけ、名前だけで判断します。
378
412
  `-l` は一致したファイルを見つかった順に 1 回ずつ表示し、`-r` の有無にかかわらず使えます。`-c` は行の代わりにファイルごとの一致行数を出します。
379
413
 
380
414
  ### 意味からの絞り込み
@@ -438,6 +472,26 @@ FILE を省くとカレントディレクトリ以下の追跡ファイルを全
438
472
  「いま変わった」扱いになります。
439
473
  ヘルプは `git sys1grep -h` です(`--help` は git が横取りして man ページを探しに行きます)。
440
474
 
475
+ `--cached`、`--untracked`、`<tree>...` は作業ツリーの代わりに探す対象を選びます。`git grep` と同じで、
476
+ 3 つのうち同時に使えるのは 1 つだけです。
477
+
478
+ ```sh
479
+ $ git sys1grep --cached -e "リトライしている" # ステージ済み、未コミットの変更も含む
480
+ $ git sys1grep --untracked -e "リトライしている" # 追跡ファイルに加え未追跡ファイルも (.gitignore は効いたまま)
481
+ $ git sys1grep -n -e "リトライしている" main v0.3.1 -- '*.py'
482
+ main:src/job.py:42: retry(job, times=3)
483
+ v0.3.1:src/job.py:40: retry(job)
484
+ ```
485
+
486
+ `<tree>`(ブランチ・タグ・コミット・`@{u}`)は `--` の前に置いた、リビジョンとして解決できる引数です。複数指定でき、
487
+ 指定順に探し、各行には解決前の名前をそのまま付けます (`@{u}:path` であって、解決したブランチ名ではありません)。
488
+ `--changed-within` は作業ツリーが要ります。blob(`--cached`、`<tree>`)には自分の mtime がないためです。
489
+ `<tree>` 自身の pathspec も sys1grep の他の場所と同じくグロブが使えます(空の tree との `git diff-tree` を使い、
490
+ `git ls-tree` 自身のリテラル・ディレクトリ前方一致のみのルールには従いません)。`--include` / `--exclude`
491
+ (名前で絞る方)はそのまま使えます。
492
+ **古い `<tree>` には、後で削除された秘密情報が普通のファイルとして残っていることがあります**。スキップ対象は
493
+ 名前で判定するだけで、変更履歴では判定しません。
494
+
441
495
  ### 「〜でない」行を全部
442
496
 
443
497
  ```sh
@@ -465,15 +519,15 @@ $ git log -z --format='%h %s %b' | ./sys1grep -z -n -e "ユーザーに見える
465
519
  ファイル名 (`-l`) と件数 (`-c`) は grep と同じく改行のままです。`-z` のとき `-n` はレコード番号、
466
520
  `-A` / `-B` / `-C` は前後のレコード数、`--chunk` は 1 リクエストのレコード数を数えます。
467
521
 
468
- ### 1 文ずつ判定する (`--sentence`)
522
+ ### 1 文ずつ判定する (`--unit=sentence-by-*`)
469
523
 
470
- `--sentence` を付けると、行ではなく文ごとに判定します。出力は grep と同じく行のままです。当たった文がかかる
524
+ `--unit=sentence-by-jev`(または後述の `sentence-by-rule`)を付けると、行ではなく文ごとに判定します。出力は grep と同じく行のままです。当たった文がかかる
471
525
  行をすべて出し、端末では文の部分を太字の黄で強調します(その中の正規表現の一致は grep と同じ太字の赤)。文に分ける前に折り返した行をつなぐので、
472
526
  複数行にまたがる文も 1 文として判定します。[`tests/prose.txt`](tests/prose.txt) には、折り返した英語と日本語の
473
527
  段落が入っています。
474
528
 
475
529
  ```sh
476
- $ ./sys1grep -n --sentence -e "the author admits they made a mistake" tests/prose.txt
530
+ $ ./sys1grep -n --unit=sentence-by-jev -e "the author admits they made a mistake" tests/prose.txt
477
531
  1:I should have checked the input
478
532
  2:before shipping, and that was my
479
533
  3:mistake. Next time I will add a test
@@ -485,14 +539,14 @@ $ ./sys1grep -n --sentence -e "the author admits they made a mistake" tests/pros
485
539
  端末で意味を 2 つ渡し、`-C 3` で前後も出すと、文がどこで始まりどこで終わるかが色で分かります。3 行目と 9 行目は、当たった文の
486
540
  終わりまでだけ色が付き、4〜7 行目は文脈行(`-`)です:
487
541
 
488
- ![--sentence -C 3 --color: 当たった文が太字の黄になる。3 行目は mistake. まで、9 行目は 返金してほしいです。 まで。4〜7 行目は文脈行](docs/sentence.svg)
542
+ ![--unit=sentence-by-jev -C 3 --color: 当たった文が太字の黄になる。3 行目は mistake. まで、9 行目は 返金してほしいです。 まで。4〜7 行目は文脈行](docs/sentence.svg)
489
543
 
490
544
  `-o` を付けると、当たった文だけを 1 行ずつ出します。`grep -o` が一致した部分だけを出すのと同じです。
491
545
  `-n` は文が始まる行の番号になります。日本語は空白を入れずにつなぎます。単語の間に空白を置かない
492
546
  中国語・タイ語・ラオ語・クメール語・ミャンマー語・チベット語も同様です。
493
547
 
494
548
  ```sh
495
- $ ./sys1grep -n -o --sentence -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
549
+ $ ./sys1grep -n -o --unit=sentence-by-jev -e "the author admits they made a mistake" -e "customer is asking for a refund" tests/prose.txt
496
550
  1:I should have checked the input before shipping, and that was my mistake.
497
551
  8:先週買った掃除機が初日から動かないので返金してほしいです。
498
552
  ```
@@ -500,19 +554,19 @@ $ ./sys1grep -n -o --sentence -e "the author admits they made a mistake" -e "cus
500
554
  `-o` なしでは、`-c` と `-A` / `-B` / `-C` はいつもどおり行で数えます。`-o` 付きでは文で数えます。
501
555
  `-z` と併用すると、レコードごとに文に分け、当たったレコードをまるごと出します。
502
556
 
503
- Jev は長い行の中からでも当たる文を自分で見つけるので、判定の精度のために `--sentence` を使う必要はありません。
557
+ Jev は長い行の中からでも当たる文を自分で見つけるので、判定の精度のために `--unit=sentence-by-jev` を使う必要はありません。
504
558
  どの文が当たったかを見たいとき、`-o` で文そのものが欲しいとき、AND を 1 つの文の中で成り立たせたいときに
505
559
  使います。式は文ごとに評価されます。そのため、`-v X` だけの式では「X でない文を 1 つでも含む行」がすべて出ます。
506
- 行全体として X でないものを探すなら、`--sentence` を付けずに使います。
560
+ 行全体として X でないものを探すなら、`--unit` を `line` のままにします。
507
561
 
508
562
  日本語や中国語では、チャットの発言、問い合わせ、メモの 1 行など、`。` で終わらない 1 行 1 件のデータがよくあります。
509
- これをつなぐと、別々の項目が 1 つの「文」になってしまいます。そこで既定(`--sentence`、`--sentence=jev` と同じ)では、
563
+ これをつなぐと、別々の項目が 1 つの「文」になってしまいます。そこで `--unit=sentence-by-jev` では、
510
564
  単語の間に空白を置かない文字に接する、句点の無い改行について、Jev に「この改行は文や項目の終わりか、文の途中の
511
565
  折り返しか」を聞きます。30 行を 1 リクエストにまとめて改行ごとに yes/no を聞き、0.7 以上なら行をつなぎません。
512
- [`tests/corpus.txt`](tests/corpus.txt) では、これで日本語の問い合わせ 4 件が 1 件ずつに分かれ、`--sentence` でも
566
+ [`tests/corpus.txt`](tests/corpus.txt) では、これで日本語の問い合わせ 4 件が 1 件ずつに分かれ、`--unit=sentence-by-jev` でも
513
567
  行単位の検索と同じ返金の要求(14 行目と 18 行目)が見つかります。規則だけでは問い合わせがつながり、18 行目を
514
568
  取りこぼしていました。追加のリクエストの費用は、意味を 1 つ増やすのと同じくらいです。避けたいときは
515
- `--sentence=rules` を使います。英語どうしの改行は聞きません。つないでも空白が残り、ピリオドで文が切れるためです。
569
+ `--unit=sentence-by-rule` を使います。英語どうしの改行は聞きません。つないでも空白が残り、ピリオドで文が切れるためです。
516
570
 
517
571
  改行が文の途中になりえないところでは、行をつなぎません。空行、括弧や `;` に接する改行(JSON やコード)、
518
572
  `-` `*` `+` `#` `>` `"` や数字で始まる行(箇条書き、見出し、引用、番号、日時)の前です。そのため JSONL は
@@ -521,12 +575,100 @@ Jev は長い行の中からでも当たる文を自分で見つけるので、
521
575
  `Mr.` のような略語の後ろでも切ります。ログは文章ではないので、`ERROR ...` の次の `WARN ...` のように
522
576
  英字で始まるログ行が続くと、つながります。
523
577
 
578
+ ### 関数ごとに判定する (`--unit=function`)
579
+
580
+ コードについての問いは、たいてい関数についての問い(「ネットワーク障害で再試行している」)で、関数の 1 行だけでは
581
+ そうと分かりません。`--unit=function` は関数ごとに判定し、当たった関数の行を出します。`-n` はファイルの行番号です。
582
+
583
+ ```sh
584
+ $ ./sys1grep -n --unit=function -e "ネットワーク障害で再試行している" src/net.js
585
+ src/net.js:3:async function fetchWithRetry(url) {
586
+ src/net.js:4: for (let i = 0; i < 5; i++) {
587
+ src/net.js:5: try { return await fetch(url); }
588
+ src/net.js:6: catch { await sleep(2 ** i * 100); }
589
+ src/net.js:7: }
590
+ src/net.js:8:}
591
+ ```
592
+
593
+ 関数は、関数名の行から次の関数名の行の前までです(`git grep -W` と同じ)。最初の関数名の行より前の行は 1 つの単位に
594
+ なり、関数の末尾の空行は出力しません。関数名の行は git と同じく、`.gitattributes` の `diff=<driver>` 属性と、git config の
595
+ `diff.<driver>.xfuncname` で決めます。無ければ、JavaScript/TypeScript(トップレベルの `function`・`class`、関数を
596
+ 代入する `const` / `let` / `var`)と Python(入れ子も含む `def` / `class`)は sys1grep の規則を使い(デコレータの行 `@retry` から関数が始まる)、それ以外は git の既定
597
+ (英字・`_`・`$` で始まる行)を使います。git 組み込みのドライバ(`diff=python` など)は読まないので、`xfuncname` の無い
598
+ ドライバも同じように扱います。
599
+
600
+ ```sh
601
+ $ printf '*.go diff=golang\n' >> .gitattributes
602
+ $ git config diff.golang.xfuncname '^(func|type)[[:space:]]'
603
+ ```
604
+
605
+ `-M` の既定は `-z` と同じ 8000 文字で、それより長い関数は先頭 8000 文字で判定します。`--chunk` は関数の数、
606
+ `-A`/`-B`/`-C` と `-c` は文の単位と同じく行の数を数えます。`-z`・`-g`・`-o` とは併用できません。
607
+
608
+ ### 関数から関数へたどる (`--step-to`)
609
+
610
+ コードについての問いは、2 段になることがよくあります。`--summarize` はどこで行をツールに渡しているか。そして、
611
+ それが失敗したらどうなるか。2 段目の「それ」は 1 段目で見つけたものです。多段階マッチングは両方を 1 回で
612
+ 答えます。`--step-to` の前の式で開始点を探し、そこから呼び出しをたどり、たどり着いた関数のうち `--step-to` の
613
+ 後ろの式に当たるものを選びます。2 つの式は Jev が判定します。どの呼び出しをたどるかはスコアで決めません。
614
+
615
+ ```sh
616
+ $ sys1grep -e "--summarize が行をツールに渡している" --step-to "ツールの起動や応答が失敗したときの処理" sys1grep.mjs
617
+ ```
618
+
619
+ 意味は、最初の例のようにダッシュで始めてもかまいません。`-e`・`-a`・`-v`・`-Q` のどの検索でも同じです。`-` で始まる値は、オプションでなければ意味として扱います(オプションには空白が入りません)。
620
+
621
+ パスは木の形で出ます。ホップ数 (開始点からの呼び出しの数)、`file:line`、関数名の順で、終点の後ろは `:`、途中の
622
+ 関数の後ろは `-` です (grep が一致行と文脈行を分けるのと同じ)。開始と終点の式が正規表現だけなら何も送らないので、
623
+ 無料で到達可能性を調べられます。Python 3.6 の `json` で、`load` からたどり着き `raise` を含む関数:
624
+
625
+ ```
626
+ $ cd /usr/lib64/python3.6/json && sys1grep -e '/^\s*def load\b/' --step-to '/\braise\b/' *.py
627
+ sys1grep: walk: 1 at hop 0, 1 at hop 1, 3 at hop 2, 1 at hop 3, 1 at hop 4, 1 at hop 5; stopped: no new unit
628
+ 0 __init__.py-274-load
629
+ 1 __init__.py:302:loads
630
+ 2 decoder.py:334:decode
631
+ 3 decoder.py:345:raw_decode
632
+ 4 scanner.py-65-scan_once
633
+ 5 scanner.py:28:_scan_once
634
+ ```
635
+
636
+ - **式。** `--step-to` の前の式が開始の式で、普通の検索と同じです。後ろの `-e` / `-a` / `-v` / `-Q` はすべて終点の式です。
637
+ `--step-to 意味` と `--step-to '/RE/'` は `--step-to -e ...` と同じで、ファイル名はその後に置きます。step はホップでは
638
+ なく検索の段です。間に何ホップあっても、`--step-to` 1 つで 2 段です。
639
+ - **辺。** `--edges` が無ければ単位は関数 (`--unit=function`) で、辺は呼び出しです。関数本体 (コメント・docstring・
640
+ 文字列は除く) の `名前(` が、その名前の関数すべてにつながります。名前は関数名の行 (デコレータの下の `def`) から
641
+ 取ります。`--edges=FILE` は別の関係をたどります。1 行 1 本で `FROM_FILE:LINE<TAB>FROM_NAME<TAB>TO_FILE:LINE<TAB>TO_NAME`、
642
+ 行はそれを含む単位を指します。`--reverse` は辺を逆にたどります (呼ばれる側から呼ぶ側へ)。
643
+ - **歩き方。** すべての開始点から同時に幅優先でたどります。関数のホップ数は、どれかの開始点からの最短距離です。
644
+ 各関数には 1 度しか着かないので、循環しても止まります。`--hops=N`・`--hops=M..N`・`--hops=M..` で終点のホップ数を
645
+ 選びます (既定 `0..`。終点の式に当たる開始点はホップ 0 の終点)。終点に着かないパスは出しません。stderr には
646
+ ホップごとにたどり着いた関数の数と、止まった理由 (新しい関数が無い、または `--hops`) を出します。
647
+ - **費用。** Jev はまずすべての関数を開始の式と 1 回のバッチで照らし、次に `--hops` の範囲でたどり着いた関数を
648
+ 終点の式ともう 1 回のバッチで照らします。`--max-cost` は 2 回目の前にも、1 回目の分を含めて確かめます。
649
+ `--dry-run` はどの問いにも 0 と答えるので、開始の式が当たりうる関数すべてから歩き、その上限を見せます。
650
+ - **併用できない**のは `-z`・`-g`・`-o`・`-c`・`-l`・`-A`/`-B`/`-C`・`--rank`・`--summarize`・`--dedup` です。
651
+ `-p` は終点の式と照らした関数にその確率を足し、`-q` は何も出さず、終点があれば終了コード 0 にします。
652
+
524
653
  ### テンプレートごとに 1 行だけ判定する (`--dedup`)
525
654
 
526
655
  費用は送るテキストの量に比例します。機械が吐くログの大半は、ID や数値だけが違う同じ骨格の繰り返しです。
527
656
  `--dedup` は ID・ハッシュ・数値・日付と時刻・パス・URL をマスクして行をまとめ、グループごとに 1 行だけ送って、
528
657
  その答えを残りの行にも使います。送るのはその行の原文で、出力も各行がそのまま出ます。
529
658
 
659
+ `--dedup` は `auto`・`always`・`never` を取り、裸の `--dedup` は `--dedup=always` です。**当面の既定は
660
+ `never`**(#143)。`auto` は、実運用の統計が既定にしてよいと言うまで見送ります。それでも毎回、送る前に
661
+ ローカルで、全種類を縮めた場合(最良のケース)にどれだけ浮くかを見積もり、それが `--dedup` 自身の質問の
662
+ 費用の 2 倍以上なら stderr に一行ヒントを出します。
663
+
664
+ ```
665
+ $ sys1grep -e "a request failed" app.log
666
+ sys1grep: 2040 units fold to at most 31 templates; --dedup=auto would save ~68 requests (~44k tokens)
667
+ ```
668
+
669
+ `--dedup=auto` はその見積もりが割に合うときだけ尋ねてまとめます。ほとんど縮まないファイルは、追加のリクエスト
670
+ なしに全行を送ります。`--verbose`・`--dry-run` はどの値でもこの判定と数字を表示します。
671
+
530
672
  ```sh
531
673
  $ ./sys1grep --dedup -n -e "a request failed" app.log
532
674
  1:worker request 3fa9c1e27b failed: connection reset
@@ -543,7 +685,7 @@ Jev に聞き、その種類はまとめません(上の 2 リクエストの
543
685
  値を何も残さずにまとめた場合の実測では、43,071 行のシステムログが 550 テンプレート(バイト数で 1.2%)に、
544
686
  `install.log` が 21.1% に縮みました。Claude Code のトランスクリプト(jsonl)は 56.8% までしか縮みません。
545
687
  機械が吐くログ向けの機能で、散文には共通の骨格がなく、時刻を読む意味ではほとんど縮みません。
546
- `-z` や `--sentence` では、行ではなくレコードや文をまとめます。
688
+ `-z` や `--unit=sentence-by-*` では、行ではなくレコードや文をまとめます。
547
689
 
548
690
  検索後の集計行には、まとめたことで浮いた分が `(2 folded by --dedup, ~235 input tokens / ~$0.000010 saved, 21%)`
549
691
  のように出ます(上の例)。まとめた行を別のリクエストとして送った場合の推定値から、値の種類を聞いたリクエストの費用を引いた
@@ -571,15 +713,132 @@ $ sys1grep -r -n --summarize -e "API キーをファイルから読んでいる"
571
713
  を尋ねた例では、Claude の入力が 22,059 トークンから 1,356 に、Jev を含む総額が $0.094 から $0.013 に下がり、答えは同じでした(#69)。
572
714
  答えが少数の行にあるときに効きます。
573
715
 
574
- - 一致した行はもう一度マシンの外へ、Anthropic に送られます。
716
+ - 一致した行はもう一度マシンの外へ、Anthropic(や TOOL の接続先)に送られます。
575
717
  - `-n`・`-A/-B/-C`・`-p`・ファイル名は表示どおりに渡し、色は付けません。一致がなければ何も渡しません(終了コード 1)。
576
- - `SYS1GREP_SUMMARIZER` は値を付けない `--summarize` の TOOL を(今は `claude` だけ)、`SYS1GREP_SUMMARIZER_MODEL` はそのモデルを決めます。
718
+ - `SYS1GREP_SUMMARIZER` は値を付けない `--summarize` の TOOL を、`SYS1GREP_SUMMARIZER_MODEL` はそのモデルを決めます。
577
719
  - `-q`・`-l`・`-c` は行を出さないので、一緒には使えません。
720
+ - `SYS1GREP_OPTS` に書くとすべての検索を要約するので、一致した行は毎回 TOOL の提供元へも送られます。
721
+ 1 回だけ止めるには `--no-summarize` を付けます。
722
+ - 答えはプレーンテキストです。何も言わなければ LLM は Markdown で書き、端末では雑音になるので、plain を頼みます。
723
+ `--format=markdown` か `=html` でそれらを頼めます(`… --format=html … > summary.html`)。
724
+ 答えは確かめずにそのまま表示します。`SYS1GREP_OPTS` に書けば常用の設定になり、`--summarize` も `--rank` も無いときは無視します。
578
725
  - `--dedup` では、Jev に送ったものと同じく、テンプレートごとの代表を 1 回だけ `(×N like it)` を付けて渡します。
579
726
  答えを使い回したすべての行は渡しません。
580
727
  - 200 KB(約 5 万トークン)を超えるときは何も渡さず、大きさを示して終了コード 2 で止まります。TOOL の費用がかかる前です。
581
728
  先頭だけの要約は全体の要約に見えてしまうので、切り詰めはしません。
582
729
 
730
+ `--summarize-prompt=TEXT` で固定の指示の後に自分の指示(長さ・観点など)を足せます。
731
+
732
+ ```sh
733
+ $ sys1grep -r -n --summarize --summarize-prompt="3 行以内で。どのファイルを直せばよいかだけ" \
734
+ -e "API キーをファイルから読んでいる" .
735
+ ```
736
+
737
+ `--summarize` が要ります。空の TEXT は指定しないのと同じです。`--format` の文より後に置くので、TEXT で書式を上書きできます。
738
+
739
+ 他の TOOL:
740
+
741
+ ```sh
742
+ $ sys1grep --summarize=llm -e "..." FILE # Simon Willison 氏の llm、ツールなし (-T を渡さない)
743
+ $ sys1grep --summarize=pi -e "..." FILE # pi --print --no-tools --no-session ...
744
+ $ SYS1GREP_SUMMARIZER_MODEL=qwen3.5:9b sys1grep --summarize=ollama -e "..." FILE
745
+ $ SYS1GREP_SUMMARIZER_MODEL=some-id sys1grep --summarize=lmstudio -e "..." FILE # モデル id は GET /v1/models から
746
+ $ SYS1GREP_SUMMARIZER_MODEL=some-id sys1grep --summarize=http://localhost:8080/v1 -e "..." FILE # llama.cpp・vLLM・LocalAI・ゲートウェイ
747
+ ```
748
+
749
+ `ollama` と `lmstudio` はローカルの OpenAI 互換サーバ (`POST /v1/chat/completions`) へ `fetch` で直接送ります。
750
+ CLI を挟まないので、一致した行がもう一度マシンの外へ出ることはありません。素の `http(s)://` URL は他の
751
+ OpenAI 互換サーバ全般です。3 つとも `SYS1GREP_SUMMARIZER_MODEL` が要ります(既定モデルが無いため)。
752
+ `SYS1GREP_SUMMARIZER_API_KEY` は URL の TOOL にだけ `Authorization: Bearer` として送られます
753
+ (Jev 用の `SYS1GREP_API_KEY` は送りません)。`OLLAMA_HOST` で ollama 側のホストを変えられます
754
+ (`ollama` CLI 自体と同じ)。
755
+
756
+ ### 良いものから順に出す (`--rank`)
757
+
758
+ 一致は grep と同じくファイル順に出ます。多いときは `--rank` で、結果を良いものから順に、番号付きの見出しの下に
759
+ 出せます。結果とは一致とその `-A/-B/-C` の行で、文脈が接する一致は 1 つの結果になります。
760
+
761
+ ```sh
762
+ $ sys1grep -n -C1 --rank -e "返金を断られた" tickets/
763
+ 1. tickets/b.txt
764
+ tickets/b.txt-2-注文番号 1234、2026-08-01 注文。
765
+ tickets/b.txt:3:注文から 30 日を過ぎているため、返金はお受けできません。
766
+ tickets/b.txt-4-サポートまでご連絡ください。
767
+
768
+ 2. tickets/a.txt
769
+ tickets/a.txt-11-注文番号 88 が届きました。
770
+ tickets/a.txt:12:返金のご依頼を受け付けました。
771
+ tickets/a.txt-13-確認いたします。
772
+ ```
773
+
774
+ - `--rank` は `--rank=jev` です。検索の後、各結果(行と文脈をまとめて)が否定でない意味に関係するかを Jev に
775
+ 聞きます。結果ごとに質問が 1 つ増え、1 リクエストに `--chunk` 個まとめます。どんな結果があるかは検索の後で
776
+ しか分からないので、`--dry-run` / `-i` は上限を出します。
777
+ - `--rank=match` は結果の中で最も高い一致の確率で並べ、リクエストを送りません。1 行ずつの yes/no の答えなので、
778
+ はっきりした一致どうしは近い値になり、文脈も読みません。
779
+ - `-p` は見出しに点数を付けます(`1. [0.96] tickets/b.txt`)。`-l` は最良の結果の順にファイル名を出します。
780
+ - `--format=markdown` は結果ごとに `## 1. tickets/b.txt` の見出しとコードブロックを、`--format=html` はテンプレート
781
+ (下記)から 1 つの自己完結した HTML 文書を書きます。ライトとダークに対応し、結果ごとに関連度のバーが付きます。
782
+ 行は表示どおりで、エスケープし、色は付けません。
783
+ - `--summarize` には順位どおりに渡します。`--dedup` では代表 1 つが 1 つの結果です。
784
+ - 意味が要ります(正規表現・`!`・`-v` だけでは並べられない)。`-c`・`-o`・`-q` とは併用できません。
785
+ `--no-rank` はそれより前の `--rank`(`SYS1GREP_OPTS` のものなど)を取り消します。
786
+
787
+ ### HTML のテンプレート (`--template`)
788
+
789
+ `--rank --format=html` と `--summarize --format=html` はテンプレートを埋めて書きます。同梱は 4 つで、`default`、`print`(白地に黒の明朝系、
790
+ 紙と PDF 向け)、`search`(Web 検索エンジン風の結果ページ)、`terminal`(暗い背景の等幅)です。`--template=NAME` で選び、環境変数か
791
+ `~/.config/sys1grep/.env` の `SYS1GREP_TEMPLATE` で既定を決められます。
792
+
793
+ ```sh
794
+ $ sys1grep -r -n -C1 --rank -p --format=html --template=print -e "返金を断られた" tickets/ > refunds.html
795
+ ```
796
+
797
+ テンプレートは 1 つの HTML ファイルです。`<!--result-->` と `<!--/result-->` の間を結果ごとに繰り返し、
798
+ その前と後は 1 回だけ書きます。次の文字列をエスケープした値に置き換え、それ以外の `{{…}}` はそのまま残します。
799
+
800
+ | 置き換える文字列 | 使える場所 | 値 |
801
+ |---|---|---|
802
+ | `{{title}}` | どこでも | `sys1grep: ` と意味 |
803
+ | `{{query}}` | どこでも | 意味。`"refund" and not "policy"` の形 |
804
+ | `{{count}}` | どこでも | 結果の数(`--summarize` では一致した行の数) |
805
+ | `{{answer}}` | どこでも | `--summarize` の答え。`--rank` では空 |
806
+ | `{{rank}}` | 結果ごと | 1, 2, … |
807
+ | `{{score}}` | 結果ごと | `-p` が出す形の点数(`0.96`)。`-p` が無ければ空 |
808
+ | `{{score_pct}}` | 結果ごと | 点数を 0〜100 の整数にしたもの。`-p` が無くても入る(バーには `style="--s:{{score_pct}}"`) |
809
+ | `{{file}}` | 結果ごと | ファイル名。1 ファイルだけを探したときは空 |
810
+ | `{{lines}}` | 結果ごと | 結果の行。表示どおり |
811
+
812
+ `--template=NAME` は `~/.config/sys1grep/templates/NAME.html` があればそれを、無ければ同梱のものを読みます。
813
+ なので自分の `default` を置けば同梱のものに代わります。`/` を含むか `.html` で終わる値はファイルのパスです。
814
+ 自分のものを作るには、同梱のものをコピーして編集します。
815
+
816
+ ```sh
817
+ $ sys1grep --install-templates # ファイルごとに "copied PATH"
818
+ $ cp ~/.config/sys1grep/templates/default.html ~/.config/sys1grep/templates/team.html
819
+ $ sys1grep -r --rank --format=html --template=team -e "..." src/ > out.html
820
+ $ sys1grep --template=list # 1 行に 1 つの名前。~/.config のものには "(user)"
821
+ ```
822
+
823
+ `{{score_pct}}` は `-p` が無くても入るので、数字を出さずに関連度をバーで見せられます。
824
+
825
+ `--summarize --format=html` では TOOL に平文を書かせ、その答えを、終わってからエスケープして `{{answer}}` に入れます。
826
+ `<!--result-->` と `<!--/result-->` の間は書きません。`--summarize` で使うテンプレートには、その外に `{{answer}}`
827
+ が要ります(同梱のものにはあります。`--rank` のときは空)。
828
+
829
+ ```sh
830
+ $ sys1grep -r --summarize --format=html --template=print -e "返金を断った理由" tickets/ > why.html
831
+ ```
832
+
833
+ テンプレートは信頼したローカルの入力として扱います。指定したパスやテンプレートのディレクトリにあるシンボリック
834
+ リンクはそのまま読み、エスケープするのは埋め込む結果だけです。
835
+
836
+ `--install-templates` は既にあるファイルを上書きしません(そのファイルには `kept` と出します)。テンプレートが
837
+ 見つからない、あるいは `<!--result-->` 1 つの後に `<!--/result-->` 1 つ、になっていないときは、そのファイルを
838
+ 示して終了コード 2 です。コマンドラインの `--template` には、`--rank` か `--summarize` と `--format=html` が要ります(`-l` とは併用不可)。
839
+ `SYS1GREP_TEMPLATE` はそれ以外では使われないだけです。`--template=list` と `--install-templates` は他の引数と
840
+ 併用できず、`SYS1GREP_OPTS` には書けません。
841
+
583
842
  ## Claude Code から使う
584
843
 
585
844
  sys1grep を代わりに走らせてくれる Claude Code のスキルがあります。探したいものを言葉で書くと、式を組み立てて
@@ -633,7 +892,11 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
633
892
  -T THRESH 否定条件の閾値。確率 < THRESH で「〜でない」と判定 (--level より優先)
634
893
  -t 0.6 -T 0.3 なら 0.3〜0.6 の曖昧な行はどちらにも当たらない
635
894
  -r ディレクトリを再帰的に探す (FILE 省略時はカレント)。.git、node_modules、
636
- バイナリ、秘密情報らしいファイル、git が無視するものは飛ばす
895
+ バイナリ、秘密情報らしいファイル、生成されたファイル、git が無視するものは飛ばす
896
+ --cached git sys1grep 限定。作業ツリーではなくインデックスを探す (git grep --cached と同じ)
897
+ --untracked git sys1grep 限定。追跡ファイルに加え未追跡ファイルも探す (.gitignore は効いたまま。git grep --untracked と同じ)
898
+ <tree>... git sys1grep 限定。-- の前のブランチ・タグ・コミット・@{u} はそのリビジョンのツリーを探す
899
+ (git grep と同じ)。出力には <tree>: を付け、名前は解決前のまま表示する
637
900
  --include=GLOB, --exclude=GLOB -r と git sys1grep で、名前が GLOB に合うファイルだけ (または合わないものだけ) を探す
638
901
  (-r ではコマンドラインで指定したファイルは必ず探す。git sys1grep の pathspec は絞り込む)
639
902
  --changed-within=WHEN -r と git sys1grep で、30m / 2h / 7d / 2w 以内、日付か日時以降、today / this-week /
@@ -651,14 +914,38 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
651
914
  判定も変わる
652
915
  -j N 同時リクエスト数 (既定 8)
653
916
  -n 行番号を付ける
654
- -z, --null-data 行ではなく NUL 終端のレコードごとに判定し、NUL 終端で出力する (前述の「複数行にまたがるレコード」を参照)
655
- --sentence[=HOW] 行ではなく文ごとに判定する。HOW は jev (既定) か rules (前述の「1 文ずつ判定する」を参照)
656
- -o --sentence と併用し、当たった文だけを出す
917
+ --unit=UNIT 判定の単位: line (既定)、zero、sentence-by-jev、sentence-by-rule、function
918
+ -z, --null-data --unit=zero。行ではなく NUL 終端のレコードごとに判定し、NUL 終端で出力する (前述の「複数行にまたがるレコード」を参照)。
919
+ --unit=sentence-by-* と併用すると、レコードごとに文に分ける
920
+ --unit=sentence-by-jev|sentence-by-rule 行ではなく文ごとに判定する (前述の「1 文ずつ判定する」を参照)
921
+ --unit=function 関数ごとに判定し、当たった関数の行を出す (前述の「関数ごとに判定する」を参照)
922
+ --step-to 式 前の式に当たる関数から呼び出しをたどり、後ろの式に当たる関数までのパスを出す
923
+ (前述の「関数から関数へたどる」を参照)
924
+ --edges=FILE 呼び出しの代わりに FILE の辺をたどる。--reverse は逆向きにたどる
925
+ --hops=N|M..N|M.. 終点のホップ数 (既定 0..)
926
+ -o --unit=sentence-by-* と併用し、当たった文だけを出す
657
927
  -p 各意味の確率を行末に表示 (閾値調整用)
658
- --dry-run 何も送らず、送信先・検索するファイル・各リクエストとその質問を表示
928
+ --dry-run 何も送らず、この検索が使う設定 (コマンドラインでなければその出どころも)・検索するファイル・
929
+ 各リクエストとその質問を表示
659
930
  --verbose 同じ表示を検索しながら stderr に出す
660
931
  -i, --interactive --dry-run と同じ内容を見せ、端末で y と答えたときだけ検索する
661
- --dedup テンプレートごとに 1 行だけ判定し、その答えを残りにも使う (前述の「テンプレートごとに 1 行だけ判定する」を参照)
932
+ -M NUM, --max-columns=NUM 行 (-z ならレコード) の先頭 NUM 文字までを送る。判定はそれでもされる
933
+ (NUM より先にしか無い一致だけ見つからない)。既定 2000、-z なら 8000
934
+ --max-filesize=SIZE 対象を先に計測 (K/M/G、既定 10M)。超えるものは rg の --max-filesize と同じく
935
+ 無条件に飛ばす (-y は効かない)。標準入力は読んでから計測し、超えていれば同じく飛ばす。
936
+ --cached / <tree>: の対象は blob で、その内容から計測する。-g のコミットはここに
937
+ 含まれない (送るときに -M ですでに上限がある)
938
+ --max-cost=USD 送る予定の入力を見積もって値段を出す。超えれば (既定 1) 端末で続けるか聞く。
939
+ -y は聞かずに yes と答える。端末が無く超えていれば終了コード 2、-q でも変わらない
940
+ (スクリプトからは -y)。-i は無条件かつこれより前に聞くので二重には聞かない
941
+ --dedup[=auto|always|never] テンプレートごとに 1 行だけ判定し、その答えを残りにも使う (当面の既定は never。
942
+ 前述の「テンプレートごとに 1 行だけ判定する」を参照)
943
+ --rank[=jev|match] 結果 (一致とその文脈) を良いものから順に番号付きの見出しの下に出す。jev (値なしの
944
+ --rank) は各結果を Jev に聞き、match は最も高い一致の確率で並べる (前述の「良いものから順に出す」を参照)。--no-rank でファイル順
945
+ --template=NAME --rank か --summarize の --format=html が書く文書。~/.config/sys1grep/templates/NAME.html、無ければ同梱のもの
946
+ (default, print, terminal)、またはファイル。既定は SYS1GREP_TEMPLATE、無ければ default。
947
+ --template=list は名前を出す (前述の「HTML のテンプレート」を参照)
948
+ --install-templates 同梱のテンプレートを ~/.config/sys1grep/templates へコピーする。既にあるファイルは残す
662
949
  --color[=WHEN] 色付け。auto (端末なら付ける、既定) / always / never。=WHEN 省略時は auto
663
950
  ファイル名・行番号は grep と同じ配色。-p の確率は閾値以上を緑、
664
951
  否定側の閾値未満を赤、あいだを黄で表示。NO_COLOR にも従う
@@ -742,14 +1029,14 @@ fmt -w 100000 essay.txt | sys1grep -n -e "the author admits they made a mistake"
742
1029
 
743
1030
  出力の 1 行が 1 段落になり、`-n` は元のファイルではなく `fmt` の出力の行番号になります。`fmt` は日本語の行をつなぐとき
744
1031
  間に空白を入れますが、Jev の判定には響きません。折り返しをまたぐ文を判定したいだけなら、どちらも要りません。
745
- `--sentence` が行をつないで文に分けます。
1032
+ `--unit=sentence-by-jev` が行をつないで文に分けます。
746
1033
 
747
1034
  ### 会話記録の JSONL を、発言 1 件ずつ判定したい
748
1035
 
749
1036
  `jq` で本文を取り出し、発言ごとに NUL で終えてから、`-z` でレコードとして判定します。
750
1037
 
751
1038
  ```sh
752
- jq -j '.content + "\u0000"' chat.jsonl | sys1grep -z -n --sentence -e "the customer is asking for a refund" | tr '\0' '\n'
1039
+ jq -j '.content + "\u0000"' chat.jsonl | sys1grep -z -n --unit=sentence-by-jev -e "the customer is asking for a refund" | tr '\0' '\n'
753
1040
  ```
754
1041
 
755
1042
  `.content` は、本文が入っている場所に合わせて書き換えてください。発言 1 件が 1 レコードになるので、文が次の話者の