@plune-ai/cli 0.27.0 → 0.28.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/dist/cli.cjs CHANGED
@@ -622,7 +622,8 @@ function makeAnthropicProvider(config, env, pricing) {
622
622
  usage: {
623
623
  input_tokens: res.usage.input_tokens,
624
624
  output_tokens: res.usage.output_tokens
625
- }
625
+ },
626
+ ...res.stop_reason === "max_tokens" ? { finish_reason: "length" } : res.stop_reason !== null && res.stop_reason !== void 0 ? { finish_reason: res.stop_reason } : {}
626
627
  };
627
628
  } catch (err) {
628
629
  throw normalizeProviderError(err, apiKey, ENV_VAR);
@@ -680,8 +681,11 @@ function makeOpenAiCompatibleProvider(opts) {
680
681
  { max_retries: maxRetries }
681
682
  );
682
683
  const reportedCost = reportsCost ? res.usage?.cost : void 0;
684
+ const choice = res.choices[0];
685
+ const finish = choice?.error !== void 0 ? "error" : choice?.finish_reason;
683
686
  return {
684
- output: res.choices[0]?.message?.content ?? "",
687
+ output: choice?.message?.content ?? "",
688
+ ...typeof finish === "string" ? { finish_reason: finish } : {},
685
689
  usage: {
686
690
  input_tokens: res.usage?.prompt_tokens ?? 0,
687
691
  output_tokens: res.usage?.completion_tokens ?? 0
@@ -891,18 +895,115 @@ var init_embeddings = __esm({
891
895
  }
892
896
  });
893
897
 
898
+ // src/util/canonical-json.ts
899
+ function normalizeValue(value2) {
900
+ if (value2 === null) return null;
901
+ if (typeof value2 === "boolean") return value2;
902
+ if (typeof value2 === "number") {
903
+ if (!isFinite(value2)) {
904
+ throw new Error(
905
+ `canonicalJson: non-finite number rejected: ${String(value2)}`
906
+ );
907
+ }
908
+ return value2 === 0 ? 0 : value2;
909
+ }
910
+ if (typeof value2 === "string") {
911
+ return value2.normalize("NFC");
912
+ }
913
+ if (Array.isArray(value2)) {
914
+ return value2.map(normalizeValue);
915
+ }
916
+ if (typeof value2 === "object") {
917
+ const proto = Object.getPrototypeOf(value2);
918
+ if (proto !== null && proto !== Object.prototype) {
919
+ const name = value2.constructor?.name ?? "unknown";
920
+ throw new Error(
921
+ `canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
922
+ );
923
+ }
924
+ const obj = value2;
925
+ const sorted = {};
926
+ for (const key of Object.keys(obj).sort()) {
927
+ sorted[key] = normalizeValue(obj[key]);
928
+ }
929
+ return sorted;
930
+ }
931
+ throw new Error(
932
+ `canonicalJson: unsupported value type: ${typeof value2}`
933
+ );
934
+ }
935
+ function canonicalJson(value2) {
936
+ return JSON.stringify(normalizeValue(value2));
937
+ }
938
+ var init_canonical_json = __esm({
939
+ "src/util/canonical-json.ts"() {
940
+ "use strict";
941
+ init_cjs_shims();
942
+ }
943
+ });
944
+
945
+ // src/util/hash.ts
946
+ function sha256(input) {
947
+ return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
948
+ }
949
+ function cacheKey(inputs, policyVersion = CACHE_POLICY_VERSION) {
950
+ const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
951
+ return sha256(
952
+ canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved, policy: policyVersion })
953
+ );
954
+ }
955
+ function configHash(config) {
956
+ return sha256(canonicalJson(config));
957
+ }
958
+ var import_node_crypto, CACHE_POLICY_VERSION;
959
+ var init_hash = __esm({
960
+ "src/util/hash.ts"() {
961
+ "use strict";
962
+ init_cjs_shims();
963
+ import_node_crypto = require("crypto");
964
+ init_canonical_json();
965
+ CACHE_POLICY_VERSION = 1;
966
+ }
967
+ });
968
+
894
969
  // src/cache/cache.ts
970
+ function isCacheable(res) {
971
+ if (res.output.trim() === "") return false;
972
+ return res.finish_reason === void 0 || !UNCACHEABLE_FINISH.has(res.finish_reason);
973
+ }
974
+ function loadDatabase() {
975
+ try {
976
+ return (0, import_node_module.createRequire)(importMetaUrl)("better-sqlite3");
977
+ } catch {
978
+ if (!missingNoted) {
979
+ missingNoted = true;
980
+ console.error(
981
+ "plune: completion cache disabled (better-sqlite3 is not installed or failed to load). Reinstall with a C++ toolchain and Python available, or ignore: runs work without the cache."
982
+ );
983
+ }
984
+ return void 0;
985
+ }
986
+ }
895
987
  function openCache(file, opts = {}) {
896
988
  const now = opts.now ?? Date.now;
989
+ const Database = loadDatabase();
990
+ if (Database === void 0) return new NoOpCache();
897
991
  try {
898
- const db = new import_better_sqlite3.default(file);
992
+ const db = new Database(file);
899
993
  try {
900
994
  ensureSchema(db);
901
995
  } catch (err) {
902
996
  db.close();
903
997
  throw err;
904
998
  }
905
- return new SqliteCache(db, now);
999
+ let stale;
1000
+ try {
1001
+ stale = markPolicy(db, opts.policyVersion ?? CACHE_POLICY_VERSION);
1002
+ } catch (err) {
1003
+ db.close();
1004
+ throw err;
1005
+ }
1006
+ return new SqliteCache(db, now, stale);
906
1007
  } catch {
907
1008
  return new NoOpCache();
908
1009
  }
@@ -917,26 +1018,45 @@ function ensureSchema(db) {
917
1018
  db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_version", current);
918
1019
  }
919
1020
  }
920
- var import_better_sqlite3, SCHEMA_VERSION, META_DDL, COMPLETIONS_DDL, SqliteCache, NoOpCache;
1021
+ function markPolicy(db, version) {
1022
+ const current = String(version);
1023
+ const stored = db.prepare("SELECT value FROM meta WHERE key = 'policy_version'").get()?.value;
1024
+ if (stored === current) return 0;
1025
+ return db.transaction(() => {
1026
+ const { n } = db.prepare("SELECT COUNT(*) AS n FROM completions").get();
1027
+ db.exec("DELETE FROM completions");
1028
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("policy_version", current);
1029
+ return n;
1030
+ })();
1031
+ }
1032
+ var import_node_module, SCHEMA_VERSION, META_DDL, COMPLETIONS_DDL, UNCACHEABLE_FINISH, missingNoted, SqliteCache, NoOpCache;
921
1033
  var init_cache = __esm({
922
1034
  "src/cache/cache.ts"() {
923
1035
  "use strict";
924
1036
  init_cjs_shims();
925
- import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1037
+ import_node_module = require("module");
1038
+ init_hash();
926
1039
  SCHEMA_VERSION = 1;
927
1040
  META_DDL = "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);";
928
1041
  COMPLETIONS_DDL = "CREATE TABLE IF NOT EXISTS completions (cache_key TEXT PRIMARY KEY, output TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL, created_at INTEGER NOT NULL);";
1042
+ UNCACHEABLE_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "error", "content_filter", "pause_turn"]);
1043
+ missingNoted = false;
929
1044
  SqliteCache = class {
930
- constructor(db, now) {
1045
+ constructor(db, now, staleEntries) {
931
1046
  this.db = db;
932
1047
  this.now = now;
1048
+ this.staleEntries = staleEntries;
933
1049
  }
934
1050
  db;
935
1051
  now;
1052
+ staleEntries;
936
1053
  get(key, opts = {}) {
937
1054
  try {
938
1055
  const row = this.db.prepare("SELECT output, input_tokens, output_tokens, created_at FROM completions WHERE cache_key = ?").get(key);
939
1056
  if (row === void 0) return void 0;
1057
+ if (typeof row.output !== "string" || !Number.isFinite(row.input_tokens) || !Number.isFinite(row.output_tokens) || !Number.isFinite(row.created_at)) {
1058
+ return void 0;
1059
+ }
940
1060
  if (opts.maxAgeMs !== void 0 && this.now() - row.created_at > opts.maxAgeMs) {
941
1061
  return void 0;
942
1062
  }
@@ -962,6 +1082,12 @@ var init_cache = __esm({
962
1082
  } catch {
963
1083
  }
964
1084
  }
1085
+ delete(key) {
1086
+ try {
1087
+ this.db.prepare("DELETE FROM completions WHERE cache_key = ?").run(key);
1088
+ } catch {
1089
+ }
1090
+ }
965
1091
  clear() {
966
1092
  try {
967
1093
  this.db.exec("DELETE FROM completions");
@@ -978,6 +1104,8 @@ var init_cache = __esm({
978
1104
  }
979
1105
  set() {
980
1106
  }
1107
+ delete() {
1108
+ }
981
1109
  clear() {
982
1110
  }
983
1111
  close() {
@@ -995,71 +1123,77 @@ var init_cache2 = __esm({
995
1123
  }
996
1124
  });
997
1125
 
998
- // src/util/canonical-json.ts
999
- function normalizeValue(value2) {
1000
- if (value2 === null) return null;
1001
- if (typeof value2 === "boolean") return value2;
1002
- if (typeof value2 === "number") {
1003
- if (!isFinite(value2)) {
1004
- throw new Error(
1005
- `canonicalJson: non-finite number rejected: ${String(value2)}`
1006
- );
1007
- }
1008
- return value2 === 0 ? 0 : value2;
1126
+ // src/assertions/json-extract.ts
1127
+ function extractJson(output, mode) {
1128
+ if (mode === "strict") {
1129
+ return tryParse(output.trim());
1009
1130
  }
1010
- if (typeof value2 === "string") {
1011
- return value2.normalize("NFC");
1131
+ const fenced = extractFencedBlock(output);
1132
+ if (fenced !== null) {
1133
+ const r = tryParse(fenced);
1134
+ if (r.ok) return r;
1012
1135
  }
1013
- if (Array.isArray(value2)) {
1014
- return value2.map(normalizeValue);
1136
+ for (const run of balancedRuns(output)) {
1137
+ const r = tryParse(run);
1138
+ if (r.ok) return r;
1015
1139
  }
1016
- if (typeof value2 === "object") {
1017
- const proto = Object.getPrototypeOf(value2);
1018
- if (proto !== null && proto !== Object.prototype) {
1019
- const name = value2.constructor?.name ?? "unknown";
1020
- throw new Error(
1021
- `canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
1022
- );
1023
- }
1024
- const obj = value2;
1025
- const sorted = {};
1026
- for (const key of Object.keys(obj).sort()) {
1027
- sorted[key] = normalizeValue(obj[key]);
1028
- }
1029
- return sorted;
1140
+ return { ok: false };
1141
+ }
1142
+ function tryParse(s) {
1143
+ try {
1144
+ return { ok: true, value: JSON.parse(s) };
1145
+ } catch {
1146
+ return { ok: false };
1030
1147
  }
1031
- throw new Error(
1032
- `canonicalJson: unsupported value type: ${typeof value2}`
1033
- );
1034
1148
  }
1035
- function canonicalJson(value2) {
1036
- return JSON.stringify(normalizeValue(value2));
1149
+ function extractFencedBlock(s) {
1150
+ const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
1151
+ return m?.[1]?.trim() ?? null;
1037
1152
  }
1038
- var init_canonical_json = __esm({
1039
- "src/util/canonical-json.ts"() {
1040
- "use strict";
1041
- init_cjs_shims();
1153
+ function* balancedRuns(s) {
1154
+ let i = 0;
1155
+ while (i < s.length) {
1156
+ const rel = s.slice(i).search(/[{[]/);
1157
+ if (rel === -1) return;
1158
+ const start = i + rel;
1159
+ const run = balancedFrom(s, start);
1160
+ if (run === null) {
1161
+ i = start + 1;
1162
+ } else {
1163
+ yield run;
1164
+ i = start + run.length;
1165
+ }
1042
1166
  }
1043
- });
1044
-
1045
- // src/util/hash.ts
1046
- function sha256(input) {
1047
- return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
1048
- }
1049
- function cacheKey(inputs) {
1050
- const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
1051
- return sha256(canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved }));
1052
1167
  }
1053
- function configHash(config) {
1054
- return sha256(canonicalJson(config));
1168
+ function balancedFrom(s, start) {
1169
+ const open = s[start];
1170
+ const close = open === "{" ? "}" : "]";
1171
+ let depth = 0;
1172
+ let inString = false;
1173
+ let escaped = false;
1174
+ for (let i = start; i < s.length; i++) {
1175
+ const ch = s[i];
1176
+ if (inString) {
1177
+ if (escaped) escaped = false;
1178
+ else if (ch === "\\") escaped = true;
1179
+ else if (ch === '"') inString = false;
1180
+ continue;
1181
+ }
1182
+ if (ch === '"') {
1183
+ inString = true;
1184
+ } else if (ch === open) {
1185
+ depth++;
1186
+ } else if (ch === close) {
1187
+ depth--;
1188
+ if (depth === 0) return s.slice(start, i + 1);
1189
+ }
1190
+ }
1191
+ return null;
1055
1192
  }
1056
- var import_node_crypto;
1057
- var init_hash = __esm({
1058
- "src/util/hash.ts"() {
1193
+ var init_json_extract = __esm({
1194
+ "src/assertions/json-extract.ts"() {
1059
1195
  "use strict";
1060
1196
  init_cjs_shims();
1061
- import_node_crypto = require("crypto");
1062
- init_canonical_json();
1063
1197
  }
1064
1198
  });
1065
1199
 
@@ -1157,80 +1291,6 @@ var init_contains = __esm({
1157
1291
  }
1158
1292
  });
1159
1293
 
1160
- // src/assertions/json-extract.ts
1161
- function extractJson(output, mode) {
1162
- if (mode === "strict") {
1163
- return tryParse(output.trim());
1164
- }
1165
- const fenced = extractFencedBlock(output);
1166
- if (fenced !== null) {
1167
- const r = tryParse(fenced);
1168
- if (r.ok) return r;
1169
- }
1170
- for (const run of balancedRuns(output)) {
1171
- const r = tryParse(run);
1172
- if (r.ok) return r;
1173
- }
1174
- return { ok: false };
1175
- }
1176
- function tryParse(s) {
1177
- try {
1178
- return { ok: true, value: JSON.parse(s) };
1179
- } catch {
1180
- return { ok: false };
1181
- }
1182
- }
1183
- function extractFencedBlock(s) {
1184
- const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
1185
- return m?.[1]?.trim() ?? null;
1186
- }
1187
- function* balancedRuns(s) {
1188
- let i = 0;
1189
- while (i < s.length) {
1190
- const rel = s.slice(i).search(/[{[]/);
1191
- if (rel === -1) return;
1192
- const start = i + rel;
1193
- const run = balancedFrom(s, start);
1194
- if (run === null) {
1195
- i = start + 1;
1196
- } else {
1197
- yield run;
1198
- i = start + run.length;
1199
- }
1200
- }
1201
- }
1202
- function balancedFrom(s, start) {
1203
- const open = s[start];
1204
- const close = open === "{" ? "}" : "]";
1205
- let depth = 0;
1206
- let inString = false;
1207
- let escaped = false;
1208
- for (let i = start; i < s.length; i++) {
1209
- const ch = s[i];
1210
- if (inString) {
1211
- if (escaped) escaped = false;
1212
- else if (ch === "\\") escaped = true;
1213
- else if (ch === '"') inString = false;
1214
- continue;
1215
- }
1216
- if (ch === '"') {
1217
- inString = true;
1218
- } else if (ch === open) {
1219
- depth++;
1220
- } else if (ch === close) {
1221
- depth--;
1222
- if (depth === 0) return s.slice(start, i + 1);
1223
- }
1224
- }
1225
- return null;
1226
- }
1227
- var init_json_extract = __esm({
1228
- "src/assertions/json-extract.ts"() {
1229
- "use strict";
1230
- init_cjs_shims();
1231
- }
1232
- });
1233
-
1234
1294
  // src/assertions/json-schema.ts
1235
1295
  var import_ajv, jsonSchema;
1236
1296
  var init_json_schema = __esm({
@@ -1692,6 +1752,11 @@ var init_results = __esm({
1692
1752
  });
1693
1753
 
1694
1754
  // src/orchestrator/run.ts
1755
+ function answerParses(output, assertions) {
1756
+ return assertions.every(
1757
+ (a) => a.type !== "json-schema" || extractJson(output, a.extract ?? "auto").ok
1758
+ );
1759
+ }
1695
1760
  function resolvePrompt(template, vars) {
1696
1761
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
1697
1762
  if (!(key in vars)) {
@@ -1719,17 +1784,35 @@ async function runRow(p) {
1719
1784
  const usage3 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
1720
1785
  return { vars: p.row.vars, output: null, cached: false, usage: usage3, assertions: [] };
1721
1786
  }
1722
- const key = cacheKey({
1723
- provider: p.providerConfig.type,
1724
- model: p.providerConfig.model,
1725
- temperature: temperature ?? 0,
1726
- max_tokens: maxTokens,
1727
- prompt_resolved: prompt5
1728
- });
1787
+ const key = cacheKey(
1788
+ {
1789
+ provider: p.providerConfig.type,
1790
+ model: p.providerConfig.model,
1791
+ temperature: temperature ?? 0,
1792
+ max_tokens: maxTokens,
1793
+ prompt_resolved: prompt5
1794
+ },
1795
+ p.policyVersion
1796
+ );
1729
1797
  let output;
1730
1798
  let usage2;
1731
1799
  let cached = false;
1732
- const hit = p.noCache ? void 0 : p.cache.get(key);
1800
+ let cacheSaved;
1801
+ let hit = p.noCache ? void 0 : p.cache.get(key, p.cacheMaxAgeMs !== void 0 ? { maxAgeMs: p.cacheMaxAgeMs } : {});
1802
+ if (hit !== void 0 && !answerParses(hit.output, p.assertions)) {
1803
+ if (p.cacheStrict !== true) p.cache.delete(key);
1804
+ hit = void 0;
1805
+ }
1806
+ if (hit === void 0 && p.cacheStrict === true) {
1807
+ return {
1808
+ vars: p.row.vars,
1809
+ output: null,
1810
+ cached: false,
1811
+ latency_ms: p.now() - t0,
1812
+ error: { code: CACHE_MISS_CODE, message: "no usable cache entry (--cache-strict)" },
1813
+ assertions: []
1814
+ };
1815
+ }
1733
1816
  if (hit !== void 0) {
1734
1817
  output = hit.output;
1735
1818
  cached = true;
@@ -1738,6 +1821,11 @@ async function runRow(p) {
1738
1821
  output_tokens: hit.usage.output_tokens,
1739
1822
  cost_usd: 0
1740
1823
  };
1824
+ cacheSaved = {
1825
+ input_tokens: hit.usage.input_tokens,
1826
+ output_tokens: hit.usage.output_tokens,
1827
+ cost_usd: p.provider.estimateCost(hit.usage).cost_usd
1828
+ };
1741
1829
  } else {
1742
1830
  try {
1743
1831
  const res = await p.provider.complete({
@@ -1755,7 +1843,9 @@ async function runRow(p) {
1755
1843
  // lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
1756
1844
  cost_usd: p.provider.estimateCost(res.usage, res.cost_usd).cost_usd
1757
1845
  };
1758
- if (!p.noCache) p.cache.set(key, res);
1846
+ if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
1847
+ p.cache.set(key, res);
1848
+ }
1759
1849
  } catch (err) {
1760
1850
  return {
1761
1851
  vars: p.row.vars,
@@ -1801,6 +1891,7 @@ async function runRow(p) {
1801
1891
  cached,
1802
1892
  usage: finalUsage,
1803
1893
  latency_ms: p.now() - t0,
1894
+ ...cacheSaved !== void 0 ? { cache_saved: cacheSaved } : {},
1804
1895
  ...assertionError !== void 0 ? { error: assertionError } : {},
1805
1896
  assertions: records
1806
1897
  };
@@ -1835,19 +1926,41 @@ function summarize(evals, durationMs) {
1835
1926
  cost_usd: 0,
1836
1927
  duration_ms: durationMs
1837
1928
  };
1929
+ const cache = {
1930
+ from_cache: 0,
1931
+ from_model: 0,
1932
+ missed: 0,
1933
+ errored: 0,
1934
+ saved: { input_tokens: 0, output_tokens: 0, cost_usd: 0 }
1935
+ };
1838
1936
  for (const ev of evals) {
1839
1937
  for (const row of ev.rows) {
1840
1938
  summary.total += 1;
1841
1939
  summary[rowStatus(row)] += 1;
1842
1940
  summary.cost_usd += row.usage?.cost_usd ?? 0;
1941
+ if (row.cached) {
1942
+ cache.from_cache += 1;
1943
+ cache.saved.input_tokens += row.cache_saved?.input_tokens ?? 0;
1944
+ cache.saved.output_tokens += row.cache_saved?.output_tokens ?? 0;
1945
+ cache.saved.cost_usd += row.cache_saved?.cost_usd ?? 0;
1946
+ } else if (row.error?.code === CACHE_MISS_CODE) cache.missed += 1;
1947
+ else if (row.output !== null) cache.from_model += 1;
1948
+ else if (row.error !== void 0) cache.errored += 1;
1843
1949
  }
1844
1950
  }
1845
- return summary;
1951
+ return { ...summary, cache };
1846
1952
  }
1847
1953
  async function runOrchestration(config, options, deps) {
1954
+ const noCache = options.noCache ?? false;
1955
+ const cacheStrict = options.cacheStrict ?? false;
1956
+ if (noCache && cacheStrict) {
1957
+ throw new RunConfigError("--no-cache and --cache-strict cannot be used together");
1958
+ }
1959
+ if (noCache && options.cacheMaxAgeMs !== void 0) {
1960
+ throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
1961
+ }
1848
1962
  const started = deps.now();
1849
1963
  const dryRun = options.dryRun ?? false;
1850
- const noCache = options.noCache ?? false;
1851
1964
  const evalResults = [];
1852
1965
  const prepared = selectEvals(config.evals, options.only).map((ev) => {
1853
1966
  const template = resolveTemplate(ev, deps.baseDir);
@@ -1874,7 +1987,9 @@ async function runOrchestration(config, options, deps) {
1874
1987
  cache: deps.cache,
1875
1988
  now: deps.now,
1876
1989
  dryRun,
1877
- noCache
1990
+ noCache,
1991
+ cacheStrict,
1992
+ ...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
1878
1993
  })
1879
1994
  );
1880
1995
  const passed = rowResults.every((r) => rowStatus(r) === "passed");
@@ -1893,7 +2008,7 @@ async function runOrchestration(config, options, deps) {
1893
2008
  evals: evalResults
1894
2009
  };
1895
2010
  }
1896
- var fs3, path3, DEFAULT_MAX_TOKENS2, PLUNE_VERSION, RunConfigError;
2011
+ var fs3, path3, DEFAULT_MAX_TOKENS2, PLUNE_VERSION, RunConfigError, CACHE_MISS_CODE;
1897
2012
  var init_run = __esm({
1898
2013
  "src/orchestrator/run.ts"() {
1899
2014
  "use strict";
@@ -1901,6 +2016,8 @@ var init_run = __esm({
1901
2016
  fs3 = __toESM(require("fs"), 1);
1902
2017
  path3 = __toESM(require("path"), 1);
1903
2018
  init_hash();
2019
+ init_json_extract();
2020
+ init_cache();
1904
2021
  init_assertions();
1905
2022
  init_judge();
1906
2023
  init_concurrency();
@@ -1915,20 +2032,29 @@ var init_run = __esm({
1915
2032
  this.name = "RunConfigError";
1916
2033
  }
1917
2034
  };
2035
+ CACHE_MISS_CODE = "CACHE_MISS";
1918
2036
  }
1919
2037
  });
1920
2038
 
1921
2039
  // src/orchestrator/exit-code.ts
2040
+ function cacheMissRows(result) {
2041
+ return result.evals.flatMap(
2042
+ (ev) => ev.rows.filter((r) => r.error?.code === CACHE_MISS_CODE).map((r) => `${ev.id} ${JSON.stringify(r.vars)}`)
2043
+ );
2044
+ }
1922
2045
  function exitCodeFor(result) {
1923
2046
  const { failed, errored } = result.summary;
1924
2047
  if (failed > 0) return 1;
1925
2048
  if (errored > 0) return 2;
1926
2049
  return 0;
1927
2050
  }
2051
+ var CACHE_STRICT_EXIT;
1928
2052
  var init_exit_code = __esm({
1929
2053
  "src/orchestrator/exit-code.ts"() {
1930
2054
  "use strict";
1931
2055
  init_cjs_shims();
2056
+ init_run();
2057
+ CACHE_STRICT_EXIT = 5;
1932
2058
  }
1933
2059
  });
1934
2060
 
@@ -1953,8 +2079,11 @@ var init_dataset = __esm({
1953
2079
  // src/orchestrator/index.ts
1954
2080
  var orchestrator_exports = {};
1955
2081
  __export(orchestrator_exports, {
2082
+ CACHE_MISS_CODE: () => CACHE_MISS_CODE,
2083
+ CACHE_STRICT_EXIT: () => CACHE_STRICT_EXIT,
1956
2084
  RunConfigError: () => RunConfigError,
1957
2085
  buildJudge: () => buildJudge,
2086
+ cacheMissRows: () => cacheMissRows,
1958
2087
  exitCodeFor: () => exitCodeFor,
1959
2088
  loadDataset: () => loadDataset,
1960
2089
  mapLimit: () => mapLimit,
@@ -2013,6 +2142,11 @@ async function handleRun(options, depsFactory) {
2013
2142
  const factory = depsFactory ?? ((c, d) => buildRealDeps(c, d, options.dryRun));
2014
2143
  const deps = factory(config, baseDir);
2015
2144
  try {
2145
+ const stale = deps.cache.staleEntries ?? 0;
2146
+ if (stale > 0) {
2147
+ process.stderr.write(`cache policy changed, ${stale} entries are stale (dropped)
2148
+ `);
2149
+ }
2016
2150
  const result = await runOrchestration(
2017
2151
  config,
2018
2152
  {
@@ -2020,7 +2154,9 @@ async function handleRun(options, depsFactory) {
2020
2154
  ...options.only !== void 0 ? { only: options.only } : {},
2021
2155
  ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
2022
2156
  ...options.noCache !== void 0 ? { noCache: options.noCache } : {},
2023
- ...options.bail !== void 0 ? { bail: options.bail } : {}
2157
+ ...options.bail !== void 0 ? { bail: options.bail } : {},
2158
+ ...options.cacheStrict === true ? { cacheStrict: true } : {},
2159
+ ...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
2024
2160
  },
2025
2161
  deps
2026
2162
  );
@@ -2045,6 +2181,7 @@ var init_run2 = __esm({
2045
2181
  init_cache2();
2046
2182
  init_orchestrator();
2047
2183
  NOOP_CACHE = { get: () => void 0, set: () => {
2184
+ }, delete: () => {
2048
2185
  }, clear: () => {
2049
2186
  }, close: () => {
2050
2187
  } };
@@ -2104,6 +2241,15 @@ function renderConsole(result, opts = {}) {
2104
2241
  lines.push(
2105
2242
  `${passedTxt} \xB7 ${failedTxt} \xB7 ${erroredTxt}${blockedTxt}${skippedTxt} \xB7 ${summary.total} total \xB7 $${summary.cost_usd.toFixed(4)} \xB7 ${summary.duration_ms}ms`
2106
2243
  );
2244
+ const c = summary.cache;
2245
+ if (c !== void 0 && c.from_cache + c.from_model + c.missed > 0) {
2246
+ const missedTxt = c.missed > 0 ? ` \xB7 ${s.yellow(`${c.missed} missed`)}` : "";
2247
+ lines.push(
2248
+ s.dim(
2249
+ `cache: ${c.from_cache} from cache \xB7 ${c.from_model} from model${missedTxt} \xB7 saved ${c.saved.input_tokens + c.saved.output_tokens} tokens, $${c.saved.cost_usd.toFixed(4)}`
2250
+ )
2251
+ );
2252
+ }
2107
2253
  lines.push("");
2108
2254
  for (const ev of result.evals) {
2109
2255
  const mark = ev.passed ? s.green("PASS") : s.red("FAIL");
@@ -3903,7 +4049,7 @@ async function handleRunStart(options = {}) {
3903
4049
  kind: options.kind ?? "automated",
3904
4050
  externalKey,
3905
4051
  // The same name the reporter would give it; `PLUNE_RUN_TITLE` replaces it, as everywhere.
3906
- title: env.config.title ?? defaultRunTitle()
4052
+ title: options.name ?? env.config.title ?? defaultRunTitle()
3907
4053
  });
3908
4054
  if (!out.ok) throw new RunCommandError(`Could not start the run \u2014 ${out.detail || out.kind}.`);
3909
4055
  const result = {
@@ -4122,6 +4268,7 @@ var init_run_result_schema = __esm({
4122
4268
  latency_ms: import_zod3.z.number().optional(),
4123
4269
  error: runErrorSchema.optional(),
4124
4270
  assertions: import_zod3.z.array(assertionResultRecordSchema2),
4271
+ cache_saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() }).optional(),
4125
4272
  /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
4126
4273
  inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
4127
4274
  });
@@ -4134,7 +4281,14 @@ var init_run_result_schema = __esm({
4134
4281
  blocked: import_zod3.z.number().optional(),
4135
4282
  skipped: import_zod3.z.number().optional(),
4136
4283
  cost_usd: import_zod3.z.number(),
4137
- duration_ms: import_zod3.z.number()
4284
+ duration_ms: import_zod3.z.number(),
4285
+ cache: import_zod3.z.object({
4286
+ from_cache: import_zod3.z.number(),
4287
+ from_model: import_zod3.z.number(),
4288
+ missed: import_zod3.z.number(),
4289
+ errored: import_zod3.z.number(),
4290
+ saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() })
4291
+ }).optional()
4138
4292
  });
4139
4293
  evalResultSchema = import_zod3.z.object({
4140
4294
  id: import_zod3.z.string(),
@@ -4309,6 +4463,7 @@ __export(plan_exports, {
4309
4463
  grepFragment: () => grepFragment,
4310
4464
  handlePlanGrep: () => handlePlanGrep,
4311
4465
  handleRunGrep: () => handleRunGrep,
4466
+ platformGet: () => platformGet,
4312
4467
  runGrepSummary: () => runGrepSummary
4313
4468
  });
4314
4469
  function grepFragment(c) {
@@ -5420,6 +5575,7 @@ async function handleRunImport(options) {
5420
5575
  token,
5421
5576
  ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
5422
5577
  ...options.key !== void 0 ? { externalKey: options.key } : {},
5578
+ ...options.name !== void 0 ? { title: options.name } : {},
5423
5579
  ...options.create === true ? { offerDiscovered: true } : {},
5424
5580
  ...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
5425
5581
  kind: "automated",
@@ -6645,6 +6801,130 @@ var init_requirements_import = __esm({
6645
6801
  }
6646
6802
  });
6647
6803
 
6804
+ // src/cli/commands/verify.ts
6805
+ var verify_exports = {};
6806
+ __export(verify_exports, {
6807
+ compare: () => compare,
6808
+ formatVerify: () => formatVerify,
6809
+ handleVerify: () => handleVerify,
6810
+ testsOfListing: () => testsOfListing
6811
+ });
6812
+ function testsOfListing(listing) {
6813
+ const found = /* @__PURE__ */ new Map();
6814
+ const walk2 = (suite, titles) => {
6815
+ for (const spec of suite.specs ?? []) {
6816
+ const annotated = spec.tests?.flatMap((t) => t.annotations ?? []).find(
6817
+ (a) => a.type === "PluneId" && a.description !== void 0 && a.description !== ""
6818
+ )?.description;
6819
+ const id = annotated ?? TOKEN4.exec(spec.title)?.[1];
6820
+ const pathTitle = [...titles, spec.title].join("#");
6821
+ if (!found.has(pathTitle))
6822
+ found.set(pathTitle, { pathTitle, ...id === void 0 ? {} : { id } });
6823
+ }
6824
+ for (const child of suite.suites ?? []) walk2(child, [...titles, child.title]);
6825
+ };
6826
+ for (const file of listing.suites ?? []) walk2(file, [file.title.replace(/\\/g, "/")]);
6827
+ return [...found.values()];
6828
+ }
6829
+ function compare(code, cases) {
6830
+ const live = cases.filter((c) => c.type === "automated" && c.state !== "deprecated");
6831
+ const byPath = /* @__PURE__ */ new Map();
6832
+ for (const c of live)
6833
+ for (const k of c.externalKeys ?? []) if (k.kind === "path-title") byPath.set(k.value, c);
6834
+ const byId = new Map(live.map((c) => [c.id, c]));
6835
+ const seen = /* @__PURE__ */ new Set();
6836
+ const onlyInCode = [];
6837
+ for (const t of code) {
6838
+ const hit = (t.id === void 0 ? void 0 : byId.get(t.id)) ?? byPath.get(t.pathTitle);
6839
+ if (hit === void 0) onlyInCode.push({ pathTitle: t.pathTitle });
6840
+ else seen.add(hit.id);
6841
+ }
6842
+ return {
6843
+ matched: seen.size,
6844
+ onlyInPlatform: live.filter((c) => !seen.has(c.id)).map((c) => ({ id: c.id, title: c.title, state: c.state, detached: c.state === "detached" })),
6845
+ onlyInCode
6846
+ };
6847
+ }
6848
+ function runPlaywrightList(args, cwd) {
6849
+ try {
6850
+ return (0, import_node_child_process3.execFileSync)("npx", ["playwright", "test", ...args], {
6851
+ cwd,
6852
+ encoding: "utf8",
6853
+ maxBuffer: 256 * 1024 * 1024,
6854
+ stdio: ["ignore", "pipe", "pipe"],
6855
+ shell: process.platform === "win32"
6856
+ });
6857
+ } catch (err) {
6858
+ const stderr = err.stderr?.trim();
6859
+ throw new SyncFileError(`could not list the Playwright tests${stderr ? `: ${stderr}` : ""}`);
6860
+ }
6861
+ }
6862
+ async function handleVerify(deps) {
6863
+ const get = platformGet(deps);
6864
+ let raw;
6865
+ if (deps.from !== void 0) {
6866
+ try {
6867
+ raw = (0, import_node_fs8.readFileSync)(deps.from, "utf8");
6868
+ } catch {
6869
+ throw new SyncFileError(`cannot read ${deps.from}`);
6870
+ }
6871
+ } else {
6872
+ const args = [
6873
+ "--list",
6874
+ "--reporter=json",
6875
+ ...deps.playwrightConfig === void 0 ? [] : ["--config", deps.playwrightConfig]
6876
+ ];
6877
+ raw = (deps.list ?? runPlaywrightList)(args, deps.cwd ?? process.cwd());
6878
+ }
6879
+ let listing;
6880
+ try {
6881
+ listing = JSON.parse(raw);
6882
+ } catch {
6883
+ throw new SyncFileError("the Playwright listing is not JSON");
6884
+ }
6885
+ const cases = [];
6886
+ let cursor = null;
6887
+ do {
6888
+ const page = await get(
6889
+ `/v1/test-cases?type=automated&stability=0&limit=200${cursor === null ? "" : `&cursor=${cursor}`}`
6890
+ );
6891
+ cases.push(...page.testCases);
6892
+ cursor = page.nextCursor;
6893
+ } while (cursor !== null);
6894
+ return compare(testsOfListing(listing), cases);
6895
+ }
6896
+ function formatVerify(r) {
6897
+ const lines = [];
6898
+ if (r.onlyInPlatform.length > 0) {
6899
+ lines.push(`In Plune, not in the code (${r.onlyInPlatform.length}):`);
6900
+ for (const c of r.onlyInPlatform) {
6901
+ lines.push(
6902
+ ` ${c.id} ${c.title} [${c.detached ? "already detached" : `${c.state} \u2014 a full run would detach it`}]`
6903
+ );
6904
+ }
6905
+ }
6906
+ if (r.onlyInCode.length > 0) {
6907
+ lines.push(`In the code, not in Plune (${r.onlyInCode.length}):`);
6908
+ for (const t of r.onlyInCode) lines.push(` ${t.pathTitle}`);
6909
+ }
6910
+ lines.push(
6911
+ r.onlyInPlatform.length + r.onlyInCode.length === 0 ? `No drift: ${r.matched} case(s) match the code.` : `${r.matched} matched. Nothing was changed.`
6912
+ );
6913
+ return lines.join("\n") + "\n";
6914
+ }
6915
+ var import_node_child_process3, import_node_fs8, TOKEN4;
6916
+ var init_verify = __esm({
6917
+ "src/cli/commands/verify.ts"() {
6918
+ "use strict";
6919
+ init_cjs_shims();
6920
+ import_node_child_process3 = require("child_process");
6921
+ import_node_fs8 = require("fs");
6922
+ init_plan();
6923
+ init_sync();
6924
+ TOKEN4 = /@P([A-Za-z0-9_-]+)/;
6925
+ }
6926
+ });
6927
+
6648
6928
  // src/cli.ts
6649
6929
  var cli_exports = {};
6650
6930
  __export(cli_exports, {
@@ -6654,11 +6934,25 @@ module.exports = __toCommonJS(cli_exports);
6654
6934
  init_cjs_shims();
6655
6935
  init_api_url();
6656
6936
  var import_commander = require("commander");
6657
- var import_node_fs8 = require("fs");
6937
+ var import_node_fs9 = require("fs");
6658
6938
  var import_node_path5 = require("path");
6659
6939
  var import_node_url = require("url");
6940
+
6941
+ // src/util/duration.ts
6942
+ init_cjs_shims();
6943
+ var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
6944
+ function parseDuration(text2) {
6945
+ const m = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(text2.trim());
6946
+ const ms = m === null ? NaN : Number(m[1]) * UNIT_MS[m[2] ?? "ms"];
6947
+ if (!Number.isFinite(ms) || ms <= 0) {
6948
+ throw new Error(`Invalid duration "${text2}" (use e.g. 90s, 15m, 12h, 7d)`);
6949
+ }
6950
+ return ms;
6951
+ }
6952
+
6953
+ // src/cli.ts
6660
6954
  var __dirname = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
6661
- var pkg = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf-8"));
6955
+ var pkg = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path5.join)(__dirname, "..", "package.json"), "utf-8"));
6662
6956
  function toFormat(s) {
6663
6957
  return s === "json" || s === "markdown" ? s : "console";
6664
6958
  }
@@ -6667,7 +6961,7 @@ function shouldColor(outputPath) {
6667
6961
  }
6668
6962
  function writeReport(text2, outputPath) {
6669
6963
  if (outputPath !== void 0) {
6670
- (0, import_node_fs8.writeFileSync)(outputPath, text2);
6964
+ (0, import_node_fs9.writeFileSync)(outputPath, text2);
6671
6965
  } else {
6672
6966
  process.stdout.write(text2 + "\n");
6673
6967
  }
@@ -6720,13 +7014,17 @@ function createProgram() {
6720
7014
  "Run a subset by eval id or tag (repeatable)",
6721
7015
  (val, prev) => [...prev, val],
6722
7016
  []
6723
- ).option("--concurrency <n>", "Override provider concurrency", (v) => parseInt(v, 10)).option("--no-cache", "Bypass the local cache for this run").option("--bail", "Stop after the first failing eval", false).option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(
7017
+ ).option("--concurrency <n>", "Override provider concurrency", (v) => parseInt(v, 10)).option("--no-cache", "Bypass the local cache for this run").option(
7018
+ "--cache-strict",
7019
+ "Every row must be in the cache: no model calls for completions; a miss ends the run with exit 5 and lists the rows",
7020
+ false
7021
+ ).option("--cache-max-age <duration>", "Treat cache entries older than this as a miss (e.g. 90s, 15m, 12h, 7d)").option("--bail", "Stop after the first failing eval", false).option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(
6724
7022
  async (options, command) => {
6725
7023
  const globals = command.optsWithGlobals();
6726
7024
  const configPath = globals.config;
6727
7025
  const [
6728
7026
  { handleRun: handleRun2 },
6729
- { exitCodeFor: exitCodeFor2, RunConfigError: RunConfigError2 },
7027
+ { exitCodeFor: exitCodeFor2, cacheMissRows: cacheMissRows2, CACHE_STRICT_EXIT: CACHE_STRICT_EXIT2, RunConfigError: RunConfigError2 },
6730
7028
  { renderReport: renderReport2 },
6731
7029
  { ConfigNotFoundError: ConfigNotFoundError2, ConfigValidationError: ConfigValidationError2, YamlParseError: YamlParseError2 },
6732
7030
  { AuthError: AuthError2 }
@@ -6738,13 +7036,36 @@ function createProgram() {
6738
7036
  Promise.resolve().then(() => (init_errors2(), errors_exports2))
6739
7037
  ]);
6740
7038
  try {
7039
+ if (options.cache === false && options.cacheMaxAge !== void 0) {
7040
+ process.stderr.write("--no-cache and --cache-max-age cannot be used together\n");
7041
+ process.exit(2);
7042
+ return;
7043
+ }
7044
+ if (options.cache === false && options.cacheStrict) {
7045
+ process.stderr.write("--no-cache and --cache-strict cannot be used together\n");
7046
+ process.exit(2);
7047
+ return;
7048
+ }
7049
+ let cacheMaxAgeMs;
7050
+ if (options.cacheMaxAge !== void 0) {
7051
+ try {
7052
+ cacheMaxAgeMs = parseDuration(options.cacheMaxAge);
7053
+ } catch (err) {
7054
+ process.stderr.write(`--cache-max-age: ${err.message}
7055
+ `);
7056
+ process.exit(2);
7057
+ return;
7058
+ }
7059
+ }
6741
7060
  const result = await handleRun2({
6742
7061
  dryRun: options.dryRun,
6743
7062
  ...configPath !== void 0 ? { configPath } : {},
6744
7063
  ...options.only.length > 0 ? { only: options.only } : {},
6745
7064
  ...typeof options.concurrency === "number" && Number.isFinite(options.concurrency) ? { concurrency: options.concurrency } : {},
6746
7065
  noCache: options.cache === false,
6747
- bail: options.bail
7066
+ bail: options.bail,
7067
+ ...options.cacheStrict ? { cacheStrict: true } : {},
7068
+ ...cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs } : {}
6748
7069
  });
6749
7070
  if (options.only.length > 0 && result.evals.length === 0) {
6750
7071
  process.stderr.write("No evals matched the selector.\n");
@@ -6754,6 +7075,15 @@ function createProgram() {
6754
7075
  });
6755
7076
  writeReport(text2, options.output);
6756
7077
  process.exitCode = exitCodeFor2(result);
7078
+ const missed = cacheMissRows2(result);
7079
+ if (missed.length > 0) {
7080
+ process.stderr.write(
7081
+ `--cache-strict: ${missed.length} row(s) have no cache entry:
7082
+ ${missed.map((m) => ` - ${m}`).join("\n")}
7083
+ `
7084
+ );
7085
+ process.exitCode = CACHE_STRICT_EXIT2;
7086
+ }
6757
7087
  } catch (err) {
6758
7088
  const verbose = globals.verbose === true;
6759
7089
  if (err instanceof ConfigValidationError2) {
@@ -6803,11 +7133,12 @@ function createProgram() {
6803
7133
  };
6804
7134
  const verboseOf = (command) => command.optsWithGlobals().verbose === true;
6805
7135
  const FAIL_ON_FAILURES = "Exit with the platform's verdict on the run: 1 on a failed or broken result (muted ones do not count), on an expected case that never ran, or when there is no verdict at all";
6806
- runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
7136
+ runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
6807
7137
  try {
6808
7138
  const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
6809
7139
  await handleRunStart2({
6810
7140
  ...options.key !== void 0 ? { key: options.key } : {},
7141
+ ...options.name !== void 0 ? { name: options.name } : {},
6811
7142
  json: options.json
6812
7143
  });
6813
7144
  } catch (err) {
@@ -6831,12 +7162,13 @@ function createProgram() {
6831
7162
  );
6832
7163
  runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description(
6833
7164
  "Open a run, run a command inside it, close the run \u2014 whatever the command returns"
6834
- ).option("--key <externalKey>", "The key several processes share; generated when absent").option("--fail-on-failures", `${FAIL_ON_FAILURES}; the command's own exit code when no result reached the run`, false).action(async (argv, options, command) => {
7165
+ ).option("--key <externalKey>", "The key several processes share; generated when absent").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--fail-on-failures", `${FAIL_ON_FAILURES}; the command's own exit code when no result reached the run`, false).action(async (argv, options, command) => {
6835
7166
  try {
6836
7167
  const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
6837
7168
  process.exitCode = await handleRunExec2({
6838
7169
  argv,
6839
7170
  ...options.key !== void 0 ? { key: options.key } : {},
7171
+ ...options.name !== void 0 ? { name: options.name } : {},
6840
7172
  ...options.failOnFailures ? { failOnFailures: true } : {}
6841
7173
  });
6842
7174
  } catch (err) {
@@ -6888,7 +7220,7 @@ function createProgram() {
6888
7220
  await failRunCommand(err, verboseOf(command));
6889
7221
  }
6890
7222
  });
6891
- runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report, or an Allure allure-results folder").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json | allure; detected from the file (a folder is allure) when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).option(
7223
+ runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report, or an Allure allure-results folder").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json | allure; detected from the file (a folder is allure) when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--create", "Offer tests nothing matched to the review queue", false).option(
6892
7224
  "--fail-on-missing-attachments",
6893
7225
  "Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
6894
7226
  false
@@ -6930,6 +7262,7 @@ function createProgram() {
6930
7262
  file,
6931
7263
  ...format !== void 0 ? { format } : {},
6932
7264
  ...options.key !== void 0 ? { key: options.key } : {},
7265
+ ...options.name !== void 0 ? { name: options.name } : {},
6933
7266
  ...options.create ? { create: true } : {},
6934
7267
  ...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
6935
7268
  ...options.timeout !== void 0 ? { timeoutSec: options.timeout } : {},
@@ -7190,6 +7523,31 @@ function createProgram() {
7190
7523
  failUnexpected(err, verbose);
7191
7524
  }
7192
7525
  });
7526
+ program.command("verify").description(
7527
+ "Compare the project's automated cases with the Playwright tests in the code, read-only \u2014 both sides of the drift, nothing written"
7528
+ ).option("--from <file>", "A saved `playwright test --list --reporter=json` document instead of running Playwright").option("--playwright-config <path>", "Passed to `playwright test --config`").option("--json", "Print the report as JSON", false).option("--fail-on-drift", "Exit 1 when either side has something the other lacks", false).action(
7529
+ async (options, command) => {
7530
+ const verbose = verboseOf(command);
7531
+ const { handleVerify: handleVerify2, formatVerify: formatVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
7532
+ const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
7533
+ try {
7534
+ const report = await handleVerify2({
7535
+ ...options.from === void 0 ? {} : { from: options.from },
7536
+ ...options.playwrightConfig === void 0 ? {} : { playwrightConfig: options.playwrightConfig }
7537
+ });
7538
+ process.stdout.write(options.json ? JSON.stringify(report, null, 2) + "\n" : formatVerify2(report));
7539
+ if (options.failOnDrift && report.onlyInPlatform.length + report.onlyInCode.length > 0) process.exitCode = 1;
7540
+ } catch (err) {
7541
+ const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
7542
+ if (code !== null) {
7543
+ maybeStack(err, verbose);
7544
+ process.exitCode = code;
7545
+ return;
7546
+ }
7547
+ failUnexpected(err, verbose);
7548
+ }
7549
+ }
7550
+ );
7193
7551
  program.on("command:*", (operands) => {
7194
7552
  process.stderr.write(`Unknown command: ${operands.join(" ")}
7195
7553
  `);
@@ -7201,7 +7559,7 @@ function isMainModule() {
7201
7559
  const entry = process.argv[1];
7202
7560
  if (entry === void 0) return false;
7203
7561
  try {
7204
- return (0, import_node_fs8.realpathSync)(entry) === (0, import_node_fs8.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
7562
+ return (0, import_node_fs9.realpathSync)(entry) === (0, import_node_fs9.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
7205
7563
  } catch {
7206
7564
  return false;
7207
7565
  }