Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .agents/skills/euthyna-en/references/fact-producers.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,15 @@ node <euthyna repo>/bin/euthyna.js coverage `
`--symbol` is repeatable — query several at once. Add `--file` to restrict to a single file.
Go profiles additionally need `--source` (see below).

**Three coverage formats are read** (auto-detected, no parameters needed):
**Five coverage formats are read** (auto-detected, no parameters needed):

| Format | Source | Notes |
|---|---|---|
| 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) |
| 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 |
| 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` |
| 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 |
| 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` |

The Go production commands (Go targets):

Expand Down Expand Up @@ -180,6 +182,32 @@ uv run --with coverage python -m coverage json -o <output path>.json

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"**.

The lcov production commands (Rust targets, measured on this machine):

```powershell
# 0. llvm-cov / llvm-profdata are not on PATH; locate rustc's own copies first
$bin = (rustc --print sysroot) + "\lib\rustlib\x86_64-pc-windows-msvc\bin"
# 1. instrumented build + run the tests
$env:RUSTFLAGS = "-C instrument-coverage"
$env:LLVM_PROFILE_FILE = "<absolute path>\prof\probe-%p-%m.profraw"
cargo test
# 2. merge the profraw files, then export lcov from the test binary
& "$bin\llvm-profdata.exe" merge -sparse <absolute path>\prof\*.profraw -o <absolute path>\probe.profdata
& "$bin\llvm-cov.exe" export --format=lcov --instr-profile=<absolute path>\probe.profdata `
"<target\debug\deps\<crate>-<hash>.exe>" --sources src > <absolute path>\lcov.info
# 3. query (lcov needs no --source: the function names are in the FN: lines)
node <euthyna repo>/bin/euthyna.js coverage `
--coverage <absolute path>\lcov.info --symbol <module::function> --file <absolute path>\src\lib.rs
```

> lcov-specific caveats (measured on a probe crate):
> 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**.
> 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.
> 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.
> 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.
> 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".
> 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).

### What the output looks like (real capture; one query hitting both result kinds)

```
Expand Down
44 changes: 43 additions & 1 deletion .agents/skills/euthyna/references/fact-producers.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,15 @@ node <euthyna 仓库>/bin/euthyna.js coverage `
`--symbol` 可重复,一次查多个。加 `--file` 可限定到某个文件。Go profile 另需 `--source`
(见下)。

**三种覆盖率格式都读**(自动识别,无需参数):
**五种覆盖率格式都读**(自动识别,无需参数):

| 格式 | 来源 | 说明 |
|---|---|---|
| c8 / v8-to-istanbul `coverage-final.json` | JS 项目(c8 默认落在 `coverage/coverage-final.json`) | 每文件有 `fnMap` + `f`(调用计数数组) |
| classic istanbul `coverage-final.json` | jest 默认 provider、nyc | 同样的 `fnMap` + `f`,另多一个 `hash` 字段(c8 没有 `hash`),两者走同一条代码路径 |
| coverage.py JSON(format 3) | Python 项目(`coverage json` 产出) | 顶层 `{meta, files}`;`functions` 里「函数名 → 已执行行列表」,**无调用计数**。从未调用的函数仍在列表中且 `executed_lines` 为空 |
| Go `-coverprofile`(文本) | Go 项目(`go test -coverprofile` 产出) | `mode:` 头 + 每区块一行 `路径:起始行.列,结束行.列 语句数 命中数`,**无函数名**。必须加 `--source <模块根目录>`:产出器读源码里的顶层 `func` 把行区间映射回函数 |
| lcov tracefile(文本) | Rust 项目(cargo-llvm-cov / cargo-tarpaulin 产出),gcov / llvm-cov 也产这个格式 | `SF:` 开记录、`end_of_record` 收尾,`FN:<行>,<符号>` 与 `FNDA:<计数>,<符号>` 给出函数名与调用计数(**两段分开出现,不是相邻配对**);**符号名是 mangled 的**,产出器按 Rust v0 mangling 的 `<十进制长度><名字>` 尾段锚定匹配你写的 `模块::函数` 路径,且绝不落在更长的 `<长度><名字>` 对内——查 `get` 不会答到 `cfg_3get` |

Go 的产出命令(Go 目标):
```bash
Expand Down Expand Up @@ -189,6 +191,46 @@ uv run --with coverage python -m coverage json -o <输出路径>.json
覆盖率文件从哪来:项目自己产出就用它。项目不产出覆盖率时,**要么用项目自己的方式跑一次
测试产出它,要么把该判据标为「不可评估」**。

lcov 的产出命令(Rust 目标,本机实测):

```powershell
# 0. llvm-cov / llvm-profdata 不在 PATH 上,先定位 rustc 自带的那一份
$bin = (rustc --print sysroot) + "\lib\rustlib\x86_64-pc-windows-msvc\bin"
# 1. 插桩构建 + 跑测试
$env:RUSTFLAGS = "-C instrument-coverage"
$env:LLVM_PROFILE_FILE = "<绝对路径>\prof\probe-%p-%m.profraw"
cargo test
# 2. 合并 profraw,再从测试二进制导出 lcov
& "$bin\llvm-profdata.exe" merge -sparse <绝对路径>\prof\*.profraw -o <绝对路径>\probe.profdata
& "$bin\llvm-cov.exe" export --format=lcov --instr-profile=<绝对路径>\probe.profdata `
"<target\debug\deps\<crate>-<hash>.exe>" --sources src > <绝对路径>\lcov.info
# 3. 查询(lcov 不需要 --source:函数名就在 FN: 行里)
node <euthyna 仓库>/bin/euthyna.js coverage `
--coverage <绝对路径>\lcov.info --symbol <模块::函数> --file <绝对路径>\src\lib.rs
```

> lcov 专属注意事项(在探针 crate 上实测):
> 1. **符号名是 mangled 的,本产出器不做 demangle**(本机没有可用的 demangler:
> `llvm-cov --Xdemangler` 试过 `rust` / `rustfilt` / `llvm-cxxfilt` / `c++filt`
> 四个名字全部报 `could not find the demangler`)。匹配靠 **Rust v0 mangling**
> (`-C instrument-coverage` 蕴含的版本)的 `<十进制长度><名字>` 尾段编码,
> 所以查询写 `模块::函数`(`split('::')` 后逐段匹配)。每段的名字必须从段边界起算——
> 名字开头、mangling 自己的 `_` 分隔符之后、或已证明的 pair 边界——且绝不落在更长的
> pair 内部:查 `get` 不会答到 `cfg_3get`。带限定的查询是子序列判定而非路径精确判定。
> `nested::dup` 只命中 `6nested3dup`,裸 `dup` 会同时命中外层模块里的同名函数——
> 每个命中各自报一条事实并带上自己的 mangled 名。已经 mangled 的查询(`_R`/`_Z` 开头)
> 只做精确匹配,**绝不半解码**。
> 2. **同一个文件可能有多条 `FN:` 指向同一行**(泛型单态化、闭包),这不是重复记录。
> 一个泛型函数实例化两次就是两条事实,各自带自己的 mangled 名。
> 3. `FN:` 行存在而 `FNDA:` 缺失时报「未评估」,**不默认成 0**——凭空造出「从未调用」
> 是这个产出器最不能犯的方向。
> 4. 同一 `SF:` 出现多条记录(或同名多条 `FNDA:`)时**计数求和**;求和不会把非零变零,
> 对唯一可行动的结论(计数为 0)安全。
> 5. 文件不在 tracefile 里报**「未评估」**(llvm-cov / gcov 只为被插桩的目标产记录,
> `--sources` 还会再过滤一层)——这与 c8 的「文件不在数据里 = 从未执行」刻意相反。
> 6. 认较新的 `FNL:`/`FNA:` 编码时会**明确拒绝**而不是按猜的形状读;`SF:` 路径是绝对
> 路径(Windows 上是带反斜杠的)。

### 输出长什么样(真实捕获,一次查询命中两种结果)

```
Expand Down
Loading
Loading