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.
- 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/.agents/skills/euthyna-en/SKILL.md +285 -0
- package/.agents/skills/euthyna-en/references/bug-classes.md +130 -0
- package/.agents/skills/euthyna-en/references/change-audit.md +212 -0
- package/.agents/skills/euthyna-en/references/dependency-audit.md +116 -0
- package/.agents/skills/euthyna-en/references/fact-contract.md +129 -0
- package/.agents/skills/euthyna-en/references/fact-producers.md +258 -0
- package/.agents/skills/euthyna-en/references/meta-mechanisms.md +174 -0
- package/.agents/skills/euthyna-en/references/verification-gates.md +291 -0
- package/.claude/commands/euthyna.md +18 -0
- package/.opencode/commands/euthyna.md +8 -0
- package/LICENSE +202 -202
- package/README.md +55 -8
- package/__init__.py +91 -0
- package/bin/euthyna.js +13 -3
- package/cordis.patch.yml +4 -0
- package/package.json +28 -3
- package/plugin/index.js +89 -0
- package/plugin.yaml +8 -0
- package/src/cli.js +189 -41
- package/src/contract.js +20 -13
- package/src/facts/coverage.js +69 -23
- package/src/facts/deps.js +81 -24
- package/src/facts/history.js +89 -25
- package/src/gate.js +171 -43
- package/src/lang.js +55 -0
package/src/facts/coverage.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { makeFact, notEvaluated as notEvaluatedEntry, KIND, STATUS, shellQuote } from '../contract.js';
|
|
23
|
+
import { T, DEFAULT_LANG } from '../lang.js';
|
|
23
24
|
|
|
24
25
|
/** c8's default exclusions. A production file matching one of these vanishes from the report. */
|
|
25
26
|
const C8_DEFAULT_EXCLUDES = [
|
|
@@ -210,7 +211,8 @@ function locateSymbolInEntry(entry, symbol) {
|
|
|
210
211
|
* @param {string} options.coverageFile
|
|
211
212
|
* @param {Array<{file?: string, symbol: string}>} options.targets
|
|
212
213
|
*/
|
|
213
|
-
export async function collectCoverageFacts({ coverageFile, targets = [] } = {}) {
|
|
214
|
+
export async function collectCoverageFacts({ coverageFile, targets = [], lang = DEFAULT_LANG } = {}) {
|
|
215
|
+
const t = T(lang);
|
|
214
216
|
const facts = [];
|
|
215
217
|
const evaluated = [];
|
|
216
218
|
const notEvaluated = [];
|
|
@@ -223,8 +225,12 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
223
225
|
notEvaluated.push(
|
|
224
226
|
notEvaluatedEntry(
|
|
225
227
|
KIND.TEST_COVERAGE,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
t(
|
|
229
|
+
`无法读取覆盖率数据 ${coverageFile}: ${error.code ?? error.message}。` +
|
|
230
|
+
'未运行测试或测试未产出覆盖率时,这属于「未评估」,不是「未被覆盖」',
|
|
231
|
+
`cannot read coverage data ${coverageFile}: ${error.code ?? error.message}. ` +
|
|
232
|
+
'When no tests ran or none produced coverage, this is "not evaluated", not "not covered"'
|
|
233
|
+
)
|
|
228
234
|
)
|
|
229
235
|
);
|
|
230
236
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
@@ -245,8 +251,13 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
245
251
|
notEvaluated.push(
|
|
246
252
|
notEvaluatedEntry(
|
|
247
253
|
KIND.TEST_COVERAGE,
|
|
248
|
-
|
|
249
|
-
'
|
|
254
|
+
t(
|
|
255
|
+
'覆盖率数据为空对象。这通常意味着测试运行没有加载到被测代码(常见于缺少 --all 或 --source),' +
|
|
256
|
+
'因此任何「未在数据中」的文件都必须按未覆盖处理,而这里连文件清单都没有',
|
|
257
|
+
'Coverage data is an empty object. This usually means the test run never loaded the code under test ' +
|
|
258
|
+
'(commonly a missing --all or --source), so any file "not in the data" must be treated as uncovered — ' +
|
|
259
|
+
'and here there is not even a file list'
|
|
260
|
+
)
|
|
250
261
|
)
|
|
251
262
|
);
|
|
252
263
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
@@ -260,15 +271,24 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
260
271
|
notEvaluated.push(
|
|
261
272
|
notEvaluatedEntry(
|
|
262
273
|
KIND.TEST_COVERAGE,
|
|
263
|
-
|
|
264
|
-
'
|
|
274
|
+
t(
|
|
275
|
+
'覆盖率数据不是可识别的报告形状(既无 c8/istanbul 的 fnMap/f,也无 coverage.py 的 functions)——' +
|
|
276
|
+
'它可能根本不是覆盖率文件,任何「符号未定位/未覆盖」的结论在此都不可信',
|
|
277
|
+
'Coverage data is not a recognizable report shape (neither c8/istanbul\'s fnMap/f nor coverage.py\'s functions) — ' +
|
|
278
|
+
'it may not be a coverage file at all, and any "symbol not located / not covered" conclusion is untrustworthy here'
|
|
279
|
+
)
|
|
265
280
|
)
|
|
266
281
|
);
|
|
267
282
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
268
283
|
}
|
|
269
284
|
|
|
270
285
|
if (targets.length === 0) {
|
|
271
|
-
notEvaluated.push(
|
|
286
|
+
notEvaluated.push(
|
|
287
|
+
notEvaluatedEntry(
|
|
288
|
+
KIND.TEST_COVERAGE,
|
|
289
|
+
t('没有指定要查询的符号(--symbol)', 'no symbol requested for query (--symbol)')
|
|
290
|
+
)
|
|
291
|
+
);
|
|
272
292
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
273
293
|
}
|
|
274
294
|
|
|
@@ -277,8 +297,12 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
277
297
|
notEvaluated.push(
|
|
278
298
|
notEvaluatedEntry(
|
|
279
299
|
KIND.TEST_COVERAGE,
|
|
280
|
-
|
|
281
|
-
|
|
300
|
+
t(
|
|
301
|
+
`${target.file} 命中默认排除规则(c8 排除 test/ 等目录),很可能根本不在覆盖率数据里。` +
|
|
302
|
+
'需要显式 --all(c8)或 --source(coverage.py)或调整 exclude 才能测到它',
|
|
303
|
+
`${target.file} matches a default exclusion rule (c8 excludes test/ etc.), so it is likely absent from the coverage data entirely. ` +
|
|
304
|
+
'Explicit --all (c8) or --source (coverage.py), or adjusting exclude, is required to measure it'
|
|
305
|
+
)
|
|
282
306
|
)
|
|
283
307
|
);
|
|
284
308
|
}
|
|
@@ -293,8 +317,12 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
293
317
|
id: `coverage-${++counter}`,
|
|
294
318
|
kind: KIND.TEST_COVERAGE,
|
|
295
319
|
statement:
|
|
296
|
-
|
|
297
|
-
|
|
320
|
+
t(
|
|
321
|
+
`${target.file} 完全没有出现在覆盖率数据中 —— 测试运行期间该文件未被加载,` +
|
|
322
|
+
`因此其中的符号 ${target.symbol} 未曾被执行`,
|
|
323
|
+
`${target.file} does not appear in the coverage data at all — the file was never loaded during the test run, ` +
|
|
324
|
+
`so the symbol ${target.symbol} inside it was never executed`
|
|
325
|
+
),
|
|
298
326
|
status: STATUS.ESTABLISHED,
|
|
299
327
|
evidence: { file: target.file },
|
|
300
328
|
method: 'command',
|
|
@@ -322,8 +350,12 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
322
350
|
id: `coverage-${++counter}`,
|
|
323
351
|
kind: KIND.TEST_COVERAGE,
|
|
324
352
|
statement:
|
|
325
|
-
|
|
326
|
-
|
|
353
|
+
t(
|
|
354
|
+
`符号 ${target.symbol} 在本次测试运行中一次都没有被调用(调用计数为 0)—— ` +
|
|
355
|
+
`任何依赖它的行为都没有被执行验证`,
|
|
356
|
+
`Symbol ${target.symbol} was never invoked in this test run (invocation count 0) — ` +
|
|
357
|
+
`any behavior depending on it was never exercised`
|
|
358
|
+
),
|
|
327
359
|
status: STATUS.ESTABLISHED,
|
|
328
360
|
evidence: { file: entryPath, line },
|
|
329
361
|
method: 'command',
|
|
@@ -341,10 +373,18 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
341
373
|
kind: KIND.TEST_COVERAGE,
|
|
342
374
|
statement:
|
|
343
375
|
isPy
|
|
344
|
-
?
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
? t(
|
|
377
|
+
`符号 ${target.symbol} 至少被调用过一次,但被调用**不能**证明任何特定调用点执行过 —— ` +
|
|
378
|
+
`本事实只能证伪,不能证实`,
|
|
379
|
+
`Symbol ${target.symbol} was invoked at least once, but being invoked **cannot** prove any specific call site ran — ` +
|
|
380
|
+
`this fact can only falsify, never confirm`
|
|
381
|
+
)
|
|
382
|
+
: t(
|
|
383
|
+
`符号 ${target.symbol} 被调用了 ${count} 次,但调用计数非零**不能**证明任何特定调用点执行过 —— ` +
|
|
384
|
+
`本事实只能证伪,不能证实`,
|
|
385
|
+
`Symbol ${target.symbol} was invoked ${count} time(s), but a non-zero invocation count **cannot** prove any specific call site ran — ` +
|
|
386
|
+
`this fact can only falsify, never confirm`
|
|
387
|
+
),
|
|
348
388
|
status: STATUS.UNKNOWN,
|
|
349
389
|
evidence: { file: entryPath, line },
|
|
350
390
|
method: 'command',
|
|
@@ -366,13 +406,19 @@ export async function collectCoverageFacts({ coverageFile, targets = [] } = {})
|
|
|
366
406
|
id: `coverage-${++counter}`,
|
|
367
407
|
kind: KIND.TEST_COVERAGE,
|
|
368
408
|
statement:
|
|
369
|
-
|
|
370
|
-
|
|
409
|
+
t(
|
|
410
|
+
`未能在覆盖率数据中定位符号 ${target.symbol}`,
|
|
411
|
+
`Could not locate symbol ${target.symbol} in the coverage data`
|
|
412
|
+
) +
|
|
413
|
+
(target.file ? t(`(限定文件 ${target.file})`, ` (restricted to file ${target.file})`) : '') +
|
|
371
414
|
(isPy
|
|
372
|
-
? ' —— 可能是被重命名、被内联,或它只在模块顶层出现'
|
|
373
|
-
:
|
|
415
|
+
? t(' —— 可能是被重命名、被内联,或它只在模块顶层出现', ' — it may have been renamed, inlined, or it only appears at module top level')
|
|
416
|
+
: t(
|
|
417
|
+
' —— 可能是被重命名、被内联,或它只在模块顶层出现(c8 把这类调用点放在 branchMap 而非 fnMap)',
|
|
418
|
+
' — it may have been renamed, inlined, or it only appears at module top level (c8 places such call sites in branchMap, not fnMap)'
|
|
419
|
+
)),
|
|
374
420
|
status: STATUS.UNKNOWN,
|
|
375
|
-
evidence: { file: target.file ?? '(未限定文件)' },
|
|
421
|
+
evidence: { file: target.file ?? t('(未限定文件)', '(unrestricted file)') },
|
|
376
422
|
method: 'static',
|
|
377
423
|
detail: { symbol: target.symbol, reason: 'symbol_not_located_in_fnmap' }
|
|
378
424
|
})
|
package/src/facts/deps.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { readFile, stat } from 'node:fs/promises';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { makeFact, notEvaluated as notEvaluatedEntry, KIND, STATUS, shellQuote } from '../contract.js';
|
|
27
|
+
import { T, DEFAULT_LANG } from '../lang.js';
|
|
27
28
|
|
|
28
29
|
/** Formats this producer can parse, and the filenames that select them. */
|
|
29
30
|
const SUPPORTED = [
|
|
@@ -149,7 +150,8 @@ function resolveGo(text, dep) {
|
|
|
149
150
|
* that cannot be read. The throw happens once, up front, so a malformed file
|
|
150
151
|
* becomes one notEvaluated reason instead of one failed fact per dependency.
|
|
151
152
|
*/
|
|
152
|
-
function makeResolver(type, text) {
|
|
153
|
+
function makeResolver(type, text, lang = DEFAULT_LANG) {
|
|
154
|
+
const t = T(lang);
|
|
153
155
|
if (type === 'npm') {
|
|
154
156
|
const root = JSON.parse(text); // throws on malformed JSON
|
|
155
157
|
// A syntactically valid but structurally empty file (e.g. `{}`) is not a
|
|
@@ -160,23 +162,38 @@ function makeResolver(type, text) {
|
|
|
160
162
|
typeof root !== 'object' ||
|
|
161
163
|
(root.packages === undefined && root.dependencies === undefined && root.lockfileVersion === undefined)
|
|
162
164
|
) {
|
|
163
|
-
throw new Error(
|
|
165
|
+
throw new Error(
|
|
166
|
+
t(
|
|
167
|
+
'package-lock.json 结构不完整(缺 lockfileVersion/packages/dependencies),不像真实的 npm 锁文件',
|
|
168
|
+
'package-lock.json is structurally incomplete (missing lockfileVersion/packages/dependencies); it does not look like a real npm lockfile'
|
|
169
|
+
)
|
|
170
|
+
);
|
|
164
171
|
}
|
|
165
172
|
return (dep) => resolveNpm(root, dep, text);
|
|
166
173
|
}
|
|
167
174
|
if (type === 'cargo') {
|
|
168
175
|
if (!/\[\[package\]\]|^version\s*=/m.test(text)) {
|
|
169
|
-
throw new Error(
|
|
176
|
+
throw new Error(
|
|
177
|
+
t(
|
|
178
|
+
'Cargo.lock 结构不完整(无 [[package]] 块或 version 头),不像真实的 Cargo 锁文件',
|
|
179
|
+
'Cargo.lock is structurally incomplete (no [[package]] blocks or version header); it does not look like a real Cargo lockfile'
|
|
180
|
+
)
|
|
181
|
+
);
|
|
170
182
|
}
|
|
171
183
|
return (dep) => resolveCargo(text, dep);
|
|
172
184
|
}
|
|
173
185
|
if (type === 'go') {
|
|
174
186
|
if (!/^module\s+\S+/m.test(text)) {
|
|
175
|
-
throw new Error(
|
|
187
|
+
throw new Error(
|
|
188
|
+
t(
|
|
189
|
+
'go.mod 结构不完整(无 module 行),不像真实的 go.mod',
|
|
190
|
+
'go.mod is structurally incomplete (no module line); it does not look like a real go.mod'
|
|
191
|
+
)
|
|
192
|
+
);
|
|
176
193
|
}
|
|
177
194
|
return (dep) => resolveGo(text, dep);
|
|
178
195
|
}
|
|
179
|
-
throw new Error(`不受支持的依赖清单格式: ${type}`);
|
|
196
|
+
throw new Error(t(`不受支持的依赖清单格式: ${type}`, `unsupported dependency manifest format: ${type}`));
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
function typeForFile(name) {
|
|
@@ -222,14 +239,20 @@ async function unsupportedIn(cwd) {
|
|
|
222
239
|
* @param {string} [options.lockfile] explicit manifest path (overrides detection)
|
|
223
240
|
* @param {string[]} options.deps dependency names to query (repeatable)
|
|
224
241
|
*/
|
|
225
|
-
export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, deps = [] } = {}) {
|
|
242
|
+
export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, deps = [], lang = DEFAULT_LANG } = {}) {
|
|
243
|
+
const t = T(lang);
|
|
226
244
|
const facts = [];
|
|
227
245
|
const evaluated = [];
|
|
228
246
|
const notEvaluated = [];
|
|
229
247
|
let counter = 0;
|
|
230
248
|
|
|
231
249
|
if (deps.length === 0) {
|
|
232
|
-
notEvaluated.push(
|
|
250
|
+
notEvaluated.push(
|
|
251
|
+
notEvaluatedEntry(
|
|
252
|
+
KIND.DEPENDENCY,
|
|
253
|
+
t('没有指定要查询的依赖(--dep)', 'no dependency requested for query (--dep)')
|
|
254
|
+
)
|
|
255
|
+
);
|
|
233
256
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
234
257
|
}
|
|
235
258
|
|
|
@@ -241,7 +264,10 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
241
264
|
await stat(filePath);
|
|
242
265
|
} catch {
|
|
243
266
|
notEvaluated.push(
|
|
244
|
-
notEvaluatedEntry(
|
|
267
|
+
notEvaluatedEntry(
|
|
268
|
+
KIND.DEPENDENCY,
|
|
269
|
+
t(`无法读取依赖清单 ${filePath}: ENOENT`, `cannot read dependency manifest ${filePath}: ENOENT`)
|
|
270
|
+
)
|
|
245
271
|
);
|
|
246
272
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
247
273
|
}
|
|
@@ -249,7 +275,10 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
249
275
|
notEvaluated.push(
|
|
250
276
|
notEvaluatedEntry(
|
|
251
277
|
KIND.DEPENDENCY,
|
|
252
|
-
|
|
278
|
+
t(
|
|
279
|
+
`${path.basename(filePath)} 不是受支持的依赖清单格式;本产出器支持 package-lock.json / Cargo.lock / go.mod`,
|
|
280
|
+
`${path.basename(filePath)} is not a supported dependency manifest format; this producer supports package-lock.json / Cargo.lock / go.mod`
|
|
281
|
+
)
|
|
253
282
|
)
|
|
254
283
|
);
|
|
255
284
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
@@ -260,8 +289,14 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
260
289
|
if (found.length === 0) {
|
|
261
290
|
const unsupported = await unsupportedIn(cwd);
|
|
262
291
|
const reason = unsupported.length
|
|
263
|
-
?
|
|
264
|
-
|
|
292
|
+
? t(
|
|
293
|
+
`检测到 ${unsupported.join('、')},但 Tier 0 暂不支持;本产出器支持 package-lock.json / Cargo.lock / go.mod`,
|
|
294
|
+
`detected ${unsupported.join(', ')}, but Tier 0 does not support them yet; this producer supports package-lock.json / Cargo.lock / go.mod`
|
|
295
|
+
)
|
|
296
|
+
: t(
|
|
297
|
+
'未找到受支持的依赖清单(package-lock.json / Cargo.lock / go.mod),没有可测量的锁定版本',
|
|
298
|
+
'no supported dependency manifest found (package-lock.json / Cargo.lock / go.mod); no pinned versions to measure'
|
|
299
|
+
);
|
|
265
300
|
notEvaluated.push(notEvaluatedEntry(KIND.DEPENDENCY, reason));
|
|
266
301
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
267
302
|
}
|
|
@@ -275,7 +310,10 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
275
310
|
notEvaluated.push(
|
|
276
311
|
notEvaluatedEntry(
|
|
277
312
|
KIND.DEPENDENCY,
|
|
278
|
-
|
|
313
|
+
t(
|
|
314
|
+
`同目录还检测到 ${unsupported.join('、')}(Tier 0 暂不支持),本次只测了 ${target.file}`,
|
|
315
|
+
`also detected ${unsupported.join(', ')} in the same directory (Tier 0 does not support them yet); only ${target.file} was measured this run`
|
|
316
|
+
)
|
|
279
317
|
)
|
|
280
318
|
);
|
|
281
319
|
}
|
|
@@ -286,26 +324,36 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
286
324
|
text = await readFile(target.path, 'utf8');
|
|
287
325
|
} catch (error) {
|
|
288
326
|
notEvaluated.push(
|
|
289
|
-
notEvaluatedEntry(
|
|
327
|
+
notEvaluatedEntry(
|
|
328
|
+
KIND.DEPENDENCY,
|
|
329
|
+
t(
|
|
330
|
+
`无法读取 ${target.path}: ${error.code ?? error.message}`,
|
|
331
|
+
`cannot read ${target.path}: ${error.code ?? error.message}`
|
|
332
|
+
)
|
|
333
|
+
)
|
|
290
334
|
);
|
|
291
335
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
let resolver;
|
|
295
339
|
try {
|
|
296
|
-
resolver = makeResolver(target.type, text);
|
|
340
|
+
resolver = makeResolver(target.type, text, lang);
|
|
297
341
|
} catch (error) {
|
|
298
342
|
notEvaluated.push(
|
|
299
343
|
notEvaluatedEntry(
|
|
300
344
|
KIND.DEPENDENCY,
|
|
301
|
-
|
|
345
|
+
t(
|
|
346
|
+
`${target.path} 解析失败(${target.type}):${error.message}`,
|
|
347
|
+
`${target.path} failed to parse (${target.type}): ${error.message}`
|
|
348
|
+
)
|
|
302
349
|
)
|
|
303
350
|
);
|
|
304
351
|
return { facts, evaluated, notEvaluated, measured: false };
|
|
305
352
|
}
|
|
306
353
|
|
|
307
|
-
const
|
|
308
|
-
const
|
|
354
|
+
const isGo = target.type === 'go';
|
|
355
|
+
const verb = isGo ? t('声明', 'declared') : t('锁定', 'locked');
|
|
356
|
+
const listLabel = isGo ? 'go.mod' : 'lockfile';
|
|
309
357
|
|
|
310
358
|
for (const dep of deps) {
|
|
311
359
|
const hits = resolver(dep);
|
|
@@ -319,10 +367,16 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
319
367
|
id: `dependency-${++counter}`,
|
|
320
368
|
kind: KIND.DEPENDENCY,
|
|
321
369
|
statement:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
370
|
+
t(
|
|
371
|
+
`依赖 ${dep} 在 ${listLabel}(${target.type})中被${verb}为版本 ${versions.join(' / ')}` +
|
|
372
|
+
(isGo
|
|
373
|
+
? ' —— 这是声明的需求版本,非最终解析版本(go.sum 不含版本,无法在此验证解析结果)'
|
|
374
|
+
: ''),
|
|
375
|
+
isGo
|
|
376
|
+
? `Dependency ${dep} is declared as version ${versions.join(' / ')} in the ${listLabel} (${target.type})` +
|
|
377
|
+
' — this is the declared requirement version, not the final resolved version (go.sum carries no versions, so the resolution cannot be verified here)'
|
|
378
|
+
: `Dependency ${dep} is locked to version ${versions.join(' / ')} in the ${listLabel} (${target.type})`
|
|
379
|
+
),
|
|
326
380
|
status: STATUS.ESTABLISHED,
|
|
327
381
|
evidence: { file: target.path, ...(first.line ? { line: first.line } : {}) },
|
|
328
382
|
method: 'command',
|
|
@@ -334,7 +388,7 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
334
388
|
// go.mod declares a requirement; calling it "resolved" would let a
|
|
335
389
|
// consumer mistake it for the final build version. The field name
|
|
336
390
|
// must not oversell what the source can prove.
|
|
337
|
-
...(
|
|
391
|
+
...(isGo ? { declaredVersions: versions } : { resolvedVersions: versions }),
|
|
338
392
|
locations: hits.map((h) => h.location)
|
|
339
393
|
}
|
|
340
394
|
})
|
|
@@ -344,7 +398,10 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
344
398
|
makeFact({
|
|
345
399
|
id: `dependency-${++counter}`,
|
|
346
400
|
kind: KIND.DEPENDENCY,
|
|
347
|
-
statement:
|
|
401
|
+
statement: t(
|
|
402
|
+
`依赖 ${dep} 未出现在 ${listLabel}(${target.type})的依赖树中 —— 声称它影响本应用的声明在此被证伪`,
|
|
403
|
+
`Dependency ${dep} does not appear in the dependency tree of the ${listLabel} (${target.type}) — the claim that it affects this application is falsified here`
|
|
404
|
+
),
|
|
348
405
|
status: STATUS.ESTABLISHED,
|
|
349
406
|
evidence: { file: target.path },
|
|
350
407
|
method: 'command',
|
|
@@ -352,7 +409,7 @@ export async function collectDependencyFacts({ cwd = process.cwd(), lockfile, de
|
|
|
352
409
|
detail: {
|
|
353
410
|
lockfileType: target.type,
|
|
354
411
|
dependency: dep,
|
|
355
|
-
...(
|
|
412
|
+
...(isGo ? { declaredVersions: [] } : { resolvedVersions: [] }),
|
|
356
413
|
reason: 'absent_from_lockfile'
|
|
357
414
|
}
|
|
358
415
|
})
|
package/src/facts/history.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { git, commitSummaries } from '../git.js';
|
|
13
13
|
import { makeFact, notEvaluated as notEvaluatedEntry, KIND, STATUS, shellQuote } from '../contract.js';
|
|
14
|
+
import { T, DEFAULT_LANG } from '../lang.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Strong signal: the commit is about security.
|
|
@@ -216,8 +217,10 @@ export async function collectHistoryFacts({
|
|
|
216
217
|
origins = false,
|
|
217
218
|
maxOrigins = 40,
|
|
218
219
|
securityPattern = SECURITY_PATTERN,
|
|
219
|
-
fixPattern = FIX_PATTERN
|
|
220
|
+
fixPattern = FIX_PATTERN,
|
|
221
|
+
lang = DEFAULT_LANG
|
|
220
222
|
} = {}) {
|
|
223
|
+
const t = T(lang);
|
|
221
224
|
const facts = [];
|
|
222
225
|
const evaluated = [];
|
|
223
226
|
const notEvaluated = [];
|
|
@@ -229,7 +232,13 @@ export async function collectHistoryFacts({
|
|
|
229
232
|
|
|
230
233
|
if (files.length === 0) {
|
|
231
234
|
notEvaluated.push(
|
|
232
|
-
notEvaluatedEntry(
|
|
235
|
+
notEvaluatedEntry(
|
|
236
|
+
'history',
|
|
237
|
+
t(
|
|
238
|
+
`范围 ${base}..${head} 内没有文件变更,没有可归属的删除行`,
|
|
239
|
+
`No file changes in ${base}..${head}; no deleted lines to attribute`
|
|
240
|
+
)
|
|
241
|
+
)
|
|
233
242
|
);
|
|
234
243
|
return { facts, evaluated, notEvaluated, files, measured: true };
|
|
235
244
|
}
|
|
@@ -271,8 +280,12 @@ export async function collectHistoryFacts({
|
|
|
271
280
|
notEvaluated.push(
|
|
272
281
|
notEvaluatedEntry(
|
|
273
282
|
'history',
|
|
274
|
-
|
|
275
|
-
|
|
283
|
+
t(
|
|
284
|
+
`范围 ${base}..${head} 内有 ${files.length} 个文件变更,但没有任何一行被删除,` +
|
|
285
|
+
'因此没有可归属的历史来源',
|
|
286
|
+
`Range ${base}..${head} changes ${files.length} file(s) but deletes no lines; ` +
|
|
287
|
+
'there is no attributable history source'
|
|
288
|
+
)
|
|
276
289
|
)
|
|
277
290
|
);
|
|
278
291
|
}
|
|
@@ -369,7 +382,10 @@ export async function collectHistoryFacts({
|
|
|
369
382
|
notEvaluated.push(
|
|
370
383
|
notEvaluatedEntry(
|
|
371
384
|
'history-origins',
|
|
372
|
-
|
|
385
|
+
t(
|
|
386
|
+
`git log -S 来源探针上限 ${maxOrigins} 已用尽,其余删除行保留 blame 归属`,
|
|
387
|
+
`git log -S origin probe cap (${maxOrigins}) exhausted; remaining deleted lines keep their blame attribution`
|
|
388
|
+
)
|
|
373
389
|
)
|
|
374
390
|
);
|
|
375
391
|
}
|
|
@@ -377,8 +393,12 @@ export async function collectHistoryFacts({
|
|
|
377
393
|
notEvaluated.push(
|
|
378
394
|
notEvaluatedEntry(
|
|
379
395
|
'history-origins',
|
|
380
|
-
|
|
381
|
-
|
|
396
|
+
t(
|
|
397
|
+
`${shortSkipped} 行内容过短(< ${MIN_PICKAXE_LINE_LENGTH} 字符),` +
|
|
398
|
+
'未做来源追溯(太通用时 -S 会指向整个历史的第一个提交而非真实来源),保留 blame 归属',
|
|
399
|
+
`${shortSkipped} line(s) too short (< ${MIN_PICKAXE_LINE_LENGTH} chars); ` +
|
|
400
|
+
'no origin tracing done (for generic content, -S points at the first commit of the whole history rather than a real origin); blame attribution kept'
|
|
401
|
+
)
|
|
382
402
|
)
|
|
383
403
|
);
|
|
384
404
|
}
|
|
@@ -386,8 +406,12 @@ export async function collectHistoryFacts({
|
|
|
386
406
|
notEvaluated.push(
|
|
387
407
|
notEvaluatedEntry(
|
|
388
408
|
'history-origins',
|
|
389
|
-
|
|
390
|
-
'
|
|
409
|
+
t(
|
|
410
|
+
'未启用 --origins,删除行的归属基于 git blame 的「最后修改」语义;' +
|
|
411
|
+
'被删行本身或提交 diff 含安全关键词时仍会标为 security',
|
|
412
|
+
'--origins not enabled; deleted lines are attributed by git blame\'s "last modified" semantics; ' +
|
|
413
|
+
'lines whose content or commit diff contains security vocabulary are still classified security'
|
|
414
|
+
)
|
|
391
415
|
)
|
|
392
416
|
);
|
|
393
417
|
}
|
|
@@ -395,8 +419,9 @@ export async function collectHistoryFacts({
|
|
|
395
419
|
// Split every blame group into its remainder (blame attribution) and its
|
|
396
420
|
// refined subsets (one fact per origin commit). Each becomes a fact.
|
|
397
421
|
const pending = [];
|
|
422
|
+
const unreadableSubject = () => t('(提交信息不可读)', '(unreadable commit message)');
|
|
398
423
|
for (const [hash, entry] of blameByCommit) {
|
|
399
|
-
const summary = summaries.get(hash) ?? { subject:
|
|
424
|
+
const summary = summaries.get(hash) ?? { subject: unreadableSubject(), author: null, date: null };
|
|
400
425
|
const groups = originGroups.get(hash);
|
|
401
426
|
if (!groups) {
|
|
402
427
|
pending.push({ hash, entry, summary, originMethod: 'blame' });
|
|
@@ -436,7 +461,7 @@ export async function collectHistoryFacts({
|
|
|
436
461
|
lines: [...group.byFile.values()].reduce((sum, a) => sum + a.length, 0),
|
|
437
462
|
byFile: group.byFile
|
|
438
463
|
},
|
|
439
|
-
summary: group.summary ?? { subject:
|
|
464
|
+
summary: group.summary ?? { subject: unreadableSubject(), author: null, date: null },
|
|
440
465
|
originMethod: 'pickaxe',
|
|
441
466
|
blameCommit: hash,
|
|
442
467
|
contents: group.contents
|
|
@@ -493,13 +518,25 @@ export async function collectHistoryFacts({
|
|
|
493
518
|
// the wrong place and makes an accurate attribution look wrong.
|
|
494
519
|
const originPhrase =
|
|
495
520
|
originMethod === 'pickaxe'
|
|
496
|
-
?
|
|
497
|
-
|
|
498
|
-
|
|
521
|
+
? t(
|
|
522
|
+
`本次变更删除了 ${entry.lines} 行代码,其内容最初由提交 ${hash.slice(0, 10)} 引入` +
|
|
523
|
+
`(git blame 的最后修改者是 ${blameCommit.slice(0, 10)})`,
|
|
524
|
+
`This change deleted ${entry.lines} lines whose content was first introduced by commit ${hash.slice(0, 10)} ` +
|
|
525
|
+
`(git blame's last modifier is ${blameCommit.slice(0, 10)})`
|
|
526
|
+
)
|
|
527
|
+
: t(
|
|
528
|
+
`本次变更删除了 ${entry.lines} 行来自提交 ${hash.slice(0, 10)} 的代码`,
|
|
529
|
+
`This change deleted ${entry.lines} lines of code from commit ${hash.slice(0, 10)}`
|
|
530
|
+
);
|
|
499
531
|
const statement =
|
|
500
532
|
originPhrase +
|
|
501
|
-
(fileCount > 1
|
|
502
|
-
|
|
533
|
+
(fileCount > 1
|
|
534
|
+
? t(`,分布在 ${fileCount} 个文件`, `, spread across ${fileCount} files`)
|
|
535
|
+
: t(`(文件:${byFile[0].file})`, ` (file: ${byFile[0].file})`)) +
|
|
536
|
+
t(
|
|
537
|
+
`。提交信息:${JSON.stringify(summary.subject)},分类:${classification}`,
|
|
538
|
+
`. Commit message: ${JSON.stringify(summary.subject)}, classification: ${classification}`
|
|
539
|
+
);
|
|
503
540
|
|
|
504
541
|
const detail = {
|
|
505
542
|
classification,
|
|
@@ -514,8 +551,14 @@ export async function collectHistoryFacts({
|
|
|
514
551
|
// covers lines in the other files too.
|
|
515
552
|
reproductionNote:
|
|
516
553
|
fileCount > 1
|
|
517
|
-
?
|
|
518
|
-
|
|
554
|
+
? t(
|
|
555
|
+
`上面的命令只复现「${primary.file}」中的归属;其余 ${fileCount - 1} 个文件的行区间见 byFile`,
|
|
556
|
+
`The command above only reproduces the attribution for "${primary.file}"; the other ${fileCount - 1} file(s) line ranges are in byFile`
|
|
557
|
+
)
|
|
558
|
+
: t(
|
|
559
|
+
'上面的命令复现本事实涉及的全部行',
|
|
560
|
+
'The command above reproduces every line this fact covers'
|
|
561
|
+
)
|
|
519
562
|
};
|
|
520
563
|
|
|
521
564
|
if (originMethod === 'pickaxe') {
|
|
@@ -552,7 +595,13 @@ export async function collectHistoryFacts({
|
|
|
552
595
|
|
|
553
596
|
if (ranked.length === 0 && linesByFile.size > 0) {
|
|
554
597
|
notEvaluated.push(
|
|
555
|
-
notEvaluatedEntry(
|
|
598
|
+
notEvaluatedEntry(
|
|
599
|
+
'history',
|
|
600
|
+
t(
|
|
601
|
+
'有删除行但 blame 未能归属到任何提交(可能是二进制文件或路径异常)',
|
|
602
|
+
'Deleted lines exist but blame could not attribute them to any commit (possibly binary files or path anomalies)'
|
|
603
|
+
)
|
|
604
|
+
)
|
|
556
605
|
);
|
|
557
606
|
}
|
|
558
607
|
|
|
@@ -565,7 +614,8 @@ export async function collectHistoryFacts({
|
|
|
565
614
|
maxPickaxe,
|
|
566
615
|
securityPattern,
|
|
567
616
|
fixPattern,
|
|
568
|
-
startCounter: counter
|
|
617
|
+
startCounter: counter,
|
|
618
|
+
lang
|
|
569
619
|
});
|
|
570
620
|
facts.push(...reintro.facts);
|
|
571
621
|
counter = reintro.counter;
|
|
@@ -573,7 +623,10 @@ export async function collectHistoryFacts({
|
|
|
573
623
|
notEvaluated.push(...reintro.notEvaluated);
|
|
574
624
|
} else {
|
|
575
625
|
notEvaluated.push(
|
|
576
|
-
notEvaluatedEntry(
|
|
626
|
+
notEvaluatedEntry(
|
|
627
|
+
'reintroduction',
|
|
628
|
+
t('未启用 --pickaxe,未检查「被移除又加回」的代码行', '--pickaxe not enabled; reintroduced lines were not checked')
|
|
629
|
+
)
|
|
577
630
|
);
|
|
578
631
|
}
|
|
579
632
|
|
|
@@ -602,8 +655,10 @@ async function collectReintroductionFacts({
|
|
|
602
655
|
maxPickaxe,
|
|
603
656
|
securityPattern,
|
|
604
657
|
fixPattern,
|
|
605
|
-
startCounter
|
|
658
|
+
startCounter,
|
|
659
|
+
lang = DEFAULT_LANG
|
|
606
660
|
}) {
|
|
661
|
+
const t = T(lang);
|
|
607
662
|
const facts = [];
|
|
608
663
|
const notEvaluated = [];
|
|
609
664
|
let counter = startCounter;
|
|
@@ -654,9 +709,15 @@ async function collectReintroductionFacts({
|
|
|
654
709
|
id: `reintroduction-${++counter}`,
|
|
655
710
|
kind: KIND.REINTRODUCTION,
|
|
656
711
|
statement:
|
|
657
|
-
|
|
712
|
+
t(
|
|
713
|
+
`本次变更加回了一行在 ${base} 中不存在的代码,而该行的出现次数曾被 ${hashes.length} 个提交改变`,
|
|
714
|
+
`This change re-adds a line that did not exist at ${base}, and whose occurrence count was changed by ${hashes.length} commit(s)`
|
|
715
|
+
) +
|
|
658
716
|
(securityOrigin
|
|
659
|
-
?
|
|
717
|
+
? t(
|
|
718
|
+
`,其中提交 ${securityOrigin.hash.slice(0, 10)} 的提交信息含安全关键词`,
|
|
719
|
+
`, and commit ${securityOrigin.hash.slice(0, 10)} has security vocabulary in its message`
|
|
720
|
+
)
|
|
660
721
|
: ''),
|
|
661
722
|
status: STATUS.ESTABLISHED,
|
|
662
723
|
evidence: { file, snippet: line.slice(0, 160), commit: hashes[0] },
|
|
@@ -682,7 +743,10 @@ async function collectReintroductionFacts({
|
|
|
682
743
|
notEvaluated.push(
|
|
683
744
|
notEvaluatedEntry(
|
|
684
745
|
'reintroduction',
|
|
685
|
-
|
|
746
|
+
t(
|
|
747
|
+
`pickaxe 探针上限 ${maxPickaxe} 已用尽,另有 ${candidates - probed} 行新增代码未检查`,
|
|
748
|
+
`pickaxe probe cap (${maxPickaxe}) exhausted; ${candidates - probed} added line(s) were not checked`
|
|
749
|
+
)
|
|
686
750
|
)
|
|
687
751
|
);
|
|
688
752
|
}
|