euthyna 0.7.0 → 0.9.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.
- package/.agents/skills/euthyna/SKILL.md +18 -1
- package/.agents/skills/euthyna/references/fact-producers.md +57 -1
- package/.agents/skills/euthyna-en/SKILL.md +22 -1
- package/.agents/skills/euthyna-en/references/fact-producers.md +37 -1
- package/.claude/commands/euthyna.md +2 -2
- package/.cursor/commands/euthyna.md +1 -1
- package/.github/prompts/euthyna.prompt.md +1 -1
- package/.opencode/commands/euthyna.md +4 -2
- package/README.md +22 -5
- package/package.json +2 -2
- package/plugin.yaml +1 -1
- package/src/cli.js +125 -37
- package/src/contract.js +6 -1
- package/src/facts/coverage.js +504 -1
- package/src/git.js +16 -0
- package/src/graph.js +73 -0
|
@@ -92,6 +92,23 @@ license: Apache-2.0
|
|
|
92
92
|
**可以做多个阶段,但阶段之间不许互相替代。**
|
|
93
93
|
特别是:**阶段 B 报出的疑似发现,必须逐个走阶段 C 验证**,不许直接写进报告当结论。
|
|
94
94
|
|
|
95
|
+
### 模式词(斜杠命令)
|
|
96
|
+
|
|
97
|
+
用户用斜杠命令进来时,按下表把模式映射到阶段与第一条命令。**词表只在这里维护**,
|
|
98
|
+
各宿主命令文件(Cursor / OpenCode / Copilot / Hermes)指向本节,不各自重抄一份。
|
|
99
|
+
|
|
100
|
+
| 你打 | 走哪条支路 | 第一条命令 |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `/euthyna pr` | 阶段 B 变更面 | `euthyna audit`(省略 `--base` 即按分叉点推断并声明) |
|
|
103
|
+
| `/euthyna graph` | 阶段 B 的来源谱系视图 | `euthyna audit --graph`(同样可省略 `--base`) |
|
|
104
|
+
| `/euthyna repo` | 阶段 A 依赖面 | `euthyna deps --all` |
|
|
105
|
+
| `/euthyna claim <结论>` | 阶段 C 结论面 | `euthyna coverage --symbol <名>` |
|
|
106
|
+
| `/euthyna gate <报告>` | 校验一份已写的报告 | `euthyna gate <报告>` |
|
|
107
|
+
| `/euthyna help` | 只打印这张表 | 不跑命令,不改变状态 |
|
|
108
|
+
|
|
109
|
+
`/euthyna graph` 不是新测量:`--graph` 只是把 `history` 已经给出的归属画成 Mermaid。
|
|
110
|
+
**图上没有第二条边不等于内容没被移过**,边界见 references/fact-producers.md。
|
|
111
|
+
|
|
95
112
|
---
|
|
96
113
|
|
|
97
114
|
## 本技能自带的两个确定性测量
|
|
@@ -106,7 +123,7 @@ license: Apache-2.0
|
|
|
106
123
|
| `coverage` | 某个符号在测试运行中**到底有没有被调用过**? | 报告里要说「测过 / 没测过」时(阶段 B / C) |
|
|
107
124
|
|
|
108
125
|
```powershell
|
|
109
|
-
#
|
|
126
|
+
# --base 可以省略:省略时按 merge-base(HEAD, origin/main) 推断,并在报告里声明为 range-inferred
|
|
110
127
|
node <euthyna 仓库>/bin/euthyna.js audit --base <改前版本> --repo <被审计的仓库>
|
|
111
128
|
|
|
112
129
|
# history / coverage 按需单独跑:
|
|
@@ -72,6 +72,10 @@ node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --no-origins
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`--base` 是**改前版本**(PR 的分叉点),`--head` 默认 `HEAD`。
|
|
75
|
+
省略 `--base` 时按 `merge-base(<被审的 head>, origin/main)` 推断,并在「范围」那一行标出「推断」、在
|
|
76
|
+
「未评估的判据」里加一条 `range-inferred`。推断出的范围**只到分叉点**,看不到更早的历史;
|
|
77
|
+
两个引用都解析不出来时报用法错误,不猜 `HEAD~1`。斜杠命令免参数进来靠的就是这条,所以
|
|
78
|
+
**看到 `range-inferred` 第一步是确认分叉点对不对**,而不是接受它。
|
|
75
79
|
分叉点不确定时用 `git merge-base <目标分支> HEAD` 的结果。
|
|
76
80
|
|
|
77
81
|
### 归属语义(两个固有近似,已确证≠精确)
|
|
@@ -149,13 +153,15 @@ node <euthyna 仓库>/bin/euthyna.js coverage `
|
|
|
149
153
|
`--symbol` 可重复,一次查多个。加 `--file` 可限定到某个文件。Go profile 另需 `--source`
|
|
150
154
|
(见下)。
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
**五种覆盖率格式都读**(自动识别,无需参数):
|
|
153
157
|
|
|
154
158
|
| 格式 | 来源 | 说明 |
|
|
155
159
|
|---|---|---|
|
|
156
160
|
| c8 / v8-to-istanbul `coverage-final.json` | JS 项目(c8 默认落在 `coverage/coverage-final.json`) | 每文件有 `fnMap` + `f`(调用计数数组) |
|
|
161
|
+
| classic istanbul `coverage-final.json` | jest 默认 provider、nyc | 同样的 `fnMap` + `f`,另多一个 `hash` 字段(c8 没有 `hash`),两者走同一条代码路径 |
|
|
157
162
|
| coverage.py JSON(format 3) | Python 项目(`coverage json` 产出) | 顶层 `{meta, files}`;`functions` 里「函数名 → 已执行行列表」,**无调用计数**。从未调用的函数仍在列表中且 `executed_lines` 为空 |
|
|
158
163
|
| Go `-coverprofile`(文本) | Go 项目(`go test -coverprofile` 产出) | `mode:` 头 + 每区块一行 `路径:起始行.列,结束行.列 语句数 命中数`,**无函数名**。必须加 `--source <模块根目录>`:产出器读源码里的顶层 `func` 把行区间映射回函数 |
|
|
164
|
+
| lcov tracefile(文本) | Rust 项目(cargo-llvm-cov / cargo-tarpaulin 产出),gcov / llvm-cov 也产这个格式 | `SF:` 开记录、`end_of_record` 收尾,`FN:<行>,<符号>` 与 `FNDA:<计数>,<符号>` 给出函数名与调用计数(**两段分开出现,不是相邻配对**);**符号名是 mangled 的**,产出器按 Rust v0 mangling 的 `<十进制长度><名字>` 尾段锚定匹配你写的 `模块::函数` 路径,且绝不落在更长的 `<长度><名字>` 对内——查 `get` 不会答到 `cfg_3get` |
|
|
159
165
|
|
|
160
166
|
Go 的产出命令(Go 目标):
|
|
161
167
|
```bash
|
|
@@ -189,6 +195,51 @@ uv run --with coverage python -m coverage json -o <输出路径>.json
|
|
|
189
195
|
覆盖率文件从哪来:项目自己产出就用它。项目不产出覆盖率时,**要么用项目自己的方式跑一次
|
|
190
196
|
测试产出它,要么把该判据标为「不可评估」**。
|
|
191
197
|
|
|
198
|
+
lcov 的产出命令(Rust 目标,本机实测):
|
|
199
|
+
|
|
200
|
+
```powershell
|
|
201
|
+
# 0. llvm-cov / llvm-profdata 不在 PATH 上,先定位 rustc 自带的那一份
|
|
202
|
+
$bin = (rustc --print sysroot) + "\lib\rustlib\x86_64-pc-windows-msvc\bin"
|
|
203
|
+
# 1. 插桩构建 + 跑测试
|
|
204
|
+
$env:RUSTFLAGS = "-C instrument-coverage"
|
|
205
|
+
$env:LLVM_PROFILE_FILE = "<绝对路径>\prof\probe-%p-%m.profraw"
|
|
206
|
+
cargo test
|
|
207
|
+
# 2. 合并 profraw,再从测试二进制导出 lcov
|
|
208
|
+
& "$bin\llvm-profdata.exe" merge -sparse <绝对路径>\prof\*.profraw -o <绝对路径>\probe.profdata
|
|
209
|
+
& "$bin\llvm-cov.exe" export --format=lcov --instr-profile=<绝对路径>\probe.profdata `
|
|
210
|
+
"<target\debug\deps\<crate>-<hash>.exe>" --sources src > <绝对路径>\lcov.info
|
|
211
|
+
# 3. 查询(lcov 不需要 --source:函数名就在 FN: 行里)
|
|
212
|
+
node <euthyna 仓库>/bin/euthyna.js coverage `
|
|
213
|
+
--coverage <绝对路径>\lcov.info --symbol <模块::函数> --file <绝对路径>\src\lib.rs
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
> lcov 专属注意事项(在探针 crate 上实测):
|
|
217
|
+
> 1. **符号名是 mangled 的,本产出器不做 demangle**(本机没有可用的 demangler:
|
|
218
|
+
> `llvm-cov --Xdemangler` 试过 `rust` / `rustfilt` / `llvm-cxxfilt` / `c++filt`
|
|
219
|
+
> 四个名字全部报 `could not find the demangler`)。匹配靠 **Rust v0 mangling**
|
|
220
|
+
> (`-C instrument-coverage` 蕴含的版本)的 `<十进制长度><名字>` 尾段编码,
|
|
221
|
+
> 所以查询写 `模块::函数`(`split('::')` 后逐段匹配)。每段的名字必须从段边界起算——
|
|
222
|
+
> 名字开头、mangling 自己的 `_` 分隔符之后、或已证明的 pair 边界——且绝不落在更长的
|
|
223
|
+
> pair 内部:查 `get` 不会答到 `cfg_3get`。带限定的查询是子序列判定而非路径精确判定。
|
|
224
|
+
> `nested::dup` 只命中 `6nested3dup`,裸 `dup` 会同时命中外层模块里的同名函数——
|
|
225
|
+
> 每个命中各自报一条事实并带上自己的 mangled 名。已经 mangled 的查询(`_R`/`_Z` 开头)
|
|
226
|
+
> 只做精确匹配,**绝不半解码**。
|
|
227
|
+
> 2. **同一个文件可能有多条 `FN:` 指向同一行**(泛型单态化、闭包),这不是重复记录。
|
|
228
|
+
> 一个泛型函数实例化两次就是两条事实,各自带自己的 mangled 名。
|
|
229
|
+
> 3. `FN:` 行存在而 `FNDA:` 缺失时报「未评估」,**不默认成 0**——凭空造出「从未调用」
|
|
230
|
+
> 是这个产出器最不能犯的方向。
|
|
231
|
+
> 4. 同一 `SF:` 出现多条记录(或同名多条 `FNDA:`)时**计数求和**;求和不会把非零变零,
|
|
232
|
+
> 对唯一可行动的结论(计数为 0)安全。
|
|
233
|
+
> 5. 文件不在 tracefile 里报**「未评估」**(llvm-cov / gcov 只为被插桩的目标产记录,
|
|
234
|
+
> `--sources` 还会再过滤一层)——这与 c8 的「文件不在数据里 = 从未执行」刻意相反。
|
|
235
|
+
> 6. 认较新的 `FNL:`/`FNA:` 编码时会**明确拒绝**而不是按猜的形状读;`SF:` 路径是绝对
|
|
236
|
+
> 路径(Windows 上是带反斜杠的)。
|
|
237
|
+
> 7. 规范允许的三种写法照读而不是拒掉:**注释记录**(顶格 `#`)可以在任何位置,而
|
|
238
|
+
> `geninfo --comment` **默认**在顶部写一条;**`KF:` 是 `SF:` 的同义词**;
|
|
239
|
+
> **`FN:<起>,<止>,<名>` 是 gcc 9+ 的形状**,止行丢弃但必须消费掉——否则 `14,` 会粘进
|
|
240
|
+
> 符号名,把一个函数拆成「有行号无计数」和「有计数无行号」两条记录,前者随即误报未评估。
|
|
241
|
+
> 缩进的 `#` 仍被拒绝。
|
|
242
|
+
|
|
192
243
|
### 输出长什么样(真实捕获,一次查询命中两种结果)
|
|
193
244
|
|
|
194
245
|
```
|
|
@@ -291,6 +342,11 @@ node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审
|
|
|
291
342
|
- `history` 的归属**默认追「最初引入」**(`git log -S`,有探针上限,只对 ≥ 12 字符的行做追溯);
|
|
292
343
|
要退回 blame 的「最后修改」语义用 `--no-origins`(更快,输出会写明)。两个近似都写在本文件「归属语义」一节。
|
|
293
344
|
- `history` 只覆盖**被删除的行**。新增的代码它不管(`--pickaxe` 除外)。
|
|
345
|
+
- `--graph` 只把上面这些归属**画出来**(Mermaid 源码,不是图片):范围 → 最后修改者 → 最初引入者。
|
|
346
|
+
它不新增任何测量,也不画严重性——图上只有已测到的状态与行数。这也是契约,不是漏写。
|
|
347
|
+
- 图上**没有第二条边不等于内容没被移过**。追溯结果被丢下有两条路:短行(`}`、`return x;`)与
|
|
348
|
+
`--no-origins` 会写成虚线的 `history-origins`;而「来源提交本身没被判为安全相关」是**静默**丢弃,
|
|
349
|
+
图上没有那条边,也没有任何说明。看图谱时把「没有边」当成中性事实,不是清白。
|
|
294
350
|
- `coverage` **只能证伪**。见上文。
|
|
295
351
|
- `coverage` 依赖调用计数,**不处理动态派发、反射、字符串调用**。
|
|
296
352
|
计数为 0 的结论在存在这些机制时要打折扣。
|
|
@@ -102,6 +102,26 @@ facts from the producers decide where the manual reading must be directed.
|
|
|
102
102
|
In particular: every suspected finding reported by Stage B must go through Stage C one by
|
|
103
103
|
one — never write it straight into the report as a conclusion.
|
|
104
104
|
|
|
105
|
+
### Modes (slash commands)
|
|
106
|
+
|
|
107
|
+
When a user arrives through a slash command, map the mode to the stage and the first
|
|
108
|
+
command using this table. **The table is maintained only here**; the host command files
|
|
109
|
+
(Cursor / OpenCode / Copilot / Hermes) point at this section instead of each keeping their
|
|
110
|
+
own copy.
|
|
111
|
+
|
|
112
|
+
| You type | Which branch | First command |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `/euthyna pr` | Stage B, the change surface | `euthyna audit` (omit `--base` and the fork point is inferred and declared) |
|
|
115
|
+
| `/euthyna graph` | Stage B, the provenance view | `euthyna audit --graph` (`--base` may be omitted here too) |
|
|
116
|
+
| `/euthyna repo` | Stage A, the dependency surface | `euthyna deps --all` |
|
|
117
|
+
| `/euthyna claim <assertion>` | Stage C, the claim surface | `euthyna coverage --symbol <name>` |
|
|
118
|
+
| `/euthyna gate <report>` | Validating a written report | `euthyna gate <report>` |
|
|
119
|
+
| `/euthyna help` | Print this table only | run nothing, change no state |
|
|
120
|
+
|
|
121
|
+
`/euthyna graph` is not a new measurement: `--graph` only draws the attribution `history`
|
|
122
|
+
already emits, as Mermaid. **A missing second edge does not mean the content never moved** —
|
|
123
|
+
the boundaries are in references/fact-producers.md.
|
|
124
|
+
|
|
105
125
|
---
|
|
106
126
|
|
|
107
127
|
## The two deterministic measurements this skill ships
|
|
@@ -117,7 +137,8 @@ reliably — are shipped by this skill as a zero-dependency CLI:
|
|
|
117
137
|
| `coverage` | Was this symbol **ever actually invoked** by a test run? | when the report claims "tested / not tested" (Stage B / C) |
|
|
118
138
|
|
|
119
139
|
```powershell
|
|
120
|
-
#
|
|
140
|
+
# --base may be omitted: then the fork point is inferred as merge-base(HEAD, origin/main) and
|
|
141
|
+
# declared as range-inferred in the report. When given, it is used exactly as given:
|
|
121
142
|
node <euthyna repo>/bin/euthyna.js audit --base <pre-change revision> --repo <audited repo>
|
|
122
143
|
|
|
123
144
|
# history / coverage run alone when needed:
|
|
@@ -72,6 +72,11 @@ node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --no-ori
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`--base` is the **pre-change revision** (the PR's fork point); `--head` defaults to `HEAD`.
|
|
75
|
+
Omitting `--base` infers it as `merge-base(<the audited head>, origin/main)`, marks the range line as inferred
|
|
76
|
+
and adds a `range-inferred` entry to the criteria not evaluated. An inferred range reaches **the
|
|
77
|
+
fork point only** — nothing earlier; when neither ref resolves the run stops with a usage error
|
|
78
|
+
instead of guessing `HEAD~1`. This is what lets a slash command arrive with no arguments, so
|
|
79
|
+
**the first thing to do with `range-inferred` is check the fork point**, not accept it.
|
|
75
80
|
When the fork point is uncertain, use the result of `git merge-base <target branch> HEAD`.
|
|
76
81
|
|
|
77
82
|
### Attribution semantics (two inherent approximations; established ≠ exact)
|
|
@@ -137,13 +142,15 @@ node <euthyna repo>/bin/euthyna.js coverage `
|
|
|
137
142
|
`--symbol` is repeatable — query several at once. Add `--file` to restrict to a single file.
|
|
138
143
|
Go profiles additionally need `--source` (see below).
|
|
139
144
|
|
|
140
|
-
**
|
|
145
|
+
**Five coverage formats are read** (auto-detected, no parameters needed):
|
|
141
146
|
|
|
142
147
|
| Format | Source | Notes |
|
|
143
148
|
|---|---|---|
|
|
144
149
|
| c8 / v8-to-istanbul `coverage-final.json` | JS projects (c8 lands at `coverage/coverage-final.json` by default) | Per-file `fnMap` + `f` (invocation-count array) |
|
|
150
|
+
| classic istanbul `coverage-final.json` | jest's default provider, nyc | The same `fnMap` + `f` plus a `hash` field (c8 has no `hash`); both resolve through the same code path |
|
|
145
151
|
| coverage.py JSON (format 3) | Python projects (produced by `coverage json`) | Top-level `{meta, files}`; in `functions`, "function name → list of executed lines", **no invocation counts**. Functions never called are still in the list with empty `executed_lines` |
|
|
146
152
|
| Go `-coverprofile` (text) | Go projects (produced by `go test -coverprofile`) | A `mode:` header plus one line per block (`path:startL.startC,endL.endC stmts count`), **no function names**. `--source <module root>` is required: the producer reads top-level `func` declarations in the source to map line ranges back to functions |
|
|
153
|
+
| lcov tracefile (text) | Rust projects (produced by cargo-llvm-cov / cargo-tarpaulin); gcov / llvm-cov produce the format too | `SF:` opens a record, `end_of_record` closes it; `FN:<line>,<symbol>` and `FNDA:<count>,<symbol>` give a function name and its invocation count (**two separate runs, not adjacent pairs**); **the symbol names are mangled**, and the producer anchors the `module::function` path you write against the `<decimal length><name>` tail segments of Rust v0 mangling, never inside a longer `<length><name>` pair — a query for `get` does not answer for `cfg_3get` |
|
|
147
154
|
|
|
148
155
|
The Go production commands (Go targets):
|
|
149
156
|
|
|
@@ -180,6 +187,33 @@ uv run --with coverage python -m coverage json -o <output path>.json
|
|
|
180
187
|
|
|
181
188
|
Where the coverage file comes from: if the project produces one, use it. When the project does not, **either run its tests once with the project's own tooling to produce it, or mark the criterion as "not evaluable"**.
|
|
182
189
|
|
|
190
|
+
The lcov production commands (Rust targets, measured on this machine):
|
|
191
|
+
|
|
192
|
+
```powershell
|
|
193
|
+
# 0. llvm-cov / llvm-profdata are not on PATH; locate rustc's own copies first
|
|
194
|
+
$bin = (rustc --print sysroot) + "\lib\rustlib\x86_64-pc-windows-msvc\bin"
|
|
195
|
+
# 1. instrumented build + run the tests
|
|
196
|
+
$env:RUSTFLAGS = "-C instrument-coverage"
|
|
197
|
+
$env:LLVM_PROFILE_FILE = "<absolute path>\prof\probe-%p-%m.profraw"
|
|
198
|
+
cargo test
|
|
199
|
+
# 2. merge the profraw files, then export lcov from the test binary
|
|
200
|
+
& "$bin\llvm-profdata.exe" merge -sparse <absolute path>\prof\*.profraw -o <absolute path>\probe.profdata
|
|
201
|
+
& "$bin\llvm-cov.exe" export --format=lcov --instr-profile=<absolute path>\probe.profdata `
|
|
202
|
+
"<target\debug\deps\<crate>-<hash>.exe>" --sources src > <absolute path>\lcov.info
|
|
203
|
+
# 3. query (lcov needs no --source: the function names are in the FN: lines)
|
|
204
|
+
node <euthyna repo>/bin/euthyna.js coverage `
|
|
205
|
+
--coverage <absolute path>\lcov.info --symbol <module::function> --file <absolute path>\src\lib.rs
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> lcov-specific caveats (measured on a probe crate):
|
|
209
|
+
> 1. **The symbol names are mangled and this producer does not demangle them** (no usable demangler on this machine: `llvm-cov --Xdemangler` was tried with `rust` / `rustfilt` / `llvm-cxxfilt` / `c++filt`, and all four report `could not find the demangler`). Matching relies on the `<decimal length><name>` tail segments of **Rust v0 mangling** (what `-C instrument-coverage` implies), so write the query as `module::function` (split on `::`, matched per segment). Each segment's name must start at a boundary — the start of the name, right after the mangling's own `_` separator, or at a proven pair boundary — and never strictly inside a longer pair: a query for `get` does not answer for `cfg_3get`. A qualified query is a subsequence test, not a path-exact one. `nested::dup` matches only `6nested3dup`, while a bare `dup` also matches the same-named function in an outer module — each hit is reported as its own fact carrying its own mangled name. An already-mangled query (starting with `_R`/`_Z`) is matched exactly and **never half-decoded**.
|
|
210
|
+
> 2. **One file may carry several `FN:` lines pointing at the same line** (generic monomorphisation, closures). That is not a duplicate record. A generic function instantiated twice yields two facts, each carrying its own mangled name.
|
|
211
|
+
> 3. When an `FN:` line exists and its `FNDA:` is missing, the result is "not evaluated", **never a default of 0** — inventing a "never invoked" out of thin air is the one direction this producer must not fail in.
|
|
212
|
+
> 4. When one `SF:` appears in several records (or several same-named `FNDA:` lines exist), the counts are **summed**; summing can never turn a non-zero into zero, which is what keeps the only actionable conclusion (a count of 0) safe.
|
|
213
|
+
> 5. A file absent from the tracefile is reported as **"not evaluated"** (llvm-cov / gcov only emit records for instrumented targets, and `--sources` filters further) — deliberately the opposite of c8's "file absent from the data = never executed".
|
|
214
|
+
> 6. The newer `FNL:`/`FNA:` encoding is **explicitly refused** rather than read out of a guessed shape; `SF:` paths are absolute (backslash-separated on Windows).
|
|
215
|
+
> 7. Three spellings the specification allows are read rather than refused: **comment records** (`#` in column 0) may appear anywhere, and `geninfo --comment` writes one at the top by default; **`KF:` is a synonym for `SF:`**; and **`FN:<start>,<end>,<name>`** is the shape gcc 9+ emits — the end line is discarded but must be *consumed*, or `14,` sticks to the symbol name and splits one function into a record with a line and no count plus one with a count and no line, the first of which then reports a spurious not-evaluated. An indented `#` is still refused.
|
|
216
|
+
|
|
183
217
|
### What the output looks like (real capture; one query hitting both result kinds)
|
|
184
218
|
|
|
185
219
|
```
|
|
@@ -278,6 +312,8 @@ that a conclusion without evidence is not handed over wearing the face of "estab
|
|
|
278
312
|
- `history`'s classifier is **biased wide**: missing a security commit is more dangerous than over-reporting, so it would rather report `fix` too. The classification is a **lead** — read the commit message yourself.
|
|
279
313
|
- `history`'s attribution **traces the "original introducer" by default** (`git log -S`, probe-capped, only lines ≥ 12 characters); to fall back to blame's "last modified" semantics use `--no-origins` (faster; the output states this). Both approximations are documented in this file's "Attribution semantics" section.
|
|
280
314
|
- `history` only covers **deleted lines**. It does not handle newly added code (except with `--pickaxe`).
|
|
315
|
+
- `--graph` only **draws** the attributions above (Mermaid source, not an image): the range → the last modifier → the commit that first introduced the content. It measures nothing new and draws no severity — a graph carries measured states and line counts, and that is the contract too, not an omission.
|
|
316
|
+
- **The absence of the second edge does not mean the content never moved.** A traced origin is dropped two ways: a generic short line (`}`, `return x;`) or `--no-origins` is written as the dotted `history-origins` edge, while an origin commit the classifier does not read as security-relevant is dropped **silently** — no edge and no note. Read a missing edge as a neutral fact, not as a clearance.
|
|
281
317
|
- `coverage` **can only falsify**. See above.
|
|
282
318
|
- `coverage` relies on invocation counts and **does not handle dynamic dispatch, reflection, or string-based calls**. Conclusions of count 0 must be discounted when such mechanisms exist.
|
|
283
319
|
- The two halves of `audit` **do not read the same thing**: `history` takes the commits named by `--base`/`--head`, while `deps` takes the manifest **currently checked out at the root of the repository** `--repo` points into (manifests are located from the root only). So the criterion is **content**: when the manifest measured differs from the one at `--head`, or does not exist in that commit at all (a gitignored lockfile is exactly that), *Criteria not evaluated* gains an entry naming the file and `--head`. The `code` (`manifest-not-head` / `manifest-unverified`) appears only on the `--json` channel. **Seeing one means checking out an end of the range and re-running** — the declaration keeps the trap from being silent; it does not measure the fact for you.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: 代码安全审计:按 euthyna 纪律审一段代码、一次变更或一条漏洞结论,产出带证据的报告
|
|
3
|
-
argument-hint: "
|
|
3
|
+
argument-hint: "<模式词 或 路径 / diff 范围>"
|
|
4
4
|
allowed-tools: Read, Grep, Glob, Bash, Task
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ allowed-tools: Read, Grep, Glob, Bash, Task
|
|
|
11
11
|
按 euthyna 技能执行一次完整审计:
|
|
12
12
|
|
|
13
13
|
1. **先加载纪律**:找到 euthyna 技能目录(`~/.claude/skills/euthyna/`、项目 `.claude/skills/euthyna/` 或 `.agents/skills/euthyna/`),读 `SKILL.md`,需要时再读 `references/`。找不到就明说「技能缺失」,不要凭印象执行。
|
|
14
|
-
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B
|
|
14
|
+
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B 为主。**模式词优先于下面全部步骤**:参数命中 `SKILL.md`「模式词(斜杠命令)」表里的模式时,只执行该模式映射的那一条命令并按其结果收口——`help` 只打印那张表,不跑任何命令、不做审计、不写报告;未命中模式词时,才走下面的通用审计步骤,其余文字按审计目标处理。
|
|
15
15
|
3. **先测量再读码**:diff 有删除行先跑 `history`;谈测试覆盖先跑 `coverage`;依赖声明先跑 `deps`(操作手册在 `references/fact-producers.md`)。
|
|
16
16
|
4. **每条结论过六道门禁**:证据到 `文件:行号`、可复现命令、影响说明;拿不出证据的降级为「观察」。
|
|
17
17
|
5. **三态裁定**:`BUG #N TRUE POSITIVE / FALSE POSITIVE / INCONCLUSIVE`。
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
按 euthyna 技能执行一次完整审计:
|
|
6
6
|
|
|
7
7
|
1. **先加载纪律**:找到 euthyna 技能目录(`~/.cursor/skills/euthyna/`、项目 `.cursor/skills/euthyna/`,或 npm 全局包内的 `.agents/skills/euthyna/`),读 `SKILL.md`,需要时再读 `references/`。找不到就明说「技能缺失」,不要凭印象执行。
|
|
8
|
-
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B
|
|
8
|
+
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B 为主。**模式词优先于下面全部步骤**:参数命中 `SKILL.md`「模式词(斜杠命令)」表里的模式时,只执行该模式映射的那一条命令并按其结果收口——`help` 只打印那张表,不跑任何命令、不做审计、不写报告;未命中模式词时,才走下面的通用审计步骤,其余文字按审计目标处理。
|
|
9
9
|
3. **先测量再读码**:第一条命令跑 `euthyna audit --base <分叉点> --repo <被审计仓库>`(history + deps 一次跑完);谈测试覆盖再跑 `coverage`(操作手册在 `references/fact-producers.md`)。
|
|
10
10
|
4. **每条结论过六道门禁**:证据到 `文件:行号`、可复现命令、影响说明;拿不出证据的降级为「观察」。
|
|
11
11
|
5. **三态裁定**:`BUG #N TRUE POSITIVE / FALSE POSITIVE / INCONCLUSIVE`。
|
|
@@ -7,7 +7,7 @@ description: 代码安全审计:按 euthyna 纪律审一段代码、一次变
|
|
|
7
7
|
按 euthyna 技能执行一次完整审计:
|
|
8
8
|
|
|
9
9
|
1. **先加载纪律**:找到 euthyna 技能目录(项目 `.github/skills/euthyna/` 或 `.agents/skills/euthyna/`,个人 `~/.copilot/skills/euthyna/`),读 `SKILL.md`,需要时再读 `references/`。找不到就明说「技能缺失」,不要凭印象执行。
|
|
10
|
-
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B
|
|
10
|
+
2. **按入口决策树走**:单条漏洞结论 → 阶段 C;一次变更 → 阶段 B;依赖面 → 阶段 A;整体审计 → 阶段 B 为主。**模式词优先于下面全部步骤**:参数命中 `SKILL.md`「模式词(斜杠命令)」表里的模式时,只执行该模式映射的那一条命令并按其结果收口——`help` 只打印那张表,不跑任何命令、不做审计、不写报告;未命中模式词时,才走下面的通用审计步骤,其余文字按审计目标处理。
|
|
11
11
|
3. **先测量再读码**:第一条命令跑 `euthyna audit --base <分叉点> --repo <被审计仓库>`(history + deps 一次跑完);谈测试覆盖再跑 `coverage`(操作手册在 `references/fact-producers.md`)。
|
|
12
12
|
4. **每条结论过六道门禁**:证据到 `文件:行号`、可复现命令、影响说明;拿不出证据的降级为「观察」。复现命令执行前须向用户报备(见技能「执行纪律」节)。
|
|
13
13
|
5. **三态裁定**:`BUG #N TRUE POSITIVE / FALSE POSITIVE / INCONCLUSIVE`。
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Run the euthyna code security audit CLI
|
|
3
3
|
---
|
|
4
|
-
|
|
4
|
+
If the arguments match a mode word in the euthyna skill’s "Modes (slash commands)" table,
|
|
5
|
+
translate them per that table and run the mapped command; the table is maintained in the
|
|
6
|
+
skill, not here. Otherwise run the CLI with the arguments as given:
|
|
5
7
|
|
|
6
8
|
euthyna $ARGUMENTS
|
|
7
9
|
|
|
8
|
-
If `euthyna` is not installed,
|
|
10
|
+
If `euthyna` is not installed, run the **same mapped arguments** through `npx -y euthyna …` — a mode word resolves through the table first, so the fallback never executes `euthyna <mode>` literally, and `help` still runs nothing. Quote the command's output verbatim, exit code included — never estimate or re-derive numbers. If the euthyna skill is installed (`.agents/skills/euthyna/` or `~/.agents/skills/euthyna/`), read its SKILL.md and interpret the output under the six-gate adjudication format; claims without evidence are downgraded to observations.
|
package/README.md
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/euthyna"><img src="https://img.shields.io/npm/dm/euthyna?style=flat&color=007EC6" alt="npm downloads"></a>
|
|
15
15
|
<a href="https://www.npmjs.com/package/euthyna"><img src="https://img.shields.io/npm/l/euthyna?style=flat&color=007EC6" alt="license: Apache-2.0"></a>
|
|
16
16
|
<a href="https://search.sigstore.dev/?logIndex=2915999059"><img src="https://img.shields.io/badge/npm%20provenance-verified-007EC6?style=flat" alt="npm provenance verified"></a>
|
|
17
|
-
<a href="https://www.skills.sh/slow-stack/euthyna"><img src="https://skills.sh/b/slow-stack/euthyna.svg" alt="Agent skills installs"></a>
|
|
18
17
|
<a href="https://awesome-dsh-plugin.com/p/slow-stack/euthyna/"><img src="https://img.shields.io/badge/DSH%20plugin-listed-blue" alt="Listed in the awesome-dsh-plugin catalog"></a>
|
|
19
18
|
</p>
|
|
20
19
|
|
|
@@ -181,6 +180,7 @@ replying in chat.
|
|
|
181
180
|
```sh
|
|
182
181
|
npm install -g euthyna
|
|
183
182
|
euthyna audit --repo <path> --base main --head HEAD # history + deps in one run
|
|
183
|
+
euthyna audit --graph # the same attribution, drawn (see below)
|
|
184
184
|
euthyna coverage --coverage coverage/coverage-final.json --symbol <name>
|
|
185
185
|
euthyna deps --repo <path> --all # or --dep <name> for a single dep
|
|
186
186
|
euthyna gate <adjudication-report.md> --verify --cwd <repo> # mechanically check the six gates
|
|
@@ -213,6 +213,15 @@ removed and are now being added back. Origin tracing is on by default (deleted l
|
|
|
213
213
|
are attributed to the commit that first introduced their content); `--no-origins`
|
|
214
214
|
falls back to blame's last-modifier attribution, which is faster.
|
|
215
215
|
|
|
216
|
+
`--graph` on `audit` or `history` draws what `history` already computed — the range, the commit
|
|
217
|
+
that last modified each deleted line, and the commit that first introduced its content — as
|
|
218
|
+
Mermaid source, renderable straight into a GitHub comment. It measures nothing new and never
|
|
219
|
+
colours a node by risk; a missing second edge is a neutral fact, not a clearance. `--base` may
|
|
220
|
+
be left out entirely: the fork point is then inferred as `merge-base(<the audited head>, origin/main)` and
|
|
221
|
+
said three times over — on the range line, among the criteria not evaluated (`range-inferred`),
|
|
222
|
+
and as a `code` on the `--json` channel — because an inferred range reaches the fork point only,
|
|
223
|
+
and a quietly smaller history is the one thing this output must not be able to imply.
|
|
224
|
+
|
|
216
225
|
### Exit codes are part of the contract
|
|
217
226
|
|
|
218
227
|
Surveying eight measurement plugins in this ecosystem found **none of them publishes a
|
|
@@ -254,10 +263,18 @@ This project tries to be explicit about the difference. Current state:
|
|
|
254
263
|
unchanged on a Go repository ([nektos/act](https://github.com/nektos/act), where the deleted
|
|
255
264
|
lines of a security fix were traced back to the commit that introduced them) and on a Rust one
|
|
256
265
|
([rustls](https://github.com/rustls/rustls), whose fix range yielded 43 provenance facts).
|
|
257
|
-
- **`coverage` on real output in
|
|
258
|
-
fnMap/f shape)
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
- **`coverage` on real output in five formats** — c8/V8 JSON, classic istanbul (jest/nyc, same
|
|
267
|
+
fnMap/f shape), coverage.py JSON (format 3), Go's text `-coverprofile`, and lcov tracefiles
|
|
268
|
+
(`llvm-cov export --format=lcov`, which is how cargo-llvm-cov and cargo-tarpaulin emit Rust
|
|
269
|
+
coverage) — distinguishing all three
|
|
270
|
+
states correctly, and refusing anything that is not a recognizable coverage report instead of
|
|
271
|
+
answering "symbol not located" against it. The lcov path was measured against a probe crate
|
|
272
|
+
built with `-C instrument-coverage`: an uncalled function reports a count of 0, a called one
|
|
273
|
+
reports a count above it, and inlining was measured **not** to flatten a called function to 0.
|
|
274
|
+
What it cannot do is demangle — this machine has no working demangler, so a query is matched
|
|
275
|
+
against the `<byte length><name>` tail segments of the mangled name; a bare `dup` matches
|
|
276
|
+
every same-named function in the file, each reported separately, and a query never answers for
|
|
277
|
+
a function that merely contains the queried name (`get` does not match `cfg_3get`).
|
|
261
278
|
- **`deps` against real manifests** — resolved versions reported with a line-level evidence
|
|
262
279
|
pointer into the lockfile, and absent dependencies reported as established absences rather
|
|
263
280
|
than silent skips. Verified against a populated npm v3 lockfile, a real `go.mod` (the *declared*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euthyna",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Deterministic fact producers for security auditing: which deleted code came from a security fix, and which changed symbols no test ever executed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./plugin/index.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@deepseek-ai/dsh-skill-filesystem": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0"
|
|
60
|
+
"@deepseek-ai/dsh-skill-filesystem": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0 || >=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@deepseek-ai/dsh-skill-filesystem": {
|
package/plugin.yaml
CHANGED