@hasna/economy 0.5.0 → 0.5.2

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/chunks/{index-kgy9b6bf.js → index-9d2aky06.js} +1 -1
  3. package/dist/chunks/{index-axh32d30.js → index-dj3tctqh.js} +11 -2
  4. package/dist/chunks/{index-kn5572kd.js → index-kzwqw3ve.js} +1 -1
  5. package/dist/chunks/{index-emvs5ph2.js → index-npsjjv96.js} +1 -1
  6. package/dist/chunks/{serve-0k69e4h9.js → serve-p2yarhm8.js} +28 -3
  7. package/dist/chunks/{tui-7f8f951w.js → tui-x64hjfrh.js} +3 -3
  8. package/dist/chunks/{watch-mm1j39vy.js → watch-bxstjpbx.js} +3 -3
  9. package/dist/cli/index.js +10 -10
  10. package/dist/db/legacy-large-schema.d.ts +6 -0
  11. package/dist/db/legacy-large-schema.d.ts.map +1 -0
  12. package/dist/db/pg-legacy-schema.d.ts +12 -0
  13. package/dist/db/pg-legacy-schema.d.ts.map +1 -0
  14. package/dist/db/pg-migrations.d.ts.map +1 -1
  15. package/dist/index.js +11 -2
  16. package/dist/mcp/index.js +11 -2
  17. package/dist/otel/index.js +11 -2
  18. package/dist/server/index.js +358 -51
  19. package/dist/server/legacy-limits.d.ts +51 -0
  20. package/dist/server/legacy-limits.d.ts.map +1 -0
  21. package/dist/server/legacy-operations.d.ts +2 -1
  22. package/dist/server/legacy-operations.d.ts.map +1 -1
  23. package/dist/server/legacy-plan.d.ts +2 -1
  24. package/dist/server/legacy-plan.d.ts.map +1 -1
  25. package/dist/server/legacy-preservation.d.ts +2 -1
  26. package/dist/server/legacy-preservation.d.ts.map +1 -1
  27. package/dist/server/service-worker.js +11 -2
  28. package/docs/historical-data-reconciliation.md +38 -0
  29. package/docs/postgresql-legacy-schema-repair.md +30 -0
  30. package/package.json +11 -2
@@ -3360,6 +3360,30 @@ function resolvePgConnection(dsn) {
3360
3360
  }
3361
3361
  var init_pg_connection = () => {};
3362
3362
 
3363
+ // src/db/legacy-large-schema.ts
3364
+ var LEGACY_LARGE_SESSION_PG_SQL = `
3365
+ CREATE INDEX idx_legacy_targets_session_operation
3366
+ ON legacy_reconciliation_targets(session_id,operation_key);
3367
+ CREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
3368
+ BEGIN
3369
+ IF EXISTS(
3370
+ SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n
3371
+ JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key
3372
+ ) OR EXISTS(
3373
+ SELECT 1 FROM inserted_legacy_targets n
3374
+ JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key
3375
+ ) THEN
3376
+ RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
3377
+ END IF;
3378
+ RETURN NULL;
3379
+ END
3380
+ $body$;
3381
+ DROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;
3382
+ CREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets
3383
+ REFERENCING NEW TABLE AS inserted_legacy_targets
3384
+ FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();
3385
+ `;
3386
+
3363
3387
  // src/db/pg-migrations.ts
3364
3388
  var PG_MIGRATIONS;
3365
3389
  var init_pg_migrations = __esm(() => {
@@ -3595,7 +3619,8 @@ var init_pg_migrations = __esm(() => {
3595
3619
  ...OBSERVATION_PG_MIGRATIONS,
3596
3620
  `ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
3597
3621
  CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
3598
- LEGACY_PG_SQL
3622
+ LEGACY_PG_SQL,
3623
+ LEGACY_LARGE_SESSION_PG_SQL
3599
3624
  ];
3600
3625
  });
3601
3626
 
@@ -4164,6 +4189,208 @@ var init_pg_migrate = __esm(() => {
4164
4189
  init_pg_connection();
4165
4190
  });
4166
4191
 
4192
+ // src/db/pg-legacy-schema.ts
4193
+ var exports_pg_legacy_schema = {};
4194
+ __export(exports_pg_legacy_schema, {
4195
+ repairLegacyCostCenterSchema: () => repairLegacyCostCenterSchema
4196
+ });
4197
+ import { createHash as createHash5 } from "crypto";
4198
+ import pg4 from "pg";
4199
+ async function assertReceiptTable(client, schema) {
4200
+ await client.query("LOCK TABLE " + quote2(schema) + ".economy_schema_repairs IN SHARE ROW EXCLUSIVE MODE");
4201
+ const shape = await relation(client, schema, "economy_schema_repairs");
4202
+ if (!shape || shape.columns.length !== 3 || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "id"), true) || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "sql_sha256"), true)) {
4203
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger columns");
4204
+ }
4205
+ const completed = shape.columns.find((c) => c.name === "completed_at");
4206
+ if (!completed || completed.type !== "timestamp with time zone" || !completed.required || completed.generated || completed.identity) {
4207
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger timestamp");
4208
+ }
4209
+ const meta = await client.query(`
4210
+ SELECT c.relpersistence='p' AND NOT c.relrowsecurity AND NOT c.relforcerowsecurity
4211
+ AND NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid=c.oid AND NOT tgisinternal)
4212
+ AND NOT EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class=c.oid)
4213
+ AND NOT EXISTS (SELECT 1 FROM pg_inherits WHERE inhrelid=c.oid OR inhparent=c.oid)
4214
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid=c.oid AND (contype<>'p' OR condeferrable))
4215
+ AS ordinary,
4216
+ ARRAY(SELECT a.attname::text FROM pg_index i
4217
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4218
+ JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
4219
+ WHERE i.indrelid=c.oid AND i.indisprimary AND i.indisvalid AND i.indisready
4220
+ ORDER BY k.ordinality) AS primary_columns,
4221
+ (SELECT pg_get_expr(d.adbin,d.adrelid) FROM pg_attrdef d JOIN pg_attribute a
4222
+ ON a.attrelid=d.adrelid AND a.attnum=d.adnum
4223
+ WHERE d.adrelid=c.oid AND a.attname='completed_at') AS clock_default
4224
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4225
+ WHERE n.nspname=$1 AND c.relname='economy_schema_repairs'`, [schema]);
4226
+ const row = meta.rows[0];
4227
+ if (!row?.ordinary || JSON.stringify(row.primary_columns) !== '["id"]' || row.clock_default !== "clock_timestamp()") {
4228
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger behavior");
4229
+ }
4230
+ }
4231
+ async function relation(client, schema, name) {
4232
+ const result = await client.query(`
4233
+ SELECT c.relkind AS kind, a.attname AS name, a.atttypid::regtype::text AS type,
4234
+ a.attnotnull AS required, a.attgenerated AS generated, a.attidentity AS identity
4235
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4236
+ LEFT JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum>0 AND NOT a.attisdropped
4237
+ WHERE n.nspname=$1 AND c.relname=$2 ORDER BY a.attnum`, [schema, name]);
4238
+ if (!result.rows.length)
4239
+ return null;
4240
+ if (result.rows[0].kind !== "r")
4241
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: expected ordinary table " + name);
4242
+ return { kind: result.rows[0].kind, columns: result.rows.filter((row) => row.name) };
4243
+ }
4244
+ function checkColumn(table, actual, required3) {
4245
+ if (!actual)
4246
+ return false;
4247
+ if (actual.type !== "text" || actual.required !== required3 || actual.generated || actual.identity) {
4248
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected column " + table + "." + actual.name);
4249
+ }
4250
+ return true;
4251
+ }
4252
+ async function missingStructure(client, schema) {
4253
+ const missing = [];
4254
+ for (const table of ["requests", "sessions", "budgets"]) {
4255
+ const found = await relation(client, schema, table);
4256
+ if (!found)
4257
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing core table " + table);
4258
+ if (!checkColumn(table, found.columns.find((column) => column.name === "cost_center_id"), false)) {
4259
+ missing.push(table + ".cost_center_id");
4260
+ }
4261
+ }
4262
+ const centers = await relation(client, schema, "cost_centers");
4263
+ if (!centers)
4264
+ missing.push("cost_centers");
4265
+ else {
4266
+ for (const [name, required3] of [
4267
+ ["id", true],
4268
+ ["kind", true],
4269
+ ["name", true],
4270
+ ["repo_path", false],
4271
+ ["labels_json", false],
4272
+ ["created_at", true],
4273
+ ["updated_at", true]
4274
+ ]) {
4275
+ if (!checkColumn("cost_centers", centers.columns.find((column) => column.name === name), required3)) {
4276
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: incomplete cost_centers table");
4277
+ }
4278
+ }
4279
+ const primary = await client.query(`
4280
+ SELECT array_agg(a.attname::text ORDER BY k.ordinality) AS columns
4281
+ FROM pg_index i JOIN pg_class c ON c.oid=i.indrelid JOIN pg_namespace n ON n.oid=c.relnamespace
4282
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4283
+ JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
4284
+ WHERE n.nspname=$1 AND c.relname='cost_centers' AND i.indisprimary
4285
+ GROUP BY i.indexrelid`, [schema]);
4286
+ if (JSON.stringify(primary.rows.map((row) => row.columns)) !== '[["id"]]') {
4287
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected cost_centers primary key");
4288
+ }
4289
+ }
4290
+ for (const [name, table, column] of INDEXES) {
4291
+ const result = await client.query(`
4292
+ SELECT t.relname AS table_name, am.amname AS method,
4293
+ i.indisunique AS unique, i.indisvalid AS valid, i.indisready AS ready,
4294
+ i.indpred IS NULL AS unfiltered, i.indexprs IS NULL AS simple,
4295
+ i.indnatts=i.indnkeyatts AS no_includes,
4296
+ ARRAY(SELECT a.attname::text FROM unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4297
+ JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=k.attnum
4298
+ ORDER BY k.ordinality) AS columns
4299
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4300
+ LEFT JOIN pg_index i ON i.indexrelid=c.oid LEFT JOIN pg_class t ON t.oid=i.indrelid
4301
+ LEFT JOIN pg_am am ON am.oid=c.relam WHERE n.nspname=$1 AND c.relname=$2`, [schema, name]);
4302
+ if (!result.rows.length) {
4303
+ missing.push(name);
4304
+ continue;
4305
+ }
4306
+ const actual = result.rows[0];
4307
+ if (actual.table_name !== table || actual.method !== "btree" || actual.unique || !actual.valid || !actual.ready || !actual.unfiltered || !actual.simple || !actual.no_includes || JSON.stringify(actual.columns) !== JSON.stringify([column])) {
4308
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected index " + name);
4309
+ }
4310
+ }
4311
+ return missing;
4312
+ }
4313
+ async function repairLegacyCostCenterSchema(dsn) {
4314
+ const result = { id: REPAIR_ID, status: "not_needed", added: [] };
4315
+ const pool = new pg4.Pool({ ...resolvePgConnection(dsn), max: 1, connectionTimeoutMillis: 1e4 });
4316
+ pool.on("error", () => {});
4317
+ let client;
4318
+ try {
4319
+ client = await pool.connect();
4320
+ client.on("error", () => {});
4321
+ await client.query("SET statement_timeout='15min'; SET lock_timeout='15s'");
4322
+ await client.query("SELECT pg_advisory_lock(hashtext('economy.pg-migrations.v1'), hashtext(current_database() || ':' || current_schema()))");
4323
+ const schema = (await client.query("SELECT current_schema() AS name")).rows[0]?.name;
4324
+ if (typeof schema !== "string" || !schema)
4325
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing migration schema");
4326
+ const ledger = await relation(client, schema, "_pg_migrations");
4327
+ if (!ledger)
4328
+ return result;
4329
+ const history = await client.query("SELECT version FROM " + quote2(schema) + "._pg_migrations WHERE version>=0 AND version<85 ORDER BY version");
4330
+ if (history.rows.length !== 85 || history.rows.some((row, index) => row.version !== index)) {
4331
+ return result;
4332
+ }
4333
+ await client.query("BEGIN");
4334
+ try {
4335
+ await client.query("SELECT set_config('search_path', $1, true)", [quote2(schema)]);
4336
+ const missing = await missingStructure(client, schema);
4337
+ const receiptTable = await relation(client, schema, "economy_schema_repairs");
4338
+ if (receiptTable) {
4339
+ await assertReceiptTable(client, schema);
4340
+ const receipt = await client.query("SELECT sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
4341
+ if (receipt.rows.length) {
4342
+ if (receipt.rows.length !== 1 || receipt.rows[0].sql_sha256 !== REPAIR_HASH || missing.length) {
4343
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: completed repair receipt or schema has changed");
4344
+ }
4345
+ await client.query("COMMIT");
4346
+ return { ...result, status: "already_applied" };
4347
+ }
4348
+ }
4349
+ if (!missing.length) {
4350
+ await client.query("COMMIT");
4351
+ return result;
4352
+ }
4353
+ await client.query(`CREATE TABLE IF NOT EXISTS economy_schema_repairs (
4354
+ id TEXT PRIMARY KEY, sql_sha256 TEXT NOT NULL,
4355
+ completed_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp()
4356
+ )`);
4357
+ await assertReceiptTable(client, schema);
4358
+ for (const sql of REPAIR_SQL)
4359
+ await client.query(sql);
4360
+ if ((await missingStructure(client, schema)).length)
4361
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair postcondition failed");
4362
+ const inserted = await client.query("INSERT INTO economy_schema_repairs (id,sql_sha256) VALUES ($1,$2) RETURNING id,sql_sha256", [REPAIR_ID, REPAIR_HASH]);
4363
+ const readback = await client.query("SELECT id,sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
4364
+ for (const receipt of [inserted, readback]) {
4365
+ if (receipt.rowCount !== 1 || receipt.rows[0]?.id !== REPAIR_ID || receipt.rows[0]?.sql_sha256 !== REPAIR_HASH) {
4366
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair receipt readback failed");
4367
+ }
4368
+ }
4369
+ await client.query("COMMIT");
4370
+ return { id: REPAIR_ID, status: "applied", added: missing };
4371
+ } catch (error) {
4372
+ await client.query("ROLLBACK").catch(() => {});
4373
+ throw error;
4374
+ }
4375
+ } finally {
4376
+ client?.release();
4377
+ await pool.end().catch(() => {});
4378
+ }
4379
+ }
4380
+ var REPAIR_ID = "legacy-cost-centers-v1", REPAIR_SQL, REPAIR_HASH, INDEXES, quote2 = (name) => '"' + name.replaceAll('"', '""') + '"';
4381
+ var init_pg_legacy_schema = __esm(() => {
4382
+ init_pg_connection();
4383
+ init_pg_migrations();
4384
+ REPAIR_SQL = [0, 38, 47, 48, 15, 54, 55, 56].map((index) => PG_MIGRATIONS[index]);
4385
+ REPAIR_HASH = createHash5("sha256").update(JSON.stringify(REPAIR_SQL)).digest("hex");
4386
+ INDEXES = [
4387
+ ["idx_cost_centers_kind", "cost_centers", "kind"],
4388
+ ["idx_requests_cost_center", "requests", "cost_center_id"],
4389
+ ["idx_sessions_cost_center", "sessions", "cost_center_id"],
4390
+ ["idx_budgets_cost_center", "budgets", "cost_center_id"]
4391
+ ];
4392
+ });
4393
+
4167
4394
  // src/server/legacy-inventory.ts
4168
4395
  var exports_legacy_inventory = {};
4169
4396
  __export(exports_legacy_inventory, {
@@ -4306,6 +4533,42 @@ var init_legacy_inventory = __esm(() => {
4306
4533
  allCandidates = LEGACY_FAMILIES.map((family) => `(${predicates[family]})`).join(" OR ");
4307
4534
  });
4308
4535
 
4536
+ // src/server/legacy-limits.ts
4537
+ function legacyMode(kind, schema) {
4538
+ if (schema === LEGACY_STANDARD[kind])
4539
+ return LEGACY_STANDARD;
4540
+ if (schema === LEGACY_SINGLE_SESSION[kind])
4541
+ return LEGACY_SINGLE_SESSION;
4542
+ throw Error(`Unsupported legacy ${kind} schema`);
4543
+ }
4544
+ var LEGACY_STANDARD, LEGACY_SINGLE_SESSION;
4545
+ var init_legacy_limits = __esm(() => {
4546
+ LEGACY_STANDARD = {
4547
+ selection: "economy.legacy-selection.v1",
4548
+ plan: "economy.legacy-plan.v1",
4549
+ capsule: "economy.legacy-preservation.v1",
4550
+ receipt: "economy.legacy-operation-receipt.v1",
4551
+ items: 100,
4552
+ sessions: 100,
4553
+ members: 1e4,
4554
+ inputBytes: 256 * 1024,
4555
+ originalBytes: 16 * 1024 * 1024,
4556
+ capsuleBytes: 32 * 1024 * 1024
4557
+ };
4558
+ LEGACY_SINGLE_SESSION = {
4559
+ selection: "economy.legacy-session-selection.v1",
4560
+ plan: "economy.legacy-session-plan.v1",
4561
+ capsule: "economy.legacy-session-preservation.v1",
4562
+ receipt: "economy.legacy-session-operation-receipt.v1",
4563
+ items: 50000,
4564
+ sessions: 1,
4565
+ members: 50000,
4566
+ inputBytes: 32 * 1024 * 1024,
4567
+ originalBytes: 64 * 1024 * 1024,
4568
+ capsuleBytes: 128 * 1024 * 1024
4569
+ };
4570
+ });
4571
+
4309
4572
  // src/server/legacy-plan.ts
4310
4573
  var exports_legacy_plan = {};
4311
4574
  __export(exports_legacy_plan, {
@@ -4316,7 +4579,7 @@ __export(exports_legacy_plan, {
4316
4579
  parseLegacySelection: () => parseLegacySelection,
4317
4580
  parseLegacyPlanOptions: () => parseLegacyPlanOptions
4318
4581
  });
4319
- import { createHash as createHash5 } from "crypto";
4582
+ import { createHash as createHash6 } from "crypto";
4320
4583
  import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
4321
4584
  import { isAbsolute } from "path";
4322
4585
  function object(value, fields, label) {
@@ -4341,10 +4604,11 @@ function digest2(value) {
4341
4604
  }
4342
4605
  function parseLegacySelection(value) {
4343
4606
  const input = object(value, ["schema", "items"], "selection");
4344
- if (input.schema !== "economy.legacy-selection.v1")
4345
- throw Error("schema must be economy.legacy-selection.v1");
4346
- if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > 100)
4347
- throw Error("items must contain 1-100 explicit request selections");
4607
+ const mode = legacyMode("selection", input.schema);
4608
+ if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > mode.items)
4609
+ throw Error(`items must contain 1-${mode.items} explicit request selections`);
4610
+ if (Buffer.byteLength(JSON.stringify(input)) > mode.inputBytes)
4611
+ throw Error(`Selection exceeds its ${mode.inputBytes}-byte artifact bound`);
4348
4612
  const ids = new Set;
4349
4613
  const items = input.items.map((item) => {
4350
4614
  const row = object(item, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref"], "item");
@@ -4367,7 +4631,7 @@ function parseLegacySelection(value) {
4367
4631
  evidence_ref: text(row.evidence_ref, 512, "evidence_ref")
4368
4632
  };
4369
4633
  }).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
4370
- return { schema: "economy.legacy-selection.v1", items };
4634
+ return { schema: mode.selection, items };
4371
4635
  }
4372
4636
  function readLegacySelectionFile(path) {
4373
4637
  if (!isAbsolute(path) || path.includes("\x00"))
@@ -4380,9 +4644,10 @@ function readLegacySelectionFile(path) {
4380
4644
  }
4381
4645
  try {
4382
4646
  const before = fstatSync(fd);
4647
+ const sizeError = "--input must be a regular JSON file of 1-262144 bytes for standard selection, or at most 33554432 bytes for explicit single-session selection";
4383
4648
  if (!before.isFile() || before.size < 1 || before.size > INPUT_MAX)
4384
- throw Error("--input must be a regular JSON file of 1-262144 bytes");
4385
- const bytes = Buffer.alloc(INPUT_MAX + 1);
4649
+ throw Error(sizeError);
4650
+ const bytes = Buffer.alloc(Math.min(INPUT_MAX + 1, before.size + 1));
4386
4651
  let offset = 0, n;
4387
4652
  while (offset < bytes.length && (n = readSync(fd, bytes, offset, bytes.length - offset, null)) > 0)
4388
4653
  offset += n;
@@ -4393,9 +4658,12 @@ function readLegacySelectionFile(path) {
4393
4658
  try {
4394
4659
  value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset)));
4395
4660
  } catch {
4396
- throw Error("--input must contain valid UTF-8 JSON");
4661
+ throw Error(before.size > LEGACY_STANDARD.inputBytes ? sizeError : "--input must contain valid UTF-8 JSON");
4397
4662
  }
4398
- return parseLegacySelection(value);
4663
+ const selection = parseLegacySelection(value);
4664
+ if (offset > legacyMode("selection", selection.schema).inputBytes)
4665
+ throw Error(sizeError);
4666
+ return selection;
4399
4667
  } finally {
4400
4668
  closeSync(fd);
4401
4669
  }
@@ -4410,6 +4678,7 @@ async function prepareLegacyPlan(client, input) {
4410
4678
  }
4411
4679
  async function withLegacyPlanSnapshot(client, input, read) {
4412
4680
  const selection = parseLegacySelection(input);
4681
+ const mode = legacyMode("selection", selection.schema);
4413
4682
  let began = false;
4414
4683
  try {
4415
4684
  await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
@@ -4444,14 +4713,16 @@ async function withLegacyPlanSnapshot(client, input, read) {
4444
4713
  return { ...item, session_id: identifier2(row.session_id, "session_id") };
4445
4714
  });
4446
4715
  const sessionIds = [...new Set(items.map((x) => x.session_id))].sort();
4716
+ if (mode.sessions === 1 && sessionIds.length !== 1)
4717
+ throw Error("Single-session reconciliation requires exactly one session");
4447
4718
  const sessions = (await client.query(`SELECT id, encode(sha256(convert_to(row_to_json(sessions)::text,'UTF8')),'hex') AS original_sha256
4448
4719
  FROM sessions WHERE id=ANY($1::text[]) ORDER BY id`, [sessionIds])).rows;
4449
4720
  if (sessions.length !== sessionIds.length)
4450
4721
  throw Error("A selected request has no session; resolve that inconsistency before reconciliation");
4451
4722
  const members = (await client.query(`SELECT id, session_id, encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256
4452
- FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${MEMBERS_MAX + 1}`, [sessionIds])).rows;
4453
- if (members.length > MEMBERS_MAX)
4454
- throw Error("Selected sessions exceed 10000 requests; split the selection by session or use a separately reviewed large-session procedure");
4723
+ FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${mode.members + 1}`, [sessionIds])).rows;
4724
+ if (members.length > mode.members)
4725
+ throw Error(`Selected sessions exceed ${mode.members} requests; complete-session reconciliation cannot truncate membership`);
4455
4726
  const populations = sessions.map((session) => {
4456
4727
  const rows = members.filter((row) => row.session_id === session.id);
4457
4728
  if (!rows.length)
@@ -4461,7 +4732,7 @@ async function withLegacyPlanSnapshot(client, input, read) {
4461
4732
  return { id: session.id, original_sha256, request_count: rows.length, members_sha256 };
4462
4733
  });
4463
4734
  const body = {
4464
- schema: "economy.legacy-plan.v1",
4735
+ schema: mode.plan,
4465
4736
  mode: "read-only",
4466
4737
  original_encoding: "postgres-row-json-text-v1",
4467
4738
  member_encoding: "sha256-json-id-digest-pairs-utf8-c-order-v1",
@@ -4505,10 +4776,11 @@ async function runLegacyPlan(selection) {
4505
4776
  await pool.end();
4506
4777
  }
4507
4778
  }
4508
- var INPUT_MAX, MEMBERS_MAX = 1e4, sha256 = (text) => createHash5("sha256").update(text).digest("hex");
4779
+ var INPUT_MAX, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
4509
4780
  var init_legacy_plan = __esm(() => {
4510
4781
  init_legacy_inventory();
4511
- INPUT_MAX = 256 * 1024;
4782
+ init_legacy_limits();
4783
+ INPUT_MAX = LEGACY_SINGLE_SESSION.inputBytes;
4512
4784
  });
4513
4785
 
4514
4786
  // src/server/legacy-preservation.ts
@@ -4524,20 +4796,21 @@ __export(exports_legacy_preservation, {
4524
4796
  parseLegacyCapsule: () => parseLegacyCapsule,
4525
4797
  assertLegacyPlanCurrent: () => assertLegacyPlanCurrent
4526
4798
  });
4527
- import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
4799
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
4528
4800
  import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
4529
4801
  import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
4530
4802
  function parseLegacyPlan(value) {
4531
4803
  const p = record(value);
4804
+ const mode = legacyMode("plan", p.schema);
4532
4805
  fields(p, ["schema", "mode", "original_encoding", "member_encoding", "snapshot", "items", "sessions", "mutations_performed", "limitations", "plan_sha256"]);
4533
- if (p.schema !== "economy.legacy-plan.v1" || p.mode !== "read-only" || p.original_encoding !== "postgres-row-json-text-v1" || p.member_encoding !== "sha256-json-id-digest-pairs-utf8-c-order-v1" || p.mutations_performed !== 0)
4806
+ if (p.mode !== "read-only" || p.original_encoding !== "postgres-row-json-text-v1" || p.member_encoding !== "sha256-json-id-digest-pairs-utf8-c-order-v1" || p.mutations_performed !== 0)
4534
4807
  fail("Unsupported legacy plan contract");
4535
4808
  const { plan_sha256, ...body } = p;
4536
4809
  if (hash(plan_sha256) !== sha2(JSON.stringify(body)))
4537
4810
  fail("Legacy plan digest mismatch");
4538
4811
  if (!Array.isArray(p.items))
4539
4812
  fail("Legacy plan items must be an array");
4540
- const selection = parseLegacySelection({ schema: "economy.legacy-selection.v1", items: p.items.map((item) => {
4813
+ const selection = parseLegacySelection({ schema: mode.selection, items: p.items.map((item) => {
4541
4814
  const row = record(item);
4542
4815
  fields(row, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref", "session_id"]);
4543
4816
  identifier3(row.session_id);
@@ -4565,7 +4838,7 @@ function parseLegacyPlan(value) {
4565
4838
  if (typeof t.relation_oid !== "string" || !/^[0-9]+$/.test(t.relation_oid))
4566
4839
  fail("Invalid relation locator");
4567
4840
  }
4568
- if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > 100)
4841
+ if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > mode.sessions)
4569
4842
  fail("Invalid legacy session population");
4570
4843
  const ids = new Set;
4571
4844
  let members = 0;
@@ -4582,7 +4855,7 @@ function parseLegacyPlan(value) {
4582
4855
  fail("Invalid legacy member count");
4583
4856
  members += s.request_count;
4584
4857
  }
4585
- if (members > MAX_MEMBERS || p.items.some((x) => !ids.has(x.session_id)) || ids.size !== new Set(p.items.map((x) => x.session_id)).size)
4858
+ if (members > mode.members || p.items.some((x) => !ids.has(x.session_id)) || ids.size !== new Set(p.items.map((x) => x.session_id)).size)
4586
4859
  fail("Incomplete or excessive legacy session membership");
4587
4860
  if (!Array.isArray(p.limitations) || p.limitations.length > 20 || p.limitations.some((x) => typeof x !== "string" || x.length > 2048))
4588
4861
  fail("Invalid legacy plan limitations");
@@ -4599,16 +4872,28 @@ function assertLegacyPlanCurrent(expected, current) {
4599
4872
  }
4600
4873
  function parseLegacyCapsule(value) {
4601
4874
  const c = record(value);
4875
+ const mode = legacyMode("capsule", c.schema);
4602
4876
  fields(c, ["schema", "plan", "captured_at", "requests", "sessions", "capsule_sha256"]);
4603
- if (c.schema !== "economy.legacy-preservation.v1" || typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
4877
+ if (typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
4604
4878
  fail("Invalid preservation capsule");
4605
4879
  const { capsule_sha256, ...body } = c;
4606
4880
  const bytes = JSON.stringify(body);
4607
- if (Buffer.byteLength(bytes) > MAX_BYTES || hash(capsule_sha256) !== sha2(bytes))
4881
+ if (Buffer.byteLength(bytes) > mode.capsuleBytes || hash(capsule_sha256) !== sha2(bytes))
4608
4882
  fail("Preservation capsule bound or digest mismatch");
4609
4883
  const plan = parseLegacyPlan(c.plan);
4610
- if (!Array.isArray(c.requests) || c.requests.length > MAX_MEMBERS || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
4884
+ if (plan.schema !== mode.plan)
4885
+ fail("Preservation capsule and plan use different artifact modes");
4886
+ if (!Array.isArray(c.requests) || c.requests.length > mode.members || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
4611
4887
  fail("Invalid original populations");
4888
+ let originalBytes = 0;
4889
+ const boundOriginal = (original) => {
4890
+ if (typeof original !== "string")
4891
+ fail("Invalid original row text");
4892
+ originalBytes += Buffer.byteLength(original);
4893
+ if (originalBytes > mode.originalBytes)
4894
+ fail("Preserved original row text exceeds its artifact mode bound");
4895
+ return original;
4896
+ };
4612
4897
  const requests = new Map;
4613
4898
  for (const value2 of c.requests) {
4614
4899
  const row = record(value2);
@@ -4616,7 +4901,7 @@ function parseLegacyCapsule(value) {
4616
4901
  const id = identifier3(row.id);
4617
4902
  if (requests.has(id) || typeof row.original !== "string")
4618
4903
  fail("Invalid or duplicate request original");
4619
- const original = record(JSON.parse(row.original));
4904
+ const original = record(JSON.parse(boundOriginal(row.original)));
4620
4905
  if (original.id !== id)
4621
4906
  fail("Request original identity mismatch");
4622
4907
  requests.set(id, { session: identifier3(original.session_id), digest: sha2(row.original) });
@@ -4630,7 +4915,7 @@ function parseLegacyCapsule(value) {
4630
4915
  fail("Invalid or duplicate session original");
4631
4916
  sessions.add(id);
4632
4917
  const expected = plan.sessions.find((s) => s.id === id) ?? fail("Session original does not match the plan");
4633
- if (record(JSON.parse(row.original)).id !== id || sha2(row.original) !== expected.original_sha256)
4918
+ if (record(JSON.parse(boundOriginal(row.original))).id !== id || sha2(row.original) !== expected.original_sha256)
4634
4919
  fail("Session original does not match the plan");
4635
4920
  const members = [...requests].filter(([, r]) => r.session === id).sort(([a], [b]) => cOrder(a, b)).map(([rid, r]) => [rid, r.digest]);
4636
4921
  if (members.length !== expected.request_count || sha2(JSON.stringify(members)) !== expected.members_sha256)
@@ -4647,18 +4932,19 @@ function parseLegacyCapsule(value) {
4647
4932
  }
4648
4933
  async function preserveLegacyOriginals(client, input) {
4649
4934
  const expected = parseLegacyPlan(input);
4650
- const selection = { schema: "economy.legacy-selection.v1", items: expected.items.map(({ session_id, ...item }) => item) };
4935
+ const mode = legacyMode("plan", expected.schema);
4936
+ const selection = { schema: mode.selection, items: expected.items.map(({ session_id, ...item }) => item) };
4651
4937
  return withLegacyPlanSnapshot(client, selection, async (current) => {
4652
4938
  assertLegacyPlanCurrent(expected, current);
4653
4939
  const ids = current.sessions.map((x) => x.id);
4654
4940
  const sizes = (await client.query(`SELECT
4655
4941
  (SELECT COALESCE(SUM(octet_length(row_to_json(requests)::text)),0) FROM requests WHERE session_id=ANY($1::text[]))::text AS request_bytes,
4656
4942
  (SELECT COALESCE(SUM(octet_length(row_to_json(sessions)::text)),0) FROM sessions WHERE id=ANY($1::text[]))::text AS session_bytes`, [ids])).rows[0];
4657
- if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(MAX_BYTES / 2))
4658
- fail("Preservation exceeds 16 MiB of original row text; split by session");
4659
- const requests = (await client.query('SELECT id,row_to_json(requests)::text AS original FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT 10001', [ids])).rows;
4660
- const sessions = (await client.query('SELECT id,row_to_json(sessions)::text AS original FROM sessions WHERE id=ANY($1::text[]) ORDER BY id COLLATE "C" LIMIT 101', [ids])).rows;
4661
- const body = { schema: "economy.legacy-preservation.v1", plan: expected, captured_at: current.snapshot.at, requests, sessions };
4943
+ if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(mode.originalBytes))
4944
+ fail(`Preservation exceeds ${mode.originalBytes / 1024 / 1024} MiB of original row text; complete sessions cannot be truncated`);
4945
+ const requests = (await client.query(`SELECT id,row_to_json(requests)::text AS original FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
4946
+ const sessions = (await client.query(`SELECT id,row_to_json(sessions)::text AS original FROM sessions WHERE id=ANY($1::text[]) ORDER BY id COLLATE "C" LIMIT ${mode.sessions + 1}`, [ids])).rows;
4947
+ const body = { schema: mode.capsule, plan: expected, captured_at: current.snapshot.at, requests, sessions };
4662
4948
  return parseLegacyCapsule({ ...body, capsule_sha256: sha2(JSON.stringify(body)) });
4663
4949
  });
4664
4950
  }
@@ -4689,11 +4975,16 @@ function readLegacyBytes(path, maxBytes = MAX_BYTES) {
4689
4975
  }
4690
4976
  function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
4691
4977
  const bytes = readLegacyBytes(path, maxBytes);
4978
+ let value;
4692
4979
  try {
4693
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
4980
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
4694
4981
  } catch {
4695
4982
  fail("Artifact must contain valid UTF-8 JSON");
4696
4983
  }
4984
+ const limit = value?.schema === LEGACY_SINGLE_SESSION.plan || value?.schema === LEGACY_SINGLE_SESSION.capsule ? LEGACY_SINGLE_SESSION.capsuleBytes : LEGACY_STANDARD.capsuleBytes;
4985
+ if (bytes.length > limit)
4986
+ fail("Artifact exceeds its schema file bound");
4987
+ return value;
4697
4988
  }
4698
4989
  function writeLegacyCapsule(path, input) {
4699
4990
  const capsule = parseLegacyCapsule(input);
@@ -4704,7 +4995,7 @@ function writeLegacyCapsule(path, input) {
4704
4995
  fail("Output parent must be an owner-only directory");
4705
4996
  const bytes = Buffer.from(JSON.stringify(capsule) + `
4706
4997
  `);
4707
- if (bytes.length > MAX_BYTES)
4998
+ if (bytes.length > legacyMode("capsule", capsule.schema).capsuleBytes)
4708
4999
  fail("Preservation capsule exceeds the file bound");
4709
5000
  const fd = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
4710
5001
  try {
@@ -4774,8 +5065,8 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
4774
5065
  const table = tables[i], expected = capsule.plan.snapshot.tables[i];
4775
5066
  if (table.table_name !== expected.table_name || table.columns_sha256 !== expected.columns_sha256)
4776
5067
  fail("Restore target schema differs from the preserved schema");
4777
- const source = quote2(table.schema_name) + "." + quote2(table.table_name);
4778
- const temp = quote2("economy_restore_" + randomUUID3().replaceAll("-", ""));
5068
+ const source = quote3(table.schema_name) + "." + quote3(table.table_name);
5069
+ const temp = quote3("economy_restore_" + randomUUID3().replaceAll("-", ""));
4779
5070
  await client.query("CREATE TEMP TABLE " + temp + " (LIKE " + source + " INCLUDING ALL) ON COMMIT DROP");
4780
5071
  const originals = table.table_name === "requests" ? capsule.requests : capsule.sessions;
4781
5072
  restoreTables.push({ source, temp, originals });
@@ -4835,15 +5126,16 @@ async function runLegacyPreservation(command, options) {
4835
5126
  await pool.end();
4836
5127
  }
4837
5128
  }
4838
- var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash6("sha256").update(s).digest("hex"), fail = (message) => {
5129
+ var MAX_BYTES, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
4839
5130
  throw Error(message);
4840
5131
  }, record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : fail("Expected a JSON object"), hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail("Expected a lowercase SHA-256 digest"), identifier3 = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024 && !v.includes("\x00") ? v : fail("Expected a nonempty identifier of at most 1024 characters without NUL"), fields = (o, names) => {
4841
5132
  if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
4842
5133
  fail("Unexpected or missing preservation fields");
4843
- }, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote2 = (s) => '"' + s.replaceAll('"', '""') + '"';
5134
+ }, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote3 = (s) => '"' + s.replaceAll('"', '""') + '"';
4844
5135
  var init_legacy_preservation = __esm(() => {
4845
5136
  init_legacy_plan();
4846
- MAX_BYTES = 32 * 1024 * 1024;
5137
+ init_legacy_limits();
5138
+ MAX_BYTES = LEGACY_SINGLE_SESSION.capsuleBytes;
4847
5139
  });
4848
5140
 
4849
5141
  // src/server/legacy-operations.ts
@@ -4858,7 +5150,7 @@ __export(exports_legacy_operations, {
4858
5150
  applyLegacyReconciliation: () => applyLegacyReconciliation,
4859
5151
  LegacyOperationInputError: () => LegacyOperationInputError
4860
5152
  });
4861
- import { createHash as createHash7 } from "crypto";
5153
+ import { createHash as createHash8 } from "crypto";
4862
5154
  import { isAbsolute as isAbsolute3 } from "path";
4863
5155
  function parseLegacyIntent(action, value) {
4864
5156
  try {
@@ -4907,6 +5199,7 @@ function matchIntent(intent, capsule, target) {
4907
5199
  }
4908
5200
  async function verifyOriginals(client, capsule) {
4909
5201
  const p = capsule.plan;
5202
+ const mode = legacyMode("capsule", capsule.schema);
4910
5203
  const context2 = (await client.query("SELECT current_database() AS database,(SELECT oid::text FROM pg_database WHERE datname=current_database()) AS database_oid")).rows[0];
4911
5204
  if (context2?.database !== p.snapshot.database || context2?.database_oid !== p.snapshot.database_oid)
4912
5205
  fail2("Legacy operation targets a different database");
@@ -4920,7 +5213,7 @@ async function verifyOriginals(client, capsule) {
4920
5213
  fail2("Legacy session originals changed; preserve later work and prepare a new review");
4921
5214
  const rows = (await client.query(`SELECT id,session_id,encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256,
4922
5215
  CASE ${LEGACY_FAMILIES.map((f) => "WHEN " + LEGACY_PREDICATES[f] + " THEN '" + f + "'").join(" ")} END AS family
4923
- FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT 10001`, [ids])).rows;
5216
+ FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
4924
5217
  if (rows.length !== capsule.requests.length)
4925
5218
  fail2("Legacy request membership changed; preserve later work and prepare a new review");
4926
5219
  const originals = new Map(capsule.requests.map((r) => [r.id, sha3(r.original)]));
@@ -4974,13 +5267,14 @@ async function observed(client, run) {
4974
5267
  }
4975
5268
  function validateReceipt(value) {
4976
5269
  const v = value;
4977
- if (!v || v.schema !== "economy.legacy-operation-receipt.v1" || !["apply", "rollback"].includes(v.action))
5270
+ if (!v || !["apply", "rollback"].includes(v.action))
4978
5271
  fail2("Invalid stored legacy receipt");
5272
+ const mode = legacyMode("receipt", v.schema);
4979
5273
  key(v.operation_key);
4980
5274
  key(v.apply_key);
4981
5275
  digest3(v.plan_sha256);
4982
5276
  digest3(v.capsule_sha256);
4983
- if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <= 1e4) || v.target_requests > 100 || v.sessions > 100 || typeof v.created_at !== "string" || !Number.isFinite(Date.parse(v.created_at)))
5277
+ if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <= mode.members) || v.target_requests > mode.items || v.sessions > mode.sessions || typeof v.created_at !== "string" || !Number.isFinite(Date.parse(v.created_at)))
4984
5278
  fail2("Invalid stored legacy receipt fields");
4985
5279
  const names = ["schema", "action", "operation_key", "apply_key", "plan_sha256", "capsule_sha256", "target_requests", "preserved_requests", "sessions", "source_rows_changed", "created_at"];
4986
5280
  if (Object.keys(v).sort().join(",") !== names.sort().join(","))
@@ -5005,7 +5299,7 @@ async function reply(client, row, expectedHash) {
5005
5299
  }
5006
5300
  function receipt(action, intent, capsule) {
5007
5301
  return {
5008
- schema: "economy.legacy-operation-receipt.v1",
5302
+ schema: legacyMode("capsule", capsule.schema).receipt,
5009
5303
  action,
5010
5304
  operation_key: intent.operation_key,
5011
5305
  apply_key: action === "apply" ? intent.operation_key : intent.apply_key,
@@ -5045,8 +5339,9 @@ async function applyLegacyReconciliation(client, input, preserved, target) {
5045
5339
  const saved = (await client.query("SELECT capsule_json,capsule_sha256 FROM legacy_reconciliations WHERE operation_key=$1", [intent.operation_key])).rows[0];
5046
5340
  if (saved?.capsule_json !== json3 || saved?.capsule_sha256 !== capsule.capsule_sha256 || parseLegacyCapsule(JSON.parse(saved.capsule_json)).capsule_sha256 !== capsule.capsule_sha256)
5047
5341
  fail2("Durable preservation readback failed");
5048
- for (const item of capsule.plan.items)
5049
- await client.query("INSERT INTO legacy_reconciliation_targets VALUES($1,$2,$3,$4,$5)", [intent.operation_key, item.id, item.session_id, item.disposition, item.original_sha256]);
5342
+ await client.query(`INSERT INTO legacy_reconciliation_targets(operation_key,request_id,session_id,disposition,original_sha256)
5343
+ SELECT $1, x.id, x.session_id, x.disposition, x.original_sha256
5344
+ FROM jsonb_to_recordset($2::jsonb) AS x(id text,session_id text,disposition text,original_sha256 text)`, [intent.operation_key, JSON.stringify(capsule.plan.items.map(({ id, session_id, disposition, original_sha256 }) => ({ id, session_id, disposition, original_sha256 })))]);
5050
5345
  const stored = await lookup(client, "apply", intent.operation_key) ?? fail2("Legacy receipt readback failed");
5051
5346
  if (stored.response_json !== responseJson)
5052
5347
  fail2("Legacy receipt readback failed");
@@ -5150,7 +5445,7 @@ async function runLegacyOperation(command, options) {
5150
5445
  await pool.end();
5151
5446
  }
5152
5447
  }
5153
- var sha3 = (s) => createHash7("sha256").update(s).digest("hex"), fail2 = (s) => {
5448
+ var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) => {
5154
5449
  throw Error(s);
5155
5450
  }, digest3 = (v, label = "digest") => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail2(label + " must be a lowercase SHA-256 digest"), key = (v, label = "operation_key") => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : fail2(label + " must contain 1-200 letters, digits, dot, underscore, colon or hyphen"), reference = (v, label) => typeof v === "string" && /^[A-Za-z0-9:/._-]{1,512}$/.test(v) ? v : fail2(label + " must contain 1-512 letters, digits, colon, slash, dot, underscore or hyphen"), LegacyOperationInputError, TABLES_SQL = `SELECT c.relname AS table_name,n.nspname AS schema_name,c.oid::text AS relation_oid,
5156
5451
  encode(sha256(convert_to(json_agg(json_build_array(a.attnum,a.attname,a.atttypid::text,a.atttypmod,a.attnotnull,a.attcollation::text) ORDER BY a.attnum)::text,'UTF8')),'hex') AS columns_sha256
@@ -5158,6 +5453,7 @@ var sha3 = (s) => createHash7("sha256").update(s).digest("hex"), fail2 = (s) =>
5158
5453
  WHERE c.oid IN ('requests'::regclass,'sessions'::regclass) AND a.attnum>0 AND NOT a.attisdropped
5159
5454
  GROUP BY c.oid,c.relname,n.nspname ORDER BY c.relname`, same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
5160
5455
  var init_legacy_operations = __esm(() => {
5456
+ init_legacy_limits();
5161
5457
  init_pg_connection();
5162
5458
  init_legacy_inventory();
5163
5459
  init_legacy_preservation();
@@ -5194,7 +5490,7 @@ function isAuthorizedRequest(req, path) {
5194
5490
  // package.json
5195
5491
  var package_default = {
5196
5492
  name: "@hasna/economy",
5197
- version: "0.5.0",
5493
+ version: "0.5.2",
5198
5494
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5199
5495
  repository: {
5200
5496
  type: "git",
@@ -5289,7 +5585,16 @@ var package_default = {
5289
5585
  },
5290
5586
  trustedDependencies: [
5291
5587
  "@hasna/projects"
5292
- ]
5588
+ ],
5589
+ overrides: {
5590
+ "fast-uri": "3.1.8",
5591
+ hono: "4.13.7",
5592
+ "ip-address": "10.7.1",
5593
+ nanoid: "5.1.16",
5594
+ "path-to-regexp": "8.4.0",
5595
+ undici: "6.28.1",
5596
+ "express-rate-limit": "8.7.0"
5597
+ }
5293
5598
  };
5294
5599
 
5295
5600
  // src/lib/package-metadata.ts
@@ -12457,10 +12762,12 @@ function parsePort(raw, label) {
12457
12762
  async function runMigrate() {
12458
12763
  const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
12459
12764
  const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
12765
+ const { repairLegacyCostCenterSchema: repairLegacyCostCenterSchema2 } = await Promise.resolve().then(() => (init_pg_legacy_schema(), exports_pg_legacy_schema));
12460
12766
  const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
12461
12767
  const dsn = getCloudDatabaseUrl2();
12462
12768
  if (!dsn)
12463
12769
  throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
12770
+ const repair = await repairLegacyCostCenterSchema2(dsn);
12464
12771
  const result = await applyPgMigrations2(dsn, cloudMigrations2(), "economy");
12465
12772
  const pool = createCloudPool2(dsn);
12466
12773
  try {
@@ -12468,7 +12775,7 @@ async function runMigrate() {
12468
12775
  } finally {
12469
12776
  await pool.end().catch(() => {});
12470
12777
  }
12471
- console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, api_keys: "ensured" }));
12778
+ console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, repair, api_keys: "ensured" }));
12472
12779
  if (result.errors.length)
12473
12780
  process.exit(1);
12474
12781
  }