jules-orchestrator-kit 0.71.0 → 0.72.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/src/config.mjs CHANGED
@@ -518,9 +518,9 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
518
518
  return "npm";
519
519
  }
520
520
 
521
- import { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
521
+ import { buildProfileStages, buildDefaultStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
522
522
 
523
- export { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
523
+ export { buildProfileStages, buildDefaultStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
524
524
 
525
525
  import {
526
526
  detectPolyglotStack,
@@ -529,6 +529,7 @@ import {
529
529
  findSubprojectRoot,
530
530
  detectCrossPackageBoundaryViolations,
531
531
  detectCircularDependencies,
532
+ isPlaceholderTestScript,
532
533
  } from "./stack-detector.mjs";
533
534
 
534
535
  export {
@@ -538,8 +539,11 @@ export {
538
539
  findSubprojectRoot,
539
540
  detectCrossPackageBoundaryViolations,
540
541
  detectCircularDependencies,
542
+ isPlaceholderTestScript,
541
543
  };
542
544
 
545
+ import { showFromOrigin, detectDefaultBranch } from "./git.mjs";
546
+
543
547
  /**
544
548
  * Autodetects verification test/build commands across 24+ polyglot tech stacks.
545
549
  */
@@ -788,6 +792,7 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
788
792
  // Named so `doctor` can tell an operator that the gate they enabled is
789
793
  // running one fewer check than they think, and why.
790
794
  profileSkipped: profilePlan?.skipped ?? [],
795
+ minTests: parsed.verify?.minTests ?? parsed.verify?.min_tests ?? (parsed.minTests ?? parsed.min_tests ?? 1),
791
796
  timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 300_000,
792
797
  },
793
798
  evidence: {
@@ -865,3 +870,342 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
865
870
 
866
871
  return config;
867
872
  }
873
+
874
+ /**
875
+ * Verifies that a bootstrap scaffold (introduced in a repository whose base has
876
+ * no gate configuration yet) does not weaken verification, lower profiles,
877
+ * bypass tests, disable strict locks, or tamper with security invariants (F06, F08).
878
+ *
879
+ * @param {string} root - repository root
880
+ * @param {object} proposedConfig - parsed .agent/config.yml if present
881
+ * @param {object} proposedJules - parsed .agent/jules.yml if present
882
+ * @param {object} autoVerify - autodetected verification commands
883
+ * @returns {{ ok: boolean, error?: string }}
884
+ */
885
+ export function checkBootstrapPolicyIntegrity(root, proposedConfig = {}, proposedJules = {}, autoVerify = {}) {
886
+ const pConfig = proposedConfig || {};
887
+ const pJules = proposedJules || {};
888
+
889
+ const testCmd = pConfig.verify?.test ?? pConfig.test_cmd ?? pJules.test_cmd ?? pJules.verify?.test;
890
+ if (testCmd !== undefined && testCmd !== null) {
891
+ const str = String(testCmd).trim();
892
+ if (str && isPlaceholderTestScript(str)) {
893
+ return {
894
+ ok: false,
895
+ error: `Bootstrap policy rejected: verify.test cannot be a placeholder command (${JSON.stringify(str)}) in uncommitted scaffold (F06).`,
896
+ };
897
+ }
898
+ if (!str && autoVerify.test) {
899
+ return {
900
+ ok: false,
901
+ error: `Bootstrap policy rejected: verify.test cannot be empty when repository has detected tests (${autoVerify.test}).`,
902
+ };
903
+ }
904
+ }
905
+
906
+ const profile = pConfig.verify?.profile ?? pConfig.profile ?? pJules.verify?.profile ?? pJules.profile;
907
+ if (profile && String(profile).toLowerCase() === "minimal") {
908
+ return {
909
+ ok: false,
910
+ error: 'Bootstrap policy rejected: verify.profile cannot be lowered to "minimal" in uncommitted scaffold (F06).',
911
+ };
912
+ }
913
+
914
+ const required = pConfig.verify?.required ?? pJules.verify?.required;
915
+ if (required === false) {
916
+ return {
917
+ ok: false,
918
+ error: "Bootstrap policy rejected: verify.required cannot be set to false in uncommitted scaffold.",
919
+ };
920
+ }
921
+
922
+ const strictLock =
923
+ pConfig.evidence?.strict_test_lock ??
924
+ pConfig.evidence?.strictTestLock ??
925
+ pJules.evidence?.strict_test_lock ??
926
+ pJules.evidence?.strictTestLock;
927
+ if (strictLock === false) {
928
+ return {
929
+ ok: false,
930
+ error: "Bootstrap policy rejected: evidence.strictTestLock cannot be disabled in uncommitted scaffold.",
931
+ };
932
+ }
933
+
934
+ const tamperGuard =
935
+ pConfig.verify?.tamper_guard ??
936
+ pConfig.verify?.tamperGuard ??
937
+ pJules.verify?.tamper_guard ??
938
+ pJules.verify?.tamperGuard;
939
+ if (tamperGuard === false) {
940
+ return {
941
+ ok: false,
942
+ error: "Bootstrap policy rejected: verify.tamperGuard cannot be disabled in uncommitted scaffold.",
943
+ };
944
+ }
945
+
946
+ const baseBranch = pConfig.base_branch ?? pConfig.baseBranch ?? pJules.base_branch ?? pJules.baseBranch;
947
+ if (baseBranch && String(baseBranch).toUpperCase() === "HEAD") {
948
+ return {
949
+ ok: false,
950
+ error: 'Bootstrap policy rejected: base_branch cannot be set to "HEAD" (F08).',
951
+ };
952
+ }
953
+
954
+ const stages = pConfig.verify?.stages ?? pJules.verify?.stages;
955
+ if (Array.isArray(stages)) {
956
+ if (stages.length === 0) {
957
+ return {
958
+ ok: false,
959
+ error: "Bootstrap policy rejected: verify.stages cannot be empty in uncommitted scaffold.",
960
+ };
961
+ }
962
+ const hasTestStage = stages.some(
963
+ (s) => (s.kind === "test" || s.kind === "unit") && s.cmd && !isPlaceholderTestScript(s.cmd)
964
+ );
965
+ if (!hasTestStage && autoVerify.test) {
966
+ return {
967
+ ok: false,
968
+ error: "Bootstrap policy rejected: verify.stages must include a valid test/unit command in uncommitted scaffold.",
969
+ };
970
+ }
971
+ }
972
+
973
+ return { ok: true };
974
+ }
975
+
976
+ /**
977
+ * Resolves the authoritative security policy and verification plan from the
978
+ * trusted base commit (or pristine bootstrap baseline), guaranteeing that
979
+ * untrusted edits in the working tree, staged index, or branch diff under
980
+ * review cannot weaken verification, disable security guards, or redirect
981
+ * the trusted base (F06, F07, F08).
982
+ *
983
+ * @param {string} root - repository root
984
+ * @param {string} [baseRef] - explicit base reference (e.g. from CLI --base)
985
+ * @param {string} [mode="working-tree"] - "working-tree" | "staged" | "committed"
986
+ * @param {object} [opts={}] - additional options (e.g. allowProtected, config)
987
+ * @returns {{
988
+ * ok: boolean,
989
+ * code?: number,
990
+ * error?: string,
991
+ * base: string,
992
+ * scope: { deny: string[], allow: string[], protect: string[] },
993
+ * verify: object,
994
+ * limits: object,
995
+ * evidence: { strictTestLock: boolean },
996
+ * isBootstrap: boolean,
997
+ * rawConfig: string | null,
998
+ * }}
999
+ */
1000
+ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode = "working-tree", opts = {}) {
1001
+ // 1. Resolve base branch
1002
+ let base = baseRef || opts.base;
1003
+ if (!base) {
1004
+ if (mode === "committed") {
1005
+ base = detectDefaultBranch(root) || "main";
1006
+ } else {
1007
+ const diskConfig = opts.config || (existsSync(join(root, ".agent/config.yml")) ? loadConfig(root) : null);
1008
+ base = diskConfig?.baseBranch || detectDefaultBranch(root) || "main";
1009
+ }
1010
+ }
1011
+
1012
+ // F08 Guard: In committed mode, base branch can never be HEAD
1013
+ // (Comparing a commit against itself would produce an empty diff and bypass the gate)
1014
+ if (mode === "committed" && (base === "HEAD" || base === "HEAD^0")) {
1015
+ return {
1016
+ ok: false,
1017
+ code: 3,
1018
+ error: 'Cannot evaluate committed diff against base "HEAD": comparing a revision to itself would bypass the safety gate (F08).',
1019
+ base,
1020
+ isBootstrap: false,
1021
+ };
1022
+ }
1023
+
1024
+ let trustedConfigRaw = null;
1025
+ let trustedJulesRaw = null;
1026
+ try {
1027
+ trustedConfigRaw = showFromOrigin(root, base, ".agent/config.yml");
1028
+ trustedJulesRaw = showFromOrigin(root, base, ".agent/jules.yml");
1029
+ } catch (_) {
1030
+ // If base branch cannot be resolved or show fails, let changedFiles report code 1
1031
+ }
1032
+
1033
+ const autoVerify = resolveVerify(root);
1034
+
1035
+ if (trustedConfigRaw || trustedJulesRaw) {
1036
+ const raw = trustedConfigRaw || trustedJulesRaw;
1037
+ let parsed = {};
1038
+ try {
1039
+ if (raw) parsed = parseYaml(raw) || {};
1040
+ } catch (_) {}
1041
+
1042
+ const trustedScope = normalizeScope(parsed);
1043
+ const trustedDiffKb = Number(parsed.limits?.diff_kb || parsed.limits?.diffKb) || 75;
1044
+
1045
+ const rawSetup = parsed.setup_cmd ?? parsed.verify?.setup;
1046
+ const rawTest = parsed.test_cmd ?? parsed.verify?.test;
1047
+ const rawLint = parsed.lint_cmd ?? parsed.verify?.lint;
1048
+ const rawFuzz = parsed.fuzz_cmd ?? parsed.verify?.fuzz;
1049
+ const rawInvariant = parsed.invariant_cmd ?? parsed.verify?.invariant;
1050
+ const rawE2e = parsed.e2e_cmd ?? parsed.verify?.e2e;
1051
+ const rawTeardown = parsed.teardown_cmd ?? parsed.verify?.teardown;
1052
+ const rawBuild = parsed.build_cmd ?? parsed.verify?.build;
1053
+ const rawUnit = parsed.verify?.unit;
1054
+ const verifyTimeoutMs = parsed.verify?.timeoutMs ?? parsed.verify?.timeout_ms ?? 300_000;
1055
+
1056
+ const mergedVerify = {
1057
+ stack: autoVerify.stack || "unknown",
1058
+ setup: rawSetup ?? autoVerify.setup ?? "",
1059
+ lint: rawLint ?? autoVerify.lint ?? "",
1060
+ test: rawTest ?? autoVerify.test,
1061
+ unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
1062
+ fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
1063
+ invariant: rawInvariant ?? autoVerify.invariant ?? "",
1064
+ e2e: rawE2e ?? autoVerify.e2e ?? "",
1065
+ teardown: rawTeardown ?? autoVerify.teardown ?? "",
1066
+ build: rawBuild ?? autoVerify.build,
1067
+ policy: parsed.verify?.policy ?? autoVerify.policy,
1068
+ scope: parsed.verify?.scope === "affected" ? "affected" : "global",
1069
+ required: parsed.verify?.required !== undefined ? parsed.verify.required !== false : true,
1070
+ minTests: parsed.verify?.minTests ?? parsed.verify?.min_tests ?? 1,
1071
+ tamperGuard: parsed.verify?.tamperGuard ?? parsed.verify?.tamper_guard ?? true,
1072
+ timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 300_000,
1073
+ };
1074
+
1075
+ const rawProfile = parsed.verify?.profile ?? parsed.profile ?? null;
1076
+ const explicitStages = parsed.verify?.stages ?? null;
1077
+ const profilePlan =
1078
+ !explicitStages && rawProfile
1079
+ ? buildProfileStages(rawProfile, { stack: autoVerify.stack, verify: mergedVerify })
1080
+ : null;
1081
+
1082
+ const stages = explicitStages ?? profilePlan?.stages ?? null;
1083
+
1084
+ return {
1085
+ ok: true,
1086
+ base,
1087
+ scope: trustedScope,
1088
+ verify: {
1089
+ ...mergedVerify,
1090
+ stages,
1091
+ profile: profilePlan?.profile ?? (rawProfile ? String(rawProfile).toLowerCase() : null),
1092
+ profileSkipped: profilePlan?.skipped ?? [],
1093
+ },
1094
+ limits: { diffKb: trustedDiffKb },
1095
+ evidence: {
1096
+ strictTestLock: parsed.evidence?.strict_test_lock ?? parsed.evidence?.strictTestLock ?? true,
1097
+ },
1098
+ isBootstrap: false,
1099
+ rawConfig: raw,
1100
+ };
1101
+ }
1102
+
1103
+ // Bootstrap mode: base has no configuration file
1104
+ // Inspect any proposed scaffold in this change (F06)
1105
+ let proposedConfig = null;
1106
+ let proposedJules = null;
1107
+
1108
+ try {
1109
+ if (mode === "committed") {
1110
+ const c = showFromOrigin(root, "HEAD", ".agent/config.yml");
1111
+ if (c) proposedConfig = parseYaml(c);
1112
+ const j = showFromOrigin(root, "HEAD", ".agent/jules.yml");
1113
+ if (j) proposedJules = parseYaml(j);
1114
+ } else {
1115
+ const configPath = join(root, ".agent/config.yml");
1116
+ if (existsSync(configPath)) proposedConfig = parseYaml(readFileSync(configPath, "utf-8"));
1117
+ const julesPath = join(root, ".agent/jules.yml");
1118
+ if (existsSync(julesPath)) proposedJules = parseYaml(readFileSync(julesPath, "utf-8"));
1119
+ }
1120
+ } catch (_) {}
1121
+
1122
+ const effectiveProposed = proposedConfig || proposedJules || opts.config || null;
1123
+
1124
+ if (proposedConfig || proposedJules) {
1125
+ const integrity = checkBootstrapPolicyIntegrity(root, proposedConfig, proposedJules, autoVerify);
1126
+ if (!integrity.ok) {
1127
+ return {
1128
+ ok: false,
1129
+ code: 3,
1130
+ error: integrity.error,
1131
+ base,
1132
+ scope: normalizeScope({}),
1133
+ verify: {
1134
+ ...autoVerify,
1135
+ stages: null,
1136
+ required: true,
1137
+ tamperGuard: true,
1138
+ minTests: 1,
1139
+ timeoutMs: 300_000,
1140
+ },
1141
+ limits: { diffKb: 75 },
1142
+ evidence: { strictTestLock: true },
1143
+ isBootstrap: true,
1144
+ rawConfig: null,
1145
+ };
1146
+ }
1147
+ }
1148
+
1149
+ const pConfig = effectiveProposed || {};
1150
+ const rawSetup = pConfig.setup_cmd ?? pConfig.verify?.setup;
1151
+ const rawTest = pConfig.test_cmd ?? pConfig.verify?.test;
1152
+ const rawLint = pConfig.lint_cmd ?? pConfig.verify?.lint;
1153
+ const rawFuzz = pConfig.fuzz_cmd ?? pConfig.verify?.fuzz;
1154
+ const rawInvariant = pConfig.invariant_cmd ?? pConfig.verify?.invariant;
1155
+ const rawE2e = pConfig.e2e_cmd ?? pConfig.verify?.e2e;
1156
+ const rawTeardown = pConfig.teardown_cmd ?? pConfig.verify?.teardown;
1157
+ const rawBuild = pConfig.build_cmd ?? pConfig.verify?.build;
1158
+ const rawUnit = pConfig.verify?.unit;
1159
+ const verifyTimeoutMs = pConfig.verify?.timeoutMs ?? pConfig.verify?.timeout_ms ?? 300_000;
1160
+
1161
+ const mergedBootstrapVerify = {
1162
+ stack: autoVerify.stack || "unknown",
1163
+ setup: rawSetup ?? autoVerify.setup ?? "",
1164
+ lint: rawLint ?? autoVerify.lint ?? "",
1165
+ test: rawTest ?? autoVerify.test,
1166
+ unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
1167
+ fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
1168
+ invariant: rawInvariant ?? autoVerify.invariant ?? "",
1169
+ e2e: rawE2e ?? autoVerify.e2e ?? "",
1170
+ teardown: rawTeardown ?? autoVerify.teardown ?? "",
1171
+ build: rawBuild ?? autoVerify.build,
1172
+ policy: pConfig.verify?.policy ?? autoVerify.policy,
1173
+ scope: pConfig.verify?.scope === "affected" ? "affected" : "global",
1174
+ required: pConfig.verify?.required !== undefined ? pConfig.verify.required !== false : true,
1175
+ minTests: pConfig.verify?.minTests ?? pConfig.verify?.min_tests ?? 1,
1176
+ tamperGuard: pConfig.verify?.tamperGuard ?? pConfig.verify?.tamper_guard ?? true,
1177
+ timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 300_000,
1178
+ };
1179
+
1180
+ const rawProfile = pConfig.verify?.profile ?? pConfig.profile ?? null;
1181
+ const explicitStages = pConfig.verify?.stages ?? null;
1182
+ const profilePlan =
1183
+ !explicitStages && rawProfile
1184
+ ? buildProfileStages(rawProfile, { stack: autoVerify.stack, verify: mergedBootstrapVerify })
1185
+ : null;
1186
+
1187
+ const stages = explicitStages ?? profilePlan?.stages ?? null;
1188
+ const trustedScope = normalizeScope(pConfig);
1189
+ const trustedLimits = {
1190
+ diffKb: Number(pConfig.limits?.diff_kb || pConfig.limits?.diffKb) || 75,
1191
+ };
1192
+ const trustedEvidence = {
1193
+ strictTestLock: pConfig.evidence?.strict_test_lock ?? pConfig.evidence?.strictTestLock ?? true,
1194
+ };
1195
+
1196
+ return {
1197
+ ok: true,
1198
+ base,
1199
+ scope: trustedScope,
1200
+ verify: {
1201
+ ...mergedBootstrapVerify,
1202
+ stages,
1203
+ profile: profilePlan?.profile ?? (rawProfile ? String(rawProfile).toLowerCase() : null),
1204
+ profileSkipped: profilePlan?.skipped ?? [],
1205
+ },
1206
+ limits: trustedLimits,
1207
+ evidence: trustedEvidence,
1208
+ isBootstrap: true,
1209
+ rawConfig: null,
1210
+ };
1211
+ }
package/src/coverage.mjs CHANGED
@@ -31,6 +31,42 @@ export function isExcludedFromCoverage(filePath = "") {
31
31
  return false;
32
32
  }
33
33
 
34
+ /**
35
+ * Extensions V8 could have observed. Coverage outside this family is not a
36
+ * measurement that returned nothing — it is no measurement at all, and a
37
+ * hard red there is how the gate gets switched off.
38
+ */
39
+ const V8_OBSERVABLE_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"]);
40
+
41
+ const isV8Observable = (file) => {
42
+ const dot = file.lastIndexOf(".");
43
+ return dot !== -1 && V8_OBSERVABLE_EXT.has(file.slice(dot).toLowerCase());
44
+ };
45
+
46
+ /**
47
+ * Lines of source text that are executable on their own — the denominator a
48
+ * line coverage measure would count. V8's own mapper only emits entries for
49
+ * lines its function ranges *observed*; a file Node never imported has no
50
+ * functions at all, so every executable line in it was previously invisible
51
+ * and a 0/0 diff was reported as a passing 100% (F12). The heuristic stays
52
+ * deliberately conservative: comments, blank lines, and pure declarations
53
+ * (imports, exports of names alone) do not count.
54
+ */
55
+ export function executableLineNumbers(sourceContent = "") {
56
+ const lines = sourceContent.split("\n");
57
+ const out = [];
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const trimmed = lines[i].trim();
60
+ if (!trimmed) continue;
61
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
62
+ if (trimmed.startsWith("import ")) continue;
63
+ if (/^(?:export\s+)?(?:default\s+)?(?:class|function|const|let|var)\s+[A-Za-z_$][\w$]*\s*$/.test(trimmed)) continue;
64
+ if (trimmed === "{" || trimmed === "}" || trimmed === "};") continue;
65
+ out.push(i + 1);
66
+ }
67
+ return out;
68
+ }
69
+
34
70
  /**
35
71
  * Maps V8 function range offsets to 1-indexed line hit counts.
36
72
  * @param {string} sourceContent - Full text of source file
@@ -267,6 +303,11 @@ export function calculateDiffCoverage(coverageByFile, diffStr = "", options = {}
267
303
  let totalLines = 0;
268
304
  let coveredLines = 0;
269
305
  const missedByFile = {};
306
+ // Files V8 can measure, but for which the test run produced no coverage
307
+ // data at all — code Node never imported. Distinct from files V8 cannot
308
+ // observe (Python, Go, …): those are genuinely not measurable here, and
309
+ // scoring them zero blocks stacks the gate never claimed to cover.
310
+ const unobservedExecutableFiles = [];
270
311
 
271
312
  for (const [file, addedLines] of addedLinesMap.entries()) {
272
313
  const absPath = resolve(root, file);
@@ -279,11 +320,20 @@ export function calculateDiffCoverage(coverageByFile, diffStr = "", options = {}
279
320
  continue;
280
321
  }
281
322
 
323
+ const v8Observable = isV8Observable(file);
282
324
  const v8Functions = coverageByFile.get(file) || [];
283
325
  const lineHits = mapV8RangesToLines(sourceContent, v8Functions);
284
326
 
327
+ // The executable denominator *from the source itself*, independent of
328
+ // whether V8 happened to map the file. A file that added executable
329
+ // lines but never appeared in V8's report ran nothing of what was added.
330
+ const executableSet = new Set(executableLineNumbers(sourceContent));
331
+ const neverObserved = v8Observable && v8Functions.length === 0 && addedLines.some((n) => executableSet.has(n));
332
+ if (neverObserved) unobservedExecutableFiles.push(file);
333
+
285
334
  const missed = [];
286
335
  for (const lineNo of addedLines) {
336
+ const isExecutable = executableSet.has(lineNo);
287
337
  if (lineHits.has(lineNo)) {
288
338
  totalLines++;
289
339
  const count = lineHits.get(lineNo);
@@ -292,6 +342,13 @@ export function calculateDiffCoverage(coverageByFile, diffStr = "", options = {}
292
342
  } else {
293
343
  missed.push(lineNo);
294
344
  }
345
+ } else if (neverObserved && isExecutable) {
346
+ // V8 could have seen this line, mapped nothing in the file, and the
347
+ // line is executable: it was not covered. Counting it is what turns
348
+ // "0/0 scored" for an unimported module into a real miss — the gate
349
+ // returning exit 0 on code no test ran is F12.
350
+ totalLines++;
351
+ missed.push(lineNo);
295
352
  }
296
353
  }
297
354
 
@@ -302,30 +359,45 @@ export function calculateDiffCoverage(coverageByFile, diffStr = "", options = {}
302
359
 
303
360
  // 100% of nothing is not 100%.
304
361
  //
305
- // The denominator counts only the added lines V8 actually mapped, and V8
306
- // maps nothing outside Node. So a Python diff adding three executable lines
307
- // measured zero of them and was reported as `score: 100` — the best possible
308
- // number, produced by a measurement that never happened, on 20-odd of the 25
309
- // stacks this kit claims to support. `mutation.mjs` had the identical bug and
310
- // was fixed in v0.57.0; this is the same shape one module over.
362
+ // Two very different ways the denominator can be zero:
363
+ //
364
+ // 1. There were no executable added lines to measure, or the diff is in
365
+ // code V8 cannot observe (Python, Go, …). The measurement is simply
366
+ // not applicable here; `ok` stays true because nothing failed to be
367
+ // covered — a gate that blocks every non-Node diff gets switched off.
368
+ // What changes is the claim: `scored: false` and a reason.
369
+ // 2. There were executable lines in code V8 *can* observe, and the test
370
+ // run mapped none of them — the file was never imported. A passing
371
+ // result on that is the same failure as a line counted uncovered:
372
+ // the suite certified code it never ran (F12). That is scored zero,
373
+ // not scored nothing.
311
374
  //
312
- // `ok` stays true because nothing failed to be covered, and a gate that
313
- // blocks every non-Node diff gets switched off. What changes is the claim:
314
- // `scored: false` and a reason, instead of a number nobody measured.
375
+ // `mutation.mjs` had the original "100% of 0/0" bug in the first family
376
+ // and was fixed in v0.57.0; this is the second family, one module over.
377
+ const measuredNothingExecutable = unobservedExecutableFiles.length > 0 && totalLines === 0;
315
378
  const scored = totalLines > 0;
316
379
  const score = scored ? Math.round((coveredLines / totalLines) * 10000) / 100 : null;
317
- const ok = scored ? score >= minCoverage : true;
380
+ // N/A only when V8 could not have measured the added code (no executable
381
+ // lines at all, or a stack V8 does not observe). Executable Node code the
382
+ // test run never reached is a failure, not an N/A.
383
+ const notApplicable = !scored && !measuredNothingExecutable;
384
+ const ok = notApplicable ? true : score >= minCoverage;
318
385
 
319
386
  return {
320
387
  ok,
321
- score,
388
+ score: notApplicable ? null : score,
322
389
  scored,
323
- ...(scored ? {} : { reason: "No added executable lines were measurable — V8 coverage only observes code Node itself ran, so nothing was scored." }),
390
+ ...(notApplicable
391
+ ? { reason: "No added executable lines were measurable — V8 coverage only observes code Node itself ran, so nothing was scored." }
392
+ : {
393
+ reason: `Added executable code was never executed by the test run (unobserved: ${unobservedExecutableFiles.join(", ")}) — V8 coverage saw no execution of it, so it counts as uncovered.`,
394
+ unobservedFiles: unobservedExecutableFiles,
395
+ }),
324
396
  minCoverage,
325
397
  totalLines,
326
398
  coveredLines,
327
399
  missedLines: totalLines - coveredLines,
328
400
  missedByFile,
329
- summary: `Diff Coverage: ${score}% (${coveredLines}/${totalLines} added executable lines covered, min: ${minCoverage}%)`,
401
+ summary: `Diff Coverage: ${notApplicable ? "null" : score}% (${coveredLines}/${totalLines} added executable lines covered, min: ${minCoverage}%)`,
330
402
  };
331
403
  }