@plune-ai/cli 0.17.1 → 0.19.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/index.cjs CHANGED
@@ -1272,6 +1272,64 @@ async function mapLimit(items, limit, fn) {
1272
1272
  return results;
1273
1273
  }
1274
1274
 
1275
+ // src/types/test-case-schema.ts
1276
+ var import_zod2 = require("zod");
1277
+ var TEST_CASE_SCHEMA_VERSION = 2;
1278
+ var externalKeySchema = import_zod2.z.object({
1279
+ kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1280
+ value: import_zod2.z.string().min(1)
1281
+ });
1282
+ var manualStepSchema = import_zod2.z.object({
1283
+ action: import_zod2.z.string().min(1),
1284
+ expected: import_zod2.z.string().min(1).optional()
1285
+ });
1286
+ var assertionResultRecordSchema = import_zod2.z.object({
1287
+ type: import_zod2.z.string().min(1),
1288
+ passed: import_zod2.z.boolean(),
1289
+ score: import_zod2.z.number().optional(),
1290
+ reason: import_zod2.z.string().optional()
1291
+ });
1292
+ var manualCaseSchema = import_zod2.z.object({
1293
+ type: import_zod2.z.literal("manual"),
1294
+ steps: import_zod2.z.array(manualStepSchema).min(1),
1295
+ expected: import_zod2.z.string().min(1).optional()
1296
+ });
1297
+ var automatedCaseSchema = import_zod2.z.object({
1298
+ type: import_zod2.z.literal("automated"),
1299
+ specRef: import_zod2.z.string().min(1)
1300
+ });
1301
+ var evalCaseSchema = import_zod2.z.object({
1302
+ type: import_zod2.z.literal("eval"),
1303
+ prompt: import_zod2.z.string().min(1),
1304
+ goldenSetId: import_zod2.z.string().min(1).optional(),
1305
+ assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
1306
+ });
1307
+ var testCaseBaseSchema = import_zod2.z.object({
1308
+ schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
1309
+ id: import_zod2.z.string().min(1),
1310
+ externalKeys: import_zod2.z.array(externalKeySchema).optional(),
1311
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1312
+ is_flaky: import_zod2.z.boolean().optional(),
1313
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1314
+ is_muted: import_zod2.z.boolean().optional()
1315
+ });
1316
+ var testCaseSchema = import_zod2.z.discriminatedUnion("type", [
1317
+ testCaseBaseSchema.merge(manualCaseSchema),
1318
+ testCaseBaseSchema.merge(automatedCaseSchema),
1319
+ testCaseBaseSchema.merge(evalCaseSchema)
1320
+ ]);
1321
+ function parseTestCase(value) {
1322
+ return testCaseSchema.parse(value);
1323
+ }
1324
+
1325
+ // src/types/results.ts
1326
+ var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1327
+ function rowStatus(row) {
1328
+ if (row.inconclusive !== void 0) return row.inconclusive;
1329
+ if (row.error !== void 0) return "errored";
1330
+ return row.assertions.every((a) => a.passed) ? "passed" : "failed";
1331
+ }
1332
+
1275
1333
  // src/orchestrator/run.ts
1276
1334
  var DEFAULT_MAX_TOKENS2 = 1024;
1277
1335
  var PLUNE_VERSION = "0.1.0";
@@ -1414,10 +1472,25 @@ function resolveTemplate(ev, baseDir) {
1414
1472
  }
1415
1473
  throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
1416
1474
  }
1417
- function classify(row) {
1418
- if (row.error !== void 0) return "errored";
1419
- if (row.assertions.some((a) => !a.passed)) return "failed";
1420
- return "passed";
1475
+ function summarize(evals, durationMs) {
1476
+ const summary = {
1477
+ total: 0,
1478
+ passed: 0,
1479
+ failed: 0,
1480
+ errored: 0,
1481
+ blocked: 0,
1482
+ skipped: 0,
1483
+ cost_usd: 0,
1484
+ duration_ms: durationMs
1485
+ };
1486
+ for (const ev of evals) {
1487
+ for (const row of ev.rows) {
1488
+ summary.total += 1;
1489
+ summary[rowStatus(row)] += 1;
1490
+ summary.cost_usd += row.usage?.cost_usd ?? 0;
1491
+ }
1492
+ }
1493
+ return summary;
1421
1494
  }
1422
1495
  async function runOrchestration(config, options, deps) {
1423
1496
  const started = deps.now();
@@ -1452,26 +1525,12 @@ async function runOrchestration(config, options, deps) {
1452
1525
  noCache
1453
1526
  })
1454
1527
  );
1455
- const passed = rowResults.every((r) => classify(r) === "passed");
1528
+ const passed = rowResults.every((r) => rowStatus(r) === "passed");
1456
1529
  evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
1457
1530
  if (options.bail === true && !passed) break;
1458
1531
  }
1459
1532
  const finished = deps.now();
1460
- const summary = {
1461
- total: 0,
1462
- passed: 0,
1463
- failed: 0,
1464
- errored: 0,
1465
- cost_usd: 0,
1466
- duration_ms: finished - started
1467
- };
1468
- for (const ev of evalResults) {
1469
- for (const row of ev.rows) {
1470
- summary.total += 1;
1471
- summary[classify(row)] += 1;
1472
- summary.cost_usd += row.usage?.cost_usd ?? 0;
1473
- }
1474
- }
1533
+ const summary = summarize(evalResults, finished - started);
1475
1534
  return {
1476
1535
  schemaVersion: 1,
1477
1536
  plune_version: PLUNE_VERSION,
@@ -1556,61 +1615,6 @@ var path5 = __toESM(require("path"), 1);
1556
1615
 
1557
1616
  // src/types/run-result-schema.ts
1558
1617
  var import_zod3 = require("zod");
1559
-
1560
- // src/types/test-case-schema.ts
1561
- var import_zod2 = require("zod");
1562
- var TEST_CASE_SCHEMA_VERSION = 2;
1563
- var externalKeySchema = import_zod2.z.object({
1564
- kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1565
- value: import_zod2.z.string().min(1)
1566
- });
1567
- var manualStepSchema = import_zod2.z.object({
1568
- action: import_zod2.z.string().min(1),
1569
- expected: import_zod2.z.string().min(1).optional()
1570
- });
1571
- var assertionResultRecordSchema = import_zod2.z.object({
1572
- type: import_zod2.z.string().min(1),
1573
- passed: import_zod2.z.boolean(),
1574
- score: import_zod2.z.number().optional(),
1575
- reason: import_zod2.z.string().optional()
1576
- });
1577
- var manualCaseSchema = import_zod2.z.object({
1578
- type: import_zod2.z.literal("manual"),
1579
- steps: import_zod2.z.array(manualStepSchema).min(1),
1580
- expected: import_zod2.z.string().min(1).optional()
1581
- });
1582
- var automatedCaseSchema = import_zod2.z.object({
1583
- type: import_zod2.z.literal("automated"),
1584
- specRef: import_zod2.z.string().min(1)
1585
- });
1586
- var evalCaseSchema = import_zod2.z.object({
1587
- type: import_zod2.z.literal("eval"),
1588
- prompt: import_zod2.z.string().min(1),
1589
- goldenSetId: import_zod2.z.string().min(1).optional(),
1590
- assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
1591
- });
1592
- var testCaseBaseSchema = import_zod2.z.object({
1593
- schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
1594
- id: import_zod2.z.string().min(1),
1595
- externalKeys: import_zod2.z.array(externalKeySchema).optional(),
1596
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1597
- is_flaky: import_zod2.z.boolean().optional(),
1598
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1599
- is_muted: import_zod2.z.boolean().optional()
1600
- });
1601
- var testCaseSchema = import_zod2.z.discriminatedUnion("type", [
1602
- testCaseBaseSchema.merge(manualCaseSchema),
1603
- testCaseBaseSchema.merge(automatedCaseSchema),
1604
- testCaseBaseSchema.merge(evalCaseSchema)
1605
- ]);
1606
- function parseTestCase(value) {
1607
- return testCaseSchema.parse(value);
1608
- }
1609
-
1610
- // src/types/results.ts
1611
- var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1612
-
1613
- // src/types/run-result-schema.ts
1614
1618
  var RUN_RESULT_SCHEMA_VERSION = 1;
1615
1619
  var binaryVerdictSchema = import_zod3.z.object({
1616
1620
  question: import_zod3.z.string().min(1),
@@ -1648,6 +1652,9 @@ var summarySchema = import_zod3.z.object({
1648
1652
  passed: import_zod3.z.number(),
1649
1653
  failed: import_zod3.z.number(),
1650
1654
  errored: import_zod3.z.number(),
1655
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
1656
+ blocked: import_zod3.z.number().optional(),
1657
+ skipped: import_zod3.z.number().optional(),
1651
1658
  cost_usd: import_zod3.z.number(),
1652
1659
  duration_ms: import_zod3.z.number()
1653
1660
  });
@@ -1696,6 +1703,22 @@ function loadToken() {
1696
1703
  }
1697
1704
  }
1698
1705
 
1706
+ // src/cli/gate.ts
1707
+ function gateExit(verdict, runnerCode) {
1708
+ if (verdict?.statusEvent === "terminated") {
1709
+ return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
1710
+ }
1711
+ const counts = verdict?.counts;
1712
+ if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
1713
+ const n = (status) => counts[status] ?? 0;
1714
+ const notRun = verdict?.notRun?.length ?? 0;
1715
+ if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
1716
+ return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
1717
+ }
1718
+ const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
1719
+ return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
1720
+ }
1721
+
1699
1722
  // src/cli/commands/sync.ts
1700
1723
  var NotLoggedInError = class extends Error {
1701
1724
  constructor() {
@@ -1788,7 +1811,18 @@ async function handleSync(deps = {}) {
1788
1811
  }
1789
1812
  const body = await res.json().catch(() => ({}));
1790
1813
  const id = typeof body.id === "string" ? body.id : "";
1791
- return { id, url: `${endpoint}/${id}` };
1814
+ const result = { id, url: `${endpoint}/${id}` };
1815
+ if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
1816
+ return result;
1817
+ }
1818
+ async function readVerdict(doFetch, url, token) {
1819
+ try {
1820
+ const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
1821
+ const body = await res.json().catch(() => void 0);
1822
+ return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
1823
+ } catch {
1824
+ return void 0;
1825
+ }
1792
1826
  }
1793
1827
  function reportSyncFailure(err, write) {
1794
1828
  if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
package/dist/index.d.cts CHANGED
@@ -5,6 +5,14 @@ interface Summary {
5
5
  passed: number;
6
6
  failed: number;
7
7
  errored: number;
8
+ /**
9
+ * plune#1245. Rows the runner said were never judged (`RowResult.inconclusive`), counted apart from
10
+ * `failed`. The runner always writes both; they are optional so a RunResult written before them is
11
+ * still valid and `schemaVersion` stays 1. With them, `passed + failed + errored + blocked + skipped`
12
+ * is `total`.
13
+ */
14
+ blocked?: number;
15
+ skipped?: number;
8
16
  cost_usd: number;
9
17
  duration_ms: number;
10
18
  }
@@ -307,12 +315,19 @@ interface SyncDeps {
307
315
  loadToken?: () => string | null;
308
316
  /** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
309
317
  fetchImpl?: typeof fetch;
318
+ /** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
319
+ failOnFailures?: boolean;
310
320
  }
311
321
  interface SyncResult {
312
322
  /** Server-assigned run id. */
313
323
  id: string;
314
324
  /** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
315
325
  url: string;
326
+ /** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
327
+ gate?: {
328
+ code: number;
329
+ line: string;
330
+ };
316
331
  }
317
332
  /**
318
333
  * Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
@@ -430,6 +445,8 @@ declare const runResultSchema: z.ZodObject<{
430
445
  passed: z.ZodNumber;
431
446
  failed: z.ZodNumber;
432
447
  errored: z.ZodNumber;
448
+ blocked: z.ZodOptional<z.ZodNumber>;
449
+ skipped: z.ZodOptional<z.ZodNumber>;
433
450
  cost_usd: z.ZodNumber;
434
451
  duration_ms: z.ZodNumber;
435
452
  }, z.core.$strip>;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,14 @@ interface Summary {
5
5
  passed: number;
6
6
  failed: number;
7
7
  errored: number;
8
+ /**
9
+ * plune#1245. Rows the runner said were never judged (`RowResult.inconclusive`), counted apart from
10
+ * `failed`. The runner always writes both; they are optional so a RunResult written before them is
11
+ * still valid and `schemaVersion` stays 1. With them, `passed + failed + errored + blocked + skipped`
12
+ * is `total`.
13
+ */
14
+ blocked?: number;
15
+ skipped?: number;
8
16
  cost_usd: number;
9
17
  duration_ms: number;
10
18
  }
@@ -307,12 +315,19 @@ interface SyncDeps {
307
315
  loadToken?: () => string | null;
308
316
  /** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
309
317
  fetchImpl?: typeof fetch;
318
+ /** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
319
+ failOnFailures?: boolean;
310
320
  }
311
321
  interface SyncResult {
312
322
  /** Server-assigned run id. */
313
323
  id: string;
314
324
  /** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
315
325
  url: string;
326
+ /** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
327
+ gate?: {
328
+ code: number;
329
+ line: string;
330
+ };
316
331
  }
317
332
  /**
318
333
  * Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
@@ -430,6 +445,8 @@ declare const runResultSchema: z.ZodObject<{
430
445
  passed: z.ZodNumber;
431
446
  failed: z.ZodNumber;
432
447
  errored: z.ZodNumber;
448
+ blocked: z.ZodOptional<z.ZodNumber>;
449
+ skipped: z.ZodOptional<z.ZodNumber>;
433
450
  cost_usd: z.ZodNumber;
434
451
  duration_ms: z.ZodNumber;
435
452
  }, z.core.$strip>;
package/dist/index.js CHANGED
@@ -1216,6 +1216,64 @@ async function mapLimit(items, limit, fn) {
1216
1216
  return results;
1217
1217
  }
1218
1218
 
1219
+ // src/types/test-case-schema.ts
1220
+ import { z as z2 } from "zod";
1221
+ var TEST_CASE_SCHEMA_VERSION = 2;
1222
+ var externalKeySchema = z2.object({
1223
+ kind: z2.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1224
+ value: z2.string().min(1)
1225
+ });
1226
+ var manualStepSchema = z2.object({
1227
+ action: z2.string().min(1),
1228
+ expected: z2.string().min(1).optional()
1229
+ });
1230
+ var assertionResultRecordSchema = z2.object({
1231
+ type: z2.string().min(1),
1232
+ passed: z2.boolean(),
1233
+ score: z2.number().optional(),
1234
+ reason: z2.string().optional()
1235
+ });
1236
+ var manualCaseSchema = z2.object({
1237
+ type: z2.literal("manual"),
1238
+ steps: z2.array(manualStepSchema).min(1),
1239
+ expected: z2.string().min(1).optional()
1240
+ });
1241
+ var automatedCaseSchema = z2.object({
1242
+ type: z2.literal("automated"),
1243
+ specRef: z2.string().min(1)
1244
+ });
1245
+ var evalCaseSchema = z2.object({
1246
+ type: z2.literal("eval"),
1247
+ prompt: z2.string().min(1),
1248
+ goldenSetId: z2.string().min(1).optional(),
1249
+ assertions: z2.array(assertionResultRecordSchema).min(1)
1250
+ });
1251
+ var testCaseBaseSchema = z2.object({
1252
+ schemaVersion: z2.literal(TEST_CASE_SCHEMA_VERSION),
1253
+ id: z2.string().min(1),
1254
+ externalKeys: z2.array(externalKeySchema).optional(),
1255
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1256
+ is_flaky: z2.boolean().optional(),
1257
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1258
+ is_muted: z2.boolean().optional()
1259
+ });
1260
+ var testCaseSchema = z2.discriminatedUnion("type", [
1261
+ testCaseBaseSchema.merge(manualCaseSchema),
1262
+ testCaseBaseSchema.merge(automatedCaseSchema),
1263
+ testCaseBaseSchema.merge(evalCaseSchema)
1264
+ ]);
1265
+ function parseTestCase(value) {
1266
+ return testCaseSchema.parse(value);
1267
+ }
1268
+
1269
+ // src/types/results.ts
1270
+ var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1271
+ function rowStatus(row) {
1272
+ if (row.inconclusive !== void 0) return row.inconclusive;
1273
+ if (row.error !== void 0) return "errored";
1274
+ return row.assertions.every((a) => a.passed) ? "passed" : "failed";
1275
+ }
1276
+
1219
1277
  // src/orchestrator/run.ts
1220
1278
  var DEFAULT_MAX_TOKENS2 = 1024;
1221
1279
  var PLUNE_VERSION = "0.1.0";
@@ -1358,10 +1416,25 @@ function resolveTemplate(ev, baseDir) {
1358
1416
  }
1359
1417
  throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
1360
1418
  }
1361
- function classify(row) {
1362
- if (row.error !== void 0) return "errored";
1363
- if (row.assertions.some((a) => !a.passed)) return "failed";
1364
- return "passed";
1419
+ function summarize(evals, durationMs) {
1420
+ const summary = {
1421
+ total: 0,
1422
+ passed: 0,
1423
+ failed: 0,
1424
+ errored: 0,
1425
+ blocked: 0,
1426
+ skipped: 0,
1427
+ cost_usd: 0,
1428
+ duration_ms: durationMs
1429
+ };
1430
+ for (const ev of evals) {
1431
+ for (const row of ev.rows) {
1432
+ summary.total += 1;
1433
+ summary[rowStatus(row)] += 1;
1434
+ summary.cost_usd += row.usage?.cost_usd ?? 0;
1435
+ }
1436
+ }
1437
+ return summary;
1365
1438
  }
1366
1439
  async function runOrchestration(config, options, deps) {
1367
1440
  const started = deps.now();
@@ -1396,26 +1469,12 @@ async function runOrchestration(config, options, deps) {
1396
1469
  noCache
1397
1470
  })
1398
1471
  );
1399
- const passed = rowResults.every((r) => classify(r) === "passed");
1472
+ const passed = rowResults.every((r) => rowStatus(r) === "passed");
1400
1473
  evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
1401
1474
  if (options.bail === true && !passed) break;
1402
1475
  }
1403
1476
  const finished = deps.now();
1404
- const summary = {
1405
- total: 0,
1406
- passed: 0,
1407
- failed: 0,
1408
- errored: 0,
1409
- cost_usd: 0,
1410
- duration_ms: finished - started
1411
- };
1412
- for (const ev of evalResults) {
1413
- for (const row of ev.rows) {
1414
- summary.total += 1;
1415
- summary[classify(row)] += 1;
1416
- summary.cost_usd += row.usage?.cost_usd ?? 0;
1417
- }
1418
- }
1477
+ const summary = summarize(evalResults, finished - started);
1419
1478
  return {
1420
1479
  schemaVersion: 1,
1421
1480
  plune_version: PLUNE_VERSION,
@@ -1500,61 +1559,6 @@ import * as path5 from "path";
1500
1559
 
1501
1560
  // src/types/run-result-schema.ts
1502
1561
  import { z as z3 } from "zod";
1503
-
1504
- // src/types/test-case-schema.ts
1505
- import { z as z2 } from "zod";
1506
- var TEST_CASE_SCHEMA_VERSION = 2;
1507
- var externalKeySchema = z2.object({
1508
- kind: z2.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1509
- value: z2.string().min(1)
1510
- });
1511
- var manualStepSchema = z2.object({
1512
- action: z2.string().min(1),
1513
- expected: z2.string().min(1).optional()
1514
- });
1515
- var assertionResultRecordSchema = z2.object({
1516
- type: z2.string().min(1),
1517
- passed: z2.boolean(),
1518
- score: z2.number().optional(),
1519
- reason: z2.string().optional()
1520
- });
1521
- var manualCaseSchema = z2.object({
1522
- type: z2.literal("manual"),
1523
- steps: z2.array(manualStepSchema).min(1),
1524
- expected: z2.string().min(1).optional()
1525
- });
1526
- var automatedCaseSchema = z2.object({
1527
- type: z2.literal("automated"),
1528
- specRef: z2.string().min(1)
1529
- });
1530
- var evalCaseSchema = z2.object({
1531
- type: z2.literal("eval"),
1532
- prompt: z2.string().min(1),
1533
- goldenSetId: z2.string().min(1).optional(),
1534
- assertions: z2.array(assertionResultRecordSchema).min(1)
1535
- });
1536
- var testCaseBaseSchema = z2.object({
1537
- schemaVersion: z2.literal(TEST_CASE_SCHEMA_VERSION),
1538
- id: z2.string().min(1),
1539
- externalKeys: z2.array(externalKeySchema).optional(),
1540
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1541
- is_flaky: z2.boolean().optional(),
1542
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1543
- is_muted: z2.boolean().optional()
1544
- });
1545
- var testCaseSchema = z2.discriminatedUnion("type", [
1546
- testCaseBaseSchema.merge(manualCaseSchema),
1547
- testCaseBaseSchema.merge(automatedCaseSchema),
1548
- testCaseBaseSchema.merge(evalCaseSchema)
1549
- ]);
1550
- function parseTestCase(value) {
1551
- return testCaseSchema.parse(value);
1552
- }
1553
-
1554
- // src/types/results.ts
1555
- var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1556
-
1557
- // src/types/run-result-schema.ts
1558
1562
  var RUN_RESULT_SCHEMA_VERSION = 1;
1559
1563
  var binaryVerdictSchema = z3.object({
1560
1564
  question: z3.string().min(1),
@@ -1592,6 +1596,9 @@ var summarySchema = z3.object({
1592
1596
  passed: z3.number(),
1593
1597
  failed: z3.number(),
1594
1598
  errored: z3.number(),
1599
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
1600
+ blocked: z3.number().optional(),
1601
+ skipped: z3.number().optional(),
1595
1602
  cost_usd: z3.number(),
1596
1603
  duration_ms: z3.number()
1597
1604
  });
@@ -1640,6 +1647,22 @@ function loadToken() {
1640
1647
  }
1641
1648
  }
1642
1649
 
1650
+ // src/cli/gate.ts
1651
+ function gateExit(verdict, runnerCode) {
1652
+ if (verdict?.statusEvent === "terminated") {
1653
+ return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
1654
+ }
1655
+ const counts = verdict?.counts;
1656
+ if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
1657
+ const n = (status) => counts[status] ?? 0;
1658
+ const notRun = verdict?.notRun?.length ?? 0;
1659
+ if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
1660
+ return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
1661
+ }
1662
+ const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
1663
+ return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
1664
+ }
1665
+
1643
1666
  // src/cli/commands/sync.ts
1644
1667
  var NotLoggedInError = class extends Error {
1645
1668
  constructor() {
@@ -1732,7 +1755,18 @@ async function handleSync(deps = {}) {
1732
1755
  }
1733
1756
  const body = await res.json().catch(() => ({}));
1734
1757
  const id = typeof body.id === "string" ? body.id : "";
1735
- return { id, url: `${endpoint}/${id}` };
1758
+ const result = { id, url: `${endpoint}/${id}` };
1759
+ if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
1760
+ return result;
1761
+ }
1762
+ async function readVerdict(doFetch, url, token) {
1763
+ try {
1764
+ const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
1765
+ const body = await res.json().catch(() => void 0);
1766
+ return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
1767
+ } catch {
1768
+ return void 0;
1769
+ }
1736
1770
  }
1737
1771
  function reportSyncFailure(err, write) {
1738
1772
  if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
@@ -215,7 +215,7 @@ function createClient(opts) {
215
215
  if (res.status === 429 && retryAfterMs(res) === null) {
216
216
  last.kind = "invalid";
217
217
  last.detail = "quota exceeded (no Retry-After)";
218
- if (said.detail) last.detail += ` \u2014 ${said.detail}`;
218
+ if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
219
219
  break;
220
220
  }
221
221
  if (attempt < RETRY_DELAYS_MS.length) {
@@ -1100,21 +1100,26 @@ async function startRun(passed, declared) {
1100
1100
  },
1101
1101
  flush,
1102
1102
  async finish(reason) {
1103
- if (done) return;
1103
+ if (done) return void 0;
1104
1104
  done = true;
1105
1105
  await flush();
1106
1106
  await offer();
1107
+ let frozen;
1107
1108
  if (runId !== null && !offline) {
1108
1109
  const out = await client.post(`/v1/runs/${runId}/events`, {
1109
1110
  event: "finish",
1110
1111
  ...reason !== void 0 ? { reason } : {}
1111
1112
  });
1112
1113
  if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
1113
- else if (typeof out.body?.detached === "number" && out.body.detached > 0) {
1114
- log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
1114
+ else {
1115
+ frozen = out.body?.summary ?? void 0;
1116
+ if (typeof out.body?.detached === "number" && out.body.detached > 0) {
1117
+ log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
1118
+ }
1115
1119
  }
1116
1120
  }
1117
1121
  summarise();
1122
+ return frozen;
1118
1123
  },
1119
1124
  async leaveOpen() {
1120
1125
  if (done) return;
@@ -221,6 +221,17 @@ interface RunRecord {
221
221
  projectId?: string;
222
222
  schemaVersion?: number;
223
223
  }
224
+ /**
225
+ * What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
226
+ * release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
227
+ * muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
228
+ */
229
+ interface FrozenSummary {
230
+ counts: Record<string, number>;
231
+ /** The expected cases that never reported. */
232
+ notRun: string[];
233
+ total: number;
234
+ }
224
235
  /** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
225
236
  * the platform refuses to resolve that by overwriting, and neither do we. */
226
237
  interface SubmitCounts {
@@ -357,8 +368,11 @@ interface RunSession {
357
368
  add(result: PendingResult): Promise<void>;
358
369
  /** Send whatever is buffered. Safe to call when nothing is. */
359
370
  flush(): Promise<void>;
360
- /** Send what is left and close the run. */
361
- finish(reason?: string): Promise<void>;
371
+ /**
372
+ * Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
373
+ * `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
374
+ */
375
+ finish(reason?: string): Promise<FrozenSummary | undefined>;
362
376
  /** Send what is left and deliberately leave the run open — the shard case (AC-03). */
363
377
  leaveOpen(): Promise<void>;
364
378
  }
@@ -592,4 +606,4 @@ interface PlatformClient {
592
606
  }
593
607
  declare function createClient(opts: ClientOptions): PlatformClient;
594
608
 
595
- export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
609
+ export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type FrozenSummary, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
@@ -221,6 +221,17 @@ interface RunRecord {
221
221
  projectId?: string;
222
222
  schemaVersion?: number;
223
223
  }
224
+ /**
225
+ * What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
226
+ * release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
227
+ * muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
228
+ */
229
+ interface FrozenSummary {
230
+ counts: Record<string, number>;
231
+ /** The expected cases that never reported. */
232
+ notRun: string[];
233
+ total: number;
234
+ }
224
235
  /** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
225
236
  * the platform refuses to resolve that by overwriting, and neither do we. */
226
237
  interface SubmitCounts {
@@ -357,8 +368,11 @@ interface RunSession {
357
368
  add(result: PendingResult): Promise<void>;
358
369
  /** Send whatever is buffered. Safe to call when nothing is. */
359
370
  flush(): Promise<void>;
360
- /** Send what is left and close the run. */
361
- finish(reason?: string): Promise<void>;
371
+ /**
372
+ * Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
373
+ * `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
374
+ */
375
+ finish(reason?: string): Promise<FrozenSummary | undefined>;
362
376
  /** Send what is left and deliberately leave the run open — the shard case (AC-03). */
363
377
  leaveOpen(): Promise<void>;
364
378
  }
@@ -592,4 +606,4 @@ interface PlatformClient {
592
606
  }
593
607
  declare function createClient(opts: ClientOptions): PlatformClient;
594
608
 
595
- export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
609
+ export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type FrozenSummary, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };