euthyna 0.2.0 → 0.3.1
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 +1 -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/README.md +54 -7
- package/__init__.py +91 -0
- package/bin/euthyna.js +13 -3
- package/package.json +6 -1
- package/plugin/index.js +30 -1
- 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/contract.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { stripVTControlCharacters } from 'node:util';
|
|
18
|
+
import { T, DEFAULT_LANG } from './lang.js';
|
|
18
19
|
|
|
19
20
|
export const SCHEMA_VERSION = '0.1';
|
|
20
21
|
|
|
@@ -189,10 +190,11 @@ export function notEvaluated(kind, reason) {
|
|
|
189
190
|
* Ordering is deliberate: what was NOT measured comes last but is never
|
|
190
191
|
* omitted, so a reader cannot finish the output without seeing it.
|
|
191
192
|
*/
|
|
192
|
-
export function renderReport(report, { write: rawWrite = console.log } = {}) {
|
|
193
|
+
export function renderReport(report, { write: rawWrite = console.log, lang = DEFAULT_LANG } = {}) {
|
|
193
194
|
// Render boundary: every repo-controlled string below passes through safeText
|
|
194
195
|
// exactly once, here, rather than at each producer. The JSON channel
|
|
195
196
|
// (cli.js) needs no equivalent — JSON.stringify escapes control bytes.
|
|
197
|
+
const t = T(lang);
|
|
196
198
|
const write = (line) => rawWrite(safeText(line));
|
|
197
199
|
const { producer, subject, facts, coverage } = report;
|
|
198
200
|
|
|
@@ -202,9 +204,9 @@ export function renderReport(report, { write: rawWrite = console.log } = {}) {
|
|
|
202
204
|
|
|
203
205
|
write('');
|
|
204
206
|
write(`euthyna ${producer.name} — ${producer.purpose || ''}`.trim());
|
|
205
|
-
write(
|
|
207
|
+
write(`${t('目标: ', 'Target: ')}${subject.repo ?? subject.coverageFile ?? t('(未指定)', '(unspecified)')}`);
|
|
206
208
|
if (subject.base || subject.head) {
|
|
207
|
-
write(
|
|
209
|
+
write(`${t('范围: ', 'Range: ')}${subject.base ?? '?'}..${subject.head ?? 'HEAD'}`);
|
|
208
210
|
}
|
|
209
211
|
write('');
|
|
210
212
|
|
|
@@ -214,26 +216,26 @@ export function renderReport(report, { write: rawWrite = console.log } = {}) {
|
|
|
214
216
|
for (const fact of list) {
|
|
215
217
|
write(` • ${fact.statement}`);
|
|
216
218
|
const where = [fact.evidence.file, fact.evidence.line].filter(Boolean).join(':');
|
|
217
|
-
write(`
|
|
218
|
-
if (fact.command) write(`
|
|
219
|
+
write(` ${t('证据: ', 'Evidence: ')}${where}${fact.evidence.commit ? ` (${fact.evidence.commit.slice(0, 10)})` : ''}`);
|
|
220
|
+
if (fact.command) write(` ${t('复现: ', 'Reproduce: ')}${fact.command}`);
|
|
219
221
|
if (fact.confidence === 'approximate') {
|
|
220
|
-
write(' ⚠ approximate —— 不得用于门禁判定');
|
|
222
|
+
write(t(' ⚠ approximate —— 不得用于门禁判定', ' ⚠ approximate — must not be used for gate adjudication'));
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
write('');
|
|
224
226
|
};
|
|
225
227
|
|
|
226
|
-
section('已确证', established);
|
|
227
|
-
section('已证伪', refuted);
|
|
228
|
-
section('无法判定', unknown);
|
|
228
|
+
section(t('已确证', 'Established'), established);
|
|
229
|
+
section(t('已证伪', 'Refuted'), refuted);
|
|
230
|
+
section(t('无法判定', 'Unknown'), unknown);
|
|
229
231
|
|
|
230
232
|
if (facts.length === 0) {
|
|
231
|
-
write(' 本次运行没有产出任何事实。');
|
|
233
|
+
write(t(' 本次运行没有产出任何事实。', ' This run produced no facts.'));
|
|
232
234
|
write('');
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
if (coverage.notEvaluated.length > 0) {
|
|
236
|
-
write('⚠ 未评估的判据(缺数据不等于干净):');
|
|
238
|
+
write(t('⚠ 未评估的判据(缺数据不等于干净):', '⚠ Criteria not evaluated (missing data is not clean):'));
|
|
237
239
|
for (const item of coverage.notEvaluated) {
|
|
238
240
|
write(` • ${item.kind}: ${item.reason}`);
|
|
239
241
|
}
|
|
@@ -241,9 +243,14 @@ export function renderReport(report, { write: rawWrite = console.log } = {}) {
|
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
if (coverage.evaluated.length > 0) {
|
|
244
|
-
write('已评估的判据:');
|
|
246
|
+
write(t('已评估的判据:', 'Evaluated criteria:'));
|
|
245
247
|
for (const item of coverage.evaluated) {
|
|
246
|
-
write(
|
|
248
|
+
write(
|
|
249
|
+
t(
|
|
250
|
+
` • ${item.kind}: ${item.count} 条 (${item.producer})`,
|
|
251
|
+
` • ${item.kind}: ${item.count} (${item.producer})`
|
|
252
|
+
)
|
|
253
|
+
);
|
|
247
254
|
}
|
|
248
255
|
write('');
|
|
249
256
|
}
|
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
|
})
|