@uehaj/sys1grep 0.5.0-next.1 → 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/jev-semgrep/)
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 秒弱で終わります。
@@ -168,7 +170,8 @@ Jev に送る行が増えるほど、費用も時間もかかります。いち
168
170
  尋ねられます。空行は送りません。`-M`/`--max-columns`(既定 2000、`-z` なら 8000)を超える行は、
169
171
  先頭からその文字数までだけを送ります。判定はそれでも行われ、超えた先にしか無い一致だけが見つかりません。
170
172
  - **何回判定するか。** [`--dedup`](#テンプレートごとに-1-行だけ判定する---dedup) は、ID・数値・時刻・パスだけが
171
- 違う行をまとめて、テンプレートごとに 1 行だけ判定します。
173
+ 違う行をまとめて、テンプレートごとに 1 行だけ判定します。`auto`/`always`/`never`、当面は既定で off。
174
+ `--dedup=auto` が割に合うときはヒントを出します。
172
175
  - **払う前に確かめる。** `--dry-run` は何も送らず、この検索が使う設定、検索するファイル、ファイルごとの
173
176
  送る行数、各リクエストとその質問を表示します。最後の行には入力トークン数と、TypeSafe 本体なら料金の
174
177
  見積もりが出ます(`~3178 input tokens, ~$0.000133`。誤差 1 割程度)。`-i` は同じ集計を端末に出し、
@@ -602,12 +605,70 @@ $ git config diff.golang.xfuncname '^(func|type)[[:space:]]'
602
605
  `-M` の既定は `-z` と同じ 8000 文字で、それより長い関数は先頭 8000 文字で判定します。`--chunk` は関数の数、
603
606
  `-A`/`-B`/`-C` と `-c` は文の単位と同じく行の数を数えます。`-z`・`-g`・`-o` とは併用できません。
604
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
+
605
653
  ### テンプレートごとに 1 行だけ判定する (`--dedup`)
606
654
 
607
655
  費用は送るテキストの量に比例します。機械が吐くログの大半は、ID や数値だけが違う同じ骨格の繰り返しです。
608
656
  `--dedup` は ID・ハッシュ・数値・日付と時刻・パス・URL をマスクして行をまとめ、グループごとに 1 行だけ送って、
609
657
  その答えを残りの行にも使います。送るのはその行の原文で、出力も各行がそのまま出ます。
610
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
+
611
672
  ```sh
612
673
  $ ./sys1grep --dedup -n -e "a request failed" app.log
613
674
  1:worker request 3fa9c1e27b failed: connection reset
@@ -656,9 +717,11 @@ $ sys1grep -r -n --summarize -e "API キーをファイルから読んでいる"
656
717
  - `-n`・`-A/-B/-C`・`-p`・ファイル名は表示どおりに渡し、色は付けません。一致がなければ何も渡しません(終了コード 1)。
657
718
  - `SYS1GREP_SUMMARIZER` は値を付けない `--summarize` の TOOL を、`SYS1GREP_SUMMARIZER_MODEL` はそのモデルを決めます。
658
719
  - `-q`・`-l`・`-c` は行を出さないので、一緒には使えません。
720
+ - `SYS1GREP_OPTS` に書くとすべての検索を要約するので、一致した行は毎回 TOOL の提供元へも送られます。
721
+ 1 回だけ止めるには `--no-summarize` を付けます。
659
722
  - 答えはプレーンテキストです。何も言わなければ LLM は Markdown で書き、端末では雑音になるので、plain を頼みます。
660
- `--summarize-format=markdown` か `=html` でそれらを頼めます(`… --summarize-format=html … > summary.html`)。
661
- 答えは確かめずにそのまま表示します。`SYS1GREP_OPTS` に書けば常用の設定になり、`--summarize` が無いときは無視します。
723
+ `--format=markdown` か `=html` でそれらを頼めます(`… --format=html … > summary.html`)。
724
+ 答えは確かめずにそのまま表示します。`SYS1GREP_OPTS` に書けば常用の設定になり、`--summarize` も `--rank` も無いときは無視します。
662
725
  - `--dedup` では、Jev に送ったものと同じく、テンプレートごとの代表を 1 回だけ `(×N like it)` を付けて渡します。
663
726
  答えを使い回したすべての行は渡しません。
664
727
  - 200 KB(約 5 万トークン)を超えるときは何も渡さず、大きさを示して終了コード 2 で止まります。TOOL の費用がかかる前です。
@@ -671,7 +734,7 @@ $ sys1grep -r -n --summarize --summarize-prompt="3 行以内で。どのファ
671
734
  -e "API キーをファイルから読んでいる" .
672
735
  ```
673
736
 
674
- `--summarize` が要ります。空の TEXT は指定しないのと同じです。`--summarize-format` の文より後に置くので、TEXT で書式を上書きできます。
737
+ `--summarize` が要ります。空の TEXT は指定しないのと同じです。`--format` の文より後に置くので、TEXT で書式を上書きできます。
675
738
 
676
739
  他の TOOL:
677
740
 
@@ -690,6 +753,92 @@ OpenAI 互換サーバ全般です。3 つとも `SYS1GREP_SUMMARIZER_MODEL` が
690
753
  (Jev 用の `SYS1GREP_API_KEY` は送りません)。`OLLAMA_HOST` で ollama 側のホストを変えられます
691
754
  (`ollama` CLI 自体と同じ)。
692
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
+
693
842
  ## Claude Code から使う
694
843
 
695
844
  sys1grep を代わりに走らせてくれる Claude Code のスキルがあります。探したいものを言葉で書くと、式を組み立てて
@@ -770,6 +919,10 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
770
919
  --unit=sentence-by-* と併用すると、レコードごとに文に分ける
771
920
  --unit=sentence-by-jev|sentence-by-rule 行ではなく文ごとに判定する (前述の「1 文ずつ判定する」を参照)
772
921
  --unit=function 関数ごとに判定し、当たった関数の行を出す (前述の「関数ごとに判定する」を参照)
922
+ --step-to 式 前の式に当たる関数から呼び出しをたどり、後ろの式に当たる関数までのパスを出す
923
+ (前述の「関数から関数へたどる」を参照)
924
+ --edges=FILE 呼び出しの代わりに FILE の辺をたどる。--reverse は逆向きにたどる
925
+ --hops=N|M..N|M.. 終点のホップ数 (既定 0..)
773
926
  -o --unit=sentence-by-* と併用し、当たった文だけを出す
774
927
  -p 各意味の確率を行末に表示 (閾値調整用)
775
928
  --dry-run 何も送らず、この検索が使う設定 (コマンドラインでなければその出どころも)・検索するファイル・
@@ -785,7 +938,14 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
785
938
  --max-cost=USD 送る予定の入力を見積もって値段を出す。超えれば (既定 1) 端末で続けるか聞く。
786
939
  -y は聞かずに yes と答える。端末が無く超えていれば終了コード 2、-q でも変わらない
787
940
  (スクリプトからは -y)。-i は無条件かつこれより前に聞くので二重には聞かない
788
- --dedup テンプレートごとに 1 行だけ判定し、その答えを残りにも使う (前述の「テンプレートごとに 1 行だけ判定する」を参照)
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 へコピーする。既にあるファイルは残す
789
949
  --color[=WHEN] 色付け。auto (端末なら付ける、既定) / always / never。=WHEN 省略時は auto
790
950
  ファイル名・行番号は grep と同じ配色。-p の確率は閾値以上を緑、
791
951
  否定側の閾値未満を赤、あいだを黄で表示。NO_COLOR にも従う
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  Background and design notes (Japanese): [Jevのキラーアプリ、「意味で探す grep」を作った](https://zenn.dev/uehaj/articles/jev-semgrep-grep-by-meaning) on Zenn.
6
6
 
7
+ > **Renamed from semgrep (0.5.0).** Two reasons. The name collided with the static-analysis tool [Semgrep](https://semgrep.dev/), a trademark. And the tool was never meant for Jev alone: it targets System 1 models in general, and I'd like it to work with any models TypeSafe AI may release after Jev under other names :). So from 0.5.0 this is **sys1grep** (System 1 + grep): npm package `@uehaj/sys1grep` (was `@uehaj/semgrep`), commands `sys1grep` / `git sys1grep`, environment `SYS1GREP_*`, config `~/.config/sys1grep/.env`. The old `SEMGREP_*` names and `~/.config/semgrep/.env` still work for one minor release, each use printing a deprecation line; 1.0.0 drops them. Details: [CHANGELOG](CHANGELOG.md#renamed). The Zenn article's URL keeps its old slug.
8
+
7
9
  A grep that finds lines by **what they mean**, not by regular expressions.
8
10
  Matching is done by **Jev**, the System One model from [TypeSafe AI](https://typesafe.ai/).
9
11
  Jev does not generate text. It answers typed questions with probabilities, so for every line
@@ -14,9 +16,9 @@ and applies a threshold.
14
16
  ./sys1grep -n -e "customer is angry or frustrated" tickets.txt
15
17
  ```
16
18
 
17
- [![sys1grep demo: a Japanese meaning finds refund requests in six languages; "asking for a refund" vs "about a refund"; -Q finds the answer](docs/demo.svg)](https://uehaj.github.io/jev-semgrep/)
19
+ [![sys1grep demo: a Japanese meaning finds refund requests in six languages; "asking for a refund" vs "about a refund"; -Q finds the answer](docs/demo.svg)](https://sys1grep.js.org/)
18
20
 
19
- <sub>▶ Click the demo, or open <a href="https://uehaj.github.io/sys1grep/">uehaj.github.io/sys1grep</a>, for the full demo on the landing page.</sub>
21
+ <sub>▶ Click the demo, or open <a href="https://sys1grep.js.org/">uehaj.github.io/sys1grep</a>, for the full demo on the landing page.</sub>
20
22
 
21
23
  - Zero dependencies. One file, Node.js 20.16+ and `fetch`.
22
24
  - Fast. 30 lines go into one request, requests run 8 at a time. A 210-line file finishes in under a second.
@@ -180,7 +182,8 @@ to the narrowest:
180
182
  its AND term's meanings. Blank lines are never sent. Only the first `-M`/`--max-columns` characters of a line
181
183
  (default 2000, 8000 with `-z`) are sent: it is still searched and judged, just not past that cutoff.
182
184
  - **How many times.** [`--dedup`](#one-line-per-template---dedup) judges one line per template: lines that
183
- differ only in ids, numbers, times or paths share one answer.
185
+ differ only in ids, numbers, times or paths share one answer. `auto`/`always`/`never`, off by default for
186
+ now: a hint says when `--dedup=auto` would pay.
184
187
  - **Check before paying.** `--dry-run` sends nothing and prints the settings the search would run with, the
185
188
  files, how many lines each would send and every request with its questions. Its last line estimates the input
186
189
  tokens and, for TypeSafe itself, the price (`~3178 input tokens, ~$0.000133`; within about 10%). `-i` shows the
@@ -620,6 +623,56 @@ $ git config diff.golang.xfuncname '^(func|type)[[:space:]]'
620
623
  `--chunk` counts functions, `-A`/`-B`/`-C` and `-c` count lines, as with the sentence units. `-z`, `-g` and
621
624
  `-o` are refused.
622
625
 
626
+ ### From one function to another (`--step-to`)
627
+
628
+ A question about code often comes in two steps: where does `--summarize` hand the lines to the tool, and then,
629
+ what happens when that fails. The "that" in the second step is what the first one found. Multi-step matching
630
+ answers both in one run: the expression before `--step-to` finds the start, sys1grep walks the calls from it,
631
+ and the expression after `--step-to` picks the functions it reaches that match. Jev judges the two
632
+ expressions; no score decides which call to follow.
633
+
634
+ ```sh
635
+ $ sys1grep -e "--summarize hands the lines to the tool" --step-to "what happens when the tool fails to start or answer" sys1grep.mjs
636
+ ```
637
+
638
+ A meaning may start with a dash, as the first one does, on any search and for `-e`, `-a`, `-v` and `-Q`: a value that starts with `-` is taken as the meaning unless it is an option (an option holds no space).
639
+
640
+ Each path prints as a tree: the hop (the number of calls from the start), `file:line` and the function's name,
641
+ `:` after an end and `-` after a function on the way, as grep marks a match and its context. A regex-only
642
+ start and end send nothing, which makes a free reachability search. In Python 3.6's `json`, the functions
643
+ that `load` reaches and that hold a `raise`:
644
+
645
+ ```
646
+ $ cd /usr/lib64/python3.6/json && sys1grep -e '/^\s*def load\b/' --step-to '/\braise\b/' *.py
647
+ 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
648
+ 0 __init__.py-274-load
649
+ 1 __init__.py:302:loads
650
+ 2 decoder.py:334:decode
651
+ 3 decoder.py:345:raw_decode
652
+ 4 scanner.py-65-scan_once
653
+ 5 scanner.py:28:_scan_once
654
+ ```
655
+
656
+ - **The expressions.** The expression before `--step-to` is the start's, as in any search; every `-e` / `-a` /
657
+ `-v` / `-Q` after it is the end's. `--step-to MEANING` and `--step-to '/RE/'` are `--step-to -e ...`; the file
658
+ names follow. A step is a search, not a hop: one `--step-to` is two steps, whatever the hops between them.
659
+ - **The edges.** Without `--edges` the unit is a function (`--unit=function`) and an edge is a call: `name(` in
660
+ a function's body, its comments, docstrings and strings left out, links to every function of that name. A name
661
+ comes from the funcname line (the `def` under a decorator). `--edges=FILE` walks any other relation instead,
662
+ one edge per line, `FROM_FILE:LINE<TAB>FROM_NAME<TAB>TO_FILE:LINE<TAB>TO_NAME`; a line stands for the unit that
663
+ holds it. `--reverse` walks the edges backwards, callee to caller.
664
+ - **The walk.** Breadth first from every start at once. A function's hop is its shortest distance from any
665
+ start, and each function is reached once, so a cycle ends. `--hops=N`, `--hops=M..N` and `--hops=M..` pick the
666
+ hops an end may be at (default `0..`: a start that matches the end expression is an end at hop 0). A path that
667
+ reaches no end is left out. stderr says how many functions each hop reached and why the walk stopped: no new
668
+ function, or `--hops`.
669
+ - **The cost.** Jev judges every function against the start expression in one batch, then the functions reached
670
+ within `--hops` against the end expression in another. `--max-cost` asks again before the second, counting the
671
+ first. `--dry-run` answers every question 0, so it walks from every function the start expression could hold
672
+ for and shows that bound.
673
+ - **Not with** `-z`, `-g`, `-o`, `-c`, `-l`, `-A`/`-B`/`-C`, `--rank`, `--summarize` or `--dedup`. `-p` adds the
674
+ end expression's scores to each function judged against it; `-q` prints nothing and exits 0 when there is an end.
675
+
623
676
  ### One line per template (`--dedup`)
624
677
 
625
678
  Cost is proportional to the text sent, and machine-generated logs are mostly one skeleton with a different
@@ -627,6 +680,19 @@ id or number in it. `--dedup` masks ids, hashes, numbers, dates and times, paths
627
680
  the result, sends one line per group and reuses its answer for the rest. What is sent is that line's
628
681
  original text, and every line is printed as itself:
629
682
 
683
+ `--dedup` takes `auto`, `always` or `never`; a bare `--dedup` is `--dedup=always`. **Default is `never`, for
684
+ now** (#143): `auto` is held back until real-run stats say it should be the default. Every run still
685
+ estimates locally, before anything is sent, what folding every kind (the best case) would save, and prints
686
+ one stderr hint when that is worth at least twice the cost of `--dedup`'s own question:
687
+
688
+ ```
689
+ $ sys1grep -e "a request failed" app.log
690
+ sys1grep: 2040 units fold to at most 31 templates; --dedup=auto would save ~68 requests (~44k tokens)
691
+ ```
692
+
693
+ `--dedup=auto` asks and folds only when that estimate says it pays; a file that barely repeats sends every
694
+ line, asking nothing extra. `--verbose` / `--dry-run` print the decision and its numbers for every value.
695
+
630
696
  ```sh
631
697
  $ ./sys1grep --dedup -n -e "a request failed" app.log
632
698
  1:worker request 3fa9c1e27b failed: connection reset
@@ -675,9 +741,11 @@ $0.094 to $0.013, with the same answer (#69). It pays when the answer sits in a
675
741
  - `-n`, `-A/-B/-C`, `-p` and file names go in as they would print; colors never do. No match runs nothing (exit 1).
676
742
  - `SYS1GREP_SUMMARIZER` picks the TOOL of a bare `--summarize`, `SYS1GREP_SUMMARIZER_MODEL` its model.
677
743
  - `-q`, `-l` and `-c` print no lines, so they cannot be combined with it.
744
+ - In `SYS1GREP_OPTS` it summarizes every search, so every match goes to TOOL's provider too;
745
+ `--no-summarize` turns it off for one search.
678
746
  - The answer is plain text: an LLM that is not told writes Markdown, noise on a terminal, so plain is asked for.
679
- `--summarize-format=markdown` or `=html` asks for those instead (`… --summarize-format=html … > summary.html`).
680
- The answer prints as it comes, unchecked. In `SYS1GREP_OPTS` it is a standing preference, ignored without `--summarize`.
747
+ `--format=markdown` or `=html` asks for those instead (`… --format=html … > summary.html`).
748
+ The answer prints as it comes, unchecked. In `SYS1GREP_OPTS` it is a standing preference, ignored without `--summarize` or `--rank`.
681
749
  - With `--dedup`, the TOOL gets what Jev got: each template's representative once, marked `(×N like it)`,
682
750
  not every line its answer was reused for.
683
751
  - More than 200 KB (about 50k tokens) is not sent at all: exit 2, with the size, before the TOOL is paid.
@@ -690,7 +758,7 @@ $ sys1grep -r -n --summarize --summarize-prompt="3 lines or fewer, just which fi
690
758
  -e "the API key is read from a file" .
691
759
  ```
692
760
 
693
- It needs `--summarize`; empty TEXT is the same as leaving it out. It comes after the `--summarize-format` sentence,
761
+ It needs `--summarize`; empty TEXT is the same as leaving it out. It comes after the `--format` sentence,
694
762
  so TEXT can override the format.
695
763
 
696
764
  Other TOOLs:
@@ -709,6 +777,93 @@ OpenAI-compatible server. All three need `SYS1GREP_SUMMARIZER_MODEL`: none has a
709
777
  `SYS1GREP_SUMMARIZER_API_KEY` goes as `Authorization: Bearer` to a URL TOOL only (never `SYS1GREP_API_KEY`,
710
778
  which is Jev's). `OLLAMA_HOST` moves ollama's host, as it does for the `ollama` CLI itself.
711
779
 
780
+ ### Best first (`--rank`)
781
+
782
+ Matches print in file order, as grep prints them. With many, `--rank` prints the results best first, each under a
783
+ numbered header. A result is a match with its `-A/-B/-C` lines; matches whose context touches are one result.
784
+
785
+ ```sh
786
+ $ sys1grep -n -C1 --rank -e "the refund was refused" tickets/
787
+ 1. tickets/b.txt
788
+ tickets/b.txt-2-Order #1234, placed 2026-08-01.
789
+ tickets/b.txt:3:Your refund was declined: the order is older than 30 days.
790
+ tickets/b.txt-4-Please contact support.
791
+
792
+ 2. tickets/a.txt
793
+ tickets/a.txt-11-Order #88 arrived.
794
+ tickets/a.txt:12:Refund request received.
795
+ tickets/a.txt-13-We will check it.
796
+ ```
797
+
798
+ - `--rank` is `--rank=jev`: after the search, Jev is asked of each result, lines and context together, whether it
799
+ is relevant to the meanings that are not negated. One more question per result, `--chunk` results to a request;
800
+ `--dry-run` / `-i` show an upper bound, since which results there are is known only after the search.
801
+ - `--rank=match` sorts by each result's highest match probability, with no request. It is the answer to a yes/no
802
+ question on one line, so clear matches sit close together, and the context is not read.
803
+ - `-p` puts the score on the header (`1. [0.96] tickets/b.txt`). `-l` lists the files by their best result.
804
+ - `--format=markdown` writes a `## 1. tickets/b.txt` heading and a fenced block per result; `--format=html` one
805
+ self-contained HTML document from a template (below), light and dark, with a relevance bar per result. The lines
806
+ are as they print, escaped and uncolored.
807
+ - `--summarize` gets the results in ranked order; with `--dedup` a representative is one result.
808
+ - It needs a meaning (a regex, `!` or `-v` alone ranks nothing), and cannot be combined with `-c`, `-o` or `-q`.
809
+ `--no-rank` turns off an earlier one, from `SYS1GREP_OPTS` say.
810
+
811
+ ### HTML templates (`--template`)
812
+
813
+ `--rank --format=html` and `--summarize --format=html` fill in a template. Four come with sys1grep: `default`, `print` (black on white, serif,
814
+ for paper and PDF), `search` (a results page like a web search engine's) and `terminal` (dark monospace).
815
+ `--template=NAME` picks one; `SYS1GREP_TEMPLATE`, in the
816
+ environment or `~/.config/sys1grep/.env`, sets the default.
817
+
818
+ ```sh
819
+ $ sys1grep -r -n -C1 --rank -p --format=html --template=print -e "the refund was refused" tickets/ > refunds.html
820
+ ```
821
+
822
+ A template is one HTML file. The part between `<!--result-->` and `<!--/result-->` is repeated for each result;
823
+ what is before and after it is written once. sys1grep replaces these, escaped, and leaves any other `{{…}}` as
824
+ written:
825
+
826
+ | placeholder | where | value |
827
+ |---|---|---|
828
+ | `{{title}}` | anywhere | `sys1grep: ` and the meanings |
829
+ | `{{query}}` | anywhere | the meanings, as in `"refund" and not "policy"` |
830
+ | `{{count}}` | anywhere | the number of results (with `--summarize`, of matching lines) |
831
+ | `{{answer}}` | anywhere | `--summarize`'s answer; empty with `--rank` |
832
+ | `{{rank}}` | per result | 1, 2, … |
833
+ | `{{score}}` | per result | the score as `-p` prints it (`0.96`); empty without `-p` |
834
+ | `{{score_pct}}` | per result | the score as a whole number 0 to 100, set even without `-p` (for a bar: `style="--s:{{score_pct}}"`) |
835
+ | `{{file}}` | per result | the file name; empty when one file is searched |
836
+ | `{{lines}}` | per result | the result's lines as they print |
837
+
838
+ `--template=NAME` reads `~/.config/sys1grep/templates/NAME.html` if it is there, else the bundled one, so your
839
+ copy of `default` replaces it. A value containing `/` or ending in `.html` is a file path. To make your own, copy
840
+ the bundled ones and edit:
841
+
842
+ ```sh
843
+ $ sys1grep --install-templates # "copied PATH" for each file
844
+ $ cp ~/.config/sys1grep/templates/default.html ~/.config/sys1grep/templates/team.html
845
+ $ sys1grep -r --rank --format=html --template=team -e "..." src/ > out.html
846
+ $ sys1grep --template=list # one name a line, "(user)" after those under ~/.config
847
+ ```
848
+
849
+ `{{score_pct}}` is set even without `-p`, so a bar can show the relevance without printing the number.
850
+
851
+ With `--summarize --format=html` the TOOL is asked for plain text, and that answer, escaped, goes into `{{answer}}`
852
+ once it has finished; the part between `<!--result-->` and `<!--/result-->` is not written. A template for
853
+ `--summarize` therefore needs `{{answer}}` outside that part (the bundled ones have it; with `--rank` it is empty).
854
+
855
+ ```sh
856
+ $ sys1grep -r --summarize --format=html --template=print -e "why the refunds were refused" tickets/ > why.html
857
+ ```
858
+
859
+ A template is trusted local input: a path you give, or a symlink in the templates directory, is read as it is;
860
+ only the results put into it are escaped.
861
+
862
+ `--install-templates` never overwrites a file that is already there (it prints `kept` for it). A missing
863
+ template, or one without exactly one `<!--result-->` before one `<!--/result-->`, is exit 2 naming the file.
864
+ `--template` on the command line needs `--format=html` with `--rank` or `--summarize` (not `-l`); `SYS1GREP_TEMPLATE` is simply unused elsewhere.
865
+ `--template=list` and `--install-templates` take no other arguments and are refused in `SYS1GREP_OPTS`.
866
+
712
867
  ## Use it from Claude Code
713
868
 
714
869
  There is a Claude Code skill that runs sys1grep for you: describe what you are looking for in plain words
@@ -790,6 +945,10 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
790
945
  with --unit=sentence-by-*, each record is split into sentences
791
946
  --unit=sentence-by-jev|sentence-by-rule judge each sentence instead of each line (see "One sentence at a time" above)
792
947
  --unit=function judge each function and print its lines (see "One function at a time" above)
948
+ --step-to EXPRESSION print the paths from the functions the expression before it matches, along the
949
+ calls, to the functions the expression after it matches (see "From one function to another" above)
950
+ --edges=FILE walk the edges in FILE instead of the calls; --reverse walks them backwards
951
+ --hops=N|M..N|M.. the hops an end may be at (default 0..)
793
952
  -o with --unit=sentence-by-*, print only the matching sentences
794
953
  -p print each meaning's probability at the end of the line
795
954
  --dry-run send nothing; print the settings the search would run with (and their source, when not the
@@ -805,7 +964,14 @@ usage: sys1grep [OPTION]... -e MEANING|-Q QUESTION [-a MEANING] [-v MEANING]...
805
964
  --max-cost=USD price the input about to be sent; over it (default 1), ask to continue on the terminal; -y
806
965
  answers yes without asking; no terminal and the limit exceeded is exit 2, unchanged by -q
807
966
  (scripts pass -y); -i already asks unconditionally and earlier, so this does not ask again
808
- --dedup judge one line per template and reuse its answer for the rest (see "One line per template" above)
967
+ --dedup[=auto|always|never] judge one line per template and reuse its answer for the rest (default never,
968
+ for now; see "One line per template" above)
969
+ --rank[=jev|match] print the results (a match with its context) best first, under numbered headers; jev
970
+ (bare --rank) asks Jev of each result, match sorts by its best match probability (see "Best first"); --no-rank: file order
971
+ --template=NAME the document --rank or --summarize --format=html writes: ~/.config/sys1grep/templates/NAME.html, else the
972
+ bundled one (default, print, terminal), or a file; default SYS1GREP_TEMPLATE, else default.
973
+ --template=list prints the names (see "HTML templates")
974
+ --install-templates copy the bundled templates to ~/.config/sys1grep/templates, keeping existing files
809
975
  --color[=WHEN] auto (default: color when stdout is a terminal) / always / never; bare --color means auto
810
976
  file and line number use grep's colors; with -p, probabilities are
811
977
  green at or above the positive threshold, red below the negative one,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uehaj/sys1grep",
3
- "version": "0.5.0-next.1",
3
+ "version": "0.5.0-next.2",
4
4
  "description": "grep by meaning, across languages. TypeSafe Jev scores every line against a meaning; combine meanings with AND/OR/NOT.",
5
5
  "keywords": [
6
6
  "grep",
@@ -29,7 +29,11 @@
29
29
  "README.ja.md",
30
30
  "docs/color.svg",
31
31
  "docs/demo.svg",
32
- "docs/sentence.svg"
32
+ "docs/sentence.svg",
33
+ "templates/default.html",
34
+ "templates/print.html",
35
+ "templates/search.html",
36
+ "templates/terminal.html"
33
37
  ],
34
38
  "engines": {
35
39
  "node": ">=20.16"
@@ -39,6 +43,8 @@
39
43
  "test:offline": "sh tests/offline.sh",
40
44
  "judge": "node --no-warnings --env-file-if-exists=.env tests/judge.mts",
41
45
  "scope-eval": "node tests/scope-eval.mjs",
46
+ "dedup-eval": "node tests/dedup-eval.mjs",
47
+ "skill-eval": "bash tests/skill-eval/run.sh",
42
48
  "release": "sh scripts/release.sh"
43
49
  }
44
50
  }