@hasna/economy 0.5.1 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/chunks/{index-r1jv3kpy.js → index-9d2aky06.js} +1 -1
- package/dist/chunks/{index-d480p2wh.js → index-dj3tctqh.js} +1 -1
- package/dist/chunks/{index-49f12css.js → index-kzwqw3ve.js} +1 -1
- package/dist/chunks/{index-xsah93s2.js → index-npsjjv96.js} +1 -1
- package/dist/chunks/{serve-2knem98x.js → serve-p2yarhm8.js} +28 -3
- package/dist/chunks/{tui-khmnkat2.js → tui-x64hjfrh.js} +3 -3
- package/dist/chunks/{watch-4y064yys.js → watch-bxstjpbx.js} +3 -3
- package/dist/cli/index.js +10 -10
- 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/index.js +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/otel/index.js +1 -1
- package/dist/server/index.js +136 -42
- 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-worker.js +1 -1
- package/docs/historical-data-reconciliation.md +38 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Support an explicit, bounded single-session reconciliation mode for large historical sessions. Preserve and verify every original before atomically recording dispositions, retain ordinary artifact limits, and optimize PostgreSQL overlap checks without weakening conflict protection.
|
|
8
|
+
|
|
3
9
|
## 0.5.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
// package.json
|
|
20
20
|
var package_default = {
|
|
21
21
|
name: "@hasna/economy",
|
|
22
|
-
version: "0.5.
|
|
22
|
+
version: "0.5.2",
|
|
23
23
|
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",
|
|
24
24
|
repository: {
|
|
25
25
|
type: "git",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./index-w3bvv0ns.js";
|
|
6
6
|
import {
|
|
7
7
|
syncAll
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-npsjjv96.js";
|
|
9
9
|
import {
|
|
10
10
|
buildBrief,
|
|
11
11
|
packageMetadata,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
queryProjectDetail,
|
|
17
17
|
queryRangeStats,
|
|
18
18
|
querySavingsSummary
|
|
19
|
-
} from "./index-
|
|
19
|
+
} from "./index-dj3tctqh.js";
|
|
20
20
|
import {
|
|
21
21
|
AGENTS,
|
|
22
22
|
INGEST_SCHEMA_SQL,
|
|
@@ -348,6 +348,30 @@ function resolvePgConnection(dsn) {
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// src/db/legacy-large-schema.ts
|
|
352
|
+
var LEGACY_LARGE_SESSION_PG_SQL = `
|
|
353
|
+
CREATE INDEX idx_legacy_targets_session_operation
|
|
354
|
+
ON legacy_reconciliation_targets(session_id,operation_key);
|
|
355
|
+
CREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
|
|
356
|
+
BEGIN
|
|
357
|
+
IF EXISTS(
|
|
358
|
+
SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n
|
|
359
|
+
JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key
|
|
360
|
+
) OR EXISTS(
|
|
361
|
+
SELECT 1 FROM inserted_legacy_targets n
|
|
362
|
+
JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key
|
|
363
|
+
) THEN
|
|
364
|
+
RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
|
|
365
|
+
END IF;
|
|
366
|
+
RETURN NULL;
|
|
367
|
+
END
|
|
368
|
+
$body$;
|
|
369
|
+
DROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;
|
|
370
|
+
CREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets
|
|
371
|
+
REFERENCING NEW TABLE AS inserted_legacy_targets
|
|
372
|
+
FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();
|
|
373
|
+
`;
|
|
374
|
+
|
|
351
375
|
// src/db/pg-migrations.ts
|
|
352
376
|
var PG_MIGRATIONS = [
|
|
353
377
|
`CREATE TABLE IF NOT EXISTS cost_centers (
|
|
@@ -579,7 +603,8 @@ var PG_MIGRATIONS = [
|
|
|
579
603
|
...OBSERVATION_PG_MIGRATIONS,
|
|
580
604
|
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
|
|
581
605
|
CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
|
|
582
|
-
LEGACY_PG_SQL
|
|
606
|
+
LEGACY_PG_SQL,
|
|
607
|
+
LEGACY_LARGE_SESSION_PG_SQL
|
|
583
608
|
];
|
|
584
609
|
|
|
585
610
|
// src/db/cloud.ts
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
printStatusLine,
|
|
7
7
|
printWaybarJson,
|
|
8
8
|
runTui
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-kzwqw3ve.js";
|
|
10
10
|
import"./index-w3bvv0ns.js";
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
import"./index-9d2aky06.js";
|
|
12
|
+
import"./index-dj3tctqh.js";
|
|
13
13
|
import"./index-ez9thsr7.js";
|
|
14
14
|
import"./index-p35cyhc4.js";
|
|
15
15
|
export {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
agentPaths,
|
|
4
4
|
syncAll
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-npsjjv96.js";
|
|
6
6
|
import {
|
|
7
7
|
getStore,
|
|
8
8
|
isCloudStore
|
|
9
|
-
} from "./index-
|
|
10
|
-
import"./index-
|
|
9
|
+
} from "./index-9d2aky06.js";
|
|
10
|
+
import"./index-dj3tctqh.js";
|
|
11
11
|
import {
|
|
12
12
|
formatSummaryValue
|
|
13
13
|
} from "./index-ez9thsr7.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -8,22 +8,22 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
buildStatusLine,
|
|
10
10
|
gatherStatusData
|
|
11
|
-
} from "../chunks/index-
|
|
11
|
+
} from "../chunks/index-kzwqw3ve.js";
|
|
12
12
|
import"../chunks/index-w3bvv0ns.js";
|
|
13
13
|
import {
|
|
14
14
|
agentPaths,
|
|
15
15
|
syncAll
|
|
16
|
-
} from "../chunks/index-
|
|
16
|
+
} from "../chunks/index-npsjjv96.js";
|
|
17
17
|
import {
|
|
18
18
|
getStore,
|
|
19
19
|
isCloudStore
|
|
20
|
-
} from "../chunks/index-
|
|
20
|
+
} from "../chunks/index-9d2aky06.js";
|
|
21
21
|
import {
|
|
22
22
|
billingDriftCheck,
|
|
23
23
|
economyCloudStorage,
|
|
24
24
|
economyTransportReport,
|
|
25
25
|
packageMetadata
|
|
26
|
-
} from "../chunks/index-
|
|
26
|
+
} from "../chunks/index-dj3tctqh.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTS,
|
|
29
29
|
MAX_INGEST_BYTES,
|
|
@@ -1357,23 +1357,23 @@ function registerExtendedCommands(program) {
|
|
|
1357
1357
|
console.log();
|
|
1358
1358
|
});
|
|
1359
1359
|
program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
|
|
1360
|
-
const { runTui } = await import("../chunks/tui-
|
|
1360
|
+
const { runTui } = await import("../chunks/tui-x64hjfrh.js");
|
|
1361
1361
|
await runTui({
|
|
1362
1362
|
watch: Boolean(opts.watch),
|
|
1363
1363
|
interval: Number(opts.interval ?? 30)
|
|
1364
1364
|
});
|
|
1365
1365
|
});
|
|
1366
1366
|
program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
|
|
1367
|
-
const { printWaybarJson } = await import("../chunks/tui-
|
|
1367
|
+
const { printWaybarJson } = await import("../chunks/tui-x64hjfrh.js");
|
|
1368
1368
|
await printWaybarJson();
|
|
1369
1369
|
});
|
|
1370
1370
|
const barCmd = program.command("bar").description("Status bar helpers (Linux)");
|
|
1371
1371
|
barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
|
|
1372
|
-
const { runTui } = await import("../chunks/tui-
|
|
1372
|
+
const { runTui } = await import("../chunks/tui-x64hjfrh.js");
|
|
1373
1373
|
await runTui({ watch: Boolean(opts.watch) });
|
|
1374
1374
|
});
|
|
1375
1375
|
barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
|
|
1376
|
-
const { printWaybarJson } = await import("../chunks/tui-
|
|
1376
|
+
const { printWaybarJson } = await import("../chunks/tui-x64hjfrh.js");
|
|
1377
1377
|
await printWaybarJson();
|
|
1378
1378
|
});
|
|
1379
1379
|
program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
|
|
@@ -3198,7 +3198,7 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
3198
3198
|
console.log();
|
|
3199
3199
|
});
|
|
3200
3200
|
program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
|
|
3201
|
-
const { watchCosts } = await import("../chunks/watch-
|
|
3201
|
+
const { watchCosts } = await import("../chunks/watch-bxstjpbx.js");
|
|
3202
3202
|
await watchCosts({
|
|
3203
3203
|
interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
|
|
3204
3204
|
daemon: Boolean(opts.daemon),
|
|
@@ -3434,7 +3434,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
|
|
|
3434
3434
|
});
|
|
3435
3435
|
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
3436
3436
|
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
3437
|
-
const { startServer } = await import("../chunks/serve-
|
|
3437
|
+
const { startServer } = await import("../chunks/serve-p2yarhm8.js");
|
|
3438
3438
|
startServer(port);
|
|
3439
3439
|
});
|
|
3440
3440
|
program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Append-only PostgreSQL optimization. Keep the original migration bytes fixed.
|
|
2
|
+
* Check the complete inserted set once: per-row checks can become quadratic
|
|
3
|
+
* with stale or small-table statistics even when suitable indexes exist.
|
|
4
|
+
* Same-operation request duplicates remain prohibited by the primary key. */
|
|
5
|
+
export declare const LEGACY_LARGE_SESSION_PG_SQL = "\nCREATE INDEX idx_legacy_targets_session_operation\n ON legacy_reconciliation_targets(session_id,operation_key);\nCREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$\nBEGIN\n IF EXISTS(\n SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n\n JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key\n ) OR EXISTS(\n SELECT 1 FROM inserted_legacy_targets n\n JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key\n ) THEN\n RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';\n END IF;\n RETURN NULL;\nEND\n$body$;\nDROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;\nCREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets\n REFERENCING NEW TABLE AS inserted_legacy_targets\n FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();\n";
|
|
6
|
+
//# sourceMappingURL=legacy-large-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy-large-schema.d.ts","sourceRoot":"","sources":["../../src/db/legacy-large-schema.ts"],"names":[],"mappings":"AAAA;;;6EAG6E;AAC7E,eAAO,MAAM,2BAA2B,ygCAqBvC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-migrations.d.ts","sourceRoot":"","sources":["../../src/db/pg-migrations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pg-migrations.d.ts","sourceRoot":"","sources":["../../src/db/pg-migrations.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH,eAAO,MAAM,aAAa,EAAE,MAAM,EAiRjC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ import { randomUUID } from "crypto";
|
|
|
61
61
|
// package.json
|
|
62
62
|
var package_default = {
|
|
63
63
|
name: "@hasna/economy",
|
|
64
|
-
version: "0.5.
|
|
64
|
+
version: "0.5.2",
|
|
65
65
|
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",
|
|
66
66
|
repository: {
|
|
67
67
|
type: "git",
|
package/dist/mcp/index.js
CHANGED
|
@@ -84,7 +84,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
84
84
|
// package.json
|
|
85
85
|
var package_default = {
|
|
86
86
|
name: "@hasna/economy",
|
|
87
|
-
version: "0.5.
|
|
87
|
+
version: "0.5.2",
|
|
88
88
|
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",
|
|
89
89
|
repository: {
|
|
90
90
|
type: "git",
|
package/dist/otel/index.js
CHANGED
|
@@ -3458,7 +3458,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3458
3458
|
// package.json
|
|
3459
3459
|
var package_default = {
|
|
3460
3460
|
name: "@hasna/economy",
|
|
3461
|
-
version: "0.5.
|
|
3461
|
+
version: "0.5.2",
|
|
3462
3462
|
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",
|
|
3463
3463
|
repository: {
|
|
3464
3464
|
type: "git",
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
|
@@ -4508,6 +4533,42 @@ var init_legacy_inventory = __esm(() => {
|
|
|
4508
4533
|
allCandidates = LEGACY_FAMILIES.map((family) => `(${predicates[family]})`).join(" OR ");
|
|
4509
4534
|
});
|
|
4510
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
|
+
|
|
4511
4572
|
// src/server/legacy-plan.ts
|
|
4512
4573
|
var exports_legacy_plan = {};
|
|
4513
4574
|
__export(exports_legacy_plan, {
|
|
@@ -4543,10 +4604,11 @@ function digest2(value) {
|
|
|
4543
4604
|
}
|
|
4544
4605
|
function parseLegacySelection(value) {
|
|
4545
4606
|
const input = object(value, ["schema", "items"], "selection");
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
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`);
|
|
4550
4612
|
const ids = new Set;
|
|
4551
4613
|
const items = input.items.map((item) => {
|
|
4552
4614
|
const row = object(item, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref"], "item");
|
|
@@ -4569,7 +4631,7 @@ function parseLegacySelection(value) {
|
|
|
4569
4631
|
evidence_ref: text(row.evidence_ref, 512, "evidence_ref")
|
|
4570
4632
|
};
|
|
4571
4633
|
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
4572
|
-
return { schema:
|
|
4634
|
+
return { schema: mode.selection, items };
|
|
4573
4635
|
}
|
|
4574
4636
|
function readLegacySelectionFile(path) {
|
|
4575
4637
|
if (!isAbsolute(path) || path.includes("\x00"))
|
|
@@ -4582,9 +4644,10 @@ function readLegacySelectionFile(path) {
|
|
|
4582
4644
|
}
|
|
4583
4645
|
try {
|
|
4584
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";
|
|
4585
4648
|
if (!before.isFile() || before.size < 1 || before.size > INPUT_MAX)
|
|
4586
|
-
throw Error(
|
|
4587
|
-
const bytes = Buffer.alloc(INPUT_MAX + 1);
|
|
4649
|
+
throw Error(sizeError);
|
|
4650
|
+
const bytes = Buffer.alloc(Math.min(INPUT_MAX + 1, before.size + 1));
|
|
4588
4651
|
let offset = 0, n;
|
|
4589
4652
|
while (offset < bytes.length && (n = readSync(fd, bytes, offset, bytes.length - offset, null)) > 0)
|
|
4590
4653
|
offset += n;
|
|
@@ -4595,9 +4658,12 @@ function readLegacySelectionFile(path) {
|
|
|
4595
4658
|
try {
|
|
4596
4659
|
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset)));
|
|
4597
4660
|
} catch {
|
|
4598
|
-
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");
|
|
4599
4662
|
}
|
|
4600
|
-
|
|
4663
|
+
const selection = parseLegacySelection(value);
|
|
4664
|
+
if (offset > legacyMode("selection", selection.schema).inputBytes)
|
|
4665
|
+
throw Error(sizeError);
|
|
4666
|
+
return selection;
|
|
4601
4667
|
} finally {
|
|
4602
4668
|
closeSync(fd);
|
|
4603
4669
|
}
|
|
@@ -4612,6 +4678,7 @@ async function prepareLegacyPlan(client, input) {
|
|
|
4612
4678
|
}
|
|
4613
4679
|
async function withLegacyPlanSnapshot(client, input, read) {
|
|
4614
4680
|
const selection = parseLegacySelection(input);
|
|
4681
|
+
const mode = legacyMode("selection", selection.schema);
|
|
4615
4682
|
let began = false;
|
|
4616
4683
|
try {
|
|
4617
4684
|
await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
|
|
@@ -4646,14 +4713,16 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4646
4713
|
return { ...item, session_id: identifier2(row.session_id, "session_id") };
|
|
4647
4714
|
});
|
|
4648
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");
|
|
4649
4718
|
const sessions = (await client.query(`SELECT id, encode(sha256(convert_to(row_to_json(sessions)::text,'UTF8')),'hex') AS original_sha256
|
|
4650
4719
|
FROM sessions WHERE id=ANY($1::text[]) ORDER BY id`, [sessionIds])).rows;
|
|
4651
4720
|
if (sessions.length !== sessionIds.length)
|
|
4652
4721
|
throw Error("A selected request has no session; resolve that inconsistency before reconciliation");
|
|
4653
4722
|
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(
|
|
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`);
|
|
4657
4726
|
const populations = sessions.map((session) => {
|
|
4658
4727
|
const rows = members.filter((row) => row.session_id === session.id);
|
|
4659
4728
|
if (!rows.length)
|
|
@@ -4663,7 +4732,7 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4663
4732
|
return { id: session.id, original_sha256, request_count: rows.length, members_sha256 };
|
|
4664
4733
|
});
|
|
4665
4734
|
const body = {
|
|
4666
|
-
schema:
|
|
4735
|
+
schema: mode.plan,
|
|
4667
4736
|
mode: "read-only",
|
|
4668
4737
|
original_encoding: "postgres-row-json-text-v1",
|
|
4669
4738
|
member_encoding: "sha256-json-id-digest-pairs-utf8-c-order-v1",
|
|
@@ -4707,10 +4776,11 @@ async function runLegacyPlan(selection) {
|
|
|
4707
4776
|
await pool.end();
|
|
4708
4777
|
}
|
|
4709
4778
|
}
|
|
4710
|
-
var INPUT_MAX,
|
|
4779
|
+
var INPUT_MAX, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
|
|
4711
4780
|
var init_legacy_plan = __esm(() => {
|
|
4712
4781
|
init_legacy_inventory();
|
|
4713
|
-
|
|
4782
|
+
init_legacy_limits();
|
|
4783
|
+
INPUT_MAX = LEGACY_SINGLE_SESSION.inputBytes;
|
|
4714
4784
|
});
|
|
4715
4785
|
|
|
4716
4786
|
// src/server/legacy-preservation.ts
|
|
@@ -4731,15 +4801,16 @@ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSyn
|
|
|
4731
4801
|
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
4732
4802
|
function parseLegacyPlan(value) {
|
|
4733
4803
|
const p = record(value);
|
|
4804
|
+
const mode = legacyMode("plan", p.schema);
|
|
4734
4805
|
fields(p, ["schema", "mode", "original_encoding", "member_encoding", "snapshot", "items", "sessions", "mutations_performed", "limitations", "plan_sha256"]);
|
|
4735
|
-
if (p.
|
|
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)
|
|
4736
4807
|
fail("Unsupported legacy plan contract");
|
|
4737
4808
|
const { plan_sha256, ...body } = p;
|
|
4738
4809
|
if (hash(plan_sha256) !== sha2(JSON.stringify(body)))
|
|
4739
4810
|
fail("Legacy plan digest mismatch");
|
|
4740
4811
|
if (!Array.isArray(p.items))
|
|
4741
4812
|
fail("Legacy plan items must be an array");
|
|
4742
|
-
const selection = parseLegacySelection({ schema:
|
|
4813
|
+
const selection = parseLegacySelection({ schema: mode.selection, items: p.items.map((item) => {
|
|
4743
4814
|
const row = record(item);
|
|
4744
4815
|
fields(row, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref", "session_id"]);
|
|
4745
4816
|
identifier3(row.session_id);
|
|
@@ -4767,7 +4838,7 @@ function parseLegacyPlan(value) {
|
|
|
4767
4838
|
if (typeof t.relation_oid !== "string" || !/^[0-9]+$/.test(t.relation_oid))
|
|
4768
4839
|
fail("Invalid relation locator");
|
|
4769
4840
|
}
|
|
4770
|
-
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length >
|
|
4841
|
+
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > mode.sessions)
|
|
4771
4842
|
fail("Invalid legacy session population");
|
|
4772
4843
|
const ids = new Set;
|
|
4773
4844
|
let members = 0;
|
|
@@ -4784,7 +4855,7 @@ function parseLegacyPlan(value) {
|
|
|
4784
4855
|
fail("Invalid legacy member count");
|
|
4785
4856
|
members += s.request_count;
|
|
4786
4857
|
}
|
|
4787
|
-
if (members >
|
|
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)
|
|
4788
4859
|
fail("Incomplete or excessive legacy session membership");
|
|
4789
4860
|
if (!Array.isArray(p.limitations) || p.limitations.length > 20 || p.limitations.some((x) => typeof x !== "string" || x.length > 2048))
|
|
4790
4861
|
fail("Invalid legacy plan limitations");
|
|
@@ -4801,16 +4872,28 @@ function assertLegacyPlanCurrent(expected, current) {
|
|
|
4801
4872
|
}
|
|
4802
4873
|
function parseLegacyCapsule(value) {
|
|
4803
4874
|
const c = record(value);
|
|
4875
|
+
const mode = legacyMode("capsule", c.schema);
|
|
4804
4876
|
fields(c, ["schema", "plan", "captured_at", "requests", "sessions", "capsule_sha256"]);
|
|
4805
|
-
if (
|
|
4877
|
+
if (typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
|
|
4806
4878
|
fail("Invalid preservation capsule");
|
|
4807
4879
|
const { capsule_sha256, ...body } = c;
|
|
4808
4880
|
const bytes = JSON.stringify(body);
|
|
4809
|
-
if (Buffer.byteLength(bytes) >
|
|
4881
|
+
if (Buffer.byteLength(bytes) > mode.capsuleBytes || hash(capsule_sha256) !== sha2(bytes))
|
|
4810
4882
|
fail("Preservation capsule bound or digest mismatch");
|
|
4811
4883
|
const plan = parseLegacyPlan(c.plan);
|
|
4812
|
-
if (
|
|
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)
|
|
4813
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
|
+
};
|
|
4814
4897
|
const requests = new Map;
|
|
4815
4898
|
for (const value2 of c.requests) {
|
|
4816
4899
|
const row = record(value2);
|
|
@@ -4818,7 +4901,7 @@ function parseLegacyCapsule(value) {
|
|
|
4818
4901
|
const id = identifier3(row.id);
|
|
4819
4902
|
if (requests.has(id) || typeof row.original !== "string")
|
|
4820
4903
|
fail("Invalid or duplicate request original");
|
|
4821
|
-
const original = record(JSON.parse(row.original));
|
|
4904
|
+
const original = record(JSON.parse(boundOriginal(row.original)));
|
|
4822
4905
|
if (original.id !== id)
|
|
4823
4906
|
fail("Request original identity mismatch");
|
|
4824
4907
|
requests.set(id, { session: identifier3(original.session_id), digest: sha2(row.original) });
|
|
@@ -4832,7 +4915,7 @@ function parseLegacyCapsule(value) {
|
|
|
4832
4915
|
fail("Invalid or duplicate session original");
|
|
4833
4916
|
sessions.add(id);
|
|
4834
4917
|
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)
|
|
4918
|
+
if (record(JSON.parse(boundOriginal(row.original))).id !== id || sha2(row.original) !== expected.original_sha256)
|
|
4836
4919
|
fail("Session original does not match the plan");
|
|
4837
4920
|
const members = [...requests].filter(([, r]) => r.session === id).sort(([a], [b]) => cOrder(a, b)).map(([rid, r]) => [rid, r.digest]);
|
|
4838
4921
|
if (members.length !== expected.request_count || sha2(JSON.stringify(members)) !== expected.members_sha256)
|
|
@@ -4849,18 +4932,19 @@ function parseLegacyCapsule(value) {
|
|
|
4849
4932
|
}
|
|
4850
4933
|
async function preserveLegacyOriginals(client, input) {
|
|
4851
4934
|
const expected = parseLegacyPlan(input);
|
|
4852
|
-
const
|
|
4935
|
+
const mode = legacyMode("plan", expected.schema);
|
|
4936
|
+
const selection = { schema: mode.selection, items: expected.items.map(({ session_id, ...item }) => item) };
|
|
4853
4937
|
return withLegacyPlanSnapshot(client, selection, async (current) => {
|
|
4854
4938
|
assertLegacyPlanCurrent(expected, current);
|
|
4855
4939
|
const ids = current.sessions.map((x) => x.id);
|
|
4856
4940
|
const sizes = (await client.query(`SELECT
|
|
4857
4941
|
(SELECT COALESCE(SUM(octet_length(row_to_json(requests)::text)),0) FROM requests WHERE session_id=ANY($1::text[]))::text AS request_bytes,
|
|
4858
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];
|
|
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:
|
|
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 };
|
|
4864
4948
|
return parseLegacyCapsule({ ...body, capsule_sha256: sha2(JSON.stringify(body)) });
|
|
4865
4949
|
});
|
|
4866
4950
|
}
|
|
@@ -4891,11 +4975,16 @@ function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
|
4891
4975
|
}
|
|
4892
4976
|
function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
4893
4977
|
const bytes = readLegacyBytes(path, maxBytes);
|
|
4978
|
+
let value;
|
|
4894
4979
|
try {
|
|
4895
|
-
|
|
4980
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
4896
4981
|
} catch {
|
|
4897
4982
|
fail("Artifact must contain valid UTF-8 JSON");
|
|
4898
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;
|
|
4899
4988
|
}
|
|
4900
4989
|
function writeLegacyCapsule(path, input) {
|
|
4901
4990
|
const capsule = parseLegacyCapsule(input);
|
|
@@ -4906,7 +4995,7 @@ function writeLegacyCapsule(path, input) {
|
|
|
4906
4995
|
fail("Output parent must be an owner-only directory");
|
|
4907
4996
|
const bytes = Buffer.from(JSON.stringify(capsule) + `
|
|
4908
4997
|
`);
|
|
4909
|
-
if (bytes.length >
|
|
4998
|
+
if (bytes.length > legacyMode("capsule", capsule.schema).capsuleBytes)
|
|
4910
4999
|
fail("Preservation capsule exceeds the file bound");
|
|
4911
5000
|
const fd = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
4912
5001
|
try {
|
|
@@ -5037,7 +5126,7 @@ async function runLegacyPreservation(command, options) {
|
|
|
5037
5126
|
await pool.end();
|
|
5038
5127
|
}
|
|
5039
5128
|
}
|
|
5040
|
-
var MAX_BYTES,
|
|
5129
|
+
var MAX_BYTES, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
|
|
5041
5130
|
throw Error(message);
|
|
5042
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) => {
|
|
5043
5132
|
if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
|
|
@@ -5045,7 +5134,8 @@ var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash7("sha256").update(s).
|
|
|
5045
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('"', '""') + '"';
|
|
5046
5135
|
var init_legacy_preservation = __esm(() => {
|
|
5047
5136
|
init_legacy_plan();
|
|
5048
|
-
|
|
5137
|
+
init_legacy_limits();
|
|
5138
|
+
MAX_BYTES = LEGACY_SINGLE_SESSION.capsuleBytes;
|
|
5049
5139
|
});
|
|
5050
5140
|
|
|
5051
5141
|
// src/server/legacy-operations.ts
|
|
@@ -5109,6 +5199,7 @@ function matchIntent(intent, capsule, target) {
|
|
|
5109
5199
|
}
|
|
5110
5200
|
async function verifyOriginals(client, capsule) {
|
|
5111
5201
|
const p = capsule.plan;
|
|
5202
|
+
const mode = legacyMode("capsule", capsule.schema);
|
|
5112
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];
|
|
5113
5204
|
if (context2?.database !== p.snapshot.database || context2?.database_oid !== p.snapshot.database_oid)
|
|
5114
5205
|
fail2("Legacy operation targets a different database");
|
|
@@ -5122,7 +5213,7 @@ async function verifyOriginals(client, capsule) {
|
|
|
5122
5213
|
fail2("Legacy session originals changed; preserve later work and prepare a new review");
|
|
5123
5214
|
const rows = (await client.query(`SELECT id,session_id,encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256,
|
|
5124
5215
|
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
|
|
5216
|
+
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
|
|
5126
5217
|
if (rows.length !== capsule.requests.length)
|
|
5127
5218
|
fail2("Legacy request membership changed; preserve later work and prepare a new review");
|
|
5128
5219
|
const originals = new Map(capsule.requests.map((r) => [r.id, sha3(r.original)]));
|
|
@@ -5176,13 +5267,14 @@ async function observed(client, run) {
|
|
|
5176
5267
|
}
|
|
5177
5268
|
function validateReceipt(value) {
|
|
5178
5269
|
const v = value;
|
|
5179
|
-
if (!v ||
|
|
5270
|
+
if (!v || !["apply", "rollback"].includes(v.action))
|
|
5180
5271
|
fail2("Invalid stored legacy receipt");
|
|
5272
|
+
const mode = legacyMode("receipt", v.schema);
|
|
5181
5273
|
key(v.operation_key);
|
|
5182
5274
|
key(v.apply_key);
|
|
5183
5275
|
digest3(v.plan_sha256);
|
|
5184
5276
|
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 <=
|
|
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)))
|
|
5186
5278
|
fail2("Invalid stored legacy receipt fields");
|
|
5187
5279
|
const names = ["schema", "action", "operation_key", "apply_key", "plan_sha256", "capsule_sha256", "target_requests", "preserved_requests", "sessions", "source_rows_changed", "created_at"];
|
|
5188
5280
|
if (Object.keys(v).sort().join(",") !== names.sort().join(","))
|
|
@@ -5207,7 +5299,7 @@ async function reply(client, row, expectedHash) {
|
|
|
5207
5299
|
}
|
|
5208
5300
|
function receipt(action, intent, capsule) {
|
|
5209
5301
|
return {
|
|
5210
|
-
schema: "
|
|
5302
|
+
schema: legacyMode("capsule", capsule.schema).receipt,
|
|
5211
5303
|
action,
|
|
5212
5304
|
operation_key: intent.operation_key,
|
|
5213
5305
|
apply_key: action === "apply" ? intent.operation_key : intent.apply_key,
|
|
@@ -5247,8 +5339,9 @@ async function applyLegacyReconciliation(client, input, preserved, target) {
|
|
|
5247
5339
|
const saved = (await client.query("SELECT capsule_json,capsule_sha256 FROM legacy_reconciliations WHERE operation_key=$1", [intent.operation_key])).rows[0];
|
|
5248
5340
|
if (saved?.capsule_json !== json3 || saved?.capsule_sha256 !== capsule.capsule_sha256 || parseLegacyCapsule(JSON.parse(saved.capsule_json)).capsule_sha256 !== capsule.capsule_sha256)
|
|
5249
5341
|
fail2("Durable preservation readback failed");
|
|
5250
|
-
|
|
5251
|
-
|
|
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 })))]);
|
|
5252
5345
|
const stored = await lookup(client, "apply", intent.operation_key) ?? fail2("Legacy receipt readback failed");
|
|
5253
5346
|
if (stored.response_json !== responseJson)
|
|
5254
5347
|
fail2("Legacy receipt readback failed");
|
|
@@ -5360,6 +5453,7 @@ var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) =>
|
|
|
5360
5453
|
WHERE c.oid IN ('requests'::regclass,'sessions'::regclass) AND a.attnum>0 AND NOT a.attisdropped
|
|
5361
5454
|
GROUP BY c.oid,c.relname,n.nspname ORDER BY c.relname`, same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
5362
5455
|
var init_legacy_operations = __esm(() => {
|
|
5456
|
+
init_legacy_limits();
|
|
5363
5457
|
init_pg_connection();
|
|
5364
5458
|
init_legacy_inventory();
|
|
5365
5459
|
init_legacy_preservation();
|
|
@@ -5396,7 +5490,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5396
5490
|
// package.json
|
|
5397
5491
|
var package_default = {
|
|
5398
5492
|
name: "@hasna/economy",
|
|
5399
|
-
version: "0.5.
|
|
5493
|
+
version: "0.5.2",
|
|
5400
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",
|
|
5401
5495
|
repository: {
|
|
5402
5496
|
type: "git",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Artifact schemas select a closed resource envelope. A larger envelope is
|
|
2
|
+
* explicit and restricted to one complete session; ordinary artifacts keep
|
|
3
|
+
* their original limits. These profiles confer no reconciliation authority. */
|
|
4
|
+
export declare const LEGACY_STANDARD: {
|
|
5
|
+
readonly selection: "economy.legacy-selection.v1";
|
|
6
|
+
readonly plan: "economy.legacy-plan.v1";
|
|
7
|
+
readonly capsule: "economy.legacy-preservation.v1";
|
|
8
|
+
readonly receipt: "economy.legacy-operation-receipt.v1";
|
|
9
|
+
readonly items: 100;
|
|
10
|
+
readonly sessions: 100;
|
|
11
|
+
readonly members: 10000;
|
|
12
|
+
readonly inputBytes: number;
|
|
13
|
+
readonly originalBytes: number;
|
|
14
|
+
readonly capsuleBytes: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const LEGACY_SINGLE_SESSION: {
|
|
17
|
+
readonly selection: "economy.legacy-session-selection.v1";
|
|
18
|
+
readonly plan: "economy.legacy-session-plan.v1";
|
|
19
|
+
readonly capsule: "economy.legacy-session-preservation.v1";
|
|
20
|
+
readonly receipt: "economy.legacy-session-operation-receipt.v1";
|
|
21
|
+
readonly items: 50000;
|
|
22
|
+
readonly sessions: 1;
|
|
23
|
+
readonly members: 50000;
|
|
24
|
+
readonly inputBytes: number;
|
|
25
|
+
readonly originalBytes: number;
|
|
26
|
+
readonly capsuleBytes: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function legacyMode(kind: 'selection' | 'plan' | 'capsule' | 'receipt', schema: unknown): {
|
|
29
|
+
readonly selection: "economy.legacy-selection.v1";
|
|
30
|
+
readonly plan: "economy.legacy-plan.v1";
|
|
31
|
+
readonly capsule: "economy.legacy-preservation.v1";
|
|
32
|
+
readonly receipt: "economy.legacy-operation-receipt.v1";
|
|
33
|
+
readonly items: 100;
|
|
34
|
+
readonly sessions: 100;
|
|
35
|
+
readonly members: 10000;
|
|
36
|
+
readonly inputBytes: number;
|
|
37
|
+
readonly originalBytes: number;
|
|
38
|
+
readonly capsuleBytes: number;
|
|
39
|
+
} | {
|
|
40
|
+
readonly selection: "economy.legacy-session-selection.v1";
|
|
41
|
+
readonly plan: "economy.legacy-session-plan.v1";
|
|
42
|
+
readonly capsule: "economy.legacy-session-preservation.v1";
|
|
43
|
+
readonly receipt: "economy.legacy-session-operation-receipt.v1";
|
|
44
|
+
readonly items: 50000;
|
|
45
|
+
readonly sessions: 1;
|
|
46
|
+
readonly members: 50000;
|
|
47
|
+
readonly inputBytes: number;
|
|
48
|
+
readonly originalBytes: number;
|
|
49
|
+
readonly capsuleBytes: number;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=legacy-limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy-limits.d.ts","sourceRoot":"","sources":["../../src/server/legacy-limits.ts"],"names":[],"mappings":"AAAA;;+EAE+E;AAC/E,eAAO,MAAM,eAAe;;;;;;;;;;;CAKlB,CAAA;AAEV,eAAO,MAAM,qBAAqB;;;;;;;;;;;CAKxB,CAAA;AAEV,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;EAI7F"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type pg from 'pg';
|
|
2
|
+
import { LEGACY_STANDARD, LEGACY_SINGLE_SESSION } from './legacy-limits.js';
|
|
2
3
|
import type { LegacyReader } from './legacy-plan.js';
|
|
3
4
|
type Action = 'apply' | 'rollback';
|
|
4
5
|
export interface LegacyIntent {
|
|
@@ -19,7 +20,7 @@ export declare function parseLegacyIntent(action: Action, value: unknown): Legac
|
|
|
19
20
|
* complements exact database/relation locators; it is not ownership approval. */
|
|
20
21
|
export declare function legacyTargetDigest(target: string | pg.PoolConfig): string;
|
|
21
22
|
interface Receipt {
|
|
22
|
-
schema:
|
|
23
|
+
schema: typeof LEGACY_STANDARD.receipt | typeof LEGACY_SINGLE_SESSION.receipt;
|
|
23
24
|
action: Action;
|
|
24
25
|
operation_key: string;
|
|
25
26
|
apply_key: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legacy-operations.d.ts","sourceRoot":"","sources":["../../src/server/legacy-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"legacy-operations.d.ts","sourceRoot":"","sources":["../../src/server/legacy-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAc,MAAM,oBAAoB,CAAA;AAGvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAQpD,KAAK,MAAM,GAAC,OAAO,GAAC,UAAU,CAAA;AAC9B,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,cAAc,EAAC,MAAM,CAAA;IACpG,SAAS,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,SAAS,CAAC,EAAC,MAAM,CAAA;CAC1D;AACD,4EAA4E;AAC5E,qBAAa,yBAA0B,SAAQ,KAAK;CAAG;AACvD,wBAAgB,iBAAiB,CAAC,MAAM,EAAC,MAAM,EAAC,KAAK,EAAC,OAAO,GAAE,YAAY,CAY1E;AACD;iFACiF;AACjF,wBAAgB,kBAAkB,CAAC,MAAM,EAAC,MAAM,GAAC,EAAE,CAAC,UAAU,GAAE,MAAM,CAOrE;AAiED,UAAU,OAAO;IACf,MAAM,EAAC,OAAO,eAAe,CAAC,OAAO,GAAC,OAAO,qBAAqB,CAAC,OAAO,CAAC;IAAA,MAAM,EAAC,MAAM,CAAC;IAAA,aAAa,EAAC,MAAM,CAAC;IAAA,SAAS,EAAC,MAAM,CAAA;IAC9H,WAAW,EAAC,MAAM,CAAC;IAAA,cAAc,EAAC,MAAM,CAAC;IAAA,eAAe,EAAC,MAAM,CAAC;IAAA,kBAAkB,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,MAAM,CAAA;IACzG,mBAAmB,EAAC,CAAC,CAAC;IAAA,UAAU,EAAC,MAAM,CAAA;CACxC;AAoCD,wBAAsB,yBAAyB,CAAC,MAAM,EAAC,YAAY,EAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,EAAC,MAAM,EAAC,MAAM;;;;;GAmChH;AACD,wBAAsB,4BAA4B,CAAC,MAAM,EAAC,YAAY,EAAC,KAAK,EAAC,OAAO,EAAC,MAAM,EAAC,MAAM;;;;;GAuBjG;AACD,4FAA4F;AAC5F,wBAAsB,mBAAmB,CAAC,MAAM,EAAC,YAAY,EAAC,SAAS,EAAC,MAAM;;;;;;;;;GAQ7E;AACD,wBAAgB,2BAA2B,CAAC,OAAO,EAAC,MAAM,EAAC,IAAI,EAAC,MAAM,EAAE,GAAE;IAAC,KAAK,CAAC,EAAC,MAAM,CAAC;IAAA,OAAO,CAAC,EAAC,MAAM,CAAC;IAAA,SAAS,CAAC,EAAC,MAAM,CAAA;CAAC,CAQ1H;AACD,wBAAsB,kBAAkB,CAAC,OAAO,EAAC,MAAM,EAAC,OAAO,EAAC,UAAU,CAAC,OAAO,2BAA2B,CAAC,iBAgC7G"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LegacyFamily } from './legacy-inventory.js';
|
|
2
|
+
import { LEGACY_STANDARD, LEGACY_SINGLE_SESSION } from './legacy-limits.js';
|
|
2
3
|
export interface LegacyReader {
|
|
3
4
|
query(sql: string, values?: unknown[]): Promise<{
|
|
4
5
|
rows: Record<string, any>[];
|
|
@@ -30,7 +31,7 @@ export interface LegacyPlan {
|
|
|
30
31
|
}
|
|
31
32
|
type Disposition = 'quarantine_aggregate' | 'mark_billing_unknown';
|
|
32
33
|
export interface LegacySelection {
|
|
33
|
-
schema:
|
|
34
|
+
schema: typeof LEGACY_STANDARD.selection | typeof LEGACY_SINGLE_SESSION.selection;
|
|
34
35
|
items: Array<{
|
|
35
36
|
id: string;
|
|
36
37
|
family: LegacyFamily;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legacy-plan.d.ts","sourceRoot":"","sources":["../../src/server/legacy-plan.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"legacy-plan.d.ts","sourceRoot":"","sources":["../../src/server/legacy-plan.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAC7F,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAc,MAAM,oBAAoB,CAAA;AAEvF,MAAM,WAAW,YAAY;IAAG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE;AAClH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;IAChF,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;KAAE,CAAA;IACpG,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvE,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvG,mBAAmB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CACxE;AACD,KAAK,WAAW,GAAG,sBAAsB,GAAG,sBAAsB,CAAA;AAClE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,eAAe,CAAC,SAAS,GAAG,OAAO,qBAAqB,CAAC,SAAS,CAAA;IACjF,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5I;AAsBD,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAmBpE;AAED,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAqBrE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAG7D;AAED;mFACmF;AACnF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAEjG;AAED,+EAA+E;AAC/E,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAClF,IAAI,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA0EpD;AAED,wBAAsB,aAAa,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAU7E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LegacyPlan, type LegacyReader } from './legacy-plan.js';
|
|
2
|
+
import { LEGACY_STANDARD, LEGACY_SINGLE_SESSION } from './legacy-limits.js';
|
|
2
3
|
/** Validate the complete emitted proposal before taking a connection or opening output. */
|
|
3
4
|
export declare function parseLegacyPlan(value: unknown): LegacyPlan;
|
|
4
5
|
export declare function assertLegacyPlanCurrent(expected: LegacyPlan, current: LegacyPlan): void;
|
|
@@ -7,7 +8,7 @@ interface Original {
|
|
|
7
8
|
original: string;
|
|
8
9
|
}
|
|
9
10
|
export interface LegacyCapsule {
|
|
10
|
-
schema:
|
|
11
|
+
schema: typeof LEGACY_STANDARD.capsule | typeof LEGACY_SINGLE_SESSION.capsule;
|
|
11
12
|
plan: LegacyPlan;
|
|
12
13
|
captured_at: string;
|
|
13
14
|
requests: Original[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legacy-preservation.d.ts","sourceRoot":"","sources":["../../src/server/legacy-preservation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"legacy-preservation.d.ts","sourceRoot":"","sources":["../../src/server/legacy-preservation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgD,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACnH,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAc,MAAM,oBAAoB,CAAA;AAevF,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CA8C1D;AAOD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAEvF;AAED,UAAU,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AACnD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,eAAe,CAAC,OAAO,GAAG,OAAO,qBAAqB,CAAC,OAAO,CAAA;IAC7E,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CA4ChE;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAkB1G;AAmBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAU,GAAG,OAAO,CAS5E;AAED;sFACsF;AACtF,wBAAgB,kBAAkB,CAAC,IAAI,EAAC,MAAM,EAAC,KAAK,EAAC,OAAO;;;;;;;;;;;EAwB3D;AAED;;kFAEkF;AAClF,wBAAsB,qBAAqB,CAAC,MAAM,EAAC,YAAY,EAAC,KAAK,EAAC,OAAO,EAAC,KAAK,GAAC,aAAa,GAAC,WAAyB;;;;;;;;;;GAwD1H;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAC,MAAM,EAAC,IAAI,EAAC,MAAM,EAAE,GAAG;IAAC,KAAK,EAAC,MAAM,CAAC;IAAA,MAAM,CAAC,EAAC,MAAM,CAAA;CAAC,CAM1G;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAC,MAAM,EAAC,OAAO,EAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAA,MAAM,CAAC,EAAC,MAAM,CAAA;CAAC,iBAgB/F"}
|
|
@@ -7266,7 +7266,7 @@ import { randomUUID } from "crypto";
|
|
|
7266
7266
|
// package.json
|
|
7267
7267
|
var package_default = {
|
|
7268
7268
|
name: "@hasna/economy",
|
|
7269
|
-
version: "0.5.
|
|
7269
|
+
version: "0.5.2",
|
|
7270
7270
|
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",
|
|
7271
7271
|
repository: {
|
|
7272
7272
|
type: "git",
|
|
@@ -122,6 +122,44 @@ database backup, production ownership attestation or disaster-recovery exercise.
|
|
|
122
122
|
Neither command authorizes quarantine or performs application or rollback of a
|
|
123
123
|
reconciliation.
|
|
124
124
|
|
|
125
|
+
## One large session
|
|
126
|
+
|
|
127
|
+
When one complete session exceeds the ordinary envelope, select it explicitly
|
|
128
|
+
with `schema: "economy.legacy-session-selection.v1"` in the same selection file.
|
|
129
|
+
Use the same maintained `legacy-plan`, `legacy-preserve` and `legacy-rehearse`
|
|
130
|
+
commands. Every target still needs its exact original digest and reviewed
|
|
131
|
+
disposition, reason and evidence reference. The planner requires exactly one
|
|
132
|
+
session, preserves its entire request population including unselected rows, and
|
|
133
|
+
emits `economy.legacy-session-plan.v1`. Preservation emits
|
|
134
|
+
`economy.legacy-session-preservation.v1`; neither schema can be substituted for
|
|
135
|
+
its ordinary counterpart.
|
|
136
|
+
|
|
137
|
+
This mode accepts at most 50,000 selected requests, 50,000 complete-session
|
|
138
|
+
members, a 32 MiB selection file, 64 MiB of original row text, and a 128 MiB
|
|
139
|
+
preservation file. Each bound refuses the operation without truncation. Ordinary
|
|
140
|
+
artifacts retain their 100-target, 10,000-member, 256 KiB selection, 16 MiB
|
|
141
|
+
original and 32 MiB preservation bounds. A population above the explicit
|
|
142
|
+
single-session bounds still needs another reviewed procedure; the tool never
|
|
143
|
+
silently increases them.
|
|
144
|
+
|
|
145
|
+
Apply uses one set-based insertion for the selected dispositions and commits
|
|
146
|
+
one immutable capsule and receipt for the whole operation. Its receipt schema
|
|
147
|
+
is `economy.legacy-session-operation-receipt.v1`. Use the same exact-target
|
|
148
|
+
intent, observation, explicit retry and rollback commands described below.
|
|
149
|
+
Session overlap remains prohibited: select all reviewed targets for that
|
|
150
|
+
session together, instead of repeatedly preserving the same whole session for
|
|
151
|
+
small target batches. File and row preservation, typed restoration, complete
|
|
152
|
+
membership preconditions, ingestion exclusion and rollback checks all apply.
|
|
153
|
+
Use a version supporting these explicit artifact schemas for every stage;
|
|
154
|
+
older versions refuse them. These limits do not establish source provenance or
|
|
155
|
+
authorize changing a production report.
|
|
156
|
+
|
|
157
|
+
The appended PostgreSQL migration checks conflicts once for the inserted set,
|
|
158
|
+
including conflicts between operations in that same set. This avoids repeated
|
|
159
|
+
scans of a growing operation when existing history has small or stale planner
|
|
160
|
+
statistics. The 15-second statement timeout and session/request conflict rules
|
|
161
|
+
remain in force.
|
|
162
|
+
|
|
125
163
|
## Apply, observe and roll back
|
|
126
164
|
|
|
127
165
|
Deploy the reviewed runtime integration and apply its appended PostgreSQL schema
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|