moru-narrow 26.45.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 taehwandev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: moru-narrow
3
+ Version: 26.45.0
4
+ Summary: Narrow `rg -w <Name>` to the files and lines you must edit after a signature change (Kotlin+Gradle, TypeScript).
5
+ Author: taehwandev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/taehwandev/moru-narrow
8
+ Project-URL: Issues, https://github.com/taehwandev/moru-narrow/issues
9
+ Keywords: ripgrep,rg,refactoring,coding-agent,kotlin,typescript,claude-code,codex
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # moru-narrow
22
+
23
+ **English** | [한국어](README.ko.md)
24
+
25
+ > **rg tells you every line that mentions a name. moru-narrow tells you which of those lines you must edit.** It keeps every hit; it only changes the order and what is shown first.
26
+
27
+ `moru-narrow` post-processes `rg -w <Name>` for coding agents and humans. It keeps every hit, ranks them, and prints the
28
+ files and **line ranges** to check first. No index, no database, no model calls, no daemon. Python standard library and
29
+ [ripgrep](https://github.com/BurntSushi/ripgrep) only.
30
+
31
+ > Scope: **signature changes** (add or change a parameter, then find what breaks). It is not a general `rg` replacement: for renaming a variable, tracing a logic bug or searching a config string, use plain `rg`.
32
+ >
33
+ > Status: alpha / experimental. MIT license. The examples use the `moru-narrow` command (alias `mnarrow`); from a clone, run `python3 -m moru_narrow` instead.
34
+
35
+ ## In short
36
+
37
+ - **What it does:** after `rg -w name`, it lists the few files and line ranges you must edit, ranked, and hides the rest behind `--all`. Nothing is dropped.
38
+ - **Does it miss files?** Checked against the compiler: in all 217 Kotlin tasks every truth file was listed (rules were partly tuned on them), and 191 of 194 tasks (99.3% of files) on a fresh TypeScript set. The rare misses are files that never mention the name.
39
+ - **Does it help a coding agent?** In 6 test setups the compile success rate never dropped below the baseline. Context tokens fell by 9.8% to 34.5% in five setups; in the sixth (Codex on TypeScript) the change cannot be told apart from noise.
40
+ - **The catch:** the savings appeared only when the agent was also told not to re-run `rg`. Without that sentence, cost went up 10.3% [+0.0003, +0.0009 USD per task] in one test (Kotlin, Haiku, 57 tasks).
41
+
42
+ ```text
43
+ $ moru-narrow getGlobalTheme --repo ./my-next-app
44
+ getGlobalTheme declared: src/lib/theme-settings.ts:28
45
+ TIER 1: check these (3 files). rg found 4 files, 6 lines.
46
+ src/app/[locale]/(walled)/manage/(protected)/admin/settings/page.tsx
47
+ 5: import { getGlobalTheme, saveGlobalTheme, type ThemeMode } from '@/lib/theme-settings';
48
+ 19-22: getGlobalTheme({ fresh: true }).then(t => {
49
+ ...
50
+ hidden: tier 2 has 0 files (use --all to list them)
51
+ ```
52
+
53
+ ## Quick start
54
+
55
+ Needs Python 3.9+ and [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) on your `PATH`. No other dependency. It does not need Moru Core or any other tool.
56
+
57
+ ```bash
58
+ pipx install git+https://github.com/taehwandev/moru-narrow # or: uv tool install git+https://... / pip install git+https://...
59
+ moru-narrow <Name> --repo /path/to/your/project # Kotlin+Gradle or TypeScript, auto-detected
60
+ ```
61
+
62
+ Without installing, from a clone: `git clone https://github.com/taehwandev/moru-narrow && cd moru-narrow && python3 -m moru_narrow <Name> --repo ...`.
63
+
64
+ Not published on PyPI or npm yet. The `pip install git+...` route was tested in a clean virtualenv on macOS with Python 3.9.6; other platforms and Python versions are untested.
65
+
66
+ ## Use with your coding agent
67
+
68
+ The tool only helps an agent when the agent is told how to use it (see the savings section: without the "do not re-run rg" sentence cost went up). One command registers that instruction:
69
+
70
+ ```bash
71
+ moru-narrow --install # agent skill in ~/.claude/skills/ (Claude Code) and ~/.agents/skills/ (Codex and other agent-skills tools)
72
+ moru-narrow --install --project # same, but into ./.claude and ./.agents so your team shares it via git
73
+ moru-narrow --install --agents-md # no skill support (e.g. Cursor): add a marked block to ./AGENTS.md
74
+ ```
75
+
76
+ Re-running is safe: an existing `SKILL.md` is kept (`--force` overwrites it), and the `AGENTS.md` block is replaced in place without touching your other text. `--target claude|agents` limits the skill install to one location. The installed text is based on the instruction used in the agent tests (worded differently, and extended as noted below):
77
+
78
+ > When a task changes a function or constructor signature (adds or changes a parameter), run `moru-narrow <Name> --repo <project root>` once and edit the call sites it lists under TIER 1. Do not run `rg` again for the same name. Use `--all` only if the list looks incomplete. For anything else (renames, logic bugs, config strings), or if moru-narrow reports an error or an unsupported language, use plain `rg`.
79
+
80
+ The exact wording was not the one measured, and the last sentence (use plain `rg` for other tasks and unsupported languages) is new: the tests only contained signature-change tasks and a prompt written for them. Any agent that can run a shell command can also call `moru-narrow` directly.
81
+
82
+ ## Why
83
+
84
+ Changing a function signature means finding every place that breaks. `rg -w name` finds all of them, plus comments,
85
+ strings, same-named functions in other modules and unrelated files. On a Kotlin monorepo it returned **60 files for a
86
+ change that broke 3 to 4**. An agent then reads all of them.
87
+
88
+ A code-graph index was the obvious fix. We measured a syntax-level code-graph index on the same Kotlin repo and it did not
89
+ pay off at the agent level: no measurable cost difference with location-only output, and about +45.5% cost when it returned source code (that run also carried MCP connection overhead, so part of the increase is not the graph itself).
90
+ It also needs an index build and a sync after every edit (stale until synced). `moru-narrow` has no build step and no
91
+ staleness, because it runs `rg` live.
92
+
93
+ ## How it works
94
+
95
+ 1. `rg -nw <Name>` collects hits (test files are excluded).
96
+ 2. Each line gets a role: `use`, `import`, `override`, `declaration`, `comment`, `string`, `named-arg`, ...
97
+ 3. A file is **tier 1** when it has a strong role (`use`/`import`/`override`) **and** can see the declaration:
98
+ - Kotlin: the module is reachable through Gradle `project()` edges, and the file is in the same package or imports it.
99
+ - TypeScript: the file imports the declaring file (relative paths and `tsconfig` aliases, through re-exporting
100
+ barrel files), or imports a wrapper that does and calls `X.name`, or is the declaring file itself.
101
+ 4. Everything else is demoted to tier 2/3. **Filters only demote. `--all` always lists everything `rg` found.**
102
+ 5. Multi-line calls are returned as ranges (`19-22`), because compilers report errors at argument lines.
103
+
104
+ ```text
105
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json] [--lang kotlin|ts]
106
+ ```
107
+
108
+ Language is auto-detected (`tsconfig.json`, or a Gradle build).
109
+
110
+ ## How to read the numbers
111
+
112
+ | Term | What it means here |
113
+ | --- | --- |
114
+ | **Truth files** | Production files that stop compiling after we add a required first parameter (`traceId`) to a function. The compiler decides, not us. |
115
+ | **Recall** | Whether the truth files appear in tier 1. The table counts tasks where all of them do (`23/23`); TypeScript rows also give files (`428/431` = 3 truth files were not listed). |
116
+ | **Tier 1** | The files moru-narrow tells you to read first. |
117
+ | **`rg -w` files / lines** | What plain ripgrep returns for the same name. This is the baseline to beat. |
118
+ | **Fresh set** | Candidates drawn after the rules were frozen. Nothing was tuned on them, so they are the fair test. |
119
+ | **Baseline vs. with moru-narrow** (agent tests) | Same coding agent, same task, same prompt. Baseline may use `rg`, `ls` and file reads. The other condition also gets the tool and the sentence "edit from the list, do not re-run rg for the same name". |
120
+ | **Compile pass** | The project compiles after the agent's edit (Kotlin: Gradle, TypeScript: `tsc --noEmit`). TypeScript runs also require that the declaration really gained the parameter, because an agent that changes nothing would otherwise pass. |
121
+ | **Context tokens** | Input tokens the model processed during the whole run, cached ones included. A proxy for how much it read. |
122
+ | **Cost** | API-equivalent price estimate reported by the Claude CLI, not an actual bill. Codex does not report it, shown as n/a. |
123
+ | **`[a, b]` interval** | 95% range from a task-paired bootstrap (10,000 resamples): tasks are resampled and the two conditions are compared on the same task. If the range includes 0, treat the difference as noise. |
124
+ | **tasks x reps** | Number of tasks times repetitions per task. Repetitions of one task are averaged before the comparison. |
125
+
126
+ ## Does it miss files?
127
+
128
+ Ground truth is the **compiler**: add a required first parameter `traceId: String` / `traceId: string` to a function (or
129
+ Kotlin constructor), compile, and take the production files that fail as the truth.
130
+
131
+ | Set | Tasks | `rg -w` files / lines | moru-narrow tier 1 files / lines | Tasks with every truth file in tier 1 |
132
+ | --- | ---: | --- | --- | ---: |
133
+ | Kotlin, 23 selected noisy functions | 23 | 60.4 / 139 | 8.8 / 18 | 23/23 |
134
+ | Kotlin, held-out functions | 120 | 68.3 / 281 | 3.3 / 5 | 120/120 |
135
+ | Kotlin, held-out noisy subset (rg >= 20 files and >= 2x the truth) | 57 | 132.9 / 571 | 3.8 / 7 | 57/57 |
136
+ | Kotlin, constructor changes | 74 | 14.9 / 36 | 8.8 / 17 | 74/74 |
137
+ | TypeScript, **fresh** functions (v3 rules frozen first) | 194 | 3.5 / 7 | 2.5 / 6 | 191/194 (files: 428/431, 99.3%) |
138
+ | TypeScript, fresh, noisy functions (rg >= 5 files) | 28 | 9.5 / 20 | 8.2 / 18 | 28/28 (files: 193/193) |
139
+
140
+ Read these carefully:
141
+
142
+ - Kotlin rules were adjusted after the first held-out misses (two regex bugs), so the Kotlin sets are **not fully
143
+ independent**. The TypeScript fresh set is: rules were frozen (sha256 recorded) before it was scored.
144
+ - TypeScript misses that remain are cases where the failing file **never mentions the name** (the type flows through
145
+ another function). No `rg`-based tool can find those.
146
+ - Line level: error lines inside the recommended ranges were 90% (Kotlin functions), 96% within +-8 lines (Kotlin
147
+ constructors) and 90% (TypeScript functions). Use ranges as hints, files as the guarantee.
148
+ - The TypeScript compiler truth is built with the TypeScript API in memory against one private Next.js repo; Kotlin on
149
+ one private Compose Multiplatform repo. Neither repo is published here.
150
+
151
+ ## Does it save an agent anything?
152
+
153
+ Same task, same prompt template, two conditions, task-paired bootstrap (10,000 resamples, 95% interval). Local logins
154
+ only. Cost is the API-equivalent estimate reported by the CLI.
155
+
156
+ | Setup | Compile pass (baseline -> with moru-narrow) | Context tokens | Cost |
157
+ | --- | --- | --- | --- |
158
+ | Kotlin, Haiku, 23 tasks x 2 | 46/46 -> 46/46 | **-34.5%** [-107k, -37k] | **-24.2%** |
159
+ | Kotlin, Haiku, 57 tasks x 2 | 112/114 -> 114/114 | **-19.2%** [-36k, -18k] | **-16.4%** |
160
+ | Kotlin, Codex terra, 23 x 2 | 44/46 -> 45/46 | **-14.2%** [-22.8%, -6.2%] | n/a |
161
+ | Kotlin, Codex terra, 57 x 2 | 112/114 -> 114/114 | **-9.8%** [-14.6%, -5.3%] | n/a |
162
+ | TypeScript, Haiku, 28 x 2 | 56/56 -> 56/56 | **-22.2%** [-29.4%, -15.3%] | **-11.1%** [-17.3%, -4.3%] |
163
+ | TypeScript, Codex terra, 28 x 2 | 56/56 -> 56/56 | -6.5% [-13.5%, 0.0%] (touches 0) | n/a |
164
+
165
+ **The agent instruction matters as much as the tool.** In the first Kotlin experiment the agent got the same list but
166
+ kept re-running `rg` afterwards, and cost went **up** 10.3% (context +12.8%) on the larger Haiku set. Adding one
167
+ sentence, "edit the call sites from this list without re-running rg for the same name; use --all only if the list looks
168
+ incomplete", changed that: with it, `rg` calls per run fell from 2.4 to 0.02 (Kotlin, Codex, 23 tasks) and from 2.8 to 0.5
169
+ (TypeScript, Haiku), and it produced the table above. That revised prompt was written after seeing the first
170
+ results, so treat the savings as exploratory, and ship the instruction together with the tool.
171
+
172
+ **What we learned the hard way.** The first TypeScript agent run failed 10 more tasks than the baseline (33/44 vs 43/44).
173
+ Every file the compiler flagged was in the list; the miss was calls *inside the declaring file*, which the first
174
+ version hid because a file does not import itself. The offline truth had also excluded the declaring file, so the offline
175
+ check could not see it. Fix: v3 keeps those lines in tier 1. Lesson: validate with the real agent loop, not only offline.
176
+
177
+ ## Limits
178
+
179
+ - Languages: Kotlin + Gradle and TypeScript. Each language needs a small profile (role patterns, how to see a
180
+ declaration). Dynamic languages need a different ground truth.
181
+ - Verified on **one change shape** (adding a required first parameter) and **one repository per language**.
182
+ - React components are out of scope (a positional first parameter does not exist in JSX).
183
+ - Savings depend on the model. Codex on the TypeScript repo showed no clear reduction (-6.5%, interval touches 0). There
184
+ `rg` was already precise (3.5 files on average for functions) and the agent ran only ~3 commands per task; we did not test why.
185
+ - Sample sizes are small (28 to 57 tasks, 2 repetitions per task). In the baseline every run passed in TypeScript, so pass rate cannot show an improvement there, only that nothing broke.
186
+ - The agent tests sent private source to the model providers (Anthropic, OpenAI) through the author's own local CLI logins.
187
+
188
+ ## Reproduce
189
+
190
+ The evaluation harnesses (agent runners, compiler-truth generators, scorers) are not in this repository yet: they carry local paths and private repository details and need a scrub first. `results/` holds the aggregate tables. The method is described above: compiler errors after adding a required first parameter are the ground truth; rules were frozen (hash recorded) before the fresh TypeScript set was scored.
191
+
192
+ ## Versioning
193
+
194
+ Weekly CalVer `YY.WW.N` (scheme `calver-weekly`), for example `26.45.0`: two-digit year, week number, and a release count that starts at `0` each week and counts published packages. Weeks follow the Korean calendar (Asia/Seoul, Monday-start weeks). Git tags use a `v` prefix (`v26.45.0`); the package metadata omits it. Python normalises a zero-padded week, so week 5 appears as `26.5.0` on PyPI. This is a date-based version, not a compatibility signal: while the tool is alpha, read the commit log before upgrading.
195
+
196
+ ## Status
197
+
198
+ Alpha. Not on PyPI or npm. No unit tests yet. The ranking was validated against compiler ground truth (see above); `--install` was only checked by hand in a temporary home directory (fresh install, re-run, `--project`, `--agents-md` idempotence). Issues and results from other repositories are welcome.
199
+
200
+ License: [MIT](LICENSE).
@@ -0,0 +1,180 @@
1
+ # moru-narrow
2
+
3
+ **English** | [한국어](README.ko.md)
4
+
5
+ > **rg tells you every line that mentions a name. moru-narrow tells you which of those lines you must edit.** It keeps every hit; it only changes the order and what is shown first.
6
+
7
+ `moru-narrow` post-processes `rg -w <Name>` for coding agents and humans. It keeps every hit, ranks them, and prints the
8
+ files and **line ranges** to check first. No index, no database, no model calls, no daemon. Python standard library and
9
+ [ripgrep](https://github.com/BurntSushi/ripgrep) only.
10
+
11
+ > Scope: **signature changes** (add or change a parameter, then find what breaks). It is not a general `rg` replacement: for renaming a variable, tracing a logic bug or searching a config string, use plain `rg`.
12
+ >
13
+ > Status: alpha / experimental. MIT license. The examples use the `moru-narrow` command (alias `mnarrow`); from a clone, run `python3 -m moru_narrow` instead.
14
+
15
+ ## In short
16
+
17
+ - **What it does:** after `rg -w name`, it lists the few files and line ranges you must edit, ranked, and hides the rest behind `--all`. Nothing is dropped.
18
+ - **Does it miss files?** Checked against the compiler: in all 217 Kotlin tasks every truth file was listed (rules were partly tuned on them), and 191 of 194 tasks (99.3% of files) on a fresh TypeScript set. The rare misses are files that never mention the name.
19
+ - **Does it help a coding agent?** In 6 test setups the compile success rate never dropped below the baseline. Context tokens fell by 9.8% to 34.5% in five setups; in the sixth (Codex on TypeScript) the change cannot be told apart from noise.
20
+ - **The catch:** the savings appeared only when the agent was also told not to re-run `rg`. Without that sentence, cost went up 10.3% [+0.0003, +0.0009 USD per task] in one test (Kotlin, Haiku, 57 tasks).
21
+
22
+ ```text
23
+ $ moru-narrow getGlobalTheme --repo ./my-next-app
24
+ getGlobalTheme declared: src/lib/theme-settings.ts:28
25
+ TIER 1: check these (3 files). rg found 4 files, 6 lines.
26
+ src/app/[locale]/(walled)/manage/(protected)/admin/settings/page.tsx
27
+ 5: import { getGlobalTheme, saveGlobalTheme, type ThemeMode } from '@/lib/theme-settings';
28
+ 19-22: getGlobalTheme({ fresh: true }).then(t => {
29
+ ...
30
+ hidden: tier 2 has 0 files (use --all to list them)
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ Needs Python 3.9+ and [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) on your `PATH`. No other dependency. It does not need Moru Core or any other tool.
36
+
37
+ ```bash
38
+ pipx install git+https://github.com/taehwandev/moru-narrow # or: uv tool install git+https://... / pip install git+https://...
39
+ moru-narrow <Name> --repo /path/to/your/project # Kotlin+Gradle or TypeScript, auto-detected
40
+ ```
41
+
42
+ Without installing, from a clone: `git clone https://github.com/taehwandev/moru-narrow && cd moru-narrow && python3 -m moru_narrow <Name> --repo ...`.
43
+
44
+ Not published on PyPI or npm yet. The `pip install git+...` route was tested in a clean virtualenv on macOS with Python 3.9.6; other platforms and Python versions are untested.
45
+
46
+ ## Use with your coding agent
47
+
48
+ The tool only helps an agent when the agent is told how to use it (see the savings section: without the "do not re-run rg" sentence cost went up). One command registers that instruction:
49
+
50
+ ```bash
51
+ moru-narrow --install # agent skill in ~/.claude/skills/ (Claude Code) and ~/.agents/skills/ (Codex and other agent-skills tools)
52
+ moru-narrow --install --project # same, but into ./.claude and ./.agents so your team shares it via git
53
+ moru-narrow --install --agents-md # no skill support (e.g. Cursor): add a marked block to ./AGENTS.md
54
+ ```
55
+
56
+ Re-running is safe: an existing `SKILL.md` is kept (`--force` overwrites it), and the `AGENTS.md` block is replaced in place without touching your other text. `--target claude|agents` limits the skill install to one location. The installed text is based on the instruction used in the agent tests (worded differently, and extended as noted below):
57
+
58
+ > When a task changes a function or constructor signature (adds or changes a parameter), run `moru-narrow <Name> --repo <project root>` once and edit the call sites it lists under TIER 1. Do not run `rg` again for the same name. Use `--all` only if the list looks incomplete. For anything else (renames, logic bugs, config strings), or if moru-narrow reports an error or an unsupported language, use plain `rg`.
59
+
60
+ The exact wording was not the one measured, and the last sentence (use plain `rg` for other tasks and unsupported languages) is new: the tests only contained signature-change tasks and a prompt written for them. Any agent that can run a shell command can also call `moru-narrow` directly.
61
+
62
+ ## Why
63
+
64
+ Changing a function signature means finding every place that breaks. `rg -w name` finds all of them, plus comments,
65
+ strings, same-named functions in other modules and unrelated files. On a Kotlin monorepo it returned **60 files for a
66
+ change that broke 3 to 4**. An agent then reads all of them.
67
+
68
+ A code-graph index was the obvious fix. We measured a syntax-level code-graph index on the same Kotlin repo and it did not
69
+ pay off at the agent level: no measurable cost difference with location-only output, and about +45.5% cost when it returned source code (that run also carried MCP connection overhead, so part of the increase is not the graph itself).
70
+ It also needs an index build and a sync after every edit (stale until synced). `moru-narrow` has no build step and no
71
+ staleness, because it runs `rg` live.
72
+
73
+ ## How it works
74
+
75
+ 1. `rg -nw <Name>` collects hits (test files are excluded).
76
+ 2. Each line gets a role: `use`, `import`, `override`, `declaration`, `comment`, `string`, `named-arg`, ...
77
+ 3. A file is **tier 1** when it has a strong role (`use`/`import`/`override`) **and** can see the declaration:
78
+ - Kotlin: the module is reachable through Gradle `project()` edges, and the file is in the same package or imports it.
79
+ - TypeScript: the file imports the declaring file (relative paths and `tsconfig` aliases, through re-exporting
80
+ barrel files), or imports a wrapper that does and calls `X.name`, or is the declaring file itself.
81
+ 4. Everything else is demoted to tier 2/3. **Filters only demote. `--all` always lists everything `rg` found.**
82
+ 5. Multi-line calls are returned as ranges (`19-22`), because compilers report errors at argument lines.
83
+
84
+ ```text
85
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json] [--lang kotlin|ts]
86
+ ```
87
+
88
+ Language is auto-detected (`tsconfig.json`, or a Gradle build).
89
+
90
+ ## How to read the numbers
91
+
92
+ | Term | What it means here |
93
+ | --- | --- |
94
+ | **Truth files** | Production files that stop compiling after we add a required first parameter (`traceId`) to a function. The compiler decides, not us. |
95
+ | **Recall** | Whether the truth files appear in tier 1. The table counts tasks where all of them do (`23/23`); TypeScript rows also give files (`428/431` = 3 truth files were not listed). |
96
+ | **Tier 1** | The files moru-narrow tells you to read first. |
97
+ | **`rg -w` files / lines** | What plain ripgrep returns for the same name. This is the baseline to beat. |
98
+ | **Fresh set** | Candidates drawn after the rules were frozen. Nothing was tuned on them, so they are the fair test. |
99
+ | **Baseline vs. with moru-narrow** (agent tests) | Same coding agent, same task, same prompt. Baseline may use `rg`, `ls` and file reads. The other condition also gets the tool and the sentence "edit from the list, do not re-run rg for the same name". |
100
+ | **Compile pass** | The project compiles after the agent's edit (Kotlin: Gradle, TypeScript: `tsc --noEmit`). TypeScript runs also require that the declaration really gained the parameter, because an agent that changes nothing would otherwise pass. |
101
+ | **Context tokens** | Input tokens the model processed during the whole run, cached ones included. A proxy for how much it read. |
102
+ | **Cost** | API-equivalent price estimate reported by the Claude CLI, not an actual bill. Codex does not report it, shown as n/a. |
103
+ | **`[a, b]` interval** | 95% range from a task-paired bootstrap (10,000 resamples): tasks are resampled and the two conditions are compared on the same task. If the range includes 0, treat the difference as noise. |
104
+ | **tasks x reps** | Number of tasks times repetitions per task. Repetitions of one task are averaged before the comparison. |
105
+
106
+ ## Does it miss files?
107
+
108
+ Ground truth is the **compiler**: add a required first parameter `traceId: String` / `traceId: string` to a function (or
109
+ Kotlin constructor), compile, and take the production files that fail as the truth.
110
+
111
+ | Set | Tasks | `rg -w` files / lines | moru-narrow tier 1 files / lines | Tasks with every truth file in tier 1 |
112
+ | --- | ---: | --- | --- | ---: |
113
+ | Kotlin, 23 selected noisy functions | 23 | 60.4 / 139 | 8.8 / 18 | 23/23 |
114
+ | Kotlin, held-out functions | 120 | 68.3 / 281 | 3.3 / 5 | 120/120 |
115
+ | Kotlin, held-out noisy subset (rg >= 20 files and >= 2x the truth) | 57 | 132.9 / 571 | 3.8 / 7 | 57/57 |
116
+ | Kotlin, constructor changes | 74 | 14.9 / 36 | 8.8 / 17 | 74/74 |
117
+ | TypeScript, **fresh** functions (v3 rules frozen first) | 194 | 3.5 / 7 | 2.5 / 6 | 191/194 (files: 428/431, 99.3%) |
118
+ | TypeScript, fresh, noisy functions (rg >= 5 files) | 28 | 9.5 / 20 | 8.2 / 18 | 28/28 (files: 193/193) |
119
+
120
+ Read these carefully:
121
+
122
+ - Kotlin rules were adjusted after the first held-out misses (two regex bugs), so the Kotlin sets are **not fully
123
+ independent**. The TypeScript fresh set is: rules were frozen (sha256 recorded) before it was scored.
124
+ - TypeScript misses that remain are cases where the failing file **never mentions the name** (the type flows through
125
+ another function). No `rg`-based tool can find those.
126
+ - Line level: error lines inside the recommended ranges were 90% (Kotlin functions), 96% within +-8 lines (Kotlin
127
+ constructors) and 90% (TypeScript functions). Use ranges as hints, files as the guarantee.
128
+ - The TypeScript compiler truth is built with the TypeScript API in memory against one private Next.js repo; Kotlin on
129
+ one private Compose Multiplatform repo. Neither repo is published here.
130
+
131
+ ## Does it save an agent anything?
132
+
133
+ Same task, same prompt template, two conditions, task-paired bootstrap (10,000 resamples, 95% interval). Local logins
134
+ only. Cost is the API-equivalent estimate reported by the CLI.
135
+
136
+ | Setup | Compile pass (baseline -> with moru-narrow) | Context tokens | Cost |
137
+ | --- | --- | --- | --- |
138
+ | Kotlin, Haiku, 23 tasks x 2 | 46/46 -> 46/46 | **-34.5%** [-107k, -37k] | **-24.2%** |
139
+ | Kotlin, Haiku, 57 tasks x 2 | 112/114 -> 114/114 | **-19.2%** [-36k, -18k] | **-16.4%** |
140
+ | Kotlin, Codex terra, 23 x 2 | 44/46 -> 45/46 | **-14.2%** [-22.8%, -6.2%] | n/a |
141
+ | Kotlin, Codex terra, 57 x 2 | 112/114 -> 114/114 | **-9.8%** [-14.6%, -5.3%] | n/a |
142
+ | TypeScript, Haiku, 28 x 2 | 56/56 -> 56/56 | **-22.2%** [-29.4%, -15.3%] | **-11.1%** [-17.3%, -4.3%] |
143
+ | TypeScript, Codex terra, 28 x 2 | 56/56 -> 56/56 | -6.5% [-13.5%, 0.0%] (touches 0) | n/a |
144
+
145
+ **The agent instruction matters as much as the tool.** In the first Kotlin experiment the agent got the same list but
146
+ kept re-running `rg` afterwards, and cost went **up** 10.3% (context +12.8%) on the larger Haiku set. Adding one
147
+ sentence, "edit the call sites from this list without re-running rg for the same name; use --all only if the list looks
148
+ incomplete", changed that: with it, `rg` calls per run fell from 2.4 to 0.02 (Kotlin, Codex, 23 tasks) and from 2.8 to 0.5
149
+ (TypeScript, Haiku), and it produced the table above. That revised prompt was written after seeing the first
150
+ results, so treat the savings as exploratory, and ship the instruction together with the tool.
151
+
152
+ **What we learned the hard way.** The first TypeScript agent run failed 10 more tasks than the baseline (33/44 vs 43/44).
153
+ Every file the compiler flagged was in the list; the miss was calls *inside the declaring file*, which the first
154
+ version hid because a file does not import itself. The offline truth had also excluded the declaring file, so the offline
155
+ check could not see it. Fix: v3 keeps those lines in tier 1. Lesson: validate with the real agent loop, not only offline.
156
+
157
+ ## Limits
158
+
159
+ - Languages: Kotlin + Gradle and TypeScript. Each language needs a small profile (role patterns, how to see a
160
+ declaration). Dynamic languages need a different ground truth.
161
+ - Verified on **one change shape** (adding a required first parameter) and **one repository per language**.
162
+ - React components are out of scope (a positional first parameter does not exist in JSX).
163
+ - Savings depend on the model. Codex on the TypeScript repo showed no clear reduction (-6.5%, interval touches 0). There
164
+ `rg` was already precise (3.5 files on average for functions) and the agent ran only ~3 commands per task; we did not test why.
165
+ - Sample sizes are small (28 to 57 tasks, 2 repetitions per task). In the baseline every run passed in TypeScript, so pass rate cannot show an improvement there, only that nothing broke.
166
+ - The agent tests sent private source to the model providers (Anthropic, OpenAI) through the author's own local CLI logins.
167
+
168
+ ## Reproduce
169
+
170
+ The evaluation harnesses (agent runners, compiler-truth generators, scorers) are not in this repository yet: they carry local paths and private repository details and need a scrub first. `results/` holds the aggregate tables. The method is described above: compiler errors after adding a required first parameter are the ground truth; rules were frozen (hash recorded) before the fresh TypeScript set was scored.
171
+
172
+ ## Versioning
173
+
174
+ Weekly CalVer `YY.WW.N` (scheme `calver-weekly`), for example `26.45.0`: two-digit year, week number, and a release count that starts at `0` each week and counts published packages. Weeks follow the Korean calendar (Asia/Seoul, Monday-start weeks). Git tags use a `v` prefix (`v26.45.0`); the package metadata omits it. Python normalises a zero-padded week, so week 5 appears as `26.5.0` on PyPI. This is a date-based version, not a compatibility signal: while the tool is alpha, read the commit log before upgrading.
175
+
176
+ ## Status
177
+
178
+ Alpha. Not on PyPI or npm. No unit tests yet. The ranking was validated against compiler ground truth (see above); `--install` was only checked by hand in a temporary home directory (fresh install, re-run, `--project`, `--agents-md` idempotence). Issues and results from other repositories are welcome.
179
+
180
+ License: [MIT](LICENSE).
File without changes
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """moru-narrow: recommend the files and lines to check after `rg -w <Name>` (Kotlin + Gradle, TypeScript).
3
+
4
+ Static, no model calls, no index, standard library + ripgrep only. Filters only demote; --all lists everything rg found.
5
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json] [--lang kotlin|ts]
6
+ moru-narrow --install [--target claude|agents|both] [--project] [--agents-md] [--force]
7
+ """
8
+ import importlib, sys
9
+ from pathlib import Path
10
+
11
+
12
+ def detect(repo):
13
+ root = Path(repo)
14
+ if (root / "tsconfig.json").exists(): return "ts"
15
+ if any(any(root.glob(g)) for g in ("settings.gradle*", "build.gradle*", "*/build.gradle*")): return "kotlin"
16
+ if (root / "package.json").exists(): return "ts"
17
+ sys.exit("moru-narrow: cannot detect the language (looked for tsconfig.json or a Gradle build at the repo root); pass --lang kotlin|ts")
18
+
19
+
20
+ def main():
21
+ argv = sys.argv[1:]; lang = None; repo = "."
22
+ if argv[:1] == ["--install"]:
23
+ from moru_narrow import install
24
+ return install.main(argv[1:])
25
+ if "--lang" in argv:
26
+ i = argv.index("--lang"); lang = argv[i + 1]; del argv[i:i + 2]
27
+ if "--repo" in argv: repo = argv[argv.index("--repo") + 1]
28
+ lang = lang or detect(repo)
29
+ mod = importlib.import_module({"kotlin": "moru_narrow.kotlin", "ts": "moru_narrow.typescript"}[lang])
30
+ sys.argv = [f"moru-narrow({lang})"] + argv
31
+ mod.main()
32
+
33
+
34
+ if __name__ == "__main__": main()
@@ -0,0 +1,63 @@
1
+ """`moru-narrow --install`: register moru-narrow with AI coding agents (agent-skills `SKILL.md`, or an `AGENTS.md` block).
2
+
3
+ moru-narrow --install [--target claude|agents|both] [--project] [--agents-md] [--force]
4
+ """
5
+ import argparse, re
6
+ from pathlib import Path
7
+
8
+ RULE = ("When a task changes a function or constructor signature (adds or changes a parameter), run "
9
+ "`moru-narrow <Name> --repo <project root>` once and edit the call sites it lists under TIER 1. "
10
+ "Do not run `rg` again for the same name. Use `--all` only if the list looks incomplete. "
11
+ "For anything else (renames, logic bugs, config strings), or if moru-narrow reports an error or an unsupported language, use plain `rg`.")
12
+
13
+ SKILL = f"""---
14
+ name: moru-narrow
15
+ description: After a Kotlin (Gradle) or TypeScript function/constructor signature change, list the few files and line ranges that break, instead of reading every `rg` hit.
16
+ ---
17
+
18
+ # moru-narrow
19
+
20
+ {RULE}
21
+
22
+ ```bash
23
+ moru-narrow getGlobalTheme --repo . # TIER 1 = files to edit; tier 2/3 are hidden, --all shows them
24
+ ```
25
+
26
+ It post-processes `rg -nw`, so every hit is still reachable with `--all`. No index, no network, no model calls.
27
+ If `moru-narrow` is not on PATH, use `python3 -m moru_narrow`.
28
+ """
29
+
30
+ BEGIN, END = "<!-- moru-narrow:begin -->", "<!-- moru-narrow:end -->"
31
+ BLOCK = f"{BEGIN}\n## moru-narrow\n\n{RULE}\n{END}\n"
32
+
33
+
34
+ def skill_dirs(target, project):
35
+ base = Path(".") if project else Path.home()
36
+ return [base / d / "skills" / "moru-narrow" for d in ((".claude",) if target == "claude" else (".agents",) if target == "agents" else (".claude", ".agents"))]
37
+
38
+
39
+ def write_skill(d, force):
40
+ f = d / "SKILL.md"
41
+ if f.exists() and not force: return f"kept {f} (exists, use --force to overwrite)"
42
+ d.mkdir(parents=True, exist_ok=True); f.write_text(SKILL)
43
+ return f"wrote {f}"
44
+
45
+
46
+ def write_agents_md(path):
47
+ text = path.read_text() if path.exists() else ""
48
+ pat = re.compile(re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?", re.S)
49
+ new = pat.sub(lambda _: BLOCK, text) if pat.search(text) else (text + ("\n" if text and not text.endswith("\n\n") else "") + BLOCK)
50
+ path.write_text(new)
51
+ return f"updated {path}"
52
+
53
+
54
+ def main(argv):
55
+ ap = argparse.ArgumentParser(prog="moru-narrow --install", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
56
+ ap.add_argument("--target", choices=("claude", "agents", "both"), default="both")
57
+ ap.add_argument("--project", action="store_true", help="write into ./.claude and ./.agents instead of your home directory")
58
+ ap.add_argument("--agents-md", action="store_true", help="insert a marked block into ./AGENTS.md instead of installing a skill")
59
+ ap.add_argument("--force", action="store_true")
60
+ a = ap.parse_args(argv)
61
+ if a.agents_md: print(write_agents_md(Path("AGENTS.md")))
62
+ else:
63
+ for d in skill_dirs(a.target, a.project): print(write_skill(d, a.force))
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """moru-narrow: recommend the files and lines to check after `rg -w <name>`, without missing any.
3
+
4
+ Static, no model calls, standard library only. Kotlin + Gradle first.
5
+ Filters only DEMOTE files into tiers; `--all` always lists everything rg found.
6
+
7
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json]
8
+ """
9
+ import argparse, json, os, re, subprocess, sys
10
+ from pathlib import Path
11
+
12
+ STRONG = ("use", "override", "import")
13
+ TEST = re.compile(r"/(src/)?\w*[Tt]est\w*/")
14
+ PKG = re.compile(r"^package\s+([\w.]+)", re.M)
15
+
16
+
17
+ def is_test(rel): return bool(TEST.search("/" + rel)) or rel.endswith("Test.kt")
18
+
19
+
20
+ class Repo:
21
+ def __init__(self, root):
22
+ self.root = Path(root).resolve(); self.src = {}
23
+ self.build = {} # module dir -> set(dependency dirs)
24
+ for g in list(self.root.rglob("build.gradle.kts")) + list(self.root.rglob("build.gradle")):
25
+ if "/build/" in str(g): continue
26
+ d = g.parent; base = self._settings_root(d)
27
+ deps = {(base / p.strip(":").replace(":", "/")).relative_to(self.root).as_posix()
28
+ for p in re.findall(r'project\(["\'](:[\w:.-]+)["\']\)', g.read_text(errors="ignore"))}
29
+ self.build[d.relative_to(self.root).as_posix()] = deps
30
+ self.reach = {m: self._closure(m) for m in self.build}
31
+
32
+ def _settings_root(self, d):
33
+ cur = d
34
+ while True:
35
+ if (cur / "settings.gradle.kts").exists() or (cur / "settings.gradle").exists(): return cur
36
+ if cur == self.root or cur == cur.parent: return self.root
37
+ cur = cur.parent
38
+
39
+ def _closure(self, m):
40
+ seen, st = set(), [m]
41
+ while st:
42
+ for y in self.build.get(st.pop(), ()):
43
+ if y not in seen: seen.add(y); st.append(y)
44
+ return seen
45
+
46
+ def text(self, rel):
47
+ if rel not in self.src: self.src[rel] = (self.root / rel).read_text(errors="ignore")
48
+ return self.src[rel]
49
+
50
+ def module(self, rel):
51
+ cur = Path(rel).parent
52
+ while str(cur) != ".":
53
+ if cur.as_posix() in self.build: return cur.as_posix()
54
+ cur = cur.parent
55
+
56
+ def package(self, rel):
57
+ m = PKG.search(self.text(rel)); return m.group(1) if m else ""
58
+
59
+
60
+ def role(line, name):
61
+ s = line.strip()
62
+ if s.startswith(("//", "*", "/*")): return "comment"
63
+ if re.match(r"import\s", s): return "import" if s.endswith("." + name) or " as " in s else "import-other"
64
+ if re.search(rf"\b(val|var)\s+{name}\b", s) and not re.search(rf"\b{name}\s*\(", s): return "property"
65
+ if re.search(rf"\bfun\s+(?:<[^>]*>\s*)?(?:[\w.<>?]+\.)?{name}\s*[(<]", s) or re.search(rf"\b(?:class|interface|object|typealias)\s+{name}\b", s):
66
+ return "override" if "override" in s else "declaration"
67
+ if re.search(rf"(?<![\w\"'])(?:\.|::|\b){name}\s*(?:\(|\{{|$|\)|,)", s): return "use"
68
+ if re.search(rf"\b{name}\s*=", s): return "named-arg"
69
+ return "other"
70
+
71
+
72
+ def call_range(lines, i):
73
+ """1-based line i; extend to the closing paren of the call that starts there (max 40 lines)."""
74
+ depth, seen = 0, False
75
+ for j in range(i - 1, min(len(lines), i + 39)):
76
+ for ch in lines[j]:
77
+ if ch == "(": depth += 1; seen = True
78
+ elif ch == ")": depth -= 1
79
+ if seen and depth <= 0: return i, j + 1
80
+ if not seen and j > i - 1: break
81
+ return i, i
82
+
83
+
84
+ def rg_hits(repo, name):
85
+ out = subprocess.run(["rg", "-nw", "--glob", "*.kt", name, "."], cwd=repo.root, capture_output=True, text=True).stdout.splitlines()
86
+ byfile = {}
87
+ for l in out:
88
+ m = re.match(r"\./(.+?\.kt):(\d+):(.*)", l)
89
+ if m and not is_test(m.group(1)): byfile.setdefault(m.group(1), []).append((int(m.group(2)), m.group(3)))
90
+ return byfile
91
+
92
+
93
+ def recommend(repo, name, decl_files=None):
94
+ byfile = rg_hits(repo, name)
95
+ decls = []
96
+ for rel, hits in byfile.items():
97
+ for ln, tx in hits:
98
+ if role(tx, name) == "declaration" and (decl_files is None or rel in decl_files): decls.append((rel, ln))
99
+ if decl_files: decls = [(f, next((ln for ln, tx in byfile.get(f, []) if role(tx, name) == "declaration"), 0)) for f in decl_files]
100
+ dfiles = {d[0] for d in decls}
101
+ dmods = {repo.module(f) for f in dfiles}; dpkgs = {repo.package(f) for f in dfiles}
102
+ tiers = {1: {}, 2: {}, 3: {}}
103
+ for rel, hits in byfile.items():
104
+ lines = repo.text(rel).split("\n")
105
+ rows = [(ln, tx, role(tx, name)) for ln, tx in hits]
106
+ strong = [r for r in rows if r[2] in STRONG]
107
+ if rel in dfiles and (decl_files is not None or not strong): continue # a file that only declares it needs no check; with --decl the user already knows
108
+ m = repo.module(rel)
109
+ in_mod = m in dmods or any(dm in repo.reach.get(m, ()) for dm in dmods)
110
+ pk = repo.package(rel); src = repo.text(rel)
111
+ near = pk in dpkgs or any(re.search(r"^import\s+" + re.escape(p) + r"[.\w]*", src, re.M) for p in dpkgs if p)
112
+ tier = 1 if (strong and in_mod and near) else 2 if (strong and in_mod) else 3
113
+ tiers[tier][rel] = [{"line": ln, "to": call_range(lines, ln)[1], "role": r, "text": tx.strip()[:160]} for ln, tx, r in (strong or rows)]
114
+ return {"name": name, "declarations": decls, "tiers": tiers, "rg_files": len(byfile),
115
+ "rg_lines": sum(len(v) for v in byfile.values())}
116
+
117
+
118
+ def render(res, show_all):
119
+ o = [f"{res['name']} declared: " + ("; ".join(f"{f}:{ln}" for f, ln in res["declarations"]) or "not found in non-test .kt")]
120
+ t1, t2, t3 = (res["tiers"][k] for k in (1, 2, 3))
121
+ o.append(f"TIER 1: check these ({len(t1)} files). rg found {res['rg_files']} files, {res['rg_lines']} lines.")
122
+ for rel, ls in sorted(t1.items()):
123
+ o.append(rel)
124
+ for r in ls: o.append(f" {r['line']}" + (f"-{r['to']}" if r["to"] > r["line"] else "") + f": {r['text']}")
125
+ for k, t, label in ((2, t2, "TIER 2: strong match, outside the declaring package or its imports"), (3, t3, "TIER 3: weak match or unreachable module")):
126
+ if show_all:
127
+ o.append(f"{label} ({len(t)} files)")
128
+ for rel, ls in sorted(t.items()):
129
+ o.append(rel)
130
+ for r in ls[:5]: o.append(f" {r['line']}: {r['text']}")
131
+ else:
132
+ o.append(f"hidden: tier {k} has {len(t)} files (use --all to list them)")
133
+ return "\n".join(o)
134
+
135
+
136
+ def main():
137
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
138
+ ap.add_argument("name"); ap.add_argument("--repo", default="."); ap.add_argument("--decl", action="append")
139
+ ap.add_argument("--all", action="store_true"); ap.add_argument("--json", action="store_true")
140
+ a = ap.parse_args(); repo = Repo(a.repo)
141
+ res = recommend(repo, a.name, set(a.decl) if a.decl else None)
142
+ print(json.dumps(res, ensure_ascii=False) if a.json else render(res, a.all))
143
+
144
+
145
+ if __name__ == "__main__": main()
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """moru-narrow for TypeScript (v3: two-hop wrapper rule + uses inside the declaring file): recommend the files and lines to check after `rg -w <name>`, without missing any.
3
+
4
+ Static, no model calls, standard library only. Filters only DEMOTE files into tiers; `--all` lists everything rg found.
5
+ Tier 1 = a strong use/import AND the file imports the declaring file (directly or through re-exporting barrel files).
6
+
7
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json]
8
+ """
9
+ import argparse, json, os, re, subprocess
10
+ from pathlib import Path
11
+
12
+ STRONG = ("use", "import")
13
+ TEST = re.compile(r"(\.test\.|\.spec\.|/__tests__/|/__mocks__/)")
14
+ EXTS = (".ts", ".tsx", ".mts")
15
+ FROM = re.compile(r"""(?:import|export)\s[^;'"]*?\bfrom\s*['"]([^'"]+)['"]""", re.S)
16
+ BARE = re.compile(r"""\bimport\s*['"]([^'"]+)['"]""")
17
+ DYN = re.compile(r"""(?:\bimport|\brequire)\s*\(\s*['"]([^'"]+)['"]\s*\)""")
18
+ REEXPORT = re.compile(r"""\bexport\s+(?:type\s+)?(?:\*(?:\s+as\s+\w+)?|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]""", re.S)
19
+
20
+
21
+ def is_test(rel): return bool(TEST.search("/" + rel))
22
+
23
+
24
+ class Repo:
25
+ def __init__(self, root):
26
+ self.root = Path(root).resolve(); self.src = {}; self._imp = {}; self._rex = {}
27
+ self.aliases = self._load_aliases()
28
+
29
+ def _load_aliases(self):
30
+ out = []
31
+ cfg = self.root / "tsconfig.json"
32
+ if cfg.exists():
33
+ raw = re.sub(r'("(?:\\.|[^"\\])*")|//[^\n]*|/\*.*?\*/', lambda m: m.group(1) or "", cfg.read_text(errors="ignore"), flags=re.S)
34
+ raw = re.sub(r",(\s*[}\]])", r"\1", raw)
35
+ try:
36
+ co = json.loads(raw).get("compilerOptions", {}); base = co.get("baseUrl", ".")
37
+ for k, v in (co.get("paths") or {}).items():
38
+ if v: out.append((k.rstrip("*"), (Path(base) / v[0].rstrip("*")).as_posix().lstrip("./")))
39
+ except ValueError:
40
+ pass
41
+ return out
42
+
43
+ def text(self, rel):
44
+ if rel not in self.src: self.src[rel] = (self.root / rel).read_text(errors="ignore")
45
+ return self.src[rel]
46
+
47
+ def resolve(self, frm, spec):
48
+ cands = []
49
+ if spec.startswith("."): cands.append(os.path.normpath(Path(frm).parent / spec))
50
+ else:
51
+ for pre, tgt in self.aliases:
52
+ if spec.startswith(pre): cands.append(os.path.normpath(Path(tgt) / spec[len(pre):]))
53
+ for c in cands:
54
+ c = str(Path(c))
55
+ for suffix in ("", *EXTS, ".d.ts", "/index.ts", "/index.tsx"):
56
+ p = c + suffix
57
+ if p.endswith(EXTS) and (self.root / p).is_file(): return p
58
+ return None
59
+
60
+ def imports(self, rel):
61
+ if rel not in self._imp:
62
+ t = self.text(rel); specs = set(FROM.findall(t)) | set(BARE.findall(t)) | set(DYN.findall(t))
63
+ self._imp[rel] = {r for r in (self.resolve(rel, s) for s in specs) if r}
64
+ return self._imp[rel]
65
+
66
+ def reexports(self, rel):
67
+ if rel not in self._rex:
68
+ self._rex[rel] = {r for r in (self.resolve(rel, s) for s in REEXPORT.findall(self.text(rel))) if r}
69
+ return self._rex[rel]
70
+
71
+ def providers(self, decl, files):
72
+ """The declaring file plus every file that re-exports it, transitively."""
73
+ prov, grew = {decl}, True
74
+ while grew:
75
+ grew = False
76
+ for f in files:
77
+ if f not in prov and self.reexports(f) & prov: prov.add(f); grew = True
78
+ return prov
79
+
80
+
81
+ def role(line, name):
82
+ s = line.strip()
83
+ if s.startswith(("//", "*", "/*")): return "comment"
84
+ if re.search(rf"\b(?:function\s*\*?|class|interface|type|enum)\s+{name}\b", s) or re.search(rf"\b(?:const|let|var)\s+{name}\b", s):
85
+ return "declaration"
86
+ if re.match(r"(?:import|export)\b", s) and (re.search(r"\bfrom\b", s) or s.endswith(("{", ","))):
87
+ return "import" if re.search(rf"\b{name}\b", s.split("from")[0]) else "import-other"
88
+ if re.search(rf"""(?<=['"`]){name}(?=['"`])""", s) and not re.search(rf"""(?<![\w'"`.]){name}(?![\w'"`])""", s): return "string"
89
+ if re.match(rf"{name}\s*\??:(?!:)", s) or re.search(rf"[{{,(]\s*{name}\s*\??:(?!:)", s): return "named-arg"
90
+ return "use"
91
+
92
+
93
+ def call_range(lines, i):
94
+ """1-based line i; extend to the closing paren (or the end of a JSX opening tag) that starts there (max 40 lines)."""
95
+ depth, seen = 0, False
96
+ for j in range(i - 1, min(len(lines), i + 39)):
97
+ for ch in lines[j]:
98
+ if ch == "(": depth += 1; seen = True
99
+ elif ch == ")": depth -= 1
100
+ if seen and depth <= 0: return i, j + 1
101
+ if not seen and j > i - 1: break
102
+ if re.search(r"<\w", lines[i - 1]) and ">" not in lines[i - 1].replace("=>", ""):
103
+ for j in range(i, min(len(lines), i + 39)):
104
+ if ">" in lines[j].replace("=>", ""): return i, j + 1
105
+ return i, i
106
+
107
+
108
+ def rg_hits(repo, name):
109
+ out = subprocess.run(["rg", "-nw", "-g", "*.ts", "-g", "*.tsx", "-g", "*.mts", "-g", "!node_modules", "-g", "!.next", "--", name, "."],
110
+ cwd=repo.root, capture_output=True, text=True).stdout.splitlines()
111
+ byfile = {}
112
+ for l in out:
113
+ m = re.match(r"\./(.+?\.(?:ts|tsx|mts)):(\d+):(.*)", l)
114
+ if m and not is_test(m.group(1)): byfile.setdefault(m.group(1), []).append((int(m.group(2)), m.group(3)))
115
+ return byfile
116
+
117
+
118
+ def recommend(repo, name, decl_files=None):
119
+ byfile = rg_hits(repo, name)
120
+ decls = []
121
+ for rel, hits in byfile.items():
122
+ for ln, tx in hits:
123
+ if role(tx, name) == "declaration" and (decl_files is None or rel in decl_files): decls.append((rel, ln))
124
+ if decl_files: decls = [(f, next((ln for ln, tx in byfile.get(f, []) if role(tx, name) == "declaration"), 0)) for f in decl_files]
125
+ dfiles = {d[0] for d in decls}
126
+ prov = set()
127
+ for d in dfiles: prov |= repo.providers(d, _all_files(repo))
128
+ tiers = {1: {}, 2: {}, 3: {}}
129
+ for rel, hits in byfile.items():
130
+ lines = repo.text(rel).split("\n")
131
+ rows = [(ln, tx, role(tx, name)) for ln, tx in hits]
132
+ strong = [r for r in rows if r[2] in STRONG]
133
+ if rel in dfiles and not strong: continue # v3: a declaring file with other uses is kept, even when --decl names it
134
+ near = rel in dfiles or bool(repo.imports(rel) & prov) # v3: calls inside the declaring file are near
135
+ # v2: a file that imports a wrapper (a file importing the declaring file) and calls `X.name` is also near
136
+ if not near and strong and any(re.search(rf"\.{re.escape(name)}\b", r[1]) for r in strong):
137
+ near = any(repo.imports(g) & prov for g in repo.imports(rel))
138
+ tier = 1 if (strong and near) else 2 if strong else 3
139
+ tiers[tier][rel] = [{"line": ln, "to": call_range(lines, ln)[1], "role": r, "text": tx.strip()[:160]} for ln, tx, r in (strong or rows)]
140
+ return {"name": name, "declarations": decls, "tiers": tiers, "rg_files": len(byfile),
141
+ "rg_lines": sum(len(v) for v in byfile.values())}
142
+
143
+
144
+ _ALL = {}
145
+
146
+
147
+ def _all_files(repo):
148
+ if repo.root not in _ALL:
149
+ out = subprocess.run(["rg", "--files", "-g", "*.ts", "-g", "*.tsx", "-g", "*.mts", "-g", "!node_modules", "-g", "!.next"], cwd=repo.root, capture_output=True, text=True).stdout.split()
150
+ _ALL[repo.root] = [f for f in out if not is_test(f) and "export" in repo.text(f)]
151
+ return _ALL[repo.root]
152
+
153
+
154
+ def render(res, show_all):
155
+ o = [f"{res['name']} declared: " + ("; ".join(f"{f}:{ln}" for f, ln in res["declarations"]) or "not found in non-test .ts/.tsx")]
156
+ t1, t2, t3 = (res["tiers"][k] for k in (1, 2, 3))
157
+ o.append(f"TIER 1: check these ({len(t1)} files). rg found {res['rg_files']} files, {res['rg_lines']} lines.")
158
+ for rel, ls in sorted(t1.items()):
159
+ o.append(rel)
160
+ for r in ls: o.append(f" {r['line']}" + (f"-{r['to']}" if r["to"] > r["line"] else "") + f": {r['text']}")
161
+ for k, t, label in ((2, t2, "TIER 2: strong match in a file that does not import the declaring file"), (3, t3, "TIER 3: weak match (comment, string, key)")):
162
+ if show_all:
163
+ o.append(f"{label} ({len(t)} files)")
164
+ for rel, ls in sorted(t.items()):
165
+ o.append(rel)
166
+ for r in ls[:5]: o.append(f" {r['line']}: {r['text']}")
167
+ else:
168
+ o.append(f"hidden: tier {k} has {len(t)} files (use --all to list them)")
169
+ return "\n".join(o)
170
+
171
+
172
+ def main():
173
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
174
+ ap.add_argument("name"); ap.add_argument("--repo", default="."); ap.add_argument("--decl", action="append")
175
+ ap.add_argument("--all", action="store_true"); ap.add_argument("--json", action="store_true")
176
+ a = ap.parse_args(); repo = Repo(a.repo)
177
+ res = recommend(repo, a.name, set(a.decl) if a.decl else None)
178
+ print(json.dumps(res, ensure_ascii=False) if a.json else render(res, a.all))
179
+
180
+
181
+ if __name__ == "__main__": main()
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: moru-narrow
3
+ Version: 26.45.0
4
+ Summary: Narrow `rg -w <Name>` to the files and lines you must edit after a signature change (Kotlin+Gradle, TypeScript).
5
+ Author: taehwandev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/taehwandev/moru-narrow
8
+ Project-URL: Issues, https://github.com/taehwandev/moru-narrow/issues
9
+ Keywords: ripgrep,rg,refactoring,coding-agent,kotlin,typescript,claude-code,codex
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # moru-narrow
22
+
23
+ **English** | [한국어](README.ko.md)
24
+
25
+ > **rg tells you every line that mentions a name. moru-narrow tells you which of those lines you must edit.** It keeps every hit; it only changes the order and what is shown first.
26
+
27
+ `moru-narrow` post-processes `rg -w <Name>` for coding agents and humans. It keeps every hit, ranks them, and prints the
28
+ files and **line ranges** to check first. No index, no database, no model calls, no daemon. Python standard library and
29
+ [ripgrep](https://github.com/BurntSushi/ripgrep) only.
30
+
31
+ > Scope: **signature changes** (add or change a parameter, then find what breaks). It is not a general `rg` replacement: for renaming a variable, tracing a logic bug or searching a config string, use plain `rg`.
32
+ >
33
+ > Status: alpha / experimental. MIT license. The examples use the `moru-narrow` command (alias `mnarrow`); from a clone, run `python3 -m moru_narrow` instead.
34
+
35
+ ## In short
36
+
37
+ - **What it does:** after `rg -w name`, it lists the few files and line ranges you must edit, ranked, and hides the rest behind `--all`. Nothing is dropped.
38
+ - **Does it miss files?** Checked against the compiler: in all 217 Kotlin tasks every truth file was listed (rules were partly tuned on them), and 191 of 194 tasks (99.3% of files) on a fresh TypeScript set. The rare misses are files that never mention the name.
39
+ - **Does it help a coding agent?** In 6 test setups the compile success rate never dropped below the baseline. Context tokens fell by 9.8% to 34.5% in five setups; in the sixth (Codex on TypeScript) the change cannot be told apart from noise.
40
+ - **The catch:** the savings appeared only when the agent was also told not to re-run `rg`. Without that sentence, cost went up 10.3% [+0.0003, +0.0009 USD per task] in one test (Kotlin, Haiku, 57 tasks).
41
+
42
+ ```text
43
+ $ moru-narrow getGlobalTheme --repo ./my-next-app
44
+ getGlobalTheme declared: src/lib/theme-settings.ts:28
45
+ TIER 1: check these (3 files). rg found 4 files, 6 lines.
46
+ src/app/[locale]/(walled)/manage/(protected)/admin/settings/page.tsx
47
+ 5: import { getGlobalTheme, saveGlobalTheme, type ThemeMode } from '@/lib/theme-settings';
48
+ 19-22: getGlobalTheme({ fresh: true }).then(t => {
49
+ ...
50
+ hidden: tier 2 has 0 files (use --all to list them)
51
+ ```
52
+
53
+ ## Quick start
54
+
55
+ Needs Python 3.9+ and [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) on your `PATH`. No other dependency. It does not need Moru Core or any other tool.
56
+
57
+ ```bash
58
+ pipx install git+https://github.com/taehwandev/moru-narrow # or: uv tool install git+https://... / pip install git+https://...
59
+ moru-narrow <Name> --repo /path/to/your/project # Kotlin+Gradle or TypeScript, auto-detected
60
+ ```
61
+
62
+ Without installing, from a clone: `git clone https://github.com/taehwandev/moru-narrow && cd moru-narrow && python3 -m moru_narrow <Name> --repo ...`.
63
+
64
+ Not published on PyPI or npm yet. The `pip install git+...` route was tested in a clean virtualenv on macOS with Python 3.9.6; other platforms and Python versions are untested.
65
+
66
+ ## Use with your coding agent
67
+
68
+ The tool only helps an agent when the agent is told how to use it (see the savings section: without the "do not re-run rg" sentence cost went up). One command registers that instruction:
69
+
70
+ ```bash
71
+ moru-narrow --install # agent skill in ~/.claude/skills/ (Claude Code) and ~/.agents/skills/ (Codex and other agent-skills tools)
72
+ moru-narrow --install --project # same, but into ./.claude and ./.agents so your team shares it via git
73
+ moru-narrow --install --agents-md # no skill support (e.g. Cursor): add a marked block to ./AGENTS.md
74
+ ```
75
+
76
+ Re-running is safe: an existing `SKILL.md` is kept (`--force` overwrites it), and the `AGENTS.md` block is replaced in place without touching your other text. `--target claude|agents` limits the skill install to one location. The installed text is based on the instruction used in the agent tests (worded differently, and extended as noted below):
77
+
78
+ > When a task changes a function or constructor signature (adds or changes a parameter), run `moru-narrow <Name> --repo <project root>` once and edit the call sites it lists under TIER 1. Do not run `rg` again for the same name. Use `--all` only if the list looks incomplete. For anything else (renames, logic bugs, config strings), or if moru-narrow reports an error or an unsupported language, use plain `rg`.
79
+
80
+ The exact wording was not the one measured, and the last sentence (use plain `rg` for other tasks and unsupported languages) is new: the tests only contained signature-change tasks and a prompt written for them. Any agent that can run a shell command can also call `moru-narrow` directly.
81
+
82
+ ## Why
83
+
84
+ Changing a function signature means finding every place that breaks. `rg -w name` finds all of them, plus comments,
85
+ strings, same-named functions in other modules and unrelated files. On a Kotlin monorepo it returned **60 files for a
86
+ change that broke 3 to 4**. An agent then reads all of them.
87
+
88
+ A code-graph index was the obvious fix. We measured a syntax-level code-graph index on the same Kotlin repo and it did not
89
+ pay off at the agent level: no measurable cost difference with location-only output, and about +45.5% cost when it returned source code (that run also carried MCP connection overhead, so part of the increase is not the graph itself).
90
+ It also needs an index build and a sync after every edit (stale until synced). `moru-narrow` has no build step and no
91
+ staleness, because it runs `rg` live.
92
+
93
+ ## How it works
94
+
95
+ 1. `rg -nw <Name>` collects hits (test files are excluded).
96
+ 2. Each line gets a role: `use`, `import`, `override`, `declaration`, `comment`, `string`, `named-arg`, ...
97
+ 3. A file is **tier 1** when it has a strong role (`use`/`import`/`override`) **and** can see the declaration:
98
+ - Kotlin: the module is reachable through Gradle `project()` edges, and the file is in the same package or imports it.
99
+ - TypeScript: the file imports the declaring file (relative paths and `tsconfig` aliases, through re-exporting
100
+ barrel files), or imports a wrapper that does and calls `X.name`, or is the declaring file itself.
101
+ 4. Everything else is demoted to tier 2/3. **Filters only demote. `--all` always lists everything `rg` found.**
102
+ 5. Multi-line calls are returned as ranges (`19-22`), because compilers report errors at argument lines.
103
+
104
+ ```text
105
+ moru-narrow <Name> [--repo DIR] [--decl FILE] [--all] [--json] [--lang kotlin|ts]
106
+ ```
107
+
108
+ Language is auto-detected (`tsconfig.json`, or a Gradle build).
109
+
110
+ ## How to read the numbers
111
+
112
+ | Term | What it means here |
113
+ | --- | --- |
114
+ | **Truth files** | Production files that stop compiling after we add a required first parameter (`traceId`) to a function. The compiler decides, not us. |
115
+ | **Recall** | Whether the truth files appear in tier 1. The table counts tasks where all of them do (`23/23`); TypeScript rows also give files (`428/431` = 3 truth files were not listed). |
116
+ | **Tier 1** | The files moru-narrow tells you to read first. |
117
+ | **`rg -w` files / lines** | What plain ripgrep returns for the same name. This is the baseline to beat. |
118
+ | **Fresh set** | Candidates drawn after the rules were frozen. Nothing was tuned on them, so they are the fair test. |
119
+ | **Baseline vs. with moru-narrow** (agent tests) | Same coding agent, same task, same prompt. Baseline may use `rg`, `ls` and file reads. The other condition also gets the tool and the sentence "edit from the list, do not re-run rg for the same name". |
120
+ | **Compile pass** | The project compiles after the agent's edit (Kotlin: Gradle, TypeScript: `tsc --noEmit`). TypeScript runs also require that the declaration really gained the parameter, because an agent that changes nothing would otherwise pass. |
121
+ | **Context tokens** | Input tokens the model processed during the whole run, cached ones included. A proxy for how much it read. |
122
+ | **Cost** | API-equivalent price estimate reported by the Claude CLI, not an actual bill. Codex does not report it, shown as n/a. |
123
+ | **`[a, b]` interval** | 95% range from a task-paired bootstrap (10,000 resamples): tasks are resampled and the two conditions are compared on the same task. If the range includes 0, treat the difference as noise. |
124
+ | **tasks x reps** | Number of tasks times repetitions per task. Repetitions of one task are averaged before the comparison. |
125
+
126
+ ## Does it miss files?
127
+
128
+ Ground truth is the **compiler**: add a required first parameter `traceId: String` / `traceId: string` to a function (or
129
+ Kotlin constructor), compile, and take the production files that fail as the truth.
130
+
131
+ | Set | Tasks | `rg -w` files / lines | moru-narrow tier 1 files / lines | Tasks with every truth file in tier 1 |
132
+ | --- | ---: | --- | --- | ---: |
133
+ | Kotlin, 23 selected noisy functions | 23 | 60.4 / 139 | 8.8 / 18 | 23/23 |
134
+ | Kotlin, held-out functions | 120 | 68.3 / 281 | 3.3 / 5 | 120/120 |
135
+ | Kotlin, held-out noisy subset (rg >= 20 files and >= 2x the truth) | 57 | 132.9 / 571 | 3.8 / 7 | 57/57 |
136
+ | Kotlin, constructor changes | 74 | 14.9 / 36 | 8.8 / 17 | 74/74 |
137
+ | TypeScript, **fresh** functions (v3 rules frozen first) | 194 | 3.5 / 7 | 2.5 / 6 | 191/194 (files: 428/431, 99.3%) |
138
+ | TypeScript, fresh, noisy functions (rg >= 5 files) | 28 | 9.5 / 20 | 8.2 / 18 | 28/28 (files: 193/193) |
139
+
140
+ Read these carefully:
141
+
142
+ - Kotlin rules were adjusted after the first held-out misses (two regex bugs), so the Kotlin sets are **not fully
143
+ independent**. The TypeScript fresh set is: rules were frozen (sha256 recorded) before it was scored.
144
+ - TypeScript misses that remain are cases where the failing file **never mentions the name** (the type flows through
145
+ another function). No `rg`-based tool can find those.
146
+ - Line level: error lines inside the recommended ranges were 90% (Kotlin functions), 96% within +-8 lines (Kotlin
147
+ constructors) and 90% (TypeScript functions). Use ranges as hints, files as the guarantee.
148
+ - The TypeScript compiler truth is built with the TypeScript API in memory against one private Next.js repo; Kotlin on
149
+ one private Compose Multiplatform repo. Neither repo is published here.
150
+
151
+ ## Does it save an agent anything?
152
+
153
+ Same task, same prompt template, two conditions, task-paired bootstrap (10,000 resamples, 95% interval). Local logins
154
+ only. Cost is the API-equivalent estimate reported by the CLI.
155
+
156
+ | Setup | Compile pass (baseline -> with moru-narrow) | Context tokens | Cost |
157
+ | --- | --- | --- | --- |
158
+ | Kotlin, Haiku, 23 tasks x 2 | 46/46 -> 46/46 | **-34.5%** [-107k, -37k] | **-24.2%** |
159
+ | Kotlin, Haiku, 57 tasks x 2 | 112/114 -> 114/114 | **-19.2%** [-36k, -18k] | **-16.4%** |
160
+ | Kotlin, Codex terra, 23 x 2 | 44/46 -> 45/46 | **-14.2%** [-22.8%, -6.2%] | n/a |
161
+ | Kotlin, Codex terra, 57 x 2 | 112/114 -> 114/114 | **-9.8%** [-14.6%, -5.3%] | n/a |
162
+ | TypeScript, Haiku, 28 x 2 | 56/56 -> 56/56 | **-22.2%** [-29.4%, -15.3%] | **-11.1%** [-17.3%, -4.3%] |
163
+ | TypeScript, Codex terra, 28 x 2 | 56/56 -> 56/56 | -6.5% [-13.5%, 0.0%] (touches 0) | n/a |
164
+
165
+ **The agent instruction matters as much as the tool.** In the first Kotlin experiment the agent got the same list but
166
+ kept re-running `rg` afterwards, and cost went **up** 10.3% (context +12.8%) on the larger Haiku set. Adding one
167
+ sentence, "edit the call sites from this list without re-running rg for the same name; use --all only if the list looks
168
+ incomplete", changed that: with it, `rg` calls per run fell from 2.4 to 0.02 (Kotlin, Codex, 23 tasks) and from 2.8 to 0.5
169
+ (TypeScript, Haiku), and it produced the table above. That revised prompt was written after seeing the first
170
+ results, so treat the savings as exploratory, and ship the instruction together with the tool.
171
+
172
+ **What we learned the hard way.** The first TypeScript agent run failed 10 more tasks than the baseline (33/44 vs 43/44).
173
+ Every file the compiler flagged was in the list; the miss was calls *inside the declaring file*, which the first
174
+ version hid because a file does not import itself. The offline truth had also excluded the declaring file, so the offline
175
+ check could not see it. Fix: v3 keeps those lines in tier 1. Lesson: validate with the real agent loop, not only offline.
176
+
177
+ ## Limits
178
+
179
+ - Languages: Kotlin + Gradle and TypeScript. Each language needs a small profile (role patterns, how to see a
180
+ declaration). Dynamic languages need a different ground truth.
181
+ - Verified on **one change shape** (adding a required first parameter) and **one repository per language**.
182
+ - React components are out of scope (a positional first parameter does not exist in JSX).
183
+ - Savings depend on the model. Codex on the TypeScript repo showed no clear reduction (-6.5%, interval touches 0). There
184
+ `rg` was already precise (3.5 files on average for functions) and the agent ran only ~3 commands per task; we did not test why.
185
+ - Sample sizes are small (28 to 57 tasks, 2 repetitions per task). In the baseline every run passed in TypeScript, so pass rate cannot show an improvement there, only that nothing broke.
186
+ - The agent tests sent private source to the model providers (Anthropic, OpenAI) through the author's own local CLI logins.
187
+
188
+ ## Reproduce
189
+
190
+ The evaluation harnesses (agent runners, compiler-truth generators, scorers) are not in this repository yet: they carry local paths and private repository details and need a scrub first. `results/` holds the aggregate tables. The method is described above: compiler errors after adding a required first parameter are the ground truth; rules were frozen (hash recorded) before the fresh TypeScript set was scored.
191
+
192
+ ## Versioning
193
+
194
+ Weekly CalVer `YY.WW.N` (scheme `calver-weekly`), for example `26.45.0`: two-digit year, week number, and a release count that starts at `0` each week and counts published packages. Weeks follow the Korean calendar (Asia/Seoul, Monday-start weeks). Git tags use a `v` prefix (`v26.45.0`); the package metadata omits it. Python normalises a zero-padded week, so week 5 appears as `26.5.0` on PyPI. This is a date-based version, not a compatibility signal: while the tool is alpha, read the commit log before upgrading.
195
+
196
+ ## Status
197
+
198
+ Alpha. Not on PyPI or npm. No unit tests yet. The ranking was validated against compiler ground truth (see above); `--install` was only checked by hand in a temporary home directory (fresh install, re-run, `--project`, `--agents-md` idempotence). Issues and results from other repositories are welcome.
199
+
200
+ License: [MIT](LICENSE).
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ moru_narrow/__init__.py
5
+ moru_narrow/__main__.py
6
+ moru_narrow/install.py
7
+ moru_narrow/kotlin.py
8
+ moru_narrow/typescript.py
9
+ moru_narrow.egg-info/PKG-INFO
10
+ moru_narrow.egg-info/SOURCES.txt
11
+ moru_narrow.egg-info/dependency_links.txt
12
+ moru_narrow.egg-info/entry_points.txt
13
+ moru_narrow.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mnarrow = moru_narrow.__main__:main
3
+ moru-narrow = moru_narrow.__main__:main
@@ -0,0 +1 @@
1
+ moru_narrow
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "moru-narrow"
3
+ version = "26.45.0"
4
+ description = "Narrow `rg -w <Name>` to the files and lines you must edit after a signature change (Kotlin+Gradle, TypeScript)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "taehwandev" }]
9
+ keywords = ["ripgrep", "rg", "refactoring", "coding-agent", "kotlin", "typescript", "claude-code", "codex"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Software Development",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/taehwandev/moru-narrow"
21
+ Issues = "https://github.com/taehwandev/moru-narrow/issues"
22
+
23
+ [project.scripts]
24
+ moru-narrow = "moru_narrow.__main__:main"
25
+ mnarrow = "moru_narrow.__main__:main"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ [tool.setuptools]
32
+ packages = ["moru_narrow"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+