euthyna 0.1.1 → 0.3.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.
Files changed (33) hide show
  1. package/.agents/skills/euthyna/SKILL.md +256 -0
  2. package/.agents/skills/euthyna/references/bug-classes.md +130 -0
  3. package/.agents/skills/euthyna/references/change-audit.md +219 -0
  4. package/.agents/skills/euthyna/references/dependency-audit.md +116 -0
  5. package/.agents/skills/euthyna/references/fact-contract.md +129 -0
  6. package/.agents/skills/euthyna/references/fact-producers.md +274 -0
  7. package/.agents/skills/euthyna/references/meta-mechanisms.md +173 -0
  8. package/.agents/skills/euthyna/references/verification-gates.md +293 -0
  9. package/.agents/skills/euthyna-en/SKILL.md +285 -0
  10. package/.agents/skills/euthyna-en/references/bug-classes.md +130 -0
  11. package/.agents/skills/euthyna-en/references/change-audit.md +212 -0
  12. package/.agents/skills/euthyna-en/references/dependency-audit.md +116 -0
  13. package/.agents/skills/euthyna-en/references/fact-contract.md +129 -0
  14. package/.agents/skills/euthyna-en/references/fact-producers.md +258 -0
  15. package/.agents/skills/euthyna-en/references/meta-mechanisms.md +174 -0
  16. package/.agents/skills/euthyna-en/references/verification-gates.md +291 -0
  17. package/.claude/commands/euthyna.md +18 -0
  18. package/.opencode/commands/euthyna.md +8 -0
  19. package/LICENSE +202 -202
  20. package/README.md +55 -8
  21. package/__init__.py +91 -0
  22. package/bin/euthyna.js +13 -3
  23. package/cordis.patch.yml +4 -0
  24. package/package.json +28 -3
  25. package/plugin/index.js +89 -0
  26. package/plugin.yaml +8 -0
  27. package/src/cli.js +189 -41
  28. package/src/contract.js +20 -13
  29. package/src/facts/coverage.js +69 -23
  30. package/src/facts/deps.js +81 -24
  31. package/src/facts/history.js +89 -25
  32. package/src/gate.js +171 -43
  33. package/src/lang.js +55 -0
package/src/gate.js CHANGED
@@ -25,6 +25,7 @@
25
25
  import { execFile } from 'node:child_process';
26
26
  import { promisify } from 'node:util';
27
27
  import { safeTextLines } from './contract.js';
28
+ import { T, DEFAULT_LANG } from './lang.js';
28
29
 
29
30
  const execFileAsync = promisify(execFile);
30
31
 
@@ -40,6 +41,30 @@ export const GATE_NAMES = Object.freeze({
40
41
  6: '环境'
41
42
  });
42
43
 
44
+ /** The gate statuses as written in a report, in either language. */
45
+ const GATE_STATUSES = Object.freeze({
46
+ PASS: 'PASS',
47
+ FAIL: 'FAIL',
48
+ PASS_ZH: '通过',
49
+ NOT_EVALUATED_ZH: '未评估',
50
+ NOT_EVALUATED_EN: 'NOT EVALUATED'
51
+ });
52
+
53
+ /** True when a parsed status means "the gate passed". */
54
+ export function isPassStatus(status) {
55
+ return status === GATE_STATUSES.PASS || status === GATE_STATUSES.PASS_ZH;
56
+ }
57
+
58
+ /** True when a parsed status means "the gate failed". */
59
+ export function isFailStatus(status) {
60
+ return status === GATE_STATUSES.FAIL;
61
+ }
62
+
63
+ /** True when a parsed status means "the gate could not be evaluated". */
64
+ export function isNotEvaluatedStatus(status) {
65
+ return status === GATE_STATUSES.NOT_EVALUATED_ZH || /^not evaluated$/i.test(status);
66
+ }
67
+
43
68
  /**
44
69
  * A finding header: `BUG #N <VERDICT> — <claim>`. The em dash is the skill's
45
70
  * form, a hyphen is tolerated, and the claim may be empty (that is itself a
@@ -50,16 +75,23 @@ const HEADER = /^BUG\s+#?(\d+)\s+(TRUE POSITIVE|FALSE POSITIVE|INCONCLUSIVE)\s*(
50
75
  /** A line that starts like a finding but does not parse - reported, not dropped. */
51
76
  const BUGLIKE = /^BUG\b/i;
52
77
 
78
+ /**
79
+ * Field markers, accepted in either language. The parser is bilingual on
80
+ * purpose and independent of the output language: a report is data, and a
81
+ * Chinese-written report must validate under an English session and vice
82
+ * versa. The English keywords are case-insensitive; the Chinese ones are
83
+ * unaffected by the flag.
84
+ */
53
85
  const FIELDS = {
54
- evidence: /^证据\s*[::]\s*(.+)$/,
55
- reproduce: /^复现\s*[::]\s*(.+)$/,
56
- impact: /^影响\s*[::]\s*(.+)$/,
57
- exploitability: /^可利用性\s*[::]\s*(.+)$/,
58
- gate: /^门禁\s*(\d+)\s*(?:[((]([^))]*)[))])?\s*(PASS|通过|FAIL|未评估)\s*[::]?\s*(.*)$/
86
+ evidence: /^(?:证据|Evidence)\s*[::]\s*(.+)$/i,
87
+ reproduce: /^(?:复现|Reproduce)\s*[::]\s*(.+)$/i,
88
+ impact: /^(?:影响|Impact)\s*[::]\s*(.+)$/i,
89
+ exploitability: /^(?:可利用性|Exploitability)\s*[::]\s*(.+)$/i,
90
+ gate: /^(?:门禁|Gate)\s*(\d+)\s*(?:[((]([^))]*)[))])?\s*(PASS|通过|FAIL|未评估|NOT EVALUATED|Not evaluated)\s*[::]?\s*(.*)$/i
59
91
  };
60
92
 
61
- /** `门禁全部通过。` may share its line with the evidence that follows it. */
62
- const ALL_PASS = /^门禁全部通过[。.]?/;
93
+ /** `门禁全部通过。` / `All gates passed.` may share its line with the evidence that follows it. */
94
+ const ALL_PASS = /^(?:门禁全部通过|All gates passed)[。.]?\s*/i;
63
95
 
64
96
  /**
65
97
  * Extract {file, line, commit} from an evidence string. Both `src/a.js:123`
@@ -178,43 +210,113 @@ export function parseGateReport(text) {
178
210
  * `violations` names every way it fails, `downgraded` is true when any fail.
179
211
  * Unparseable BUG-like lines are returned separately as downgrade candidates.
180
212
  */
181
- export function validateFindings(findings) {
213
+ export function validateFindings(findings, { lang = DEFAULT_LANG } = {}) {
214
+ const t = T(lang);
182
215
  return findings.map((finding) => {
183
216
  const violations = [];
184
- const fails = finding.gates.filter((g) => g.status === 'FAIL');
185
- const notEval = finding.gates.filter((g) => g.status === '未评估');
186
- const passes = finding.gates.filter((g) => g.status === 'PASS' || g.status === '通过');
217
+ const fails = finding.gates.filter((g) => isFailStatus(g.status));
218
+ const notEval = finding.gates.filter((g) => isNotEvaluatedStatus(g.status));
219
+ const passes = finding.gates.filter((g) => isPassStatus(g.status));
187
220
 
188
221
  for (const gate of finding.gates) {
189
222
  if (!(gate.number in GATE_NAMES)) {
190
- violations.push(`门禁编号 ${gate.number} 不在 1..6 内`);
223
+ violations.push(
224
+ t(`门禁编号 ${gate.number} 不在 1..6 内`, `gate number ${gate.number} is not in 1..6`)
225
+ );
191
226
  }
192
- if ((gate.status === 'FAIL' || gate.status === '未评估') && !gate.reason.trim()) {
193
- violations.push(`门禁 ${gate.number} ${gate.status} 缺少理由`);
227
+ if ((isFailStatus(gate.status) || isNotEvaluatedStatus(gate.status)) && !gate.reason.trim()) {
228
+ violations.push(
229
+ t(`门禁 ${gate.number} ${gate.status} 缺少理由`, `gate ${gate.number} ${gate.status} lacks a reason`)
230
+ );
194
231
  }
195
232
  }
196
233
 
197
- if (!finding.claim.trim()) violations.push('缺少结论描述(BUG 标题后的说明)');
234
+ if (!finding.claim.trim()) {
235
+ violations.push(
236
+ t('缺少结论描述(BUG 标题后的说明)', 'missing claim description (the note after the BUG header)')
237
+ );
238
+ }
198
239
 
199
240
  if (finding.verdict === 'TRUE POSITIVE') {
200
241
  if (!finding.evidence) {
201
- violations.push('缺少证据(TRUE POSITIVE 必须带 证据:path:L123)');
242
+ violations.push(
243
+ t(
244
+ '缺少证据(TRUE POSITIVE 必须带 证据:path:L123)',
245
+ 'missing evidence (a TRUE POSITIVE must carry Evidence: path:L123)'
246
+ )
247
+ );
202
248
  } else if (finding.evidence.line === null) {
203
- violations.push('证据缺少行号(需要 path:L123 形式,禁止只写文件名)');
249
+ violations.push(
250
+ t(
251
+ '证据缺少行号(需要 path:L123 形式,禁止只写文件名)',
252
+ 'evidence lacks a line number (path:L123 form required; a bare file name is not allowed)'
253
+ )
254
+ );
204
255
  }
205
256
  if (!finding.reproduce) {
206
- violations.push('缺少复现命令(TRUE POSITIVE 必须带 复现:<命令> 或 PoC)');
257
+ violations.push(
258
+ t(
259
+ '缺少复现命令(TRUE POSITIVE 必须带 复现:<命令> 或 PoC)',
260
+ 'missing reproduce command (a TRUE POSITIVE must carry Reproduce: <command> or a PoC)'
261
+ )
262
+ );
263
+ }
264
+ if (!finding.impact) {
265
+ violations.push(
266
+ t('缺少影响说明(门禁 3 的证据)', 'missing impact statement (the evidence for gate 3)')
267
+ );
207
268
  }
208
- if (!finding.impact) violations.push('缺少影响说明(门禁 3 的证据)');
209
269
  const allPassed = finding.allPass || passes.length === 6;
210
- if (!allPassed) violations.push('门禁未全部通过(需要 门禁全部通过。 或六条 PASS)');
211
- if (fails.length) violations.push(`TRUE POSITIVE 含 FAIL 门禁(${fails.map((g) => g.number).join(', ')})——应裁定为 FALSE POSITIVE`);
212
- if (notEval.length) violations.push(`TRUE POSITIVE 含未评估门禁(${notEval.map((g) => g.number).join(', ')})——应裁定为 INCONCLUSIVE`);
270
+ if (!allPassed) {
271
+ violations.push(
272
+ t(
273
+ '门禁未全部通过(需要 门禁全部通过。 或六条 PASS)',
274
+ 'not all gates pass (requires "All gates passed." or six PASS lines)'
275
+ )
276
+ );
277
+ }
278
+ if (fails.length) {
279
+ violations.push(
280
+ t(
281
+ `TRUE POSITIVE 含 FAIL 门禁(${fails.map((g) => g.number).join(', ')})——应裁定为 FALSE POSITIVE`,
282
+ `TRUE POSITIVE contains FAIL gates (${fails.map((g) => g.number).join(', ')}) — should be ruled FALSE POSITIVE`
283
+ )
284
+ );
285
+ }
286
+ if (notEval.length) {
287
+ violations.push(
288
+ t(
289
+ `TRUE POSITIVE 含未评估门禁(${notEval.map((g) => g.number).join(', ')})——应裁定为 INCONCLUSIVE`,
290
+ `TRUE POSITIVE contains not-evaluated gates (${notEval.map((g) => g.number).join(', ')}) — should be ruled INCONCLUSIVE`
291
+ )
292
+ );
293
+ }
213
294
  } else if (finding.verdict === 'FALSE POSITIVE') {
214
- if (fails.length === 0) violations.push('FALSE POSITIVE 必须至少有一条 门禁 N FAIL:<具体证据>');
295
+ if (fails.length === 0) {
296
+ violations.push(
297
+ t(
298
+ 'FALSE POSITIVE 必须至少有一条 门禁 N FAIL:<具体证据>',
299
+ 'a FALSE POSITIVE must have at least one Gate N FAIL: <specific evidence>'
300
+ )
301
+ );
302
+ }
215
303
  } else {
216
- if (notEval.length === 0) violations.push('INCONCLUSIVE 必须至少有一条 门禁 N 未评估:<为什么>');
217
- if (fails.length) violations.push('INCONCLUSIVE 含 FAIL 门禁——应裁定为 FALSE POSITIVE');
304
+ if (notEval.length === 0) {
305
+ violations.push(
306
+ t(
307
+ 'INCONCLUSIVE 必须至少有一条 门禁 N 未评估:<为什么>',
308
+ 'an INCONCLUSIVE must have at least one Gate N NOT EVALUATED: <why>'
309
+ )
310
+ );
311
+ }
312
+ if (fails.length) {
313
+ violations.push(
314
+ t(
315
+ 'INCONCLUSIVE 含 FAIL 门禁——应裁定为 FALSE POSITIVE',
316
+ 'INCONCLUSIVE contains FAIL gates — should be ruled FALSE POSITIVE'
317
+ )
318
+ );
319
+ }
218
320
  }
219
321
 
220
322
  return { finding, violations, downgraded: violations.length > 0 };
@@ -321,27 +423,48 @@ export async function verifyFinding(
321
423
  * Render the gate result for a terminal reader. Report-derived text passes the
322
424
  * render boundary here exactly once, like the fact report channel.
323
425
  */
324
- export function renderGateReport(result, { write: rawWrite = console.log } = {}) {
426
+ export function renderGateReport(result, { write: rawWrite = console.log, lang = DEFAULT_LANG } = {}) {
427
+ const t = T(lang);
325
428
  const write = (line) => rawWrite(safeTextLines(line));
326
429
  const { file, findings, unparseable, downgraded, verify } = result;
327
430
 
328
431
  write('');
329
- write('euthyna gate — 6 门禁契约校验(不测量,只核对报告的自我声明)');
330
- write(`报告: ${file}`);
432
+ write(
433
+ t(
434
+ 'euthyna gate — 6 门禁契约校验(不测量,只核对报告的自我声明)',
435
+ 'euthyna gate — six-gate contract check (no measurement; only the report\'s own claims are checked)'
436
+ )
437
+ );
438
+ write(`${t('报告: ', 'Report: ')}${file}`);
331
439
  if (verify) {
332
- write('⚠ --verify 以当前用户权限执行报告中的复现命令(按 argv 执行,不经过 shell)。');
333
- write(' 默认只执行 git 命令;解释器命令(node/npm/python)需要显式 --allow-exec。');
334
- write(' 它不是一个安全沙箱:只对你自己信任的报告使用。');
440
+ write(
441
+ t(
442
+ '⚠ --verify 以当前用户权限执行报告中的复现命令(按 argv 执行,不经过 shell)。',
443
+ '⚠ --verify runs the report\'s reproduce commands with your privileges (as argv, no shell).'
444
+ )
445
+ );
446
+ write(
447
+ t(
448
+ ' 默认只执行 git 命令;解释器命令(node/npm/python)需要显式 --allow-exec。',
449
+ ' Only git commands run by default; interpreter commands (node/npm/python) need explicit --allow-exec.'
450
+ )
451
+ );
452
+ write(
453
+ t(
454
+ ' 它不是一个安全沙箱:只对你自己信任的报告使用。',
455
+ ' It is not a sandbox: use it only on reports you trust.'
456
+ )
457
+ );
335
458
  }
336
459
  write('');
337
460
 
338
461
  for (const entry of findings) {
339
462
  const f = entry.finding;
340
- const head = ` BUG #${f.number} ${f.verdict} — ${f.claim || '(无结论描述)'}`;
463
+ const head = ` BUG #${f.number} ${f.verdict} — ${f.claim || t('(无结论描述)', '(no claim)')}`;
341
464
  if (entry.downgraded) {
342
- write(`${head} — ✗ 降级为「观察」`);
465
+ write(`${head} — ✗ ${t('降级为「观察」', 'downgraded to "observation"')}`);
343
466
  } else {
344
- write(`${head} — ✓ 通过`);
467
+ write(`${head} — ✓ ${t('通过', 'passed')}`);
345
468
  }
346
469
  if (entry.verification && f.verdict === 'TRUE POSITIVE') {
347
470
  const v = entry.verification;
@@ -352,30 +475,35 @@ export function renderGateReport(result, { write: rawWrite = console.log } = {})
352
475
  : v.status === 'failed'
353
476
  ? `(exit ${v.exitCode ?? '?'}${v.detail ? `: ${v.detail}` : ''})`
354
477
  : v.status === 'refused'
355
- ? `(拒绝运行 ${v.tool} —— 不在白名单)`
478
+ ? t(`(拒绝运行 ${v.tool} —— 不在白名单)`, `(refused to run ${v.tool} — not on the allowlist)`)
356
479
  : v.status === 'needs-consent'
357
- ? `(未执行 ${v.tool} 解释器命令 —— 需要 --allow-exec)`
480
+ ? t(`(未执行 ${v.tool} 解释器命令 —— 需要 --allow-exec)`, `(interpreter command ${v.tool} not run — needs --allow-exec)`)
358
481
  : v.status === 'unparseable'
359
- ? '(命令无法拆分为 argv)'
360
- : '(无复现命令)';
361
- write(` 复现核验 ${mark} ${how}`);
482
+ ? t('(命令无法拆分为 argv)', '(command could not be split into argv)')
483
+ : t('(无复现命令)', '(no reproduce command)');
484
+ write(`${t(' 复现核验 ', ' Reproduce check ')}${mark} ${how}`);
362
485
  }
363
486
  for (const violation of entry.violations) {
364
- write(` 缺: ${violation}`);
487
+ write(`${t(' 缺: ', ' missing: ')}${violation}`);
365
488
  }
366
489
  }
367
490
 
368
491
  if (unparseable.length) {
369
492
  write('');
370
- write('⚠ 无法解析的 BUG 行(计入降级):');
493
+ write(t('⚠ 无法解析的 BUG 行(计入降级):', '⚠ Unparseable BUG lines (counted as downgrades):'));
371
494
  for (const line of unparseable) write(` • ${line}`);
372
495
  }
373
496
 
374
497
  write('');
375
498
  if (downgraded === 0) {
376
- write('全部 finding 通过门禁契约。');
499
+ write(t('全部 finding 通过门禁契约。', 'All findings pass the gate contract.'));
377
500
  } else {
378
- write(`⚠ ${downgraded} 个 finding 被降级为「观察」——缺证据、缺复现或门禁不一致,不得当作已确证的结论。`);
501
+ write(
502
+ t(
503
+ `⚠ ${downgraded} 个 finding 被降级为「观察」——缺证据、缺复现或门禁不一致,不得当作已确证的结论。`,
504
+ `⚠ ${downgraded} finding${downgraded === 1 ? '' : 's'} downgraded to "observation" — missing evidence, missing reproduction, or inconsistent gates; must not be read as established findings.`
505
+ )
506
+ );
379
507
  }
380
508
  write('');
381
509
  }
package/src/lang.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Language selection for the CLI (i18n).
3
+ *
4
+ * Zero-dependency on purpose, like everything else in this project: no i18n
5
+ * framework is pulled in. The CLI currently ships two languages, chosen by an
6
+ * explicit switch rather than by locale sniffing, because a locale-derived
7
+ * answer can surprise ("I asked for English and got Chinese") and is hard to
8
+ * reproduce in CI.
9
+ *
10
+ * Precedence: an explicit `--lang` flag beats the EUTHYNA_LANG environment
11
+ * variable, which beats the default (Chinese). The default stays Chinese so a
12
+ * user who never asks for English sees exactly the output they saw before.
13
+ */
14
+
15
+ export const LANGS = Object.freeze(['zh', 'en']);
16
+ export const DEFAULT_LANG = 'zh';
17
+
18
+ /** True for a value the CLI accepts as a language. */
19
+ export function isLang(value) {
20
+ return value === 'zh' || value === 'en';
21
+ }
22
+
23
+ /**
24
+ * Resolve the effective language.
25
+ *
26
+ * Precedence: an explicit flag wins, then the environment variable, then the
27
+ * default. cli.js validates the flag value before calling this, so a non-zh/en
28
+ * flag never reaches here.
29
+ *
30
+ * @param {object} [options]
31
+ * @param {string|boolean} [options.flag] the value of --lang (true when the
32
+ * flag was passed without a value)
33
+ * @param {object} [options.env] defaults to process.env
34
+ * @returns {'zh'|'en'}
35
+ */
36
+ export function resolveLang({ flag, env = process.env } = {}) {
37
+ if (flag === 'en' || flag === 'zh') return flag;
38
+ if (env.EUTHYNA_LANG === 'en') return 'en';
39
+ return DEFAULT_LANG;
40
+ }
41
+
42
+ /**
43
+ * The translation helper. Call it once per scope with the active language:
44
+ *
45
+ * const t = T(lang);
46
+ * write(t('已确证', 'Established'));
47
+ *
48
+ * The Chinese text is always the first argument and stays the canonical form;
49
+ * the English is the second. Each call site keeps both strings next to the
50
+ * interpolation that builds them, so a translation cannot drift from the
51
+ * message it translates.
52
+ */
53
+ export function T(lang) {
54
+ return (zh, en) => (lang === 'en' ? en : zh);
55
+ }