euthyna 0.1.0 → 0.2.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 +256 -0
- package/.agents/skills/euthyna/references/bug-classes.md +130 -0
- package/.agents/skills/euthyna/references/change-audit.md +219 -0
- package/.agents/skills/euthyna/references/dependency-audit.md +116 -0
- package/.agents/skills/euthyna/references/fact-contract.md +129 -0
- package/.agents/skills/euthyna/references/fact-producers.md +274 -0
- package/.agents/skills/euthyna/references/meta-mechanisms.md +173 -0
- package/.agents/skills/euthyna/references/verification-gates.md +293 -0
- package/LICENSE +202 -202
- package/README.md +65 -17
- package/cordis.patch.yml +4 -0
- package/package.json +23 -3
- package/plugin/index.js +60 -0
- package/src/cli.js +157 -6
- package/src/contract.js +2 -1
- package/src/facts/coverage.js +55 -2
- package/src/facts/deps.js +366 -0
- package/src/facts/history.js +322 -25
- package/src/gate.js +381 -0
package/plugin/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DSH half of euthyna: mounts the packaged skill on `ctx.skills`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI (`bin/euthyna.js`) produces the facts; this plugin is what makes the
|
|
5
|
+
* discipline that consumes them installable with `dsh plugin add euthyna`. It
|
|
6
|
+
* serves the skill directory shipped inside this package, so the bundle carries
|
|
7
|
+
* the skill itself rather than pointing at a checkout.
|
|
8
|
+
*
|
|
9
|
+
* The official filesystem provider is imported instead of reimplemented:
|
|
10
|
+
* frontmatter parsing and root ranking then stay byte-identical with the
|
|
11
|
+
* built-in provider, and the only thing this package decides is which root to
|
|
12
|
+
* serve.
|
|
13
|
+
*
|
|
14
|
+
* @module euthyna/plugin
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
|
|
20
|
+
|
|
21
|
+
export const name = 'euthyna';
|
|
22
|
+
|
|
23
|
+
/** Without the skill registry there is nothing to register onto. */
|
|
24
|
+
export const inject = ['skills'];
|
|
25
|
+
|
|
26
|
+
/** This module's directory: `<package>/plugin`, in the repo and in the tarball alike. */
|
|
27
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
/** The packaged skill root, at the same relative path in both layouts. */
|
|
30
|
+
export const SKILLS_ROOT = join(MODULE_DIR, '..', '.agents', 'skills');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the packaged skill directory as a provider.
|
|
34
|
+
*
|
|
35
|
+
* `includeDefaultRoots: false` keeps this provider to euthyna's own skill: the
|
|
36
|
+
* built-in provider goes on serving the user's other skills, so installing this
|
|
37
|
+
* bundle adds a skill rather than replacing the catalogue. Registration is a
|
|
38
|
+
* plain call — the provider disposes itself through the control signal it is
|
|
39
|
+
* handed.
|
|
40
|
+
*
|
|
41
|
+
* A missing bundle throws rather than mounting an empty provider, because a
|
|
42
|
+
* plugin that installs cleanly and contributes nothing is worse than one that
|
|
43
|
+
* fails where someone can see it.
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - the cordis context, which must expose `skills`.
|
|
46
|
+
* @param config - optional `{ skillsDir }` override for a relocated skill root.
|
|
47
|
+
*/
|
|
48
|
+
export function apply(ctx, config = {}) {
|
|
49
|
+
const root = resolve(config.skillsDir ?? SKILLS_ROOT);
|
|
50
|
+
const bundle = join(root, 'euthyna', 'SKILL.md');
|
|
51
|
+
if (!existsSync(bundle)) {
|
|
52
|
+
throw new Error(`euthyna: no skill bundle at ${bundle} — set config.skillsDir to the directory holding euthyna/SKILL.md`);
|
|
53
|
+
}
|
|
54
|
+
ctx.skills.registerProvider((control) => new FileSystemSkillProvider(ctx, control, {
|
|
55
|
+
providerName: 'euthyna',
|
|
56
|
+
includeDefaultRoots: false,
|
|
57
|
+
customSkillDirs: [root],
|
|
58
|
+
watch: false,
|
|
59
|
+
}));
|
|
60
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import process from 'node:process';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
18
19
|
import { collectHistoryFacts } from './facts/history.js';
|
|
19
20
|
import { collectCoverageFacts } from './facts/coverage.js';
|
|
21
|
+
import { collectDependencyFacts } from './facts/deps.js';
|
|
22
|
+
import { parseGateReport, validateFindings, verifyFinding, renderGateReport } from './gate.js';
|
|
20
23
|
import { makeReport, renderReport, safeTextLines, KIND } from './contract.js';
|
|
21
24
|
import { repoToplevel, revParse } from './git.js';
|
|
22
25
|
|
|
@@ -37,6 +40,11 @@ const COVERAGE_PRODUCER = {
|
|
|
37
40
|
version: '0.1.0',
|
|
38
41
|
purpose: '符号调用计数(只能证伪)'
|
|
39
42
|
};
|
|
43
|
+
const DEPS_PRODUCER = {
|
|
44
|
+
name: 'euthyna-deps',
|
|
45
|
+
version: '0.1.0',
|
|
46
|
+
purpose: '依赖锁定版本(读取 lockfile,不含漏洞判定)'
|
|
47
|
+
};
|
|
40
48
|
|
|
41
49
|
const HELP = `
|
|
42
50
|
euthyna —— 给 AI 编码 agent 用的确定性事实产出器
|
|
@@ -44,24 +52,40 @@ euthyna —— 给 AI 编码 agent 用的确定性事实产出器
|
|
|
44
52
|
它不是扫描器。它只回答两个模型算不准的问题,并把答案写成带证据的事实。
|
|
45
53
|
|
|
46
54
|
用法:
|
|
47
|
-
euthyna history --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--json]
|
|
55
|
+
euthyna history --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--origins] [--json]
|
|
48
56
|
euthyna coverage --coverage <file> --symbol <name> [--file <path>] [--json]
|
|
57
|
+
euthyna deps [--repo <dir>] [--lockfile <file>] --dep <name> [--dep <name>] [--json]
|
|
58
|
+
euthyna gate <报告文件> [--verify] [--cwd <dir>] [--json]
|
|
49
59
|
|
|
50
60
|
命令:
|
|
51
61
|
history 本次变更删掉了哪些代码、它们分别由哪个提交引入、该提交是不是安全修复
|
|
52
62
|
--base 必填,比较的基线版本(如 main、HEAD~5、某个 commit)
|
|
53
63
|
--head 可选,默认 HEAD
|
|
54
64
|
--pickaxe 额外检查「曾被移除又加回来」的新增行(有探针上限)
|
|
65
|
+
--origins 用 git log -S 把删除行归属到「最初引入」该内容的提交,
|
|
66
|
+
而非 blame 的「最后修改者」(有探针上限,比默认慢)
|
|
55
67
|
coverage 某个符号在测试运行中到底有没有被调用过
|
|
56
68
|
--coverage 覆盖率数据文件,c8 的 coverage-final.json
|
|
57
69
|
--symbol 要查询的符号名,可重复
|
|
58
70
|
--file 可选,限定到某个文件
|
|
71
|
+
deps 某个依赖在 lockfile 里被锁定/声明成什么版本(供应链声明的裁决依据)
|
|
72
|
+
--repo 可选,依赖清单所在目录(默认当前目录,自动检测)
|
|
73
|
+
--lockfile 可选,显式指定清单文件(支持 package-lock.json / Cargo.lock / go.mod)
|
|
74
|
+
--dep 要查询的依赖名,可重复
|
|
75
|
+
注:只报版本事实,不判「是否含漏洞」——版本到 CVE 的映射归判定层
|
|
76
|
+
gate 检查一份审计报告是否符合 6 门禁契约(不测量,只核对报告的自我声明)
|
|
77
|
+
<报告文件> 报告的 markdown 文件,裁定格式见技能 SKILL.md
|
|
78
|
+
--verify 重跑每条 TRUE POSITIVE 的复现命令(按 argv 执行,不经过 shell)
|
|
79
|
+
默认只执行 git 命令;⚠ 执行结果以你的权限生效,只对你信任的报告用
|
|
80
|
+
--allow-exec 允许 --verify 执行解释器命令(node/npm/python)——
|
|
81
|
+
它们能跑报告里的任意代码,加了它就等于你为该报告背书
|
|
82
|
+
--cwd <dir> --verify 的工作目录(默认当前目录)
|
|
59
83
|
|
|
60
84
|
退出码:
|
|
61
|
-
0
|
|
62
|
-
10 已测量,且存在被分类为 security
|
|
85
|
+
0 已测量,没有安全相关的发现;或 gate 报告全部通过门禁契约
|
|
86
|
+
10 已测量,且存在被分类为 security 的事实;或 gate 报告有 finding 被降级
|
|
63
87
|
1 用法错误
|
|
64
|
-
2
|
|
88
|
+
2 完全无法测量(此时**不得**当作干净);或 gate 报告无法读取/没有可校验的 finding
|
|
65
89
|
|
|
66
90
|
注意: 缺数据不等于干净。无法测量的判据会列在输出的「未评估的判据」一节。
|
|
67
91
|
`;
|
|
@@ -146,7 +170,8 @@ async function runHistory(flags) {
|
|
|
146
170
|
cwd: toplevel,
|
|
147
171
|
base,
|
|
148
172
|
head,
|
|
149
|
-
pickaxe: flags.pickaxe === true
|
|
173
|
+
pickaxe: flags.pickaxe === true,
|
|
174
|
+
origins: flags.origins === true
|
|
150
175
|
});
|
|
151
176
|
|
|
152
177
|
const report = makeReport({
|
|
@@ -198,6 +223,123 @@ async function runCoverage(flags) {
|
|
|
198
223
|
return { exit: measured ? EXIT.CLEAN : EXIT.UNMEASURED, report };
|
|
199
224
|
}
|
|
200
225
|
|
|
226
|
+
async function runDeps(flags) {
|
|
227
|
+
const cwd = path.resolve(flags.repo ? String(flags.repo) : process.cwd());
|
|
228
|
+
const deps = asArray(flags.dep).map(String);
|
|
229
|
+
const lockfile = flags.lockfile ? String(flags.lockfile) : undefined;
|
|
230
|
+
|
|
231
|
+
// Asking for nothing is a caller mistake, not a measurement failure.
|
|
232
|
+
if (deps.length === 0) {
|
|
233
|
+
return { exit: EXIT.USAGE, error: 'deps 需要至少一个 --dep <name>' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { facts, evaluated, notEvaluated, measured } = await collectDependencyFacts({
|
|
237
|
+
cwd,
|
|
238
|
+
lockfile,
|
|
239
|
+
deps
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const report = makeReport({
|
|
243
|
+
producer: DEPS_PRODUCER,
|
|
244
|
+
subject: { repo: cwd, lockfile: lockfile ?? '(自动检测)', deps },
|
|
245
|
+
facts,
|
|
246
|
+
evaluated,
|
|
247
|
+
notEvaluated
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return { exit: measured ? EXIT.CLEAN : EXIT.UNMEASURED, report };
|
|
251
|
+
}
|
|
252
|
+
async function runGate(flags, positional) {
|
|
253
|
+
const file = positional[1];
|
|
254
|
+
if (!file) {
|
|
255
|
+
return { exit: EXIT.USAGE, error: 'gate 需要 <报告文件>(markdown,裁定格式见技能 SKILL.md)' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let text;
|
|
259
|
+
try {
|
|
260
|
+
text = await readFile(path.resolve(file), 'utf8');
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// An unreadable report is not a clean pass: there is nothing to check, and
|
|
263
|
+
// "nothing was checked" must not read as "everything passed".
|
|
264
|
+
return { exit: EXIT.UNMEASURED, error: `无法读取报告 ${file}: ${error.message}` };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { findings, unparseable } = parseGateReport(text);
|
|
268
|
+
if (findings.length === 0 && unparseable.length === 0) {
|
|
269
|
+
return { exit: EXIT.UNMEASURED, error: `报告 ${file} 中没有可校验的 finding(需要 BUG #N <VERDICT> — 说明 形式)` };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const validated = validateFindings(findings);
|
|
273
|
+
for (const line of unparseable) {
|
|
274
|
+
validated.push({ unparseableLine: line, violations: ['无法解析的 BUG 行'], downgraded: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (flags.verify) {
|
|
278
|
+
const cwd = path.resolve(flags.cwd ? String(flags.cwd) : process.cwd());
|
|
279
|
+
const allowInterpreters = flags['allow-exec'] === true;
|
|
280
|
+
for (const entry of validated) {
|
|
281
|
+
if (entry.unparseableLine) continue;
|
|
282
|
+
entry.verification = await verifyFinding(entry.finding, { cwd, allowInterpreters });
|
|
283
|
+
const v = entry.verification;
|
|
284
|
+
// Every status other than "verified" means the reproduction claim was
|
|
285
|
+
// not actually checked, which is a downgrade: a refused tool, an
|
|
286
|
+
// interpreter awaiting consent, an unparseable command and a failed run
|
|
287
|
+
// are different failures, but none of them is a verified reproduction.
|
|
288
|
+
if (v.status === 'failed') {
|
|
289
|
+
entry.violations.push(`复现命令未通过(exit ${v.exitCode ?? '?'}${v.detail ? `: ${v.detail}` : ''})`);
|
|
290
|
+
entry.downgraded = true;
|
|
291
|
+
} else if (v.status === 'refused') {
|
|
292
|
+
entry.violations.push(`复现命令被拒绝(${v.tool} 不在白名单)——复现未验证`);
|
|
293
|
+
entry.downgraded = true;
|
|
294
|
+
} else if (v.status === 'needs-consent') {
|
|
295
|
+
entry.violations.push(`复现命令是解释器命令(${v.tool}),未执行——复现未验证;信任该报告时加 --allow-exec`);
|
|
296
|
+
entry.downgraded = true;
|
|
297
|
+
} else if (v.status === 'unparseable') {
|
|
298
|
+
entry.violations.push('复现命令无法拆分为 argv——复现未验证');
|
|
299
|
+
entry.downgraded = true;
|
|
300
|
+
}
|
|
301
|
+
// no-command is already a structural violation (a TRUE POSITIVE without a
|
|
302
|
+
// reproduce line), so --verify does not double-report it.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const downgraded = validated.filter(e => e.downgraded).length;
|
|
307
|
+
const result = { file, findings: validated, unparseable, downgraded, verify: flags.verify === true };
|
|
308
|
+
|
|
309
|
+
if (flags.json) {
|
|
310
|
+
return {
|
|
311
|
+
exit: downgraded > 0 ? EXIT.FLAGGED : EXIT.CLEAN,
|
|
312
|
+
json: JSON.stringify(
|
|
313
|
+
{
|
|
314
|
+
command: 'gate',
|
|
315
|
+
file,
|
|
316
|
+
findings: validated.map(e => ({
|
|
317
|
+
...(e.unparseableLine
|
|
318
|
+
? { unparseable: e.unparseableLine }
|
|
319
|
+
: {
|
|
320
|
+
number: e.finding.number,
|
|
321
|
+
verdict: e.finding.verdict,
|
|
322
|
+
claim: e.finding.claim,
|
|
323
|
+
evidence: e.finding.evidence,
|
|
324
|
+
reproduce: e.finding.reproduce,
|
|
325
|
+
impact: e.finding.impact,
|
|
326
|
+
gates: e.finding.gates
|
|
327
|
+
}),
|
|
328
|
+
violations: e.violations,
|
|
329
|
+
downgraded: e.downgraded
|
|
330
|
+
})),
|
|
331
|
+
downgraded
|
|
332
|
+
},
|
|
333
|
+
null,
|
|
334
|
+
2
|
|
335
|
+
)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
renderGateReport(result);
|
|
340
|
+
return { exit: downgraded > 0 ? EXIT.FLAGGED : EXIT.CLEAN, report: null };
|
|
341
|
+
}
|
|
342
|
+
|
|
201
343
|
/** Entry point. Returns the process exit code. */
|
|
202
344
|
export async function main(argv = process.argv.slice(2)) {
|
|
203
345
|
const { positional, flags } = parseArgs(argv);
|
|
@@ -213,6 +355,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
213
355
|
result = await runHistory(flags);
|
|
214
356
|
} else if (command === 'coverage') {
|
|
215
357
|
result = await runCoverage(flags);
|
|
358
|
+
} else if (command === 'deps') {
|
|
359
|
+
result = await runDeps(flags);
|
|
360
|
+
} else if (command === 'gate') {
|
|
361
|
+
result = await runGate(flags, positional);
|
|
216
362
|
} else {
|
|
217
363
|
// stderr boundary, mirroring the render boundary in contract.js: text that
|
|
218
364
|
// reaches the error channel may carry user or repo-controlled bytes (an
|
|
@@ -228,9 +374,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
228
374
|
return result.exit;
|
|
229
375
|
}
|
|
230
376
|
|
|
377
|
+
if (result.json) {
|
|
378
|
+
process.stdout.write(`${result.json}\n`);
|
|
379
|
+
return result.exit;
|
|
380
|
+
}
|
|
381
|
+
|
|
231
382
|
if (flags.json) {
|
|
232
383
|
process.stdout.write(`${JSON.stringify(result.report, null, 2)}\n`);
|
|
233
|
-
} else {
|
|
384
|
+
} else if (result.report) {
|
|
234
385
|
renderReport(result.report);
|
|
235
386
|
}
|
|
236
387
|
|
package/src/contract.js
CHANGED
|
@@ -66,7 +66,8 @@ export function safeTextLines(value) {
|
|
|
66
66
|
export const KIND = Object.freeze({
|
|
67
67
|
HISTORY: 'history',
|
|
68
68
|
REINTRODUCTION: 'reintroduction',
|
|
69
|
-
TEST_COVERAGE: 'test_coverage'
|
|
69
|
+
TEST_COVERAGE: 'test_coverage',
|
|
70
|
+
DEPENDENCY: 'dependency'
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
/** Fact status. `unknown` is a first-class value, not an error. */
|
package/src/facts/coverage.js
CHANGED
|
@@ -62,14 +62,18 @@ function reproduceCommand({ coverageFile, symbol, file }) {
|
|
|
62
62
|
/**
|
|
63
63
|
* Read a coverage report.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* Three formats are accepted, all identified by shape rather than by filename:
|
|
66
66
|
*
|
|
67
67
|
* 1. c8 / v8-to-istanbul `coverage-final.json` — per-file keys are
|
|
68
68
|
* path/all/statementMap/s/branchMap/b/fnMap/f. There is no istanbul `hash`,
|
|
69
69
|
* and `branchMap` is a relabelled V8 block range rather than an if/else
|
|
70
70
|
* model, so a consumer written against classic istanbul field lists reads
|
|
71
71
|
* the wrong thing.
|
|
72
|
-
* 2.
|
|
72
|
+
* 2. classic istanbul (jest's default provider, nyc) `coverage-final.json` —
|
|
73
|
+
* the same per-file statementMap/s/branchMap/b/fnMap/f plus a `hash` field.
|
|
74
|
+
* The symbol locator only reads fnMap/f, which classic istanbul and c8 emit
|
|
75
|
+
* in the same shape, so both resolve through the same code path.
|
|
76
|
+
* 3. coverage.py JSON (`coverage json`, format 3) — top level is
|
|
73
77
|
* `{meta, files}`, and per-file `functions` maps a function name
|
|
74
78
|
* (`name`, or `Class.method` for methods) to
|
|
75
79
|
* `{executed_lines, missing_lines, start_line, ...}`. There is **no
|
|
@@ -77,6 +81,10 @@ function reproduceCommand({ coverageFile, symbol, file }) {
|
|
|
77
81
|
* evidence that the function was never entered. coverage.py reports
|
|
78
82
|
* functions that were never called as long as their module was loaded,
|
|
79
83
|
* which is exactly what makes "never invoked" an established fact.
|
|
84
|
+
*
|
|
85
|
+
* Anything that matches none of these shapes is **not a coverage report this
|
|
86
|
+
* producer can read**, and is reported as notEvaluated — never fed through the
|
|
87
|
+
* locator, where it would answer "symbol not located" with a straight face.
|
|
80
88
|
*/
|
|
81
89
|
export async function loadCoverage(coverageFile) {
|
|
82
90
|
const raw = await readFile(coverageFile, 'utf8');
|
|
@@ -98,6 +106,35 @@ function isCoveragePyReport(coverage) {
|
|
|
98
106
|
);
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Identify the coverage format by shape, or null when the file is not a
|
|
111
|
+
* coverage report at all. c8 and classic istanbul share the fnMap/f shape the
|
|
112
|
+
* locator reads; they differ only in `hash` (istanbul has it, c8 does not) and
|
|
113
|
+
* `all` (c8 has it), neither of which the locator reads — but naming the format
|
|
114
|
+
* honestly is still part of the fact, so the distinction is kept.
|
|
115
|
+
*/
|
|
116
|
+
export function detectCoverageFormat(coverage) {
|
|
117
|
+
if (isCoveragePyReport(coverage)) return 'coverage.py';
|
|
118
|
+
|
|
119
|
+
const entries = Object.entries(coverage).filter(([, v]) => v && typeof v === 'object');
|
|
120
|
+
if (entries.length === 0) return null;
|
|
121
|
+
|
|
122
|
+
// A real c8/istanbul entry carries both `fnMap` (function metadata) and `f`
|
|
123
|
+
// (per-index invocation counts). Requiring both stops a partial shape — a
|
|
124
|
+
// file with `fnMap` but no `f`, say — from being classified as coverage and
|
|
125
|
+
// then emitting "never invoked" for a count a missing `f` defaults to zero.
|
|
126
|
+
// coverage.py's per-file `functions` is only valid inside a `meta.files`
|
|
127
|
+
// report, which isCoveragePyReport already handled above; a bare `functions`
|
|
128
|
+
// object is not a JS report and must not be accepted here.
|
|
129
|
+
const jsShaped = entries.some(
|
|
130
|
+
([, e]) =>
|
|
131
|
+
e.fnMap && typeof e.fnMap === 'object' && e.f && typeof e.f === 'object'
|
|
132
|
+
);
|
|
133
|
+
if (!jsShaped) return null;
|
|
134
|
+
|
|
135
|
+
return entries.some(([, e]) => typeof e === 'object' && 'hash' in e) ? 'istanbul' : 'c8';
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
/**
|
|
102
139
|
* Locate a symbol inside one file entry and normalise the hit.
|
|
103
140
|
*
|
|
@@ -215,6 +252,21 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
215
252
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
216
253
|
}
|
|
217
254
|
|
|
255
|
+
// A non-empty object that matches no known shape is not a coverage report at
|
|
256
|
+
// all. Feeding it through the locator would answer "symbol not located" with
|
|
257
|
+
// a straight face — the confident wrong answer this producer exists to refuse.
|
|
258
|
+
const format = detectCoverageFormat(coverage);
|
|
259
|
+
if (format === null) {
|
|
260
|
+
notEvaluated.push(
|
|
261
|
+
notEvaluatedEntry(
|
|
262
|
+
KIND.TEST_COVERAGE,
|
|
263
|
+
'覆盖率数据不是可识别的报告形状(既无 c8/istanbul 的 fnMap/f,也无 coverage.py 的 functions)——' +
|
|
264
|
+
'它可能根本不是覆盖率文件,任何「符号未定位/未覆盖」的结论在此都不可信'
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
return { facts, evaluated, notEvaluated, measured: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
218
270
|
if (targets.length === 0) {
|
|
219
271
|
notEvaluated.push(notEvaluatedEntry(KIND.TEST_COVERAGE, '没有指定要查询的符号(--symbol)'));
|
|
220
272
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
@@ -331,6 +383,7 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
331
383
|
evaluated.push({
|
|
332
384
|
kind: KIND.TEST_COVERAGE,
|
|
333
385
|
producer: 'euthyna-coverage',
|
|
386
|
+
format,
|
|
334
387
|
count: facts.length
|
|
335
388
|
});
|
|
336
389
|
|