@idlekit/cli 0.4.0 → 0.5.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/main.js CHANGED
@@ -8397,7 +8397,7 @@ var init_validate = __esm(() => {
8397
8397
  OFFLINE_ACTORS = new Set(["player", "automation"]);
8398
8398
  });
8399
8399
 
8400
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_validateReport.mjs
8400
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_validateReport.mjs
8401
8401
  var _validateReport = (array) => {
8402
8402
  const isAncestor = (ancestor, descendant) => descendant === ancestor || descendant.startsWith(`${ancestor}.`) || descendant.startsWith(`${ancestor}[`);
8403
8403
  const reportable = (path) => {
@@ -8422,7 +8422,7 @@ var _validateReport = (array) => {
8422
8422
  };
8423
8423
  var init__validateReport = () => {};
8424
8424
 
8425
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_createStandardSchema.mjs
8425
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_createStandardSchema.mjs
8426
8426
  var _createStandardSchema = (fn) => Object.assign(fn, { "~standard": {
8427
8427
  version: 1,
8428
8428
  vendor: "typia",
@@ -18414,7 +18414,7 @@ var init_checks3 = __esm(() => {
18414
18414
  FAIL_ON = ["error", "warn", "inconclusive", "unsupported"];
18415
18415
  });
18416
18416
 
18417
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_accessExpressionAsString.mjs
18417
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_accessExpressionAsString.mjs
18418
18418
  var _accessExpressionAsString = (str) => variable(str) ? `.${str}` : `[${JSON.stringify(str)}]`, variable = (str) => reserved(str) === false && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/g.test(str), reserved = (str) => RESERVED.has(str), RESERVED;
18419
18419
  var init__accessExpressionAsString = __esm(() => {
18420
18420
  RESERVED = /* @__PURE__ */ new Set([
@@ -18961,7 +18961,7 @@ var init_judge = __esm(() => {
18961
18961
  // ../core/dist/sim/analysis/flow.js
18962
18962
  var init_flow = () => {};
18963
18963
 
18964
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_isTypeUint32.mjs
18964
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_isTypeUint32.mjs
18965
18965
  var _isTypeUint32 = (value) => Math.floor(value) === value && MINIMUM <= value && value <= MAXIMUM, MINIMUM = 0, MAXIMUM;
18966
18966
  var init__isTypeUint32 = __esm(() => {
18967
18967
  MAXIMUM = 2 ** 32 - 1;
@@ -19570,7 +19570,7 @@ var init_bun = __esm(() => {
19570
19570
  var require_package = __commonJS(function(exports, module) {
19571
19571
  module.exports = {
19572
19572
  name: "@idlekit/cli",
19573
- version: "0.4.0",
19573
+ version: "0.5.0",
19574
19574
  description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
19575
19575
  license: "MIT",
19576
19576
  type: "module",
@@ -19623,7 +19623,7 @@ var require_package = __commonJS(function(exports, module) {
19623
19623
  "@clack/prompts": "1.8.1",
19624
19624
  "@gunshi/plugin-completion": "0.37.3",
19625
19625
  "@gunshi/plugin-suggestion": "0.37.3",
19626
- "@idlekit/core": "0.4.0",
19626
+ "@idlekit/core": "0.4.1",
19627
19627
  ajv: "^8.20.0",
19628
19628
  gunshi: "0.37.3",
19629
19629
  "proper-lockfile": "4.1.2",
@@ -35201,8 +35201,10 @@ async function readScenarioFileWithMeta(path) {
35201
35201
  }
35202
35202
 
35203
35203
  // src/io/writeOutput.ts
35204
+ init_errors3();
35204
35205
  init_bun();
35205
35206
  import { resolve as resolve5 } from "path";
35207
+ import { realpath, stat } from "fs/promises";
35206
35208
  function toCsv(data) {
35207
35209
  if (!Array.isArray(data)) {
35208
35210
  return `value
@@ -35276,6 +35278,27 @@ async function writeOutput(args) {
35276
35278
  return;
35277
35279
  }
35278
35280
  const abs = resolve5(args.outPath);
35281
+ for (const input of args.protectedInputs ?? []) {
35282
+ const source = resolve5(input);
35283
+ let same = source === abs;
35284
+ if (!same) {
35285
+ try {
35286
+ const [outputReal, inputReal, outputStat, inputStat] = await Promise.all([
35287
+ realpath(abs),
35288
+ realpath(source),
35289
+ stat(abs, { bigint: true }),
35290
+ stat(source, { bigint: true })
35291
+ ]);
35292
+ same = outputReal === inputReal || outputStat.ino !== 0n && outputStat.dev === inputStat.dev && outputStat.ino === inputStat.ino;
35293
+ } catch (error) {
35294
+ const code = error.code;
35295
+ if (code !== "ENOENT" && code !== "ENOTDIR")
35296
+ throw error;
35297
+ }
35298
+ }
35299
+ if (same)
35300
+ throw usageError(`Output path must not overwrite an input file: ${input}`);
35301
+ }
35279
35302
  await writeTextFile(abs, body);
35280
35303
  console.log(`Wrote ${args.format} output to ${abs}`);
35281
35304
  }
@@ -36449,6 +36472,15 @@ init_outputMeta();
36449
36472
  import { extname as extname3, resolve as resolve8 } from "path";
36450
36473
 
36451
36474
  // src/lib/ltvCalibration.ts
36475
+ function assertFiniteNumbers(value, path) {
36476
+ if (typeof value === "number" && !Number.isFinite(value)) {
36477
+ throw new RangeError(`Calibration requires finite numbers; numeric overflow at ${path}`);
36478
+ }
36479
+ if (typeof value === "object" && value !== null) {
36480
+ for (const [key, child] of Object.entries(value))
36481
+ assertFiniteNumbers(child, `${path}.${key}`);
36482
+ }
36483
+ }
36452
36484
  function clamp012(v) {
36453
36485
  if (v < 0)
36454
36486
  return 0;
@@ -36521,7 +36553,7 @@ function estimateCorrelation(x, y) {
36521
36553
  varianceY
36522
36554
  };
36523
36555
  }
36524
- const raw = clampCorrelation(cov / Math.sqrt(vx * vy));
36556
+ const raw = clampCorrelation(cov / Math.sqrt(vx) / Math.sqrt(vy));
36525
36557
  const ci95 = 1.96 / Math.sqrt(Math.max(1, n - 3));
36526
36558
  const sampleConfidence = clamp012(1 - ci95);
36527
36559
  const varianceConfidenceX = varianceX / (varianceX + 0.01);
@@ -36642,7 +36674,7 @@ function calibrateMonetization(rows) {
36642
36674
  conversionAd: estimatedCorrelationPair.conversionAd.confidence,
36643
36675
  arppuAd: estimatedCorrelationPair.arppuAd.confidence
36644
36676
  };
36645
- return {
36677
+ const result = {
36646
36678
  monetization: {
36647
36679
  cohorts: {
36648
36680
  baseUsers: cohortSize
@@ -36650,8 +36682,8 @@ function calibrateMonetization(rows) {
36650
36682
  retention: {
36651
36683
  d1: clamp012(d1),
36652
36684
  d7: clamp012(Math.min(d1, d7)),
36653
- d30: clamp012(Math.min(d7, d30)),
36654
- d90: clamp012(Math.min(d30, d90)),
36685
+ d30: clamp012(Math.min(d1, d7, d30)),
36686
+ d90: clamp012(Math.min(d1, d7, d30, d90)),
36655
36687
  longTailDailyDecay: 0.02
36656
36688
  },
36657
36689
  revenue: {
@@ -36710,6 +36742,8 @@ function calibrateMonetization(rows) {
36710
36742
  correlationDiagnostics: estimatedCorrelationPair
36711
36743
  }
36712
36744
  };
36745
+ assertFiniteNumbers(result, "calibration");
36746
+ return result;
36713
36747
  }
36714
36748
  // src/lib/ltvProjection.ts
36715
36749
  function clamp013(v) {
@@ -36992,6 +37026,135 @@ function estimateLtvDistribution(args) {
36992
37026
  quantiles: byQuantile
36993
37027
  };
36994
37028
  }
37029
+ // src/lib/cohortObservation.ts
37030
+ var RETENTION_DAYS = [1, 7, 30, 90];
37031
+ function observeCohort(rows, observedThroughDay) {
37032
+ if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
37033
+ throw new Error("observed-through-day must be a nonnegative safe integer");
37034
+ if (rows.length === 0)
37035
+ throw new Error("Observed cohort requires at least one install record");
37036
+ const users = new Map;
37037
+ for (const row of rows) {
37038
+ if (!row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
37039
+ throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
37040
+ }
37041
+ if (![row.iapRevenue, row.adRevenue, ...row.acquisitionCost === undefined ? [] : [row.acquisitionCost]].every((v) => Number.isFinite(v) && v >= 0)) {
37042
+ throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
37043
+ }
37044
+ if (row.active !== undefined && typeof row.active !== "boolean")
37045
+ throw new Error("Observed cohort active must be boolean");
37046
+ const user = users.get(row.userId) ?? { days: new Set, active: new Set };
37047
+ if (user.days.has(row.day))
37048
+ throw new Error("Observed cohort requires one aggregated row per user and day");
37049
+ user.days.add(row.day);
37050
+ if (row.active ?? true)
37051
+ user.active.add(row.day);
37052
+ users.set(row.userId, user);
37053
+ }
37054
+ for (const user of users.values()) {
37055
+ if (!user.days.has(0))
37056
+ throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
37057
+ }
37058
+ const n = users.size;
37059
+ const observation = {
37060
+ observedThroughDay,
37061
+ users: n,
37062
+ retention: RETENTION_DAYS.map((day) => {
37063
+ if (day > observedThroughDay)
37064
+ return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
37065
+ const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
37066
+ const rate = activeUsers / n;
37067
+ const z2 = 1.959963984540054 ** 2;
37068
+ const denominator = 1 + z2 / n;
37069
+ const center = (rate + z2 / (2 * n)) / denominator;
37070
+ const radius = Math.sqrt(z2 * (rate * (1 - rate) / n + z2 / (4 * n * n))) / denominator;
37071
+ return { day, status: "observed", users: n, activeUsers, rate, interval95: { min: activeUsers === 0 ? 0 : Math.max(0, center - radius), max: activeUsers === n ? 1 : Math.min(1, center + radius) } };
37072
+ })
37073
+ };
37074
+ assertCohortObservation(observation);
37075
+ return observation;
37076
+ }
37077
+ function assertCohortObservation(observation) {
37078
+ if (!Number.isSafeInteger(observation.users) || observation.users < 1 || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
37079
+ throw new Error("Invalid cohort population or observation cutoff");
37080
+ }
37081
+ if (observation.retention.length !== RETENTION_DAYS.length)
37082
+ throw new Error("Cohort retention must contain D1, D7, D30 and D90");
37083
+ for (const [index, point] of observation.retention.entries()) {
37084
+ if (point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
37085
+ throw new Error("Cohort retention days and population must match the observation");
37086
+ const observed = point.day <= observation.observedThroughDay;
37087
+ if (point.status !== (observed ? "observed" : "not-observed"))
37088
+ throw new Error("Cohort retention status must match the observation cutoff");
37089
+ if (!observed) {
37090
+ if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
37091
+ throw new Error("Future retention must remain unobserved");
37092
+ continue;
37093
+ }
37094
+ if (point.activeUsers === null || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > point.users || point.rate !== point.activeUsers / point.users) {
37095
+ throw new Error("Retained users and retention rate must match the installed population");
37096
+ }
37097
+ const interval = point.interval95;
37098
+ if (point.rate === null || !interval || !Number.isFinite(interval.min) || !Number.isFinite(interval.max) || interval.min < 0 || interval.max > 1 || interval.min > point.rate || interval.max < point.rate) {
37099
+ throw new Error("Retention confidence interval must be finite and contain the observed rate");
37100
+ }
37101
+ }
37102
+ }
37103
+ function observedRetentionRates(observation) {
37104
+ return Object.fromEntries(observation.retention.filter((row) => row.rate !== null).map((row) => [`d${row.day}`, row.rate]));
37105
+ }
37106
+ function planRetentionPatch(observation, assumptions = {}) {
37107
+ const rates = observedRetentionRates(observation);
37108
+ if (Object.keys(rates).length === 0)
37109
+ return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
37110
+ const merged = { ...assumptions, ...rates };
37111
+ const warnings = [];
37112
+ for (const day of RETENTION_DAYS) {
37113
+ if (merged[`d${day}`] === undefined)
37114
+ warnings.push(`Unobserved D${day} has no explicit retained assumption. An LTV default could conflict with the observations; supply this assumption in --scenario or observe this day before applying a patch.`);
37115
+ }
37116
+ let previous;
37117
+ for (const day of RETENTION_DAYS) {
37118
+ const rate = merged[`d${day}`];
37119
+ if (rate === undefined)
37120
+ continue;
37121
+ if (previous && previous.rate < rate)
37122
+ warnings.push(`D${previous.day}=${previous.rate} is below D${day}=${rate}. LTV requires d1 >= d7 >= d30 >= d90; review observations and retained assumptions before applying a patch.`);
37123
+ previous = { day, rate };
37124
+ }
37125
+ return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
37126
+ }
37127
+ function compareRetention(observation, assumptions) {
37128
+ return observation.retention.map((row) => {
37129
+ const assumption = assumptions[`d${row.day}`] ?? null;
37130
+ return {
37131
+ day: row.day,
37132
+ assumption,
37133
+ observed: row.rate,
37134
+ differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
37135
+ status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
37136
+ };
37137
+ });
37138
+ }
37139
+ function renderCohortMarkdown(observation, comparison, patch) {
37140
+ const percent = (value) => value === null ? "unavailable" : `${(value * 100).toFixed(2)}%`;
37141
+ const lines = [
37142
+ "# Observed player retention",
37143
+ "",
37144
+ `${observation.users} installed users; complete activity export through day ${observation.observedThroughDay}. Exact-day retention counts every installed user, including those who never return.`,
37145
+ "",
37146
+ "| Day | Retained / installed | Observed rate | Wilson 95% interval | Assumption | Difference (percentage points) | Status |",
37147
+ "| --- | --- | --- | --- | --- | --- | --- |"
37148
+ ];
37149
+ for (const [index, row] of observation.retention.entries()) {
37150
+ const match = comparison?.[index];
37151
+ lines.push(`| D${row.day} | ${row.activeUsers === null ? "unavailable" : `${row.activeUsers} / ${row.users}`} | ${percent(row.rate)} | ${row.interval95 ? `${percent(row.interval95.min)} to ${percent(row.interval95.max)}` : "unavailable"} | ${percent(match?.assumption ?? null)} | ${match?.differencePercentagePoints == null ? "unavailable" : match.differencePercentagePoints.toFixed(2)} | ${match?.status ?? row.status} |`);
37152
+ }
37153
+ lines.push("", `Retention patch: **${patch.status}**.`, "", ...patch.warnings.map((warning) => `- ${warning}`), "", "Positive differences mean observed retention exceeds the configured assumption. Only apply a ready patch. Unobserved days are omitted from scenarioPatch; merge its retention fields into the scenario and preserve the remaining assumptions.", "", "Use a separate held-out cohort to validate a fitted model. These observations and intervals describe this cohort; they do not establish fun, causal improvement, or future revenue. Mixed install ages, missing activity exports, and omitted nonreturning users invalidate this comparison.");
37154
+ return lines.join(`
37155
+ `);
37156
+ }
37157
+
36995
37158
  // src/commands/calibrate.ts
36996
37159
  init_bun();
36997
37160
  function parseCsvRows(raw) {
@@ -37064,21 +37227,46 @@ function parseNumeric(raw, row, field, opts) {
37064
37227
  }
37065
37228
  return n;
37066
37229
  }
37067
- function parseCsvTelemetry(raw) {
37230
+ function parseActive(value, strict) {
37231
+ if (value === undefined)
37232
+ return true;
37233
+ if (typeof value === "boolean")
37234
+ return value;
37235
+ if (typeof value === "number" && (value === 0 || value === 1))
37236
+ return value === 1;
37237
+ if (typeof value === "string") {
37238
+ if (/^(1|true|yes|y)$/i.test(value))
37239
+ return true;
37240
+ if (/^(0|false|no|n)$/i.test(value))
37241
+ return false;
37242
+ }
37243
+ if (strict)
37244
+ throw usageError("Observed cohort active must be true/false, yes/no, or 1/0");
37245
+ return typeof value === "string" ? /^(1|true|yes|y)$/i.test(value) : Boolean(value);
37246
+ }
37247
+ function parseCsvTelemetry(raw, strictCohort = false) {
37068
37248
  const rows2d = parseCsvRows(raw);
37069
37249
  if (rows2d.length < 2) {
37070
37250
  throw cliError("CLI_USAGE", "CSV telemetry requires a header and at least one data row");
37071
37251
  }
37072
37252
  const headers = (rows2d[0] ?? []).map(normalizeHeader);
37253
+ if (strictCohort && new Set(headers).size !== headers.length)
37254
+ throw usageError("Observed cohort CSV has duplicate headers");
37073
37255
  const rows = [];
37074
37256
  for (let i = 1;i < rows2d.length; i++) {
37075
37257
  const cols = rows2d[i] ?? [];
37258
+ if (strictCohort && cols.length !== headers.length)
37259
+ throw usageError(`Observed cohort CSV row ${i + 1} has the wrong number of columns`);
37076
37260
  const rec = {};
37077
37261
  headers.forEach((h, idx) => {
37078
37262
  rec[h] = cols[idx] ?? "";
37079
37263
  });
37080
- const userId = pick2(rec, ["user_id", "userid", "user", "id"]) ?? `anon-${i}`;
37081
- const dayRaw = pick2(rec, ["day", "day_index", "days_since_install"]) ?? "0";
37264
+ const userInput = pick2(rec, ["user_id", "userid", "user", "id"]);
37265
+ const dayInput = pick2(rec, ["day", "day_index", "days_since_install"]);
37266
+ if (strictCohort && (!userInput?.trim() || !dayInput?.trim()))
37267
+ throw usageError("Observed cohort requires explicit user_id and day fields");
37268
+ const userId = userInput ?? `anon-${i}`;
37269
+ const dayRaw = dayInput ?? "0";
37082
37270
  const iapRaw = pick2(rec, ["iap_revenue", "revenue", "iap", "purchase_revenue"]) ?? "0";
37083
37271
  const adRaw = pick2(rec, ["ad_revenue", "adrevenue", "ads"]) ?? "0";
37084
37272
  const activeRaw = pick2(rec, ["active", "is_active"]);
@@ -37087,7 +37275,7 @@ function parseCsvTelemetry(raw) {
37087
37275
  const iapRevenue = parseNumeric(iapRaw, i + 1, "revenue");
37088
37276
  const adRevenue = parseNumeric(adRaw, i + 1, "ad_revenue");
37089
37277
  const acquisitionCost = acqRaw !== undefined ? parseNumeric(acqRaw, i + 1, "acquisition_cost", { allowEmpty: true }) : undefined;
37090
- const active = activeRaw === undefined ? true : /^(1|true|yes|y)$/i.test(activeRaw);
37278
+ const active = parseActive(activeRaw, strictCohort);
37091
37279
  rows.push({
37092
37280
  userId,
37093
37281
  day,
@@ -37099,7 +37287,7 @@ function parseCsvTelemetry(raw) {
37099
37287
  }
37100
37288
  return rows;
37101
37289
  }
37102
- function parseJsonTelemetry(raw) {
37290
+ function parseJsonTelemetry(raw, strictCohort = false) {
37103
37291
  const parsed = JSON.parse(raw);
37104
37292
  if (!Array.isArray(parsed)) {
37105
37293
  throw cliError("CLI_USAGE", "JSON telemetry must be an array");
@@ -37110,12 +37298,21 @@ function parseJsonTelemetry(raw) {
37110
37298
  throw cliError("CLI_USAGE", `JSON telemetry row ${idx} must be an object`);
37111
37299
  }
37112
37300
  const r = row;
37113
- const userId = String(r.userId ?? r.user_id ?? r.user ?? r.id ?? `anon-${idx}`);
37114
- const day = Number(r.day ?? r.dayIndex ?? r.daysSinceInstall ?? 0);
37301
+ const userInput = r.userId ?? r.user_id ?? r.user ?? r.id;
37302
+ const dayInput = r.day ?? r.dayIndex ?? r.daysSinceInstall;
37303
+ if (strictCohort && (typeof userInput !== "string" || !userInput.trim() || typeof dayInput !== "number"))
37304
+ throw usageError("Observed cohort JSON requires explicit string userId and numeric day fields");
37305
+ if (strictCohort)
37306
+ for (const key of ["iapRevenue", "iap_revenue", "revenue", "adRevenue", "ad_revenue", "acquisitionCost", "acquisition_cost", "cpi"]) {
37307
+ if (r[key] !== undefined && typeof r[key] !== "number")
37308
+ throw usageError(`Observed cohort JSON ${key} must be numeric`);
37309
+ }
37310
+ const userId = String(userInput ?? `anon-${idx}`);
37311
+ const day = Number(dayInput ?? 0);
37115
37312
  const iapRevenue = Number(r.iapRevenue ?? r.iap_revenue ?? r.revenue ?? 0);
37116
37313
  const adRevenue = Number(r.adRevenue ?? r.ad_revenue ?? 0);
37117
37314
  const acquisitionCost = r.acquisitionCost !== undefined || r.acquisition_cost !== undefined || r.cpi !== undefined ? Number(r.acquisitionCost ?? r.acquisition_cost ?? r.cpi) : undefined;
37118
- const active = r.active === undefined ? true : Boolean(r.active);
37315
+ const active = parseActive(r.active, strictCohort);
37119
37316
  if (!Number.isFinite(day) || !Number.isFinite(iapRevenue) || !Number.isFinite(adRevenue)) {
37120
37317
  throw cliError("CLI_USAGE", `Invalid numeric value in json row ${idx}`);
37121
37318
  }
@@ -37137,26 +37334,65 @@ var calibrate_default = defineCommand({
37137
37334
  "input-format": option(_enum(["auto", "csv", "json"]).default("auto"), {
37138
37335
  description: "Telemetry file format"
37139
37336
  }),
37337
+ "observed-through-day": option(exports_coerce.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(), {
37338
+ description: "Complete activity cutoff for one install cohort; requires day 0 records for every installed user"
37339
+ }),
37340
+ scenario: option(string2().optional(), { description: "Compare observed retention with this scenario's explicit assumptions (requires --observed-through-day)" }),
37140
37341
  out: option(string2().optional(), { description: "Output path" }),
37141
37342
  format: option(_enum(["json", "md", "csv"]).default("json"), { description: "Output format" })
37142
37343
  },
37143
37344
  async handler({ flags, positional }) {
37144
37345
  const telemetryPath = positional[0];
37145
- if (!telemetryPath) {
37346
+ if (!telemetryPath || positional.length !== 1) {
37146
37347
  throw usageError("Usage: idk calibrate <telemetry.csv|json> [--input-format auto|csv|json]");
37147
37348
  }
37349
+ if (flags.scenario && flags["observed-through-day"] === undefined)
37350
+ throw usageError("--scenario requires --observed-through-day for a mature cohort comparison");
37148
37351
  const abs = resolve8(process.cwd(), telemetryPath);
37149
37352
  const raw = await readTextFile(abs);
37150
37353
  const format = flags["input-format"] === "auto" ? extname3(abs).toLowerCase() : flags["input-format"];
37151
- const rows = format === ".csv" || format === "csv" ? parseCsvTelemetry(raw) : parseJsonTelemetry(raw);
37152
- const calibrated = calibrateMonetization(rows);
37354
+ const rows = format === ".csv" || format === "csv" ? parseCsvTelemetry(raw, flags["observed-through-day"] !== undefined) : parseJsonTelemetry(raw, flags["observed-through-day"] !== undefined);
37355
+ let observation;
37356
+ if (flags["observed-through-day"] !== undefined) {
37357
+ try {
37358
+ observation = observeCohort(rows, flags["observed-through-day"]);
37359
+ } catch (error) {
37360
+ throw usageError(error instanceof Error ? error.message : String(error));
37361
+ }
37362
+ }
37363
+ let calibrated;
37364
+ try {
37365
+ calibrated = calibrateMonetization(rows);
37366
+ } catch (error) {
37367
+ if (error instanceof RangeError)
37368
+ throw usageError(error.message);
37369
+ throw error;
37370
+ }
37371
+ const retentionRates = observation ? observedRetentionRates(observation) : undefined;
37372
+ const scenario = flags.scenario ? await readScenarioFile(flags.scenario) : undefined;
37373
+ const rate = number2().min(0).max(1).optional();
37374
+ const parsedScenario = scenario === undefined ? undefined : object({
37375
+ schemaVersion: literal(1),
37376
+ monetization: object({ retention: object({ d1: rate, d7: rate, d30: rate, d90: rate }).passthrough().optional() }).passthrough().optional()
37377
+ }).passthrough().safeParse(scenario);
37378
+ if (parsedScenario && !parsedScenario.success)
37379
+ throw usageError(`Invalid scenario retention assumptions: ${parsedScenario.error.message}`);
37380
+ const comparison = observation && parsedScenario?.success ? compareRetention(observation, parsedScenario.data.monetization?.retention ?? {}) : undefined;
37381
+ const patch = observation ? planRetentionPatch(observation, parsedScenario?.success ? parsedScenario.data.monetization?.retention : undefined) : undefined;
37382
+ const monetization = observation ? {
37383
+ ...calibrated.monetization,
37384
+ retention: { longTailDailyDecay: calibrated.monetization?.retention?.longTailDailyDecay, ...patch?.retention }
37385
+ } : calibrated.monetization;
37153
37386
  const outputMeta = buildOutputMeta({
37154
37387
  command: "calibrate",
37155
- telemetry: rows
37388
+ telemetry: observation ? { rows, observedThroughDay: observation.observedThroughDay } : rows,
37389
+ scenario,
37390
+ scenarioPath: flags.scenario ? resolve8(flags.scenario) : undefined
37156
37391
  });
37157
37392
  await writeOutput({
37158
37393
  format: flags.format,
37159
37394
  outPath: flags.out,
37395
+ protectedInputs: [abs, ...flags.scenario ? [resolve8(flags.scenario)] : []],
37160
37396
  data: flags.format === "json" ? {
37161
37397
  ok: true,
37162
37398
  source: abs,
@@ -37164,12 +37400,27 @@ var calibrate_default = defineCommand({
37164
37400
  rows: rows.length,
37165
37401
  users: calibrated.diagnostics.users
37166
37402
  },
37167
- monetization: calibrated.monetization,
37168
- diagnostics: calibrated.diagnostics,
37169
- scenarioPatch: {
37403
+ monetization,
37404
+ diagnostics: observation ? { ...calibrated.diagnostics, retention: retentionRates, observedThroughDay: observation.observedThroughDay } : calibrated.diagnostics,
37405
+ ...observation ? { observation } : {},
37406
+ ...comparison ? { comparison } : {},
37407
+ ...patch ? { patchStatus: patch.status, patchWarnings: patch.warnings } : {},
37408
+ scenarioPatch: observation ? { monetization: { retention: patch.retention } } : {
37170
37409
  monetization: calibrated.monetization
37171
37410
  }
37172
- } : [
37411
+ } : observation ? flags.format === "md" ? renderCohortMarkdown(observation, comparison, patch) : observation.retention.map((row, index) => ({
37412
+ day: row.day,
37413
+ users: row.users,
37414
+ activeUsers: row.activeUsers,
37415
+ observedRate: row.rate,
37416
+ interval95Min: row.interval95?.min ?? null,
37417
+ interval95Max: row.interval95?.max ?? null,
37418
+ assumption: comparison?.[index]?.assumption ?? null,
37419
+ differencePercentagePoints: comparison?.[index]?.differencePercentagePoints ?? null,
37420
+ status: comparison?.[index]?.status ?? row.status,
37421
+ patchStatus: patch.status,
37422
+ patchWarnings: patch.warnings.join(" ")
37423
+ })) : [
37173
37424
  {
37174
37425
  source: abs,
37175
37426
  rows: rows.length,
@@ -37191,12 +37442,13 @@ var calibrate_default = defineCommand({
37191
37442
 
37192
37443
  // src/commands/balance.ts
37193
37444
  init_zod();
37445
+ import { resolve as resolve11 } from "path";
37194
37446
 
37195
37447
  // src/balance/workflow.ts
37196
37448
  init_dist2();
37197
37449
  init_zod();
37198
37450
  import { resolve as resolve10, dirname as dirname8 } from "path";
37199
- import { stat } from "fs/promises";
37451
+ import { stat as stat2 } from "fs/promises";
37200
37452
 
37201
37453
  // src/cliMeta.ts
37202
37454
  var import_package2 = __toESM(require_package(), 1);
@@ -37442,7 +37694,7 @@ async function runPacingChecks(input, evaluator) {
37442
37694
  // src/balance/sheet.ts
37443
37695
  init_bun();
37444
37696
  import { constants } from "fs";
37445
- import { lstat, mkdir, open as open2, realpath, rename, rm } from "fs/promises";
37697
+ import { lstat, mkdir, open as open2, realpath as realpath2, rename, rm } from "fs/promises";
37446
37698
  import { dirname as dirname7, isAbsolute as isAbsolute2, join, relative as relative2, resolve as resolve9, sep as sep2 } from "path";
37447
37699
  var SHEET_LIMITS = Object.freeze({
37448
37700
  maxCsvBytes: 2 * 1024 * 1024,
@@ -37816,7 +38068,7 @@ async function snapshotInputs(inputFiles, contextValue, limits) {
37816
38068
  let total = 0;
37817
38069
  for (const name of validInputNames(inputFiles, limits)) {
37818
38070
  const path = resolve9(inputFiles[name]);
37819
- const realPath = await realpath(path);
38071
+ const realPath = await realpath2(path);
37820
38072
  const bytes = await boundedRead(realPath, Math.min(limits.maxInputBytes, limits.maxTotalInputBytes - total));
37821
38073
  total += bytes.length;
37822
38074
  if (total > limits.maxTotalInputBytes)
@@ -37843,7 +38095,7 @@ async function snapshotInputs(inputFiles, contextValue, limits) {
37843
38095
  async function canonicalOutput(path) {
37844
38096
  path = resolve9(path);
37845
38097
  try {
37846
- return await realpath(path);
38098
+ return await realpath2(path);
37847
38099
  } catch (error) {
37848
38100
  if (error.code !== "ENOENT")
37849
38101
  throw error;
@@ -37880,7 +38132,7 @@ async function refreshSheetBundle(options) {
37880
38132
  validInputNames(options.inputFiles, limits);
37881
38133
  const output = await canonicalOutput(options.outputDir);
37882
38134
  for (const [name, path] of Object.entries(options.inputFiles)) {
37883
- if (isWithin(output, resolve9(path)) || isWithin(output, await realpath(path)))
38135
+ if (isWithin(output, resolve9(path)) || isWithin(output, await realpath2(path)))
37884
38136
  throw new Error(`Output/input collision: ${name}`);
37885
38137
  }
37886
38138
  await mkdir(output, { recursive: true });
@@ -37940,7 +38192,7 @@ async function refreshSheetBundle(options) {
37940
38192
  if (manifestBytes.length > 1024 * 1024)
37941
38193
  throw new Error("Bundle manifest exceeds byte limit");
37942
38194
  await mkdir(generations, { recursive: true });
37943
- if (await realpath(generations) !== generations)
38195
+ if (await realpath2(generations) !== generations)
37944
38196
  throw new Error("Bundle generations directory must not be a symlink");
37945
38197
  await mkdir(stage);
37946
38198
  for (const name of names)
@@ -38029,10 +38281,10 @@ async function readCurrentBundle(outputDir, inputFiles, options = {}) {
38029
38281
  if (!isRecord5(pointer) || pointer.version !== 1 || typeof pointer.generation !== "string" || !generationText.test(pointer.generation) || typeof pointer.manifestSha256 !== "string" || !sha256Text2.test(pointer.manifestSha256))
38030
38282
  throw new Error("Invalid bundle pointer");
38031
38283
  const generationPath = join(output, "generations", pointer.generation);
38032
- if (await realpath(generationPath) !== generationPath)
38284
+ if (await realpath2(generationPath) !== generationPath)
38033
38285
  throw new Error("Bundle generation must not be a symlink");
38034
38286
  const manifestPath = join(generationPath, "manifest.json");
38035
- if (await realpath(manifestPath) !== manifestPath)
38287
+ if (await realpath2(manifestPath) !== manifestPath)
38036
38288
  throw new Error("Bundle manifest must not be a symlink");
38037
38289
  const bytes = await boundedRead(manifestPath, 1024 * 1024);
38038
38290
  if (sha256Hex(bytes) !== pointer.manifestSha256)
@@ -38041,7 +38293,7 @@ async function readCurrentBundle(outputDir, inputFiles, options = {}) {
38041
38293
  let outputBytesRead = 0;
38042
38294
  for (const [name, artifact] of Object.entries(manifest.artifacts)) {
38043
38295
  try {
38044
- if (await realpath(artifact.path) !== artifact.path)
38296
+ if (await realpath2(artifact.path) !== artifact.path)
38045
38297
  throw new Error("Artifact is a symlink");
38046
38298
  const artifactBytes = await boundedRead(artifact.path, Math.min(limits.maxOutputBytes, limits.maxTotalOutputBytes - outputBytesRead));
38047
38299
  outputBytesRead += artifactBytes.length;
@@ -38141,7 +38393,7 @@ function normalizeRunDocument(document, strategy, horizon) {
38141
38393
  }
38142
38394
  async function refreshBalanceWorkflow(configPath, flags, check = false) {
38143
38395
  const absoluteConfig = resolve10(configPath);
38144
- const configInfo = await stat(absoluteConfig);
38396
+ const configInfo = await stat2(absoluteConfig);
38145
38397
  if (!configInfo.isFile() || configInfo.size > SHEET_LIMITS.maxInputBytes)
38146
38398
  throw new Error("Workflow config is not a bounded regular file");
38147
38399
  const rawConfig = await readTextFile(absoluteConfig);
@@ -38337,6 +38589,46 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38337
38589
  return { ...manifest, outcome };
38338
38590
  }
38339
38591
 
38592
+ // src/balance/report.ts
38593
+ function cell(value) {
38594
+ return String(value).replaceAll("|", "\\|").replace(/[\r\n]/g, " ");
38595
+ }
38596
+ function explain(result) {
38597
+ if (result.status === "breach" && result.value !== null) {
38598
+ if (result.min !== undefined && result.value < result.min)
38599
+ return `below the minimum by ${result.min - result.value} ${result.unit}`;
38600
+ if (result.max !== undefined && result.value > result.max)
38601
+ return `above the maximum by ${result.value - result.max} ${result.unit}`;
38602
+ }
38603
+ if (result.status === "pass")
38604
+ return "within the inclusive target bounds";
38605
+ return result.message ?? (result.status === "unreached" ? "not reached within the horizon" : "measurement unavailable");
38606
+ }
38607
+ function renderBalanceReport(report, generationPath) {
38608
+ const lines = [
38609
+ "# Balance targets",
38610
+ "",
38611
+ `Result: **${report.status}**. Simulated ${report.horizonSec} seconds using strategy ${cell(report.strategy)}; seeds ${report.seeds.join(", ")}.`,
38612
+ "A target is a design constraint on this model under these conditions. End net worth is modeled asset value; it can differ from spendable wallet money.",
38613
+ "",
38614
+ "| Variant | Seed | Metric | Measured | Target | Status | Explanation |",
38615
+ "| --- | --- | --- | --- | --- | --- | --- |"
38616
+ ];
38617
+ for (const entry of report.runs)
38618
+ for (const result of entry.results) {
38619
+ const target = [result.min === undefined ? undefined : `minimum ${result.min} ${result.unit}`, result.max === undefined ? undefined : `maximum ${result.max} ${result.unit}`].filter(Boolean).join(", ");
38620
+ const measured = result.value === null ? "unavailable" : `${result.value} ${result.unit}`;
38621
+ lines.push(`| ${cell(entry.run.variantId)} | ${entry.run.seed} | ${cell(result.metric)} | ${cell(measured)} | ${cell(target)} | ${result.status} | ${cell(explain(result))} |`);
38622
+ }
38623
+ lines.push("", report.ok ? "All declared targets passed." : "A target breached, was not reached, or could not be measured. The command exits 1; inspect the rows before changing the design.", "", `Saved generation: ${cell(generationPath)}`, "The generation contains exact values in results.json and reproduction inputs in provenance.json. Passing targets does not establish fun or player retention.");
38624
+ return lines.join(`
38625
+ `);
38626
+ }
38627
+ function renderBalanceFreshness(result) {
38628
+ return ["# Balance result freshness", "", `State: **${result.state}**. This checks saved inputs and artifacts; it does not check target success. Run balance without --check to evaluate the targets.`, ...result.staleReasons.map((reason) => `- ${cell(reason)}`)].join(`
38629
+ `);
38630
+ }
38631
+
38340
38632
  // src/commands/balance.ts
38341
38633
  init_errors3();
38342
38634
  init_bun();
@@ -38345,13 +38637,15 @@ var balance_default = defineCommand({
38345
38637
  description: "Refresh a typed CSV balance sheet into scenario, pacing checks, and result sheets",
38346
38638
  options: {
38347
38639
  ...pluginOptions(),
38348
- check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" })
38640
+ check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" }),
38641
+ format: option(_enum(["json", "md"]).default("json"), { description: "JSON manifest or human-readable target explanations" })
38349
38642
  },
38350
38643
  async handler({ flags, positional }) {
38351
38644
  if (positional.length !== 1)
38352
38645
  throw usageError("Usage: idk balance <workflow.json> [--check] [plugin options]");
38353
38646
  const result = await refreshBalanceWorkflow(positional[0], flags, flags.check);
38354
- await writeStdout(JSON.stringify(result, null, 2) + `
38647
+ const body = flags.format === "json" ? JSON.stringify(result, null, 2) : ("state" in result) ? renderBalanceFreshness(result) : renderBalanceReport(JSON.parse(await readTextFile(resolve11(result.generationPath, "results.json"))), result.generationPath);
38648
+ await writeStdout(body + `
38355
38649
  `);
38356
38650
  if (flags.check && "state" in result && result.state !== "current")
38357
38651
  process.exitCode = 1;
@@ -38363,7 +38657,7 @@ var balance_default = defineCommand({
38363
38657
  // src/commands/compare.ts
38364
38658
  init_dist2();
38365
38659
  init_zod();
38366
- import { resolve as resolve13 } from "path";
38660
+ import { resolve as resolve14 } from "path";
38367
38661
 
38368
38662
  // src/commands/_shared/compareEval.ts
38369
38663
  function formatEtaLabel(seconds, reached) {
@@ -38390,12 +38684,12 @@ init_errors3();
38390
38684
  init_outputMeta();
38391
38685
 
38392
38686
  // src/io/replayPolicy.ts
38393
- import { resolve as resolve12 } from "path";
38687
+ import { resolve as resolve13 } from "path";
38394
38688
 
38395
38689
  // src/io/replayArtifact.ts
38396
38690
  init_outputMeta();
38397
38691
  init_bun();
38398
- import { resolve as resolve11 } from "path";
38692
+ import { resolve as resolve12 } from "path";
38399
38693
  var REDACTED_KEYS = new Set([
38400
38694
  "generatedAt",
38401
38695
  "scenario",
@@ -38461,7 +38755,7 @@ function toReplayCommandLine(args) {
38461
38755
  return `bun run --cwd packages/cli dev -- ${rendered}`;
38462
38756
  }
38463
38757
  async function writeReplayArtifact(args) {
38464
- const abs = resolve11(args.outPath);
38758
+ const abs = resolve12(args.outPath);
38465
38759
  const runId = args.meta.runId;
38466
38760
  const seed = args.meta.seed;
38467
38761
  const scenarioHash = args.meta.scenarioHash;
@@ -38537,7 +38831,7 @@ var PATH_FLAG_KINDS = {
38537
38831
  function normalizePathLike(pathLike, cwd) {
38538
38832
  if (pathLike.trim().length === 0)
38539
38833
  return pathLike;
38540
- return resolve12(cwd, pathLike);
38834
+ return resolve13(cwd, pathLike);
38541
38835
  }
38542
38836
  function normalizeCsvPaths(raw, cwd) {
38543
38837
  return raw.split(",").map((x) => x.trim()).filter(Boolean).map((x) => normalizePathLike(x, cwd)).join(",");
@@ -38612,7 +38906,7 @@ function plain(value) {
38612
38906
  return "n/a";
38613
38907
  return String(value).replace(/[\r\n]+/g, " ");
38614
38908
  }
38615
- function cell(value) {
38909
+ function cell2(value) {
38616
38910
  return plain(value).replaceAll("|", "\\|");
38617
38911
  }
38618
38912
  function inlineCode(value) {
@@ -38640,9 +38934,9 @@ function renderCompareMarkdown(args) {
38640
38934
  const higher = ["endMoney", "endNetWorth", "visibleChangesPerMinute"].includes(metric);
38641
38935
  const lower = ["droppedRate", "etaToTargetWorth", "timeToMilestone", "maxNoRewardGapSec"].includes(metric);
38642
38936
  const seconds = ["etaToTargetWorth", "timeToMilestone", "maxNoRewardGapSec"].includes(metric);
38643
- const value = (item) => seconds && (typeof item === "number" || typeof item === "string" && item.trim().length > 0) && Number.isFinite(Number(item)) ? `${cell(item)}s` : cell(item);
38937
+ const value = (item) => seconds && (typeof item === "number" || typeof item === "string" && item.trim().length > 0) && Number.isFinite(Number(item)) ? `${cell2(item)}s` : cell2(item);
38644
38938
  const better = result.better === "a" || result.better === "b" ? result.better.toUpperCase() : result.better === "tie" ? "tie" : "undetermined";
38645
- lines.push(`| ${cell(metric)} | ${value(a)} | ${value(b)} | ${higher ? "higher" : lower ? "lower" : "n/a"} | ${better} |`);
38939
+ lines.push(`| ${cell2(metric)} | ${value(a)} | ${value(b)} | ${higher ? "higher" : lower ? "lower" : "n/a"} | ${better} |`);
38646
38940
  if (result.better === "a" || result.better === "b")
38647
38941
  preferences[result.better].push(metric);
38648
38942
  }
@@ -39319,8 +39613,8 @@ var compare_default = defineCommand({
39319
39613
  }
39320
39614
  } : singleResults[0];
39321
39615
  if (flags["artifact-out"]) {
39322
- const aAbs = resolve13(process.cwd(), aPath);
39323
- const bAbs = resolve13(process.cwd(), bPath);
39616
+ const aAbs = resolve14(process.cwd(), aPath);
39617
+ const bAbs = resolve14(process.cwd(), bPath);
39324
39618
  await writeCommandReplayArtifact({
39325
39619
  outPath: flags["artifact-out"],
39326
39620
  command: "compare",
@@ -39358,7 +39652,7 @@ init_outputMeta();
39358
39652
  init_errors3();
39359
39653
  init_bun();
39360
39654
  init_selfCli();
39361
- import { basename as basename4, dirname as dirname9, relative as relative3, resolve as resolve14 } from "path";
39655
+ import { basename as basename4, dirname as dirname9, relative as relative3, resolve as resolve15 } from "path";
39362
39656
  var START_MARKER = "# >>> idk completions >>>";
39363
39657
  var END_MARKER = "# <<< idk completions <<<";
39364
39658
  function userHome() {
@@ -39384,13 +39678,13 @@ function defaultRcPath(shell) {
39384
39678
  const home = userHome();
39385
39679
  switch (shell) {
39386
39680
  case "zsh":
39387
- return resolve14(home, ".zshrc");
39681
+ return resolve15(home, ".zshrc");
39388
39682
  case "bash":
39389
- return resolve14(home, ".bashrc");
39683
+ return resolve15(home, ".bashrc");
39390
39684
  case "fish":
39391
- return resolve14(home, ".config/fish/config.fish");
39685
+ return resolve15(home, ".config/fish/config.fish");
39392
39686
  case "powershell":
39393
- return resolve14(home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1");
39687
+ return resolve15(home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1");
39394
39688
  }
39395
39689
  }
39396
39690
  function completionsSourceLine(shell, executable = "idk") {
@@ -39423,7 +39717,7 @@ function replaceManagedBlock(existing, block) {
39423
39717
  }
39424
39718
  async function installCompletions(args) {
39425
39719
  const shell = detectShell(args.shell);
39426
- const rcPath = resolve14(args.rcPath ?? defaultRcPath(shell));
39720
+ const rcPath = resolve15(args.rcPath ?? defaultRcPath(shell));
39427
39721
  const line = completionsSourceLine(shell, args.executable ?? "idk");
39428
39722
  const script = runSelfCli(["completions", shell]);
39429
39723
  if (script.exitCode !== 0) {
@@ -39467,7 +39761,7 @@ ${END_MARKER}
39467
39761
  }
39468
39762
  async function readCompletionSetup(args) {
39469
39763
  const shell = detectShell(args.shell);
39470
- const rcPath = resolve14(args.rcPath ?? defaultRcPath(shell));
39764
+ const rcPath = resolve15(args.rcPath ?? defaultRcPath(shell));
39471
39765
  const line = completionsSourceLine(shell, args.executable ?? "idk");
39472
39766
  const exists = await fileExists(rcPath);
39473
39767
  if (!exists) {
@@ -39478,7 +39772,7 @@ async function readCompletionSetup(args) {
39478
39772
  return { shell, rcPath, installed, line };
39479
39773
  }
39480
39774
  function commonDir(paths) {
39481
- const parts = paths.map((value) => dirname9(resolve14(value)).split("/").filter(Boolean));
39775
+ const parts = paths.map((value) => dirname9(resolve15(value)).split("/").filter(Boolean));
39482
39776
  if (parts.length === 0)
39483
39777
  return process.cwd();
39484
39778
  const first = parts[0] ?? [];
@@ -39499,13 +39793,13 @@ async function writePluginTrust(args) {
39499
39793
  if (args.plugins.length === 0) {
39500
39794
  throw usageError("setup plugin-trust requires at least one --plugin path.");
39501
39795
  }
39502
- const outPath = resolve14(args.outPath);
39796
+ const outPath = resolve15(args.outPath);
39503
39797
  if (await fileExists(outPath) && !args.force) {
39504
39798
  throw usageError(`Output file already exists: ${outPath}`, "Pass --force true to overwrite.");
39505
39799
  }
39506
39800
  const entries = {};
39507
39801
  for (const plugin of args.plugins) {
39508
- const path = resolve14(plugin);
39802
+ const path = resolve15(plugin);
39509
39803
  const digest = sha256Hex(await readTextFile(path));
39510
39804
  const key = args.relative === false ? path : relative3(dirname9(outPath), path);
39511
39805
  entries[key] = digest;
@@ -39517,18 +39811,18 @@ async function writePluginTrust(args) {
39517
39811
  `);
39518
39812
  return {
39519
39813
  outPath,
39520
- plugins: args.plugins.map((plugin) => resolve14(plugin)),
39814
+ plugins: args.plugins.map((plugin) => resolve15(plugin)),
39521
39815
  pluginCount: args.plugins.length,
39522
39816
  pluginEntries: entries,
39523
- pluginRootSuggestion: commonDir(args.plugins.map((plugin) => resolve14(plugin))),
39817
+ pluginRootSuggestion: commonDir(args.plugins.map((plugin) => resolve15(plugin))),
39524
39818
  updated: true
39525
39819
  };
39526
39820
  }
39527
39821
  function parsePluginList(input) {
39528
- return input.split(",").map((value) => value.trim()).filter(Boolean).map((value) => resolve14(value));
39822
+ return input.split(",").map((value) => value.trim()).filter(Boolean).map((value) => resolve15(value));
39529
39823
  }
39530
39824
  function suggestedTrustPath(baseDir = process.cwd()) {
39531
- return resolve14(baseDir, ".idk/plugin-trust.json");
39825
+ return resolve15(baseDir, ".idk/plugin-trust.json");
39532
39826
  }
39533
39827
  function describePluginTrustFlags(result) {
39534
39828
  return [
@@ -39540,7 +39834,7 @@ function describePluginTrustFlags(result) {
39540
39834
  }
39541
39835
  function inferredTuneWizardPath(scenarioPath) {
39542
39836
  const file = basename4(scenarioPath).replace(/\.json$/i, "");
39543
- return resolve14(process.cwd(), `${file}-tune.json`);
39837
+ return resolve15(process.cwd(), `${file}-tune.json`);
39544
39838
  }
39545
39839
 
39546
39840
  // src/commands/doctor.ts
@@ -39912,7 +40206,7 @@ var eta_default = defineCommand({
39912
40206
  // src/commands/experiment.ts
39913
40207
  init_dist2();
39914
40208
  init_zod();
39915
- import { resolve as resolve15 } from "path";
40209
+ import { resolve as resolve16 } from "path";
39916
40210
  init_errors3();
39917
40211
  init_outputMeta();
39918
40212
  init_bun();
@@ -39942,7 +40236,7 @@ var experiment_default = defineCommand({
39942
40236
  const spec = checked.data;
39943
40237
  if (spec.scenario.ref.length === 0)
39944
40238
  throw usageError("Usage: idk experiment <spec>");
39945
- const scenarioPath = resolve15(spec.scenario.ref);
40239
+ const scenarioPath = resolve16(spec.scenario.ref);
39946
40240
  const scenario = await readScenarioFile(scenarioPath);
39947
40241
  const loaded = await loadRegistriesFromFlags(flags);
39948
40242
  const valid = validateScenarioV1(scenario, loaded.modelRegistry);
@@ -39991,12 +40285,12 @@ var experiment_default = defineCommand({
39991
40285
  // src/commands/evaluate.ts
39992
40286
  init_dist2();
39993
40287
  init_zod();
39994
- import { resolve as resolve17 } from "path";
40288
+ import { resolve as resolve18 } from "path";
39995
40289
 
39996
40290
  // src/commands/ltv.ts
39997
40291
  init_dist2();
39998
40292
  init_zod();
39999
- import { resolve as resolve16 } from "path";
40293
+ import { resolve as resolve17 } from "path";
40000
40294
  init_errors3();
40001
40295
  init_outputMeta();
40002
40296
  var strategySchema2 = string2().min(1).optional();
@@ -40365,7 +40659,7 @@ var ltv_default = defineCommand({
40365
40659
  };
40366
40660
  const output = flags.format === "json" ? jsonOutput : analysis.horizons;
40367
40661
  if (flags["artifact-out"]) {
40368
- const scenarioAbs = resolve16(process.cwd(), scenarioPath);
40662
+ const scenarioAbs = resolve17(process.cwd(), scenarioPath);
40369
40663
  await writeCommandReplayArtifact({
40370
40664
  outPath: flags["artifact-out"],
40371
40665
  command: "ltv",
@@ -40444,7 +40738,7 @@ async function loadBalanceChecks(flags) {
40444
40738
  if (!failOn) {
40445
40739
  throw usageError("--fail-on must list error, warn, inconclusive, or unsupported");
40446
40740
  }
40447
- const path = resolve17(process.cwd(), flags.checks);
40741
+ const path = resolve18(process.cwd(), flags.checks);
40448
40742
  let raw;
40449
40743
  try {
40450
40744
  raw = await readJsonFile(path);
@@ -40525,7 +40819,7 @@ var evaluate_default = defineCommand({
40525
40819
  throw scenarioInvalidError(valid.issues);
40526
40820
  }
40527
40821
  const scenario = valid.scenario;
40528
- const scenarioAbs = resolve17(process.cwd(), scenarioPath);
40822
+ const scenarioAbs = resolve18(process.cwd(), scenarioPath);
40529
40823
  const unseeded = prepareResolvedRun({
40530
40824
  scenario: valid.scenario,
40531
40825
  modelRegistry: loaded.modelRegistry,
@@ -40702,9 +40996,9 @@ var evaluate_default = defineCommand({
40702
40996
  stageScope
40703
40997
  });
40704
40998
  const files = {
40705
- simulate: resolve17(process.cwd(), flags["out-dir"] ?? ".", "simulate.json"),
40706
- experience: resolve17(process.cwd(), flags["out-dir"] ?? ".", "experience.json"),
40707
- ltv: resolve17(process.cwd(), flags["out-dir"] ?? ".", "ltv.json")
40999
+ simulate: resolve18(process.cwd(), flags["out-dir"] ?? ".", "simulate.json"),
41000
+ experience: resolve18(process.cwd(), flags["out-dir"] ?? ".", "experience.json"),
41001
+ ltv: resolve18(process.cwd(), flags["out-dir"] ?? ".", "ltv.json")
40708
41002
  };
40709
41003
  const output = {
40710
41004
  ok: balance?.exitCode !== 2,
@@ -40735,12 +41029,12 @@ var evaluate_default = defineCommand({
40735
41029
  ...balance ? { balanceChecks: balance.exitCode === 2 ? toBalanceCheckError(balance) : balance } : {}
40736
41030
  };
40737
41031
  if (flags["out-dir"]) {
40738
- const outDir = resolve17(process.cwd(), flags["out-dir"]);
41032
+ const outDir = resolve18(process.cwd(), flags["out-dir"]);
40739
41033
  await ensureDir(outDir);
40740
- files.simulate = resolve17(outDir, "simulate.json");
40741
- files.experience = resolve17(outDir, "experience.json");
40742
- files.ltv = resolve17(outDir, "ltv.json");
40743
- const summaryPath = resolve17(process.cwd(), flags["out-dir"], flags.format === "md" ? "summary.md" : "summary.json");
41034
+ files.simulate = resolve18(outDir, "simulate.json");
41035
+ files.experience = resolve18(outDir, "experience.json");
41036
+ files.ltv = resolve18(outDir, "ltv.json");
41037
+ const summaryPath = resolve18(process.cwd(), flags["out-dir"], flags.format === "md" ? "summary.md" : "summary.json");
40744
41038
  files.summary = summaryPath;
40745
41039
  await writeTextFile(files.simulate, `${JSON.stringify({ ...simulate, _meta: simulateMeta }, null, 2)}
40746
41040
  `);
@@ -41170,7 +41464,7 @@ var report_default = defineCommand({
41170
41464
  // src/commands/simulate.ts
41171
41465
  init_dist2();
41172
41466
  init_zod();
41173
- import { resolve as resolve18 } from "path";
41467
+ import { resolve as resolve19 } from "path";
41174
41468
 
41175
41469
  // src/commands/_shared/simulateView.ts
41176
41470
  function resolveEventLog(args) {
@@ -41324,11 +41618,11 @@ var simulate_default = defineCommand({
41324
41618
  const input = await readScenarioFile(scenarioPath);
41325
41619
  const loaded = await loadRegistriesFromFlags(flags);
41326
41620
  const scenario = assertValidScenario2(validateScenarioV1(input, loaded.modelRegistry));
41327
- const resumedJson = flags.resume ? await readJsonFile(resolve18(process.cwd(), flags.resume)).then((raw) => {
41621
+ const resumedJson = flags.resume ? await readJsonFile(resolve19(process.cwd(), flags.resume)).then((raw) => {
41328
41622
  try {
41329
41623
  return parseSimStateJSON(raw);
41330
41624
  } catch (error) {
41331
- throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve18(process.cwd(), flags.resume)}`, {
41625
+ throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve19(process.cwd(), flags.resume)}`, {
41332
41626
  detail: errorDetail(error),
41333
41627
  cause: error
41334
41628
  });
@@ -41336,7 +41630,7 @@ var simulate_default = defineCommand({
41336
41630
  }).catch((error) => {
41337
41631
  if (error instanceof Error && error.name === "CliError")
41338
41632
  throw error;
41339
- throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve18(process.cwd(), flags.resume)}`, {
41633
+ throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve19(process.cwd(), flags.resume)}`, {
41340
41634
  detail: errorDetail(error),
41341
41635
  cause: error
41342
41636
  });
@@ -41453,7 +41747,7 @@ var simulate_default = defineCommand({
41453
41747
  const run = runScenario(effectiveScenario);
41454
41748
  const netWorth = effectiveScenario.model.netWorth?.(effectiveScenario.ctx, run.end) ?? run.end.wallet.money;
41455
41749
  const totalElapsedSec = priorElapsedSec + (offlineRun ? runElapsedSec(offlineRun) : 0) + runElapsedSec(run);
41456
- const stateOutPath = flags["state-out"] ? resolve18(process.cwd(), flags["state-out"]) : undefined;
41750
+ const stateOutPath = flags["state-out"] ? resolve19(process.cwd(), flags["state-out"]) : undefined;
41457
41751
  const seed = deterministicSeed;
41458
41752
  const offlineEndWorth = offlineRun && (runScenarioInput.model.netWorth?.(runScenarioInput.ctx, offlineRun.end) ?? offlineRun.end.wallet.money);
41459
41753
  if (stateOutPath) {
@@ -41517,7 +41811,7 @@ var simulate_default = defineCommand({
41517
41811
  eventLog: run.eventLog
41518
41812
  };
41519
41813
  if (flags["artifact-out"]) {
41520
- const scenarioAbs = resolve18(process.cwd(), scenarioPath);
41814
+ const scenarioAbs = resolve19(process.cwd(), scenarioPath);
41521
41815
  await writeCommandReplayArtifact({
41522
41816
  outPath: flags["artifact-out"],
41523
41817
  command: "simulate",
@@ -41552,7 +41846,7 @@ var simulate_default = defineCommand({
41552
41846
  // src/commands/tune.ts
41553
41847
  init_dist2();
41554
41848
  init_zod();
41555
- import { resolve as resolve20 } from "path";
41849
+ import { resolve as resolve21 } from "path";
41556
41850
  init_errors3();
41557
41851
  init_outputMeta();
41558
41852
 
@@ -41560,7 +41854,7 @@ init_outputMeta();
41560
41854
  init_dist2();
41561
41855
  init_errors3();
41562
41856
  init_bun();
41563
- import { basename as basename5, resolve as resolve19 } from "path";
41857
+ import { basename as basename5, resolve as resolve20 } from "path";
41564
41858
  var OBJECTIVE_OPTIONS = [
41565
41859
  {
41566
41860
  value: "endNetWorthLog10",
@@ -41634,7 +41928,7 @@ async function runTuneWizard(args) {
41634
41928
  }
41635
41929
  const scenario = valid.scenario;
41636
41930
  const strategyId = String(scenario.strategy?.id ?? "greedy");
41637
- const suggestedOut = resolve19(args.outPath?.trim() || inferredTuneWizardPath(args.scenarioPath));
41931
+ const suggestedOut = resolve20(args.outPath?.trim() || inferredTuneWizardPath(args.scenarioPath));
41638
41932
  args.prompt.intro(`Tune wizard for ${basename5(args.scenarioPath)}`);
41639
41933
  const objectiveId = await args.prompt.select("Objective", {
41640
41934
  options: OBJECTIVE_OPTIONS.map((entry) => ({ ...entry })),
@@ -41651,7 +41945,7 @@ async function runTuneWizard(args) {
41651
41945
  const tunePathRaw = await args.prompt.text("Tune spec output path", {
41652
41946
  default: suggestedOut
41653
41947
  });
41654
- const tunePath = resolve19(String(tunePathRaw));
41948
+ const tunePath = resolve20(String(tunePathRaw));
41655
41949
  if (await fileExists(tunePath) && !args.force) {
41656
41950
  throw usageError(`Output file already exists: ${tunePath}`, "Pass --force true to overwrite the generated tune spec.");
41657
41951
  }
@@ -41868,7 +42162,7 @@ var tune_default = defineCommand({
41868
42162
  throw usageError("Usage: idk tune <scenario> --tune <tunespec>");
41869
42163
  }
41870
42164
  const tuneSpecInput = wizardResult?.tuneSpec ?? await readScenarioFile(tunePath);
41871
- const baselinePath = flags["baseline-artifact"] ? resolve20(flags["baseline-artifact"]) : undefined;
42165
+ const baselinePath = flags["baseline-artifact"] ? resolve21(flags["baseline-artifact"]) : undefined;
41872
42166
  const baselineRaw = baselinePath ? await readJsonFile(baselinePath) : undefined;
41873
42167
  const seed = flags.seed ?? deriveDeterministicSeed({
41874
42168
  command: "tune",
@@ -41932,8 +42226,8 @@ var tune_default = defineCommand({
41932
42226
  };
41933
42227
  })();
41934
42228
  if (flags["artifact-out"]) {
41935
- const scenarioAbs = resolve20(process.cwd(), scenarioPath);
41936
- const tuneAbs = resolve20(process.cwd(), tunePath);
42229
+ const scenarioAbs = resolve21(process.cwd(), scenarioPath);
42230
+ const tuneAbs = resolve21(process.cwd(), tunePath);
41937
42231
  await writeCommandReplayArtifact({
41938
42232
  outPath: flags["artifact-out"],
41939
42233
  command: "tune",
@@ -41999,13 +42293,13 @@ var validate_default = defineCommand({
41999
42293
  // src/commands/initScenario.ts
42000
42294
  init_zod();
42001
42295
  init_errors3();
42002
- import { resolve as resolve22 } from "path";
42296
+ import { resolve as resolve23 } from "path";
42003
42297
 
42004
42298
  // src/lib/initWizard.ts
42005
42299
  init_errors3();
42006
42300
 
42007
42301
  // src/templates/scenario.ts
42008
- import { resolve as resolve21, parse as parse5 } from "path";
42302
+ import { resolve as resolve22, parse as parse5 } from "path";
42009
42303
  var TEMPLATE_TRACKS = ["intro", "design", "personal"];
42010
42304
  var TEMPLATE_PRESETS = ["session", "builder", "longrun"];
42011
42305
  var INTRO_PRESET_DEFAULT = {
@@ -42873,14 +43167,14 @@ function buildInitTemplatePlan(args) {
42873
43167
  const tuneSpec = createPersonalTuneSpec(preset, baseScenario);
42874
43168
  applyPersonalBundleIdentity({ base: baseScenario, compare: compareScenario, tune: tuneSpec }, { stem, displayName });
42875
43169
  return [
42876
- { kind: "scenario", path: resolve21(dir, `${stem}${ext}`), content: baseScenario },
42877
- { kind: "compare", path: resolve21(dir, `${stem}-compare-b${ext}`), content: compareScenario },
42878
- { kind: "tune", path: resolve21(dir, `${stem}-tune${ext}`), content: tuneSpec }
43170
+ { kind: "scenario", path: resolve22(dir, `${stem}${ext}`), content: baseScenario },
43171
+ { kind: "compare", path: resolve22(dir, `${stem}-compare-b${ext}`), content: compareScenario },
43172
+ { kind: "tune", path: resolve22(dir, `${stem}-tune${ext}`), content: tuneSpec }
42879
43173
  ];
42880
43174
  }
42881
43175
  const content = track === "intro" ? clone2(preset === "session" ? introSession : preset === "builder" ? introBuilder : introLongrun) : createDesignScenario(preset);
42882
43176
  applyScenarioOverrides(content, args.overrides);
42883
- return [{ kind: "scenario", path: resolve21(args.outPath), content }];
43177
+ return [{ kind: "scenario", path: resolve22(args.outPath), content }];
42884
43178
  }
42885
43179
  function serializeTemplate(template) {
42886
43180
  return `${JSON.stringify(template, null, 2)}
@@ -43137,7 +43431,7 @@ var initScenario_default = defineCommand({
43137
43431
  })
43138
43432
  },
43139
43433
  async handler({ flags, prompt, terminal, runtime }) {
43140
- const outPath = resolve22(process.cwd(), flags.out);
43434
+ const outPath = resolve23(process.cwd(), flags.out);
43141
43435
  const wizard = flags.wizard;
43142
43436
  const wizardResult = wizard ? await runInitWizard({
43143
43437
  prompt,
@@ -43207,11 +43501,11 @@ var init_default = defineGroup({
43207
43501
  init_dist2();
43208
43502
  init_zod();
43209
43503
  init_outputMeta();
43210
- import { resolve as resolve23 } from "path";
43504
+ import { resolve as resolve24 } from "path";
43211
43505
  init_bun();
43212
43506
  async function runKpiRegression(flags) {
43213
- const baselinePath = resolve23(flags.baseline);
43214
- const currentPath = resolve23(flags.current);
43507
+ const baselinePath = resolve24(flags.baseline);
43508
+ const currentPath = resolve24(flags.current);
43215
43509
  const baseline = await readJsonFile(baselinePath);
43216
43510
  const current = await readJsonFile(currentPath);
43217
43511
  const output = adaptKpiRegression({
@@ -43229,6 +43523,7 @@ async function runKpiRegression(flags) {
43229
43523
  await writeOutput({
43230
43524
  format: flags.format,
43231
43525
  outPath: flags.out,
43526
+ protectedInputs: [baselinePath, currentPath],
43232
43527
  data: output,
43233
43528
  meta: buildOutputMeta({ command: "kpi.regress" })
43234
43529
  });
@@ -43282,7 +43577,7 @@ init_zod();
43282
43577
  // src/commands/_shared/listOutput.ts
43283
43578
  init_outputMeta();
43284
43579
  init_bun();
43285
- import { resolve as resolve24 } from "path";
43580
+ import { resolve as resolve25 } from "path";
43286
43581
  async function writeListOutput(args) {
43287
43582
  const outputForRender = args.format === "json" ? {
43288
43583
  ...args.payload,
@@ -43293,7 +43588,7 @@ async function writeListOutput(args) {
43293
43588
  process.stdout.write(body);
43294
43589
  return;
43295
43590
  }
43296
- const path = resolve24(args.out);
43591
+ const path = resolve25(args.out);
43297
43592
  await writeTextFile(path, body);
43298
43593
  console.log(`Wrote ${args.format} output to ${path}`);
43299
43594
  }
@@ -43442,7 +43737,7 @@ var objectives_default2 = defineGroup({
43442
43737
  init_zod();
43443
43738
  init_errors3();
43444
43739
  init_outputMeta();
43445
- import { resolve as resolve25 } from "path";
43740
+ import { resolve as resolve26 } from "path";
43446
43741
  init_bun();
43447
43742
  init_selfCli();
43448
43743
  function deepEqual(a, b) {
@@ -43509,7 +43804,7 @@ var replayVerify_default = defineCommand({
43509
43804
  if (!artifactPath) {
43510
43805
  throw usageError("Usage: idk replay verify <artifact.json>");
43511
43806
  }
43512
- const artifactAbs = resolve25(process.cwd(), artifactPath);
43807
+ const artifactAbs = resolve26(process.cwd(), artifactPath);
43513
43808
  const raw = await readJsonFile(artifactAbs).catch((error) => {
43514
43809
  if (error instanceof Error && error.name === "CliError")
43515
43810
  throw error;
@@ -43810,4 +44105,4 @@ export {
43810
44105
  cli2 as cli
43811
44106
  };
43812
44107
 
43813
- //# debugId=4657B842FAC22F1D64756E2164756E21
44108
+ //# debugId=4AAADDD1336AF66164756E2164756E21