@uehaj/sys1grep 0.5.0-next.1 → 0.5.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.
@@ -0,0 +1,49 @@
1
+ // A small demo service: an entry point calls through a few layers down to where things can go wrong.
2
+ export async function main() {
3
+ await handleRequest();
4
+ }
5
+
6
+ async function handleRequest() {
7
+ try {
8
+ await processOrder();
9
+ } catch (err) {
10
+ throw err;
11
+ }
12
+ }
13
+
14
+ async function processOrder() {
15
+ await chargeCard();
16
+ }
17
+
18
+ async function chargeCard() {
19
+ if (!cardIsValid()) fatal('card declined');
20
+ throw new Error('card declined');
21
+ }
22
+
23
+ function cardIsValid() {
24
+ return false;
25
+ }
26
+
27
+ function fatal(message) {
28
+ console.error(message);
29
+ process.exit(1);
30
+ }
31
+
32
+ // reads the API key from the environment first, then a local settings file
33
+ function loadApiKey() {
34
+ return process.env.SYS1GREP_API_KEY || readSettingsFile().apiKey;
35
+ }
36
+
37
+ function readSettingsFile() {
38
+ return { apiKey: '' };
39
+ }
40
+
41
+ function tryOptionalStep() {
42
+ try {
43
+ riskyStep();
44
+ } catch (err) {} // ignored: this step is optional and failing it is not fatal
45
+ }
46
+
47
+ function riskyStep() {
48
+ return true;
49
+ }
@@ -0,0 +1,54 @@
1
+ """A small demo CLI: the same kind of call chain as app.js, in Python."""
2
+ import os
3
+ import sys
4
+
5
+
6
+ def main():
7
+ run_job()
8
+
9
+
10
+ def run_job():
11
+ try:
12
+ execute()
13
+ except Exception:
14
+ raise
15
+
16
+
17
+ def execute():
18
+ if not is_ready():
19
+ raise RuntimeError('not ready')
20
+ charge()
21
+
22
+
23
+ def is_ready():
24
+ return False
25
+
26
+
27
+ def charge():
28
+ if not confirm_charge():
29
+ sys.exit(1)
30
+ raise RuntimeError('charge failed')
31
+
32
+
33
+ def confirm_charge():
34
+ return False
35
+
36
+
37
+ def load_api_key():
38
+ # reads the API key from the environment first, then a local settings file
39
+ return os.environ.get('SYS1GREP_API_KEY') or read_settings_file()
40
+
41
+
42
+ def read_settings_file():
43
+ return ''
44
+
45
+
46
+ def try_optional_step():
47
+ try:
48
+ risky_step()
49
+ except Exception:
50
+ pass # ignored: optional, not fatal
51
+
52
+
53
+ def risky_step():
54
+ return True
@@ -0,0 +1,19 @@
1
+ 2026-01-05 03:12:01 INFO server started, listening on port 8080
2
+ 2026-01-05 03:12:04 INFO connected to the primary database
3
+ 2026-01-05 03:14:40 WARN slow query took 2800ms: SELECT * FROM orders WHERE status = 'pending'
4
+ 2026-01-05 03:20:11 ERROR connection reset by peer while calling the payment service
5
+ 2026-01-05 03:20:12 INFO retrying the payment request (attempt 2 of 3)
6
+ 2026-01-05 03:20:18 ERROR timeout after 5000ms waiting for the payment service
7
+ 2026-01-05 03:20:19 INFO retrying the payment request (attempt 3 of 3)
8
+ 2026-01-05 03:20:25 FATAL giving up after 3 attempts: the payment service is unreachable
9
+ 2026-01-05 04:02:00 INFO nightly backup completed
10
+ 2026-01-05 04:05:30 WARN disk usage at 92% on /var/log
11
+ 2026-01-05 04:10:44 ERROR DNS lookup failed for the billing API
12
+ 2026-01-05 04:10:46 INFO DNS lookup succeeded on retry
13
+ 2026-01-05 05:00:00 ERROR health check failed: no response from the application server
14
+ 2026-01-05 05:00:05 ERROR health check failed again: no response from the application server
15
+ 2026-01-05 05:00:10 FATAL marking the application server as down after 2 failed health checks
16
+ 2026-01-05 05:30:00 INFO the application server is back and answering health checks
17
+ 2026-01-05 06:00:00 WARN an admin ran DROP TABLE sessions; on the staging database by mistake
18
+ 2026-01-05 06:05:00 INFO restored the sessions table from last night's backup
19
+ 2026-01-05 07:00:00 INFO it is a quiet morning, nothing else to report
@@ -0,0 +1,10 @@
1
+ I have to admit, the bug was my fault: I shipped the migration without testing it on a copy of production data first.
2
+ Looking back, I should have reviewed the diff more carefully before merging; the mistake was mine, not the reviewer's.
3
+ The weather today is mild, with a light breeze from the west and clear skies through the evening.
4
+ Lunch today was a simple rice bowl with grilled vegetables from the place around the corner.
5
+ Next Friday is a public holiday, so the office will be closed for the long weekend.
6
+ 会議は明日の10時からに変更になりました。資料は前日までに共有してください。
7
+ 今日の天気は晴れ、最高気温は28度の見込みです。
8
+ 料金プランの変更方法については、設定画面の「プラン」から行えます。
9
+ 振り返ると、あのバグは自分のミスでした。テストを十分にせずにリリースしてしまったのが原因です。
10
+ 新機能のリリースは来月に延期されましたが、理由は品質を優先したためです。
@@ -0,0 +1,9 @@
1
+ Ticket #4821 (support, EN): I want my money back. The item arrived broken and nobody answered my emails.
2
+ Ticket #4822 (support, EN): Could you change the delivery address for order #910? I moved last week.
3
+ Ticket #4823 (support, EN): This is infuriating. Three days with no reply and my account is still locked.
4
+ Ticket #4824 (support, EN): Thanks so much, the replacement part fixed it and the team was lovely.
5
+ Ticket #4825 (support, EN): The app crashed once but it worked fine the second time, no need for a refund.
6
+ チケット #4826(サポート, JA): 注文した覚えのない請求が来ています。至急確認してください。本当に困っています。
7
+ チケット #4827(サポート, JA): 配送先の住所を変更したいです。来週には新しい住所に届けてほしいです。
8
+ チケット #4828(サポート, JA): 対応いただきありがとうございました。問題は解決しました。
9
+ チケット #4829(サポート, JA): サポートに何度連絡しても返事がなく、とても腹が立っています。
@@ -0,0 +1,49 @@
1
+ // Scripts written without spaces between words: joining their wrapped lines must not add a space between them,
2
+ // and sys1grep's token estimate prices them per character (cjkBytesOf).
3
+ export const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}\p{Script=Tibetan}ー、。]/u;
4
+
5
+ // --unit=sentence-by-*: the unit is a sentence. Lines are joined as wrapped prose, except where a newline cannot be inside
6
+ // a sentence: at a blank line, next to structure characters (JSON, code), or before a list item, heading, quote or
7
+ // number. Each joined piece is then split by Intl.Segmenter (Unicode UAX #29 sentence boundaries).
8
+ const SENTENCES = new Intl.Segmenter(undefined, { granularity: 'sentence' });
9
+ const hardBreak = (prev, next) => !prev || !next || /[{}\[\]<>|;]$/.test(prev) || /^[{}\[\]<>|"\-*+#>\d]/.test(next)
10
+ || /[{}\[\];]$/.test(next); // a line ending like code is not a continuation of prose either
11
+ // lines -> [{ text, spans }]. A span [unit, from, to] is where the sentence lies in the original units: the unit
12
+ // is the 1-based line (or record) number from unitOf, from/to are character offsets inside it (baseOf shifts a
13
+ // line's offset inside its record with --unit=zero). Output maps matching sentences back to these units.
14
+ export function toSentences(lines, unitOf, baseOf, split = new Set()) { // split: breaks Jev judged to end an entry
15
+ const out = [];
16
+ let text = '', marks = []; // marks: { o: offset in text, unit, base: offset in the unit, len }
17
+ const flush = () => {
18
+ for (const seg of SENTENCES.segment(text)) {
19
+ const t = seg.segment.trim();
20
+ if (!t) continue;
21
+ const start = seg.index + seg.segment.length - seg.segment.trimStart().length, end = start + t.length;
22
+ const spans = [];
23
+ for (const m of marks) {
24
+ const a = Math.max(start, m.o), b = Math.min(end, m.o + m.len);
25
+ if (a < b) spans.push([m.unit, m.base + a - m.o, m.base + b - m.o]);
26
+ }
27
+ out.push({ text: t, spans });
28
+ }
29
+ text = ''; marks = [];
30
+ };
31
+ lines.forEach((line, i) => {
32
+ const prev = text.trimEnd(), next = line.trim();
33
+ const mark = { unit: unitOf(i), base: baseOf(i) + line.length - line.trimStart().length, len: next.length };
34
+ if (hardBreak(prev, next) || split.has(i)) { flush(); if (next) { text = next; marks.push({ ...mark, o: 0 }); } return; }
35
+ text = prev + (CJK.test(prev.at(-1)) && CJK.test(next[0]) ? '' : ' '); // no space for scripts without word spaces
36
+ marks.push({ ...mark, o: text.length });
37
+ text += next;
38
+ });
39
+ flush();
40
+ return out;
41
+ }
42
+
43
+ // Where the rules would join, a break still needs judging if a script without word spaces touches it and the line
44
+ // does not end a sentence: in Japanese or Chinese, entries often end without 。, and joining leaves no trace of the
45
+ // break. A line starting with closing punctuation continues the previous one (kinsoku), and so does a line
46
+ // ending in 、.
47
+ const CLOSING = /^[。、,.」』)】〕!?]/;
48
+ export const needsJudging = (prev, next) => prev && next && !hardBreak(prev, next) && !/[。!?!?、,]$/.test(prev) && !CLOSING.test(next)
49
+ && (CJK.test(prev.at(-1)) || CJK.test(next[0]));
package/options.mjs ADDED
@@ -0,0 +1,101 @@
1
+ // sys1grep.mjs's own node:util.parseArgs option table and default-options gate, pulled out so serve.mjs's
2
+ // settings-panel opts validation runs the exact same parser instead of a hand-rolled regex/cluster check that kept
3
+ // missing forms the parser itself already understands (#199: -nr, -rC1, --r, -h all slipped past earlier regexes
4
+ // in turn; parseArgs splits a cluster, resolves a glued value and a long alias for a one-letter name correctly by
5
+ // construction, so there is nothing left to keep in sync by hand).
6
+ import { parseArgs } from 'node:util';
7
+
8
+ export const OPTIONS = {
9
+ e: { type: 'string', multiple: true },
10
+ P: { type: 'string', multiple: true }, // a regex term (/RE/flags), OR'd like -e; -a/-v after it AND/NOT onto it
11
+ a: { type: 'string', multiple: true },
12
+ v: { type: 'string', multiple: true },
13
+ question: { type: 'string', multiple: true, short: 'Q' },
14
+ level: { type: 'string', default: 'normal' }, // strictness preset: loose / normal / strict
15
+ r: { type: 'boolean', default: false }, // recurse into directories
16
+ cached: { type: 'boolean', default: false }, // git sys1grep only: search the index instead of the working tree
17
+ untracked: { type: 'boolean', default: false }, // git sys1grep only: also search untracked files (.gitignore still applies)
18
+ l: { type: 'boolean', default: false }, // print only matching file names
19
+ 'with-filename': { type: 'boolean', short: 'H' }, // prefix file names even for one file; --no-filename: never
20
+ t: { type: 'string' }, // positive threshold: match when p >= t (default from preset)
21
+ T: { type: 'string' }, // negative threshold: "not X" when p < T (default from preset)
22
+ chunk: { type: 'string', default: '30' }, // lines per request
23
+ c: { type: 'boolean', default: false }, // count of matching lines per file (grep -c)
24
+ quiet: { type: 'boolean', short: 'q', default: false }, // print nothing, exit status only (grep -q)
25
+ j: { type: 'string', default: '8' }, // concurrent requests
26
+ A: { type: 'string' }, // N lines of trailing context
27
+ B: { type: 'string' }, // N lines of leading context
28
+ C: { type: 'string' }, // N lines of context on both sides
29
+ n: { type: 'boolean', default: false }, // line numbers
30
+ z: { type: 'boolean', default: false }, // records are NUL-terminated, on input and output (grep -z); --unit=zero
31
+ unit: { type: 'string', default: 'line' }, // line / zero / sentence-by-jev / sentence-by-rule / function
32
+ o: { type: 'boolean', default: false }, // with --unit=sentence-by-*, print only the matching sentences (grep -o)
33
+ p: { type: 'boolean', default: false }, // print each meaning's probability
34
+ dedup: { type: 'string', default: 'never' }, // auto|always|never: judge one representative per template, reuse its answer
35
+ rank: { type: 'string' }, // jev|match: print the results best first, scored by Jev's answer on each or by its best match
36
+ // multi-step matching (#163): the expression before --step-to finds the start, the one after it the end;
37
+ // --step-reverse-to walks the edges backwards (callee to caller)
38
+ 'step-to': { type: 'boolean', multiple: true },
39
+ 'step-reverse-to': { type: 'boolean', multiple: true },
40
+ edges: { type: 'string' }, // FILE: the edges to walk, one per line; default: the calls between functions
41
+ reverse: { type: 'boolean' }, // removed (--step-reverse-to): parsed only to say so
42
+ hops: { type: 'string', default: '0..' }, // N, M..N or M..: the hops an end may be at
43
+ 'dry-run': { type: 'boolean', default: false }, // print the files and requests, send nothing
44
+ verbose: { type: 'boolean', default: false }, // print the files and requests to stderr while searching
45
+ interactive: { type: 'boolean', short: 'i', default: false }, // show what --dry-run would send, search on a yes
46
+ yes: { type: 'boolean', short: 'y', default: false }, // skip the size/cost guard's question, answering yes (#58)
47
+ 'max-columns': { type: 'string', short: 'M' }, // a unit past this many characters is skipped (rg's -M/--max-columns)
48
+ 'max-filesize': { type: 'string' }, // a target past this size (K/M/G) is listed and confirmed (rg's name)
49
+ 'max-cost': { type: 'string', default: '1' }, // ask before sending when the estimated price is over this many USD
50
+ // which files -r finds and git sys1grep lists; with -r a file named on the command line is always searched
51
+ include: { type: 'string', multiple: true }, // only names matching one of these globs
52
+ exclude: { type: 'string', multiple: true }, // not names matching one of these globs
53
+ 'changed-within': { type: 'string' }, // only files modified within 30m / 2h / 7d / 2w, since a date, today, ...
54
+ gitlog: { type: 'boolean', short: 'g', default: false }, // search git log's commits, one record each; auto-scope picks which
55
+ 'auto-scope': { type: 'boolean', default: true }, // narrow those files by what Jev says a meaning restricts to; --no-auto-scope: don't
56
+ color: { type: 'string', default: 'auto' }, // auto / always / never
57
+ summarize: { type: 'string' }, // pipe what would print to this LLM CLI and print its answer instead
58
+ 'summarize-prompt': { type: 'string' }, // the user's own instruction, added after the fixed one
59
+ format: { type: 'string', default: 'plain' }, // plain / markdown / html: --rank's output, or asked of the summarizer
60
+ template: { type: 'string' }, // --rank's or --summarize's --format=html document: a NAME under the templates dirs, or a file; list: the names
61
+ 'install-templates': { type: 'boolean', default: false }, // copy the bundled templates to ~/.config/sys1grep/templates
62
+ cache: { type: 'string' }, // on|off|only: reuse Jev's stored answers (#252); clear: empty the cache
63
+ 'summarize-format': { type: 'string' }, // removed (--format): parsed only to say so
64
+ // the API settings, each overriding its environment variable
65
+ 'sys1-model': { type: 'string' }, // SYS1GREP_MODEL
66
+ 'sys1-url': { type: 'string' }, // SYS1GREP_URL
67
+ 'sys1-api-key': { type: 'string' }, // SYS1GREP_API_KEY / TYPESAFE_API_KEY
68
+ help: { type: 'boolean', short: 'h', default: false },
69
+ version: { type: 'boolean', short: 'V', default: false },
70
+ lang: { type: 'string' }, // en|ja: --help's language, and --serve's page default (SYS1GREP_LANG, settings.json lang)
71
+ };
72
+ // --no-rank / --no-summarize: parseArgs negates booleans only, so a bare one becomes a value no real argument can
73
+ // hold (this NUL), cleared by the caller once parsed; the later of --no-rank/--rank=X (or --no-summarize/
74
+ // --summarize=X) wins, as for any option.
75
+ export const OFF = '\0';
76
+ export const REVERSE_REMOVED = '--reverse was replaced by --step-reverse-to';
77
+ // A bare --color / --dedup / --rank / --summarize / --cache means an implied default value, as grep's bare --color does;
78
+ // parseArgs cannot express an optional value, so this fills it in before parseArgs ever sees it. --no-filename is
79
+ // grep's name for --no-with-filename, --null-data grep's name for -z. summarizer: the TOOL a bare --summarize
80
+ // fills in (sys1grep.mjs's own SYS1GREP_SUMMARIZER-or-"claude"; a caller that only validates shape, never runs the
81
+ // result, may pass any placeholder).
82
+ export const fill = (a, summarizer) => (a === '--color' ? '--color=auto' : a === '--null-data' ? '-z' : a === '--no-filename' ? '--no-with-filename'
83
+ : a === '--cache' ? '--cache=on' : a === '--summarize' ? `--summarize=${summarizer || 'claude'}` : a === '--dedup' ? '--dedup=always' : a === '--rank' ? '--rank=jev'
84
+ : a === '--no-rank' ? `--rank=${OFF}` : a === '--no-summarize' ? `--summarize=${OFF}` : a);
85
+ // Parses a default-options array (SYS1GREP_OPTS / settings.json opts), already fill()ed, with sys1grep.mjs's own
86
+ // parseArgs configuration, and refuses anything that is not a plain search option: a positional (a target or any
87
+ // other stray value, #196), a meaning or expression (-e/-a/-v/-Q/--step-to go on the command line, not in
88
+ // defaults), or a command (--install-templates, --template=list, --cache=clear: these do something instead of searching). Shared
89
+ // by sys1grep.mjs's own gate and serve.mjs's settings-panel validation, so the two can never accept something the
90
+ // other would refuse. Throws (a parseArgs error, or the refusal above) with sys1grep's own wording either way;
91
+ // returns { tokens } — every parsed token, so a caller can read what option each one named (its canonical
92
+ // OPTIONS key, however it was spelled: -r, --r, or inside a cluster like -rC1 or -nr) — on success.
93
+ export function parseOpts(filledArgs) {
94
+ const { tokens } = parseArgs({ args: filledArgs, options: OPTIONS, allowPositionals: true, allowNegative: true, tokens: true });
95
+ const command = k => k.name === 'install-templates' || (k.name === 'template' && k.value === 'list') || (k.name === 'cache' && k.value === 'clear');
96
+ if (tokens.some(k => k.name === 'reverse')) throw new Error(REVERSE_REMOVED);
97
+ const bad = tokens.find(k => k.kind !== 'option' || ['e', 'P', 'a', 'v', 'question', 'step-to', 'step-reverse-to', 'cached', 'untracked'].includes(k.name) || command(k));
98
+ const reason = n => (n.startsWith('step-') ? 'expressions go on the command line' : n === 'P' ? 'a regex goes on the command line' : 'meanings go on the command line');
99
+ if (bad) throw new Error(bad.kind !== 'option' ? `'${bad.value ?? '--'}' is not an option` : command(bad) ? `${bad.rawName}${bad.value === undefined ? '' : `=${bad.value}`} is not allowed (it does something instead of searching)` : ['cached', 'untracked'].includes(bad.name) ? `--${bad.name} is not allowed (what is searched goes on the command line)` : `${bad.name.length > 1 ? '--' : '-'}${bad.name} is not allowed (${reason(bad.name)})`);
100
+ return { tokens };
101
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uehaj/sys1grep",
3
- "version": "0.5.0-next.1",
4
- "description": "grep by meaning, across languages. TypeSafe Jev scores every line against a meaning; combine meanings with AND/OR/NOT.",
3
+ "version": "0.5.0",
4
+ "description": "grep by meaning, across languages. TypeSafe Jev scores each line (or record, sentence, function) against a meaning; combine meanings and regexes with AND/OR/NOT. Includes git sys1grep and a local search page (--serve).",
5
5
  "keywords": [
6
6
  "grep",
7
7
  "semantic",
@@ -9,7 +9,10 @@
9
9
  "typesafe",
10
10
  "jev",
11
11
  "cli",
12
- "japanese"
12
+ "japanese",
13
+ "git",
14
+ "regex",
15
+ "llm"
13
16
  ],
14
17
  "license": "MIT",
15
18
  "author": "Junji UEHARA",
@@ -24,12 +27,25 @@
24
27
  },
25
28
  "files": [
26
29
  "sys1grep.mjs",
30
+ "serve.mjs",
31
+ "settings.mjs",
32
+ "options.mjs",
27
33
  "git-sys1grep.mjs",
34
+ "lib/sentences.mjs",
35
+ "examples/app.js",
36
+ "examples/app.py",
37
+ "examples/logs.txt",
38
+ "examples/prose.txt",
39
+ "examples/tickets.txt",
28
40
  "README.md",
29
41
  "README.ja.md",
30
42
  "docs/color.svg",
31
43
  "docs/demo.svg",
32
- "docs/sentence.svg"
44
+ "docs/sentence.svg",
45
+ "templates/default.html",
46
+ "templates/print.html",
47
+ "templates/search.html",
48
+ "templates/terminal.html"
33
49
  ],
34
50
  "engines": {
35
51
  "node": ">=20.16"
@@ -39,6 +55,14 @@
39
55
  "test:offline": "sh tests/offline.sh",
40
56
  "judge": "node --no-warnings --env-file-if-exists=.env tests/judge.mts",
41
57
  "scope-eval": "node tests/scope-eval.mjs",
42
- "release": "sh scripts/release.sh"
58
+ "dedup-eval": "node tests/dedup-eval.mjs",
59
+ "cause-eval": "node tests/cause-eval.mjs",
60
+ "skill-eval": "bash tests/skill-eval/run.sh",
61
+ "serve": "node sys1grep.mjs --serve",
62
+ "demo": "node scripts/demo-serve.mjs",
63
+ "serve:portless": "portless run --name sys1grep npm run serve:port --",
64
+ "serve:port": "node sys1grep.mjs --serve=$PORT",
65
+ "release": "sh scripts/release.sh",
66
+ "smoke": "node --no-warnings tests/smoke.mts"
43
67
  }
44
68
  }