jules-orchestrator-kit 0.70.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
@@ -205,7 +205,13 @@ const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
205
205
 
206
206
  function coerce(val) {
207
207
  if (!val) return "";
208
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
208
+ if (val.startsWith("'") && val.endsWith("'") && val.length >= 2) {
209
+ // `''` is how a single-quoted YAML scalar escapes a quote, and it is what
210
+ // `yamlScalar` emits. Reading the quotes off without undoing the escape
211
+ // meant the emitter and the parser disagreed about the same file.
212
+ return val.slice(1, -1).replace(/''/g, "'");
213
+ }
214
+ if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) {
209
215
  return val.slice(1, -1);
210
216
  }
211
217
  if (val === "true") return true;
@@ -315,6 +321,86 @@ export function normalizeScope(parsed = {}) {
315
321
  };
316
322
  }
317
323
 
324
+ /**
325
+ * A YAML scalar quoted only as much as YAML requires.
326
+ *
327
+ * The scaffold wrapped every generated string in double quotes, which is valid
328
+ * YAML and idiomatic nowhere. On a repository that lints its own YAML that is a
329
+ * hard red on the kit's own files: measured on `unjs/unimport`, following the
330
+ * README verbatim produced 40 eslint errors — `yaml/quotes` and
331
+ * `yaml/plain-scalar` — across `.agent/config.yml` and `.agent/jules.yml`, so a
332
+ * newcomer's very first `agentctl check` failed on output `agentctl init` had
333
+ * just written, in a repository that was green a minute earlier. Teaching a new
334
+ * user that the gate is broken is the most expensive thing this tool can do.
335
+ *
336
+ * Nothing here is specific to eslint or to that repository. Emitting a plain
337
+ * scalar where YAML permits one, and single quotes where it does not, is simply
338
+ * how YAML is written; the linters that check it are agreeing with the spec.
339
+ *
340
+ * The quoting test is deliberately conservative — anything that could change
341
+ * meaning unquoted gets quotes. A glob beginning with a star stays quoted,
342
+ * because a leading `*` is an alias reference; `pnpm -r test` does not, because
343
+ * there is nothing in it to misread.
344
+ */
345
+ const YAML_NEEDS_QUOTES =
346
+ /^$|^[-?:,[\]{}#&*!|>'"%@`]|^\s|\s$|:\s|\s#|:$|[\n\r\t]|^(?:true|false|yes|no|on|off|null|~)$/i;
347
+
348
+ export function yamlScalar(value) {
349
+ const s = value === null || value === undefined ? "" : String(value);
350
+ if (!YAML_NEEDS_QUOTES.test(s) && !/^[+-]?(?:\d[\d_]*)(?:\.[\d_]*)?(?:[eE][+-]?\d+)?$/.test(s)) return s;
351
+ return `'${s.replace(/'/g, "''")}'`;
352
+ }
353
+
354
+ /**
355
+ * Can a quoted scalar begin at this offset?
356
+ *
357
+ * A quote character is only an opening quote where a value may start — after
358
+ * `key:`, after a `-` list marker, at the head of a flow collection, or at the
359
+ * start of the line. Anywhere else it is an ordinary apostrophe, and treating
360
+ * `test: it's fine` as an unterminated string would swallow the rest of it.
361
+ */
362
+ function opensQuotedScalar(line, i) {
363
+ return /(?:^\s*|:\s+|-\s+|[[{,]\s*)$/.test(line.slice(0, i));
364
+ }
365
+
366
+ /**
367
+ * Strip a trailing `#` comment without cutting into a quoted scalar.
368
+ *
369
+ * `rawLine.indexOf("#")` stood here, which is not the YAML rule twice over: a
370
+ * `#` opens a comment only when it begins the line or follows whitespace, and
371
+ * never inside quotes. So any configured value containing a hash was silently
372
+ * truncated on the way in — `test: 'pytest -k "not #slow"'` became
373
+ * `test: 'pytest -k "not`, and the gate then ran a command the user never
374
+ * wrote. Silent truncation of a verification command is the worst shape this
375
+ * file can produce, because every later phase reports on whatever did run.
376
+ *
377
+ * It matters more now that `yamlScalar` exists: an emitter and a parser that
378
+ * disagree about the same file is this project's recurring defect with both
379
+ * halves in one module.
380
+ */
381
+ function stripYamlComment(rawLine) {
382
+ let quote = null;
383
+ for (let i = 0; i < rawLine.length; i++) {
384
+ const ch = rawLine[i];
385
+ if (quote) {
386
+ if (quote === '"' && ch === "\\") {
387
+ i++;
388
+ } else if (ch === quote) {
389
+ // `''` is an escaped quote inside a single-quoted scalar, not the end.
390
+ if (quote === "'" && rawLine[i + 1] === "'") i++;
391
+ else quote = null;
392
+ }
393
+ continue;
394
+ }
395
+ if ((ch === '"' || ch === "'") && opensQuotedScalar(rawLine, i)) {
396
+ quote = ch;
397
+ continue;
398
+ }
399
+ if (ch === "#" && (i === 0 || /\s/.test(rawLine[i - 1]))) return rawLine.slice(0, i);
400
+ }
401
+ return rawLine;
402
+ }
403
+
318
404
  /**
319
405
  * Indent-stack zero-dependency YAML parser with prototype pollution protection.
320
406
  */
@@ -331,8 +417,7 @@ export function parseYaml(src) {
331
417
  const lines = src.split("\n");
332
418
 
333
419
  for (let rawLine of lines) {
334
- const commentIdx = rawLine.indexOf("#");
335
- const line = commentIdx !== -1 ? rawLine.slice(0, commentIdx) : rawLine;
420
+ const line = stripYamlComment(rawLine);
336
421
  const trimmed = line.trim();
337
422
  if (!trimmed) continue;
338
423
 
@@ -433,9 +518,9 @@ export function detectPackageManager(root = process.cwd(), pkg = {}) {
433
518
  return "npm";
434
519
  }
435
520
 
436
- import { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
521
+ import { buildProfileStages, buildDefaultStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS } from "./profiles.mjs";
437
522
 
438
- export { buildProfileStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
523
+ export { buildProfileStages, buildDefaultStages, PROFILE_NAMES, PROFILE_DESCRIPTIONS };
439
524
 
440
525
  import {
441
526
  detectPolyglotStack,
@@ -444,6 +529,7 @@ import {
444
529
  findSubprojectRoot,
445
530
  detectCrossPackageBoundaryViolations,
446
531
  detectCircularDependencies,
532
+ isPlaceholderTestScript,
447
533
  } from "./stack-detector.mjs";
448
534
 
449
535
  export {
@@ -453,8 +539,11 @@ export {
453
539
  findSubprojectRoot,
454
540
  detectCrossPackageBoundaryViolations,
455
541
  detectCircularDependencies,
542
+ isPlaceholderTestScript,
456
543
  };
457
544
 
545
+ import { showFromOrigin, detectDefaultBranch } from "./git.mjs";
546
+
458
547
  /**
459
548
  * Autodetects verification test/build commands across 24+ polyglot tech stacks.
460
549
  */
@@ -703,6 +792,7 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
703
792
  // Named so `doctor` can tell an operator that the gate they enabled is
704
793
  // running one fewer check than they think, and why.
705
794
  profileSkipped: profilePlan?.skipped ?? [],
795
+ minTests: parsed.verify?.minTests ?? parsed.verify?.min_tests ?? (parsed.minTests ?? parsed.min_tests ?? 1),
706
796
  timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 300_000,
707
797
  },
708
798
  evidence: {
@@ -780,3 +870,342 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
780
870
 
781
871
  return config;
782
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
  }