@hasna/economy 0.5.1 → 0.5.3
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/CHANGELOG.md +12 -0
- package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
- package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
- package/dist/chunks/{index-d480p2wh.js → index-1vz1arz5.js} +14 -13
- package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
- package/dist/chunks/{index-49f12css.js → index-bn04v378.js} +2 -2
- package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
- package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
- package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
- package/dist/chunks/{index-r1jv3kpy.js → index-mhrkwvtk.js} +3 -3
- package/dist/chunks/{index-xsah93s2.js → index-s9mvven6.js} +2 -2
- package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
- package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
- package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
- package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
- package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
- package/dist/chunks/{serve-2knem98x.js → serve-8yqe7nhs.js} +44 -11
- package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-13nhr1z0.js} +2 -2
- package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-gamn65xp.js} +2 -2
- package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
- package/dist/chunks/{tui-khmnkat2.js → tui-fe94bvp9.js} +4 -4
- package/dist/chunks/{watch-4y064yys.js → watch-ct8dvrpr.js} +5 -5
- package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
- package/dist/cli/index.js +27 -27
- package/dist/db/database.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/legacy-large-schema.d.ts +6 -0
- package/dist/db/legacy-large-schema.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +15 -14
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/periods.d.ts +20 -1
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +3 -3
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/mcp/index.js +18 -17
- package/dist/otel/index.js +1 -1
- package/dist/server/index.js +213 -74
- package/dist/server/legacy-limits.d.ts +51 -0
- package/dist/server/legacy-limits.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +2 -1
- package/dist/server/legacy-operations.d.ts.map +1 -1
- package/dist/server/legacy-plan.d.ts +2 -1
- package/dist/server/legacy-plan.d.ts.map +1 -1
- package/dist/server/legacy-preservation.d.ts +2 -1
- package/dist/server/legacy-preservation.d.ts.map +1 -1
- package/dist/server/service-operations.d.ts.map +1 -1
- package/dist/server/service-worker.js +78 -33
- package/docs/historical-data-reconciliation.md +38 -0
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1118,12 +1118,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
|
|
|
1118
1118
|
function ymd(date2) {
|
|
1119
1119
|
return date2.toISOString().substring(0, 10);
|
|
1120
1120
|
}
|
|
1121
|
-
function
|
|
1121
|
+
function resolveReportingInterval(interval, now = new Date) {
|
|
1122
1122
|
if (!Number.isFinite(now.getTime()))
|
|
1123
1123
|
throw new RangeError("Invalid reporting clock");
|
|
1124
|
-
const start = new Date(
|
|
1124
|
+
const start = new Date(now);
|
|
1125
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1125
1126
|
const end = new Date(start);
|
|
1126
|
-
|
|
1127
|
+
if (interval.kind === "rolling") {
|
|
1128
|
+
if (!Number.isSafeInteger(interval.count) || interval.count < 1)
|
|
1129
|
+
throw new RangeError("Invalid reporting window");
|
|
1130
|
+
if (interval.unit === "hour") {
|
|
1131
|
+
start.setTime(now.getTime() - interval.count * 3600000);
|
|
1132
|
+
if (!Number.isFinite(start.getTime()))
|
|
1133
|
+
throw new RangeError("Invalid reporting window");
|
|
1134
|
+
return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
|
|
1135
|
+
}
|
|
1136
|
+
if (interval.unit !== "day")
|
|
1137
|
+
throw new RangeError("Invalid reporting unit");
|
|
1138
|
+
start.setUTCDate(start.getUTCDate() - interval.count + 1);
|
|
1139
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
1140
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
|
|
1141
|
+
throw new RangeError("Invalid reporting window");
|
|
1142
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1143
|
+
}
|
|
1144
|
+
if (interval.kind !== "calendar")
|
|
1145
|
+
throw new RangeError("Invalid reporting interval");
|
|
1146
|
+
switch (interval.period) {
|
|
1127
1147
|
case "today":
|
|
1128
1148
|
end.setUTCDate(end.getUTCDate() + 1);
|
|
1129
1149
|
break;
|
|
@@ -1146,11 +1166,15 @@ function calendarPeriodBounds(period, now = new Date) {
|
|
|
1146
1166
|
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
1147
1167
|
break;
|
|
1148
1168
|
case "all":
|
|
1149
|
-
return { time_zone: "UTC",
|
|
1169
|
+
return { time_zone: "UTC", start: null, end_exclusive: null };
|
|
1150
1170
|
default:
|
|
1151
1171
|
throw new RangeError("Invalid reporting period");
|
|
1152
1172
|
}
|
|
1153
|
-
return { time_zone: "UTC",
|
|
1173
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1174
|
+
}
|
|
1175
|
+
function calendarPeriodBounds(period, now = new Date) {
|
|
1176
|
+
const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
|
|
1177
|
+
return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
|
|
1154
1178
|
}
|
|
1155
1179
|
function calendarPeriodWhere(period, column, now = new Date) {
|
|
1156
1180
|
if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
|
|
@@ -2209,20 +2233,22 @@ function queryAccountBreakdownSince(db, since) {
|
|
|
2209
2233
|
ORDER BY api_equivalent_usd DESC
|
|
2210
2234
|
`).all(since, since);
|
|
2211
2235
|
}
|
|
2212
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
2236
|
+
function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
|
|
2237
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
2213
2238
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
2214
|
-
const params = machine ? [
|
|
2239
|
+
const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
|
|
2215
2240
|
return db.prepare(`
|
|
2216
2241
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
2217
2242
|
FROM economy_effective_requests
|
|
2218
|
-
WHERE timestamp >=
|
|
2243
|
+
WHERE timestamp >= ? AND timestamp < ?${machineClause}
|
|
2219
2244
|
GROUP BY DATE(timestamp), agent
|
|
2220
2245
|
ORDER BY date ASC
|
|
2221
2246
|
`).all(...params);
|
|
2222
2247
|
}
|
|
2223
|
-
function queryHourlyBreakdown(db, machine, hours) {
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
2248
|
+
function queryHourlyBreakdown(db, machine, hours, now = new Date) {
|
|
2249
|
+
const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
|
|
2250
|
+
const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
|
|
2251
|
+
const params = [bounds.start, bounds.end_exclusive];
|
|
2226
2252
|
if (machine) {
|
|
2227
2253
|
clauses.push("machine_id = ?");
|
|
2228
2254
|
params.push(machine);
|
|
@@ -3143,15 +3169,16 @@ function translateSqliteDates(sql) {
|
|
|
3143
3169
|
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
3144
3170
|
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
3145
3171
|
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
3172
|
+
s = s.replace(/STRFTIME\('%Y-%m-%dT%H:%M:%fZ',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, col) => `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`);
|
|
3146
3173
|
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
3147
3174
|
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
|
|
3148
3175
|
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
|
|
3149
|
-
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
3176
|
+
return `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', '${pgFmt}')`;
|
|
3150
3177
|
});
|
|
3151
3178
|
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
3152
3179
|
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
3153
3180
|
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
3154
|
-
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${
|
|
3181
|
+
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\)/gi, (_m, col) => `substr(${col.split(".").map((part) => `"${part}"`).join(".")}, 1, 10)`);
|
|
3155
3182
|
return s;
|
|
3156
3183
|
}
|
|
3157
3184
|
function resolveWorkerPath() {
|
|
@@ -3360,6 +3387,30 @@ function resolvePgConnection(dsn) {
|
|
|
3360
3387
|
}
|
|
3361
3388
|
var init_pg_connection = () => {};
|
|
3362
3389
|
|
|
3390
|
+
// src/db/legacy-large-schema.ts
|
|
3391
|
+
var LEGACY_LARGE_SESSION_PG_SQL = `
|
|
3392
|
+
CREATE INDEX idx_legacy_targets_session_operation
|
|
3393
|
+
ON legacy_reconciliation_targets(session_id,operation_key);
|
|
3394
|
+
CREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
|
|
3395
|
+
BEGIN
|
|
3396
|
+
IF EXISTS(
|
|
3397
|
+
SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n
|
|
3398
|
+
JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key
|
|
3399
|
+
) OR EXISTS(
|
|
3400
|
+
SELECT 1 FROM inserted_legacy_targets n
|
|
3401
|
+
JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key
|
|
3402
|
+
) THEN
|
|
3403
|
+
RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
|
|
3404
|
+
END IF;
|
|
3405
|
+
RETURN NULL;
|
|
3406
|
+
END
|
|
3407
|
+
$body$;
|
|
3408
|
+
DROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;
|
|
3409
|
+
CREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets
|
|
3410
|
+
REFERENCING NEW TABLE AS inserted_legacy_targets
|
|
3411
|
+
FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();
|
|
3412
|
+
`;
|
|
3413
|
+
|
|
3363
3414
|
// src/db/pg-migrations.ts
|
|
3364
3415
|
var PG_MIGRATIONS;
|
|
3365
3416
|
var init_pg_migrations = __esm(() => {
|
|
@@ -3595,7 +3646,8 @@ var init_pg_migrations = __esm(() => {
|
|
|
3595
3646
|
...OBSERVATION_PG_MIGRATIONS,
|
|
3596
3647
|
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
|
|
3597
3648
|
CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
|
|
3598
|
-
LEGACY_PG_SQL
|
|
3649
|
+
LEGACY_PG_SQL,
|
|
3650
|
+
LEGACY_LARGE_SESSION_PG_SQL
|
|
3599
3651
|
];
|
|
3600
3652
|
});
|
|
3601
3653
|
|
|
@@ -4004,12 +4056,21 @@ var init_config = __esm(() => {
|
|
|
4004
4056
|
function detectCostSpikes(dailyTotals, opts) {
|
|
4005
4057
|
const windowDays = opts?.windowDays ?? 7;
|
|
4006
4058
|
const multiplier = opts?.multiplier ?? 2;
|
|
4059
|
+
if (!Number.isSafeInteger(windowDays) || windowDays < 1)
|
|
4060
|
+
throw new RangeError("Invalid spike window");
|
|
4061
|
+
if (!Number.isFinite(multiplier) || multiplier <= 0)
|
|
4062
|
+
throw new RangeError("Invalid spike multiplier");
|
|
4007
4063
|
const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
|
|
4008
4064
|
const spikes = [];
|
|
4009
4065
|
for (let i = windowDays;i < sorted.length; i++) {
|
|
4010
4066
|
const window = sorted.slice(i - windowDays, i);
|
|
4011
|
-
const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
|
|
4012
4067
|
const current = sorted[i];
|
|
4068
|
+
const previous = new Date(current.date + "T00:00:00.000Z");
|
|
4069
|
+
previous.setUTCDate(previous.getUTCDate() - 1);
|
|
4070
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: windowDays }, previous);
|
|
4071
|
+
if (window[0].date !== bounds.start || window[window.length - 1].date !== previous.toISOString().slice(0, 10) || new Set(window.map((day) => day.date)).size !== windowDays)
|
|
4072
|
+
continue;
|
|
4073
|
+
const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
|
|
4013
4074
|
if (avg > 0 && current.cost_usd > avg * multiplier) {
|
|
4014
4075
|
spikes.push({
|
|
4015
4076
|
date: current.date,
|
|
@@ -4021,21 +4082,23 @@ function detectCostSpikes(dailyTotals, opts) {
|
|
|
4021
4082
|
}
|
|
4022
4083
|
return spikes;
|
|
4023
4084
|
}
|
|
4024
|
-
function queryRecentSpikes(db, days = 14) {
|
|
4085
|
+
function queryRecentSpikes(db, days = 14, now = new Date) {
|
|
4086
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
4025
4087
|
const rows = db.prepare(`
|
|
4026
4088
|
SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
4027
4089
|
FROM economy_effective_requests
|
|
4028
|
-
WHERE timestamp >=
|
|
4090
|
+
WHERE timestamp >= ? AND timestamp < ?
|
|
4029
4091
|
GROUP BY DATE(timestamp)
|
|
4030
4092
|
ORDER BY date ASC
|
|
4031
|
-
`).all(
|
|
4093
|
+
`).all(bounds.start, bounds.end_exclusive);
|
|
4032
4094
|
return detectCostSpikes(rows);
|
|
4033
4095
|
}
|
|
4034
|
-
function getTodaySpike(db) {
|
|
4035
|
-
const today =
|
|
4036
|
-
const spikes = queryRecentSpikes(db, 14);
|
|
4096
|
+
function getTodaySpike(db, now = new Date) {
|
|
4097
|
+
const today = resolveReportingInterval({ kind: "calendar", period: "today" }, now).start;
|
|
4098
|
+
const spikes = queryRecentSpikes(db, 14, now);
|
|
4037
4099
|
return spikes.find((s) => s.date === today) ?? null;
|
|
4038
4100
|
}
|
|
4101
|
+
var init_spikes = () => {};
|
|
4039
4102
|
|
|
4040
4103
|
// src/lib/webhooks.ts
|
|
4041
4104
|
var exports_webhooks = {};
|
|
@@ -4102,6 +4165,7 @@ async function fireWebhook(url, payload) {
|
|
|
4102
4165
|
var init_webhooks = __esm(() => {
|
|
4103
4166
|
init_config();
|
|
4104
4167
|
init_database();
|
|
4168
|
+
init_spikes();
|
|
4105
4169
|
});
|
|
4106
4170
|
|
|
4107
4171
|
// src/db/pg-migrate.ts
|
|
@@ -4508,6 +4572,42 @@ var init_legacy_inventory = __esm(() => {
|
|
|
4508
4572
|
allCandidates = LEGACY_FAMILIES.map((family) => `(${predicates[family]})`).join(" OR ");
|
|
4509
4573
|
});
|
|
4510
4574
|
|
|
4575
|
+
// src/server/legacy-limits.ts
|
|
4576
|
+
function legacyMode(kind, schema) {
|
|
4577
|
+
if (schema === LEGACY_STANDARD[kind])
|
|
4578
|
+
return LEGACY_STANDARD;
|
|
4579
|
+
if (schema === LEGACY_SINGLE_SESSION[kind])
|
|
4580
|
+
return LEGACY_SINGLE_SESSION;
|
|
4581
|
+
throw Error(`Unsupported legacy ${kind} schema`);
|
|
4582
|
+
}
|
|
4583
|
+
var LEGACY_STANDARD, LEGACY_SINGLE_SESSION;
|
|
4584
|
+
var init_legacy_limits = __esm(() => {
|
|
4585
|
+
LEGACY_STANDARD = {
|
|
4586
|
+
selection: "economy.legacy-selection.v1",
|
|
4587
|
+
plan: "economy.legacy-plan.v1",
|
|
4588
|
+
capsule: "economy.legacy-preservation.v1",
|
|
4589
|
+
receipt: "economy.legacy-operation-receipt.v1",
|
|
4590
|
+
items: 100,
|
|
4591
|
+
sessions: 100,
|
|
4592
|
+
members: 1e4,
|
|
4593
|
+
inputBytes: 256 * 1024,
|
|
4594
|
+
originalBytes: 16 * 1024 * 1024,
|
|
4595
|
+
capsuleBytes: 32 * 1024 * 1024
|
|
4596
|
+
};
|
|
4597
|
+
LEGACY_SINGLE_SESSION = {
|
|
4598
|
+
selection: "economy.legacy-session-selection.v1",
|
|
4599
|
+
plan: "economy.legacy-session-plan.v1",
|
|
4600
|
+
capsule: "economy.legacy-session-preservation.v1",
|
|
4601
|
+
receipt: "economy.legacy-session-operation-receipt.v1",
|
|
4602
|
+
items: 50000,
|
|
4603
|
+
sessions: 1,
|
|
4604
|
+
members: 50000,
|
|
4605
|
+
inputBytes: 32 * 1024 * 1024,
|
|
4606
|
+
originalBytes: 64 * 1024 * 1024,
|
|
4607
|
+
capsuleBytes: 128 * 1024 * 1024
|
|
4608
|
+
};
|
|
4609
|
+
});
|
|
4610
|
+
|
|
4511
4611
|
// src/server/legacy-plan.ts
|
|
4512
4612
|
var exports_legacy_plan = {};
|
|
4513
4613
|
__export(exports_legacy_plan, {
|
|
@@ -4543,10 +4643,11 @@ function digest2(value) {
|
|
|
4543
4643
|
}
|
|
4544
4644
|
function parseLegacySelection(value) {
|
|
4545
4645
|
const input = object(value, ["schema", "items"], "selection");
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4646
|
+
const mode = legacyMode("selection", input.schema);
|
|
4647
|
+
if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > mode.items)
|
|
4648
|
+
throw Error(`items must contain 1-${mode.items} explicit request selections`);
|
|
4649
|
+
if (Buffer.byteLength(JSON.stringify(input)) > mode.inputBytes)
|
|
4650
|
+
throw Error(`Selection exceeds its ${mode.inputBytes}-byte artifact bound`);
|
|
4550
4651
|
const ids = new Set;
|
|
4551
4652
|
const items = input.items.map((item) => {
|
|
4552
4653
|
const row = object(item, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref"], "item");
|
|
@@ -4569,7 +4670,7 @@ function parseLegacySelection(value) {
|
|
|
4569
4670
|
evidence_ref: text(row.evidence_ref, 512, "evidence_ref")
|
|
4570
4671
|
};
|
|
4571
4672
|
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
4572
|
-
return { schema:
|
|
4673
|
+
return { schema: mode.selection, items };
|
|
4573
4674
|
}
|
|
4574
4675
|
function readLegacySelectionFile(path) {
|
|
4575
4676
|
if (!isAbsolute(path) || path.includes("\x00"))
|
|
@@ -4582,9 +4683,10 @@ function readLegacySelectionFile(path) {
|
|
|
4582
4683
|
}
|
|
4583
4684
|
try {
|
|
4584
4685
|
const before = fstatSync(fd);
|
|
4686
|
+
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";
|
|
4585
4687
|
if (!before.isFile() || before.size < 1 || before.size > INPUT_MAX)
|
|
4586
|
-
throw Error(
|
|
4587
|
-
const bytes = Buffer.alloc(INPUT_MAX + 1);
|
|
4688
|
+
throw Error(sizeError);
|
|
4689
|
+
const bytes = Buffer.alloc(Math.min(INPUT_MAX + 1, before.size + 1));
|
|
4588
4690
|
let offset = 0, n;
|
|
4589
4691
|
while (offset < bytes.length && (n = readSync(fd, bytes, offset, bytes.length - offset, null)) > 0)
|
|
4590
4692
|
offset += n;
|
|
@@ -4595,9 +4697,12 @@ function readLegacySelectionFile(path) {
|
|
|
4595
4697
|
try {
|
|
4596
4698
|
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset)));
|
|
4597
4699
|
} catch {
|
|
4598
|
-
throw Error("--input must contain valid UTF-8 JSON");
|
|
4700
|
+
throw Error(before.size > LEGACY_STANDARD.inputBytes ? sizeError : "--input must contain valid UTF-8 JSON");
|
|
4599
4701
|
}
|
|
4600
|
-
|
|
4702
|
+
const selection = parseLegacySelection(value);
|
|
4703
|
+
if (offset > legacyMode("selection", selection.schema).inputBytes)
|
|
4704
|
+
throw Error(sizeError);
|
|
4705
|
+
return selection;
|
|
4601
4706
|
} finally {
|
|
4602
4707
|
closeSync(fd);
|
|
4603
4708
|
}
|
|
@@ -4612,6 +4717,7 @@ async function prepareLegacyPlan(client, input) {
|
|
|
4612
4717
|
}
|
|
4613
4718
|
async function withLegacyPlanSnapshot(client, input, read) {
|
|
4614
4719
|
const selection = parseLegacySelection(input);
|
|
4720
|
+
const mode = legacyMode("selection", selection.schema);
|
|
4615
4721
|
let began = false;
|
|
4616
4722
|
try {
|
|
4617
4723
|
await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
|
|
@@ -4646,14 +4752,16 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4646
4752
|
return { ...item, session_id: identifier2(row.session_id, "session_id") };
|
|
4647
4753
|
});
|
|
4648
4754
|
const sessionIds = [...new Set(items.map((x) => x.session_id))].sort();
|
|
4755
|
+
if (mode.sessions === 1 && sessionIds.length !== 1)
|
|
4756
|
+
throw Error("Single-session reconciliation requires exactly one session");
|
|
4649
4757
|
const sessions = (await client.query(`SELECT id, encode(sha256(convert_to(row_to_json(sessions)::text,'UTF8')),'hex') AS original_sha256
|
|
4650
4758
|
FROM sessions WHERE id=ANY($1::text[]) ORDER BY id`, [sessionIds])).rows;
|
|
4651
4759
|
if (sessions.length !== sessionIds.length)
|
|
4652
4760
|
throw Error("A selected request has no session; resolve that inconsistency before reconciliation");
|
|
4653
4761
|
const members = (await client.query(`SELECT id, session_id, encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256
|
|
4654
|
-
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${
|
|
4655
|
-
if (members.length >
|
|
4656
|
-
throw Error(
|
|
4762
|
+
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${mode.members + 1}`, [sessionIds])).rows;
|
|
4763
|
+
if (members.length > mode.members)
|
|
4764
|
+
throw Error(`Selected sessions exceed ${mode.members} requests; complete-session reconciliation cannot truncate membership`);
|
|
4657
4765
|
const populations = sessions.map((session) => {
|
|
4658
4766
|
const rows = members.filter((row) => row.session_id === session.id);
|
|
4659
4767
|
if (!rows.length)
|
|
@@ -4663,7 +4771,7 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4663
4771
|
return { id: session.id, original_sha256, request_count: rows.length, members_sha256 };
|
|
4664
4772
|
});
|
|
4665
4773
|
const body = {
|
|
4666
|
-
schema:
|
|
4774
|
+
schema: mode.plan,
|
|
4667
4775
|
mode: "read-only",
|
|
4668
4776
|
original_encoding: "postgres-row-json-text-v1",
|
|
4669
4777
|
member_encoding: "sha256-json-id-digest-pairs-utf8-c-order-v1",
|
|
@@ -4707,10 +4815,11 @@ async function runLegacyPlan(selection) {
|
|
|
4707
4815
|
await pool.end();
|
|
4708
4816
|
}
|
|
4709
4817
|
}
|
|
4710
|
-
var INPUT_MAX,
|
|
4818
|
+
var INPUT_MAX, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
|
|
4711
4819
|
var init_legacy_plan = __esm(() => {
|
|
4712
4820
|
init_legacy_inventory();
|
|
4713
|
-
|
|
4821
|
+
init_legacy_limits();
|
|
4822
|
+
INPUT_MAX = LEGACY_SINGLE_SESSION.inputBytes;
|
|
4714
4823
|
});
|
|
4715
4824
|
|
|
4716
4825
|
// src/server/legacy-preservation.ts
|
|
@@ -4731,15 +4840,16 @@ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSyn
|
|
|
4731
4840
|
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
4732
4841
|
function parseLegacyPlan(value) {
|
|
4733
4842
|
const p = record(value);
|
|
4843
|
+
const mode = legacyMode("plan", p.schema);
|
|
4734
4844
|
fields(p, ["schema", "mode", "original_encoding", "member_encoding", "snapshot", "items", "sessions", "mutations_performed", "limitations", "plan_sha256"]);
|
|
4735
|
-
if (p.
|
|
4845
|
+
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)
|
|
4736
4846
|
fail("Unsupported legacy plan contract");
|
|
4737
4847
|
const { plan_sha256, ...body } = p;
|
|
4738
4848
|
if (hash(plan_sha256) !== sha2(JSON.stringify(body)))
|
|
4739
4849
|
fail("Legacy plan digest mismatch");
|
|
4740
4850
|
if (!Array.isArray(p.items))
|
|
4741
4851
|
fail("Legacy plan items must be an array");
|
|
4742
|
-
const selection = parseLegacySelection({ schema:
|
|
4852
|
+
const selection = parseLegacySelection({ schema: mode.selection, items: p.items.map((item) => {
|
|
4743
4853
|
const row = record(item);
|
|
4744
4854
|
fields(row, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref", "session_id"]);
|
|
4745
4855
|
identifier3(row.session_id);
|
|
@@ -4767,7 +4877,7 @@ function parseLegacyPlan(value) {
|
|
|
4767
4877
|
if (typeof t.relation_oid !== "string" || !/^[0-9]+$/.test(t.relation_oid))
|
|
4768
4878
|
fail("Invalid relation locator");
|
|
4769
4879
|
}
|
|
4770
|
-
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length >
|
|
4880
|
+
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > mode.sessions)
|
|
4771
4881
|
fail("Invalid legacy session population");
|
|
4772
4882
|
const ids = new Set;
|
|
4773
4883
|
let members = 0;
|
|
@@ -4784,7 +4894,7 @@ function parseLegacyPlan(value) {
|
|
|
4784
4894
|
fail("Invalid legacy member count");
|
|
4785
4895
|
members += s.request_count;
|
|
4786
4896
|
}
|
|
4787
|
-
if (members >
|
|
4897
|
+
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)
|
|
4788
4898
|
fail("Incomplete or excessive legacy session membership");
|
|
4789
4899
|
if (!Array.isArray(p.limitations) || p.limitations.length > 20 || p.limitations.some((x) => typeof x !== "string" || x.length > 2048))
|
|
4790
4900
|
fail("Invalid legacy plan limitations");
|
|
@@ -4801,16 +4911,28 @@ function assertLegacyPlanCurrent(expected, current) {
|
|
|
4801
4911
|
}
|
|
4802
4912
|
function parseLegacyCapsule(value) {
|
|
4803
4913
|
const c = record(value);
|
|
4914
|
+
const mode = legacyMode("capsule", c.schema);
|
|
4804
4915
|
fields(c, ["schema", "plan", "captured_at", "requests", "sessions", "capsule_sha256"]);
|
|
4805
|
-
if (
|
|
4916
|
+
if (typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
|
|
4806
4917
|
fail("Invalid preservation capsule");
|
|
4807
4918
|
const { capsule_sha256, ...body } = c;
|
|
4808
4919
|
const bytes = JSON.stringify(body);
|
|
4809
|
-
if (Buffer.byteLength(bytes) >
|
|
4920
|
+
if (Buffer.byteLength(bytes) > mode.capsuleBytes || hash(capsule_sha256) !== sha2(bytes))
|
|
4810
4921
|
fail("Preservation capsule bound or digest mismatch");
|
|
4811
4922
|
const plan = parseLegacyPlan(c.plan);
|
|
4812
|
-
if (
|
|
4923
|
+
if (plan.schema !== mode.plan)
|
|
4924
|
+
fail("Preservation capsule and plan use different artifact modes");
|
|
4925
|
+
if (!Array.isArray(c.requests) || c.requests.length > mode.members || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
|
|
4813
4926
|
fail("Invalid original populations");
|
|
4927
|
+
let originalBytes = 0;
|
|
4928
|
+
const boundOriginal = (original) => {
|
|
4929
|
+
if (typeof original !== "string")
|
|
4930
|
+
fail("Invalid original row text");
|
|
4931
|
+
originalBytes += Buffer.byteLength(original);
|
|
4932
|
+
if (originalBytes > mode.originalBytes)
|
|
4933
|
+
fail("Preserved original row text exceeds its artifact mode bound");
|
|
4934
|
+
return original;
|
|
4935
|
+
};
|
|
4814
4936
|
const requests = new Map;
|
|
4815
4937
|
for (const value2 of c.requests) {
|
|
4816
4938
|
const row = record(value2);
|
|
@@ -4818,7 +4940,7 @@ function parseLegacyCapsule(value) {
|
|
|
4818
4940
|
const id = identifier3(row.id);
|
|
4819
4941
|
if (requests.has(id) || typeof row.original !== "string")
|
|
4820
4942
|
fail("Invalid or duplicate request original");
|
|
4821
|
-
const original = record(JSON.parse(row.original));
|
|
4943
|
+
const original = record(JSON.parse(boundOriginal(row.original)));
|
|
4822
4944
|
if (original.id !== id)
|
|
4823
4945
|
fail("Request original identity mismatch");
|
|
4824
4946
|
requests.set(id, { session: identifier3(original.session_id), digest: sha2(row.original) });
|
|
@@ -4832,7 +4954,7 @@ function parseLegacyCapsule(value) {
|
|
|
4832
4954
|
fail("Invalid or duplicate session original");
|
|
4833
4955
|
sessions.add(id);
|
|
4834
4956
|
const expected = plan.sessions.find((s) => s.id === id) ?? fail("Session original does not match the plan");
|
|
4835
|
-
if (record(JSON.parse(row.original)).id !== id || sha2(row.original) !== expected.original_sha256)
|
|
4957
|
+
if (record(JSON.parse(boundOriginal(row.original))).id !== id || sha2(row.original) !== expected.original_sha256)
|
|
4836
4958
|
fail("Session original does not match the plan");
|
|
4837
4959
|
const members = [...requests].filter(([, r]) => r.session === id).sort(([a], [b]) => cOrder(a, b)).map(([rid, r]) => [rid, r.digest]);
|
|
4838
4960
|
if (members.length !== expected.request_count || sha2(JSON.stringify(members)) !== expected.members_sha256)
|
|
@@ -4849,18 +4971,19 @@ function parseLegacyCapsule(value) {
|
|
|
4849
4971
|
}
|
|
4850
4972
|
async function preserveLegacyOriginals(client, input) {
|
|
4851
4973
|
const expected = parseLegacyPlan(input);
|
|
4852
|
-
const
|
|
4974
|
+
const mode = legacyMode("plan", expected.schema);
|
|
4975
|
+
const selection = { schema: mode.selection, items: expected.items.map(({ session_id, ...item }) => item) };
|
|
4853
4976
|
return withLegacyPlanSnapshot(client, selection, async (current) => {
|
|
4854
4977
|
assertLegacyPlanCurrent(expected, current);
|
|
4855
4978
|
const ids = current.sessions.map((x) => x.id);
|
|
4856
4979
|
const sizes = (await client.query(`SELECT
|
|
4857
4980
|
(SELECT COALESCE(SUM(octet_length(row_to_json(requests)::text)),0) FROM requests WHERE session_id=ANY($1::text[]))::text AS request_bytes,
|
|
4858
4981
|
(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];
|
|
4859
|
-
if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(
|
|
4860
|
-
fail(
|
|
4861
|
-
const requests = (await client.query(
|
|
4862
|
-
const sessions = (await client.query(
|
|
4863
|
-
const body = { schema:
|
|
4982
|
+
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))
|
|
4983
|
+
fail(`Preservation exceeds ${mode.originalBytes / 1024 / 1024} MiB of original row text; complete sessions cannot be truncated`);
|
|
4984
|
+
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;
|
|
4985
|
+
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;
|
|
4986
|
+
const body = { schema: mode.capsule, plan: expected, captured_at: current.snapshot.at, requests, sessions };
|
|
4864
4987
|
return parseLegacyCapsule({ ...body, capsule_sha256: sha2(JSON.stringify(body)) });
|
|
4865
4988
|
});
|
|
4866
4989
|
}
|
|
@@ -4891,11 +5014,16 @@ function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
|
4891
5014
|
}
|
|
4892
5015
|
function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
4893
5016
|
const bytes = readLegacyBytes(path, maxBytes);
|
|
5017
|
+
let value;
|
|
4894
5018
|
try {
|
|
4895
|
-
|
|
5019
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
4896
5020
|
} catch {
|
|
4897
5021
|
fail("Artifact must contain valid UTF-8 JSON");
|
|
4898
5022
|
}
|
|
5023
|
+
const limit = value?.schema === LEGACY_SINGLE_SESSION.plan || value?.schema === LEGACY_SINGLE_SESSION.capsule ? LEGACY_SINGLE_SESSION.capsuleBytes : LEGACY_STANDARD.capsuleBytes;
|
|
5024
|
+
if (bytes.length > limit)
|
|
5025
|
+
fail("Artifact exceeds its schema file bound");
|
|
5026
|
+
return value;
|
|
4899
5027
|
}
|
|
4900
5028
|
function writeLegacyCapsule(path, input) {
|
|
4901
5029
|
const capsule = parseLegacyCapsule(input);
|
|
@@ -4906,7 +5034,7 @@ function writeLegacyCapsule(path, input) {
|
|
|
4906
5034
|
fail("Output parent must be an owner-only directory");
|
|
4907
5035
|
const bytes = Buffer.from(JSON.stringify(capsule) + `
|
|
4908
5036
|
`);
|
|
4909
|
-
if (bytes.length >
|
|
5037
|
+
if (bytes.length > legacyMode("capsule", capsule.schema).capsuleBytes)
|
|
4910
5038
|
fail("Preservation capsule exceeds the file bound");
|
|
4911
5039
|
const fd = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
4912
5040
|
try {
|
|
@@ -5037,7 +5165,7 @@ async function runLegacyPreservation(command, options) {
|
|
|
5037
5165
|
await pool.end();
|
|
5038
5166
|
}
|
|
5039
5167
|
}
|
|
5040
|
-
var MAX_BYTES,
|
|
5168
|
+
var MAX_BYTES, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
|
|
5041
5169
|
throw Error(message);
|
|
5042
5170
|
}, 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) => {
|
|
5043
5171
|
if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
|
|
@@ -5045,7 +5173,8 @@ var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash7("sha256").update(s).
|
|
|
5045
5173
|
}, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote3 = (s) => '"' + s.replaceAll('"', '""') + '"';
|
|
5046
5174
|
var init_legacy_preservation = __esm(() => {
|
|
5047
5175
|
init_legacy_plan();
|
|
5048
|
-
|
|
5176
|
+
init_legacy_limits();
|
|
5177
|
+
MAX_BYTES = LEGACY_SINGLE_SESSION.capsuleBytes;
|
|
5049
5178
|
});
|
|
5050
5179
|
|
|
5051
5180
|
// src/server/legacy-operations.ts
|
|
@@ -5109,6 +5238,7 @@ function matchIntent(intent, capsule, target) {
|
|
|
5109
5238
|
}
|
|
5110
5239
|
async function verifyOriginals(client, capsule) {
|
|
5111
5240
|
const p = capsule.plan;
|
|
5241
|
+
const mode = legacyMode("capsule", capsule.schema);
|
|
5112
5242
|
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];
|
|
5113
5243
|
if (context2?.database !== p.snapshot.database || context2?.database_oid !== p.snapshot.database_oid)
|
|
5114
5244
|
fail2("Legacy operation targets a different database");
|
|
@@ -5122,7 +5252,7 @@ async function verifyOriginals(client, capsule) {
|
|
|
5122
5252
|
fail2("Legacy session originals changed; preserve later work and prepare a new review");
|
|
5123
5253
|
const rows = (await client.query(`SELECT id,session_id,encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256,
|
|
5124
5254
|
CASE ${LEGACY_FAMILIES.map((f) => "WHEN " + LEGACY_PREDICATES[f] + " THEN '" + f + "'").join(" ")} END AS family
|
|
5125
|
-
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT
|
|
5255
|
+
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
|
|
5126
5256
|
if (rows.length !== capsule.requests.length)
|
|
5127
5257
|
fail2("Legacy request membership changed; preserve later work and prepare a new review");
|
|
5128
5258
|
const originals = new Map(capsule.requests.map((r) => [r.id, sha3(r.original)]));
|
|
@@ -5176,13 +5306,14 @@ async function observed(client, run) {
|
|
|
5176
5306
|
}
|
|
5177
5307
|
function validateReceipt(value) {
|
|
5178
5308
|
const v = value;
|
|
5179
|
-
if (!v ||
|
|
5309
|
+
if (!v || !["apply", "rollback"].includes(v.action))
|
|
5180
5310
|
fail2("Invalid stored legacy receipt");
|
|
5311
|
+
const mode = legacyMode("receipt", v.schema);
|
|
5181
5312
|
key(v.operation_key);
|
|
5182
5313
|
key(v.apply_key);
|
|
5183
5314
|
digest3(v.plan_sha256);
|
|
5184
5315
|
digest3(v.capsule_sha256);
|
|
5185
|
-
if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <=
|
|
5316
|
+
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)))
|
|
5186
5317
|
fail2("Invalid stored legacy receipt fields");
|
|
5187
5318
|
const names = ["schema", "action", "operation_key", "apply_key", "plan_sha256", "capsule_sha256", "target_requests", "preserved_requests", "sessions", "source_rows_changed", "created_at"];
|
|
5188
5319
|
if (Object.keys(v).sort().join(",") !== names.sort().join(","))
|
|
@@ -5207,7 +5338,7 @@ async function reply(client, row, expectedHash) {
|
|
|
5207
5338
|
}
|
|
5208
5339
|
function receipt(action, intent, capsule) {
|
|
5209
5340
|
return {
|
|
5210
|
-
schema: "
|
|
5341
|
+
schema: legacyMode("capsule", capsule.schema).receipt,
|
|
5211
5342
|
action,
|
|
5212
5343
|
operation_key: intent.operation_key,
|
|
5213
5344
|
apply_key: action === "apply" ? intent.operation_key : intent.apply_key,
|
|
@@ -5247,8 +5378,9 @@ async function applyLegacyReconciliation(client, input, preserved, target) {
|
|
|
5247
5378
|
const saved = (await client.query("SELECT capsule_json,capsule_sha256 FROM legacy_reconciliations WHERE operation_key=$1", [intent.operation_key])).rows[0];
|
|
5248
5379
|
if (saved?.capsule_json !== json3 || saved?.capsule_sha256 !== capsule.capsule_sha256 || parseLegacyCapsule(JSON.parse(saved.capsule_json)).capsule_sha256 !== capsule.capsule_sha256)
|
|
5249
5380
|
fail2("Durable preservation readback failed");
|
|
5250
|
-
|
|
5251
|
-
|
|
5381
|
+
await client.query(`INSERT INTO legacy_reconciliation_targets(operation_key,request_id,session_id,disposition,original_sha256)
|
|
5382
|
+
SELECT $1, x.id, x.session_id, x.disposition, x.original_sha256
|
|
5383
|
+
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 })))]);
|
|
5252
5384
|
const stored = await lookup(client, "apply", intent.operation_key) ?? fail2("Legacy receipt readback failed");
|
|
5253
5385
|
if (stored.response_json !== responseJson)
|
|
5254
5386
|
fail2("Legacy receipt readback failed");
|
|
@@ -5360,6 +5492,7 @@ var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) =>
|
|
|
5360
5492
|
WHERE c.oid IN ('requests'::regclass,'sessions'::regclass) AND a.attnum>0 AND NOT a.attisdropped
|
|
5361
5493
|
GROUP BY c.oid,c.relname,n.nspname ORDER BY c.relname`, same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
5362
5494
|
var init_legacy_operations = __esm(() => {
|
|
5495
|
+
init_legacy_limits();
|
|
5363
5496
|
init_pg_connection();
|
|
5364
5497
|
init_legacy_inventory();
|
|
5365
5498
|
init_legacy_preservation();
|
|
@@ -5396,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5396
5529
|
// package.json
|
|
5397
5530
|
var package_default = {
|
|
5398
5531
|
name: "@hasna/economy",
|
|
5399
|
-
version: "0.5.
|
|
5532
|
+
version: "0.5.3",
|
|
5400
5533
|
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",
|
|
5401
5534
|
repository: {
|
|
5402
5535
|
type: "git",
|
|
@@ -11535,7 +11668,8 @@ function buildBrief(db, opts = {}) {
|
|
|
11535
11668
|
}
|
|
11536
11669
|
|
|
11537
11670
|
// src/lib/analytics.ts
|
|
11538
|
-
function queryProjectDetail(db, nameOrPath) {
|
|
11671
|
+
function queryProjectDetail(db, nameOrPath, now = new Date) {
|
|
11672
|
+
const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
|
|
11539
11673
|
const like = `%${nameOrPath}%`;
|
|
11540
11674
|
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
11541
11675
|
FROM economy_effective_sessions
|
|
@@ -11549,8 +11683,8 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
11549
11683
|
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
11550
11684
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
11551
11685
|
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
11552
|
-
AND r.timestamp >=
|
|
11553
|
-
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
11686
|
+
AND r.timestamp >= ? AND r.timestamp < ?
|
|
11687
|
+
GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
|
|
11554
11688
|
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
11555
11689
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
11556
11690
|
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
@@ -11589,18 +11723,17 @@ function queryRangeStats(db, from, to) {
|
|
|
11589
11723
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
11590
11724
|
}
|
|
11591
11725
|
function queryForecast(db, now = new Date) {
|
|
11592
|
-
const
|
|
11593
|
-
const
|
|
11594
|
-
const
|
|
11595
|
-
const
|
|
11726
|
+
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
|
|
11727
|
+
const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
|
|
11728
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
|
|
11729
|
+
const dayOfMonth = now.getUTCDate();
|
|
11730
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
|
|
11596
11731
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
11597
11732
|
const projected = dailyAvg * daysInMonth;
|
|
11598
|
-
const
|
|
11599
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
11600
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
11733
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
|
|
11601
11734
|
const last7DailyAvg = last7.cost / 7;
|
|
11602
11735
|
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
11603
|
-
FROM economy_effective_requests WHERE
|
|
11736
|
+
FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
|
|
11604
11737
|
return {
|
|
11605
11738
|
day_of_month: dayOfMonth,
|
|
11606
11739
|
days_in_month: daysInMonth,
|
|
@@ -11777,7 +11910,13 @@ function createDataHandler(db, actorId) {
|
|
|
11777
11910
|
if (path === "/api/daily" && method === "GET") {
|
|
11778
11911
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
11779
11912
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
11780
|
-
|
|
11913
|
+
const now = new Date;
|
|
11914
|
+
try {
|
|
11915
|
+
resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
11916
|
+
} catch {
|
|
11917
|
+
return err("days must be a positive integer within the supported UTC date range");
|
|
11918
|
+
}
|
|
11919
|
+
return ok(queryDailyBreakdown(db, days, machine, now));
|
|
11781
11920
|
}
|
|
11782
11921
|
if (path === "/api/hourly" && method === "GET") {
|
|
11783
11922
|
const machine = url.searchParams.get("machine") ?? undefined;
|