@carllee1983/dbcli 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,7 @@ var package_default;
51
51
  var init_package = __esm(() => {
52
52
  package_default = {
53
53
  name: "@carllee1983/dbcli",
54
- version: "2.0.0",
54
+ version: "3.0.0",
55
55
  description: "Database CLI for AI agents",
56
56
  type: "module",
57
57
  publishConfig: {
@@ -133,7 +133,9 @@ var init_package = __esm(() => {
133
133
  "platform:check": "bun run scripts/check-platform-parity.ts",
134
134
  "agent-core:check": "bun run scripts/check-agent-core-purity.ts",
135
135
  "core-stdout:check": "bun run scripts/check-core-no-stdout.ts",
136
+ "plan:check": "bun run scripts/check-plan-acceptance.ts",
136
137
  typecheck: "tsc --noEmit --pretty false",
138
+ "typecheck:tests": "tsc --noEmit --pretty false -p tsconfig.tests.json",
137
139
  "test:perf": "bun test ./tests/perf/*.bench.ts",
138
140
  lint: "eslint src tests scripts --ext .ts --max-warnings=0",
139
141
  "format:check": "prettier --check .",
@@ -8903,6 +8905,15 @@ var init_messages = __esm(() => {
8903
8905
  health: {
8904
8906
  description: "Show AuditLogger health snapshot (size, entries, lock state, last write)"
8905
8907
  },
8908
+ write_gate: {
8909
+ description: "Summarize tier-two write-gate decisions (ADR 0010's measurement)",
8910
+ header: "Write-gate summary \u2014 connection: {conn}",
8911
+ window: "Audit entries scanned: {scanned}, {from} \u2192 {to}",
8912
+ decisions: "Tier-two evaluations: {count}, {from} \u2192 {to}",
8913
+ tier_one: "Tier one, counted apart: {count} ({outcomes})",
8914
+ no_decisions: "Tier two was never reached in this window. Per ADR 0010 that falsifies the criterion, not the gate: the shapes it looks for are not the shapes people write.",
8915
+ no_entries: "The audit log is empty, so this says nothing about the gate either way. Run some writes, or check `dbcli audit health`."
8916
+ },
8906
8917
  disabled_hint: "Audit is disabled (audit.enabled = false in .dbcli). Use 'dbcli audit health' for details.",
8907
8918
  no_entries: "No audit entries.",
8908
8919
  show_no_match: "No audit entry matches '{prefix}'.",
@@ -9211,6 +9222,15 @@ var init_messages2 = __esm(() => {
9211
9222
  health: {
9212
9223
  description: "\u986F\u793A AuditLogger \u5065\u5EB7\u72C0\u614B\uFF08\u6A94\u6848\u5927\u5C0F\u3001\u7B46\u6578\u3001lock \u72C0\u614B\u3001\u6700\u5F8C\u5BEB\u5165\u7D50\u679C\uFF09"
9213
9224
  },
9225
+ write_gate: {
9226
+ description: "\u7D71\u8A08\u7B2C\u4E8C\u7D1A\u5BEB\u5165\u9598\u9580\u7684\u5224\u5B9A\uFF08ADR 0010 \u62BC\u6CE8\u7684\u90A3\u500B\u91CF\u6E2C\uFF09",
9227
+ header: "\u5BEB\u5165\u9598\u9580\u6458\u8981 \u2014 \u9023\u7DDA\uFF1A{conn}",
9228
+ window: "\u6383\u904E\u7684\u7A3D\u6838\u7D00\u9304\uFF1A{scanned} \u7B46\uFF0C{from} \u2192 {to}",
9229
+ decisions: "\u7B2C\u4E8C\u7D1A\u5224\u5B9A\uFF1A{count} \u6B21\uFF0C{from} \u2192 {to}",
9230
+ tier_one: "\u7B2C\u4E00\u7D1A\uFF0C\u5206\u958B\u8A08\u7B97\uFF1A{count} \u6B21\uFF08{outcomes}\uFF09",
9231
+ no_decisions: "\u9019\u6BB5\u5340\u9593\u5167\u7B2C\u4E8C\u7D1A\u4E00\u6B21\u90FD\u6C92\u88AB\u89F8\u53CA\u3002\u4F9D ADR 0010\uFF0C\u9019\u63A8\u7FFB\u7684\u662F\u5224\u6E96\u800C\u4E0D\u662F\u9598\u9580\uFF1A\u5B83\u5728\u627E\u7684\u5F62\u72C0\u4E0D\u662F\u5927\u5BB6\u5BE6\u969B\u6703\u5BEB\u7684\u5F62\u72C0\u3002",
9232
+ no_entries: "\u7A3D\u6838\u7D00\u9304\u662F\u7A7A\u7684\uFF0C\u6240\u4EE5\u9019\u5C0D\u9598\u9580\u6C92\u6709\u4EFB\u4F55\u7D50\u8AD6\u3002\u5148\u8DD1\u4E00\u4E9B\u5BEB\u5165\uFF0C\u6216\u7528 `dbcli audit health` \u6AA2\u67E5\u3002"
9233
+ },
9214
9234
  disabled_hint: "Audit \u5DF2\u505C\u7528\uFF08.dbcli \u4E2D audit.enabled = false\uFF09\u3002\u57F7\u884C `dbcli audit health` \u67E5\u770B\u8A73\u60C5\u3002",
9215
9235
  no_entries: "\u6C92\u6709 audit \u7D00\u9304\u3002",
9216
9236
  show_no_match: "\u627E\u4E0D\u5230\u7B26\u5408 '{prefix}' \u7684 audit \u7D00\u9304\u3002",
@@ -9422,7 +9442,8 @@ var init_shell = __esm(() => {
9422
9442
  multiline_cancelled: "Multiline input cancelled.",
9423
9443
  timing_display: "Time: {ms}ms",
9424
9444
  rows_display: "{count} row(s)",
9425
- unknown_command: "Unknown command: '{command}'. Type '.help' for available commands."
9445
+ unknown_command: "Unknown command: '{command}'. Type '.help' for available commands.",
9446
+ subcommand_prefix_hint: "This line is being read as SQL and is waiting for its `;`. If you meant the dbcli subcommand, type {command} instead. Ctrl-C cancels."
9426
9447
  };
9427
9448
  });
9428
9449
 
@@ -9448,7 +9469,8 @@ var init_shell2 = __esm(() => {
9448
9469
  multiline_cancelled: "\u5DF2\u53D6\u6D88\u591A\u884C\u8F38\u5165\u3002",
9449
9470
  timing_display: "\u8017\u6642\uFF1A{ms}ms",
9450
9471
  rows_display: "{count} \u7B46\u8CC7\u6599",
9451
- unknown_command: "\u672A\u77E5\u6307\u4EE4\uFF1A'{command}'\u3002\u8F38\u5165 '.help' \u67E5\u770B\u53EF\u7528\u6307\u4EE4\u3002"
9472
+ unknown_command: "\u672A\u77E5\u6307\u4EE4\uFF1A'{command}'\u3002\u8F38\u5165 '.help' \u67E5\u770B\u53EF\u7528\u6307\u4EE4\u3002",
9473
+ subcommand_prefix_hint: "\u9019\u4E00\u884C\u88AB\u7576\u6210 SQL\uFF0C\u6B63\u5728\u7B49\u5206\u865F\u3002\u5982\u679C\u4F60\u8981\u7684\u662F dbcli \u5B50\u6307\u4EE4\uFF0C\u8ACB\u6539\u6253 {command}\u3002Ctrl-C \u53EF\u53D6\u6D88\u3002"
9452
9474
  };
9453
9475
  });
9454
9476
 
@@ -9482,14 +9504,20 @@ var init_ceremony = __esm(() => {
9482
9504
  gate_ddl_warning_untargeted: "\u26A0\uFE0F This is a destructive schema change and cannot be undone.",
9483
9505
  gate_unparseable_warning: "\u26A0\uFE0F dbcli could not parse this statement, so it is treated as affecting everything.",
9484
9506
  gate_multiple_statements_warning: "\u26A0\uFE0F This is more than one statement, and dbcli will not judge them one at a time.",
9507
+ gate_multi_table_warning: "This write joins another table. How many rows of the target it touches depends on the data, not on the statement, so it cannot be checked before it runs.",
9508
+ gate_nested_write_warning: "\u26A0\uFE0F This statement carries a second write inside it, and dbcli will not read the tier of that write off this one.",
9485
9509
  gate_typed_prompt: "Type {phrase} to run it",
9486
9510
  gate_typed_mismatch: "That did not match {phrase}. Nothing was run.",
9487
9511
  gate_refused: "Refused: this statement is not limited to specific rows and nobody is here to confirm it (reason={reason}).",
9512
+ gate_refused_in_shell: "Refused: a dbcli subcommand started from the shell has no way to ask you, even though you are right here (reason={reason}).",
9488
9513
  gate_remedy_no_where: "Add a WHERE clause \u2014 WHERE 1=1 if you really do mean every row \u2014 or a LIMIT, then run it again.",
9489
9514
  gate_remedy_ddl_destruction: "No flag skips this. Run it from an interactive terminal, or apply the schema change through a reviewed migration.",
9490
9515
  gate_remedy_unparseable: "dbcli could not parse this statement and will not guess. Simplify it, or run it from an interactive terminal.",
9491
9516
  gate_remedy_multiple_statements: "Run one statement per command.",
9517
+ gate_remedy_multi_table: "Rewrite it as a single-table statement \u2014 move the other table into a subquery in the WHERE \u2014 or run it where someone can confirm it.",
9518
+ gate_remedy_nested_write: "Run the nested write as its own statement, or run this one where someone can confirm it.",
9492
9519
  gate_remedy_non_unique_where: "Match on a primary key or a uniquely indexed column, or run it from an interactive terminal.",
9520
+ gate_remedy_in_shell: "Type the statement at the dbcli> prompt instead \u2014 there the shell asks you to type the target table name.",
9493
9521
  recovery_none: "This cannot be undone automatically \u2014 restore from a backup to get the previous values back.",
9494
9522
  recovery_retry: "Re-run with --recovery to write a recovery plan, then read it with: dbcli recover",
9495
9523
  blacklist_hint: "Run `dbcli blacklist list` to see what is protected"
@@ -9526,14 +9554,20 @@ var init_ceremony2 = __esm(() => {
9526
9554
  gate_ddl_warning_untargeted: "\u26A0\uFE0F \u9019\u662F\u7834\u58DE\u6027\u7684 schema \u8B8A\u66F4\uFF0C\u57F7\u884C\u5F8C\u7121\u6CD5\u5FA9\u539F\u3002",
9527
9555
  gate_unparseable_warning: "\u26A0\uFE0F dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u56E0\u6B64\u4E00\u5F8B\u8996\u70BA\u6703\u5F71\u97FF\u5168\u90E8\u8CC7\u6599\u3002",
9528
9556
  gate_multiple_statements_warning: "\u26A0\uFE0F \u9019\u88E1\u4E0D\u53EA\u4E00\u53E5\u8A9E\u53E5\uFF0Cdbcli \u4E0D\u6703\u9010\u53E5\u5224\u65B7\u3002",
9557
+ gate_multi_table_warning: "\u9019\u500B\u5BEB\u5165\u63A5\u4E86\u53E6\u4E00\u5F35\u8868\u3002\u5B83\u6703\u52D5\u5230\u76EE\u6A19\u8868\u5E7E\u5217\u53D6\u6C7A\u65BC\u8CC7\u6599\u800C\u4E0D\u662F\u8A9E\u53E5\u672C\u8EAB\uFF0C\u57F7\u884C\u524D\u7121\u6CD5\u6AA2\u67E5\u3002",
9558
+ gate_nested_write_warning: "\u26A0\uFE0F \u9019\u53E5\u88E1\u9762\u9084\u593E\u8457\u53E6\u4E00\u500B\u5BEB\u5165\uFF0Cdbcli \u4E0D\u6703\u62FF\u9019\u53E5\u7684\u5C64\u7D1A\u4EE3\u66FF\u90A3\u500B\u5BEB\u5165\u7684\u5C64\u7D1A\u3002",
9529
9559
  gate_typed_prompt: "\u8F38\u5165 {phrase} \u624D\u6703\u57F7\u884C",
9530
9560
  gate_typed_mismatch: "\u8F38\u5165\u7684\u5167\u5BB9\u8207 {phrase} \u4E0D\u7B26\uFF0C\u6C92\u6709\u57F7\u884C\u4EFB\u4F55\u8A9E\u53E5\u3002",
9531
9561
  gate_refused: "\u5DF2\u62D2\u7D55\uFF1A\u9019\u53E5\u6C92\u6709\u9650\u5B9A\u8981\u52D5\u54EA\u4E9B\u5217\uFF0C\u800C\u73FE\u5728\u6C92\u6709\u4EBA\u80FD\u78BA\u8A8D\uFF08reason={reason}\uFF09\u3002",
9562
+ gate_refused_in_shell: "\u5DF2\u62D2\u7D55\uFF1A\u5F9E shell \u555F\u52D5\u7684 dbcli \u5B50\u6307\u4EE4\u6C92\u6709\u8FA6\u6CD5\u5411\u4F60\u63D0\u554F\uFF0C\u5373\u4F7F\u4F60\u4EBA\u5C31\u5728\u9019\u88E1\uFF08reason={reason}\uFF09\u3002",
9532
9563
  gate_remedy_no_where: "\u88DC\u4E0A WHERE\uFF08\u771F\u7684\u8981\u5168\u8868\u5C31\u5BEB WHERE 1=1\uFF09\u6216 LIMIT\uFF0C\u7136\u5F8C\u91CD\u8DD1\u3002",
9533
9564
  gate_remedy_ddl_destruction: "\u6C92\u6709\u4EFB\u4F55\u65D7\u6A19\u53EF\u4EE5\u8DF3\u904E\u9019\u4E00\u95DC\u3002\u8ACB\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\uFF0C\u6216\u6539\u8D70\u7D93\u904E\u5BE9\u67E5\u7684 migration\u3002",
9534
9565
  gate_remedy_unparseable: "dbcli \u7121\u6CD5\u89E3\u6790\u9019\u53E5\uFF0C\u4E5F\u4E0D\u6703\u7528\u731C\u7684\u3002\u8ACB\u7C21\u5316\u8A9E\u53E5\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
9535
9566
  gate_remedy_multiple_statements: "\u4E00\u6B21\u53EA\u57F7\u884C\u4E00\u53E5\u8A9E\u53E5\u3002",
9567
+ gate_remedy_multi_table: "\u6539\u5BEB\u6210\u55AE\u8868\u8A9E\u53E5\u2014\u2014\u628A\u53E6\u4E00\u5F35\u8868\u79FB\u9032 WHERE \u7684\u5B50\u67E5\u8A62\u88E1\u2014\u2014\u6216\u6539\u5728\u6709\u4EBA\u53EF\u4EE5\u78BA\u8A8D\u7684\u5730\u65B9\u57F7\u884C\u3002",
9568
+ gate_remedy_nested_write: "\u628A\u88E1\u9762\u90A3\u500B\u5BEB\u5165\u62C6\u6210\u7368\u7ACB\u7684\u4E00\u53E5\u4F86\u57F7\u884C\uFF0C\u6216\u6539\u5728\u6709\u4EBA\u53EF\u4EE5\u78BA\u8A8D\u7684\u5730\u65B9\u57F7\u884C\u3002",
9536
9569
  gate_remedy_non_unique_where: "\u6539\u7528\u4E3B\u9375\u6216\u552F\u4E00\u7D22\u5F15\u6B04\u4F4D\u7576\u689D\u4EF6\uFF0C\u6216\u5728\u4E92\u52D5\u7D42\u7AEF\u4E0B\u57F7\u884C\u3002",
9570
+ gate_remedy_in_shell: "\u8ACB\u76F4\u63A5\u5728 dbcli> \u63D0\u793A\u7B26\u6253\u9019\u53E5\u8A9E\u53E5\u2014\u2014\u5728\u90A3\u88E1 dbcli \u6703\u8ACB\u4F60\u6253\u51FA\u76EE\u6A19\u8CC7\u6599\u8868\u7684\u540D\u7A31\u4F86\u78BA\u8A8D\u3002",
9537
9571
  recovery_none: "\u6B64\u64CD\u4F5C\u7121\u6CD5\u81EA\u52D5\u9084\u539F\uFF0C\u8981\u53D6\u56DE\u5148\u524D\u7684\u8CC7\u6599\u8ACB\u5F9E\u5099\u4EFD\u56DE\u5FA9\u3002",
9538
9572
  recovery_retry: "\u52A0\u4E0A --recovery \u91CD\u8DD1\u53EF\u7522\u751F\u56DE\u5FA9\u8A08\u756B\uFF0C\u518D\u7528 dbcli recover \u8B80\u53D6",
9539
9573
  blacklist_hint: "\u57F7\u884C `dbcli blacklist list` \u67E5\u770B\u53D7\u4FDD\u8B77\u7684\u9805\u76EE"
@@ -18007,7 +18041,7 @@ async function writeAuditEntry(config, commandName, options, outcome) {
18007
18041
  const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
18008
18042
  const engine = config.connection?.system || "postgresql";
18009
18043
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
18010
- let tier = getEngineCapability(engine, commandName).tier;
18044
+ let tier = outcome.sideEffectTier ?? getEngineCapability(engine, commandName).tier;
18011
18045
  if (options.dryRun || options.plan) {
18012
18046
  tier = "dry-run";
18013
18047
  }
@@ -18940,6 +18974,7 @@ __export(exports_permission_guard, {
18940
18974
  checkPermissionForClassification: () => checkPermissionForClassification,
18941
18975
  checkPermission: () => checkPermission,
18942
18976
  SQL_WRITE_OR_DDL_KEYWORDS: () => SQL_WRITE_OR_DDL_KEYWORDS,
18977
+ SQL_LOCK_CLAUSE: () => SQL_LOCK_CLAUSE,
18943
18978
  SQL_DIALECTS: () => SQL_DIALECTS,
18944
18979
  PermissionError: () => PermissionError
18945
18980
  });
@@ -22358,7 +22393,8 @@ async function enforceTierTwo(request, attended) {
22358
22393
  const { verdict } = request;
22359
22394
  const reason = verdict.reason ?? "no_where";
22360
22395
  if (!attended) {
22361
- throw new WriteGateRefusal(`${t_vars("ceremony.gate_refused", { reason })} ${t(REMEDY_KEY[reason])}`, reason, verdict.table);
22396
+ const inShell = process.env.DBCLI_SHELL_SUBCOMMAND === "1";
22397
+ throw new WriteGateRefusal(inShell ? `${t_vars("ceremony.gate_refused_in_shell", { reason })} ${t("ceremony.gate_remedy_in_shell")}` : `${t_vars("ceremony.gate_refused", { reason })} ${t(REMEDY_KEY[reason])}`, reason, verdict.table);
22362
22398
  }
22363
22399
  writeSummary(request);
22364
22400
  const warning = verdict.table === undefined && reason === "ddl_destruction" ? t("ceremony.gate_ddl_warning_untargeted") : t_vars(WARNING_KEY[reason], { table: verdict.table ?? "" });
@@ -22366,7 +22402,13 @@ async function enforceTierTwo(request, attended) {
22366
22402
  ${warning}
22367
22403
  ${t(REMEDY_KEY[reason])}
22368
22404
  `);
22369
- const typed = await promptUser.text(t_vars("ceremony.gate_typed_prompt", { phrase: verdict.confirmationPhrase }));
22405
+ const ask = request.ask ?? promptUser.text;
22406
+ const typed = await ask(t_vars("ceremony.gate_typed_prompt", { phrase: verdict.confirmationPhrase }));
22407
+ if (typed === null) {
22408
+ process.stderr.write(`${t("ceremony.gate_cancelled")}
22409
+ `);
22410
+ return false;
22411
+ }
22370
22412
  const matched = typed.trim().toLowerCase() === verdict.confirmationPhrase.toLowerCase();
22371
22413
  if (!matched) {
22372
22414
  process.stderr.write(`${t_vars("ceremony.gate_typed_mismatch", { phrase: verdict.confirmationPhrase })}
@@ -22404,14 +22446,18 @@ var init_write_gate_prompt = __esm(() => {
22404
22446
  ddl_destruction: "ceremony.gate_remedy_ddl_destruction",
22405
22447
  unparseable: "ceremony.gate_remedy_unparseable",
22406
22448
  non_unique_where: "ceremony.gate_remedy_non_unique_where",
22407
- multiple_statements: "ceremony.gate_remedy_multiple_statements"
22449
+ multiple_statements: "ceremony.gate_remedy_multiple_statements",
22450
+ multi_table: "ceremony.gate_remedy_multi_table",
22451
+ nested_write: "ceremony.gate_remedy_nested_write"
22408
22452
  };
22409
22453
  WARNING_KEY = {
22410
22454
  no_where: "ceremony.gate_full_table_warning",
22411
22455
  ddl_destruction: "ceremony.gate_ddl_warning",
22412
22456
  unparseable: "ceremony.gate_unparseable_warning",
22413
22457
  non_unique_where: "ceremony.gate_full_table_warning",
22414
- multiple_statements: "ceremony.gate_multiple_statements_warning"
22458
+ multiple_statements: "ceremony.gate_multiple_statements_warning",
22459
+ multi_table: "ceremony.gate_multi_table_warning",
22460
+ nested_write: "ceremony.gate_nested_write_warning"
22415
22461
  };
22416
22462
  });
22417
22463
 
@@ -22446,6 +22492,12 @@ async function classifySqlWriteGate(sql, options = {}) {
22446
22492
  if (containsMultipleStatements(sql, dialect)) {
22447
22493
  return verdict("two", "multiple_statements", undefined, type);
22448
22494
  }
22495
+ const nested = nestedWrite(sql, dialect);
22496
+ if (nested)
22497
+ return verdict("two", "nested_write", undefined, nested);
22498
+ const merge = classifyMerge(sql, dialect);
22499
+ if (merge)
22500
+ return merge;
22449
22501
  if (type === "DROP" || type === "TRUNCATE") {
22450
22502
  const matched = DDL_TARGET.exec(sql)?.[1];
22451
22503
  const table = matched ? matched.replace(/[`"[\]]/g, "") : undefined;
@@ -22464,20 +22516,139 @@ async function classifyQualification(sql, dialect, type) {
22464
22516
  return unparseable(sql, dialect, type);
22465
22517
  }
22466
22518
  const statement = Array.isArray(ast) ? ast[0] : ast;
22467
- if (!statement)
22519
+ if (!statement || statement.type !== "update" && statement.type !== "delete") {
22468
22520
  return unparseable(sql, dialect, type);
22469
- const table = tableName(statement.from?.[0] ?? statement.table?.[0]);
22470
- if (statement.where != null || statement.limit != null) {
22521
+ }
22522
+ const scope = [...statement.table ?? [], ...statement.from ?? []];
22523
+ const sources = new Set(scope.map((ref, index) => refIdentity(ref) ?? `#unnamed-${index}`));
22524
+ if (sources.size > 1) {
22525
+ return verdict("two", "multi_table", undefined, type);
22526
+ }
22527
+ const target = normaliseRef(statement.table?.[0] ?? scope[0]);
22528
+ const table = tableName(target);
22529
+ if (statement.limit != null)
22530
+ return verdict("one", undefined, table, type);
22531
+ if (statement.where != null && narrowsTarget(statement.where, refNames(target))) {
22471
22532
  return verdict("one", undefined, table, type);
22472
22533
  }
22473
22534
  return verdict("two", "no_where", table, type);
22474
22535
  }
22536
+ function normaliseRef(ref) {
22537
+ if (ref?.table?.toUpperCase() !== "ONLY")
22538
+ return ref;
22539
+ if (typeof ref.as !== "string" || ref.as.length === 0)
22540
+ return ref;
22541
+ return {
22542
+ db: ref.db,
22543
+ on: ref.on,
22544
+ as: null,
22545
+ table: typeof ref.as === "string" ? ref.as : undefined
22546
+ };
22547
+ }
22548
+ function refIdentity(reference) {
22549
+ const ref = normaliseRef(reference);
22550
+ if (typeof ref?.table !== "string")
22551
+ return;
22552
+ return (ref.db ? `${ref.db}.${ref.table}` : ref.table).toLowerCase();
22553
+ }
22554
+ function refNames(reference) {
22555
+ const ref = normaliseRef(reference);
22556
+ const name = typeof ref?.as === "string" && ref.as.length > 0 ? ref.as : ref?.table;
22557
+ return typeof name === "string" ? [name.toLowerCase()] : [];
22558
+ }
22559
+ function narrowsTarget(where, targetNames) {
22560
+ const names = new Set(targetNames);
22561
+ const { owners, correlated, skippedSubquery } = scanCondition(where);
22562
+ if (owners.length === 0 && correlated.length === 0)
22563
+ return !skippedSubquery;
22564
+ return owners.some((owner) => owner === null || names.has(owner)) || correlated.some((owner) => names.has(owner));
22565
+ }
22566
+ function mergeScans(scans) {
22567
+ return {
22568
+ owners: scans.flatMap((scan) => scan.owners),
22569
+ correlated: scans.flatMap((scan) => scan.correlated),
22570
+ skippedSubquery: scans.some((scan) => scan.skippedSubquery)
22571
+ };
22572
+ }
22573
+ function scanCondition(node, bound) {
22574
+ if (node === null || typeof node !== "object")
22575
+ return EMPTY_SCAN;
22576
+ const record = node;
22577
+ const inner = record.ast ?? (record.type === "select" ? record : undefined);
22578
+ const nested = inner !== undefined;
22579
+ if (record.type === "column_ref") {
22580
+ const owner = record.table;
22581
+ const name = owner == null ? null : String(owner).toLowerCase();
22582
+ if (bound === undefined)
22583
+ return { owners: [name], correlated: [], skippedSubquery: false };
22584
+ if (name === null || bound.has(name))
22585
+ return EMPTY_SCAN;
22586
+ return { owners: [], correlated: [name], skippedSubquery: false };
22587
+ }
22588
+ const scope = nested ? new Set([...bound ?? [], ...boundNames(inner)]) : bound;
22589
+ const scans = Object.values(record).map((value) => Array.isArray(value) ? mergeScans(value.map((item) => scanCondition(item, scope))) : scanCondition(value, scope));
22590
+ const merged = mergeScans(scans);
22591
+ return nested ? { ...merged, skippedSubquery: true } : merged;
22592
+ }
22593
+ function boundNames(select2) {
22594
+ const from = select2?.from ?? [];
22595
+ return Array.isArray(from) ? from.flatMap((ref) => refNames(ref)) : [];
22596
+ }
22597
+ function stripParenthesised(text2) {
22598
+ let previous = text2;
22599
+ for (;; ) {
22600
+ const next = previous.replace(/\([^()]*\)/g, " ");
22601
+ if (next === previous)
22602
+ return next;
22603
+ previous = next;
22604
+ }
22605
+ }
22475
22606
  function unparseable(sql, dialect, type) {
22476
22607
  const executable = stripCommentsAndStrings(sql, dialect ? { dialect } : {});
22608
+ const statementLevel = stripParenthesised(executable);
22609
+ const singleTable = SINGLE_TARGET.test(executable) && !SECOND_SOURCE_ANYWHERE.test(executable) && !SECOND_SOURCE_AT_STATEMENT_LEVEL.some((pattern) => pattern.test(statementLevel));
22610
+ if (!singleTable)
22611
+ return verdict("two", "unparseable", undefined, type);
22477
22612
  if (/\bWHERE\b|\bLIMIT\b/i.test(executable))
22478
22613
  return verdict("one", undefined, undefined, type);
22479
22614
  return verdict("two", "unparseable", undefined, type);
22480
22615
  }
22616
+ function classifyMerge(sql, dialect) {
22617
+ const executable = stripParenthesised(stripCommentsAndStrings(sql, dialect ? { dialect } : {}));
22618
+ if (!MERGE_STATEMENT.test(executable))
22619
+ return;
22620
+ const matched = MERGE_TARGET.exec(executable)?.[1];
22621
+ const table = matched === undefined || matched.endsWith(".") || /^USING$/i.test(matched) ? undefined : matched;
22622
+ const destructive = /\bTHEN\s+DELETE\b/i.test(executable) ? "DELETE" : /\bTHEN\s+UPDATE\b/i.test(executable) ? "UPDATE" : undefined;
22623
+ if (!destructive)
22624
+ return verdict("one", undefined, table, "INSERT");
22625
+ return verdict("two", "multi_table", table, destructive);
22626
+ }
22627
+ function nestedWrite(sql, dialect) {
22628
+ const executable = stripCommentsAndStrings(sql, dialect ? { dialect } : {}).replace(SQL_LOCK_CLAUSE, " ").replace(REFERENTIAL_ACTION, " ");
22629
+ const match = /\b(INSERT|UPDATE|DELETE)\b|\bMERGE\s+INTO\b/i.exec(insideParentheses(executable));
22630
+ if (match === null)
22631
+ return;
22632
+ return keywordToType(match[1] ?? "MERGE");
22633
+ }
22634
+ function insideParentheses(text2) {
22635
+ let depth = 0;
22636
+ let result = "";
22637
+ for (const char of text2) {
22638
+ if (char === "(") {
22639
+ depth += 1;
22640
+ result += " ";
22641
+ continue;
22642
+ }
22643
+ if (char === ")") {
22644
+ depth = Math.max(0, depth - 1);
22645
+ result += " ";
22646
+ continue;
22647
+ }
22648
+ result += depth > 0 ? char : " ";
22649
+ }
22650
+ return result;
22651
+ }
22481
22652
  function classifyStructuredWriteGate(request) {
22482
22653
  const conditions = new Set(Object.keys(request.where).map((key) => key.toLowerCase()));
22483
22654
  const identifies = uniqueSelectors(request.schema).some((selector) => selector.length > 0 && selector.every((column) => conditions.has(column.toLowerCase())));
@@ -22511,7 +22682,7 @@ function verdict(tier, reason, table, operation) {
22511
22682
  confirmationPhrase: table ?? WRITE_GATE_FALLBACK_PHRASE
22512
22683
  };
22513
22684
  }
22514
- var WRITE_GATE_FALLBACK_PHRASE = "CONFIRM", PARSER_DIALECTS, WRITE_TYPES, DDL_TARGET;
22685
+ var WRITE_GATE_FALLBACK_PHRASE = "CONFIRM", PARSER_DIALECTS, WRITE_TYPES, DDL_TARGET, EMPTY_SCAN, SINGLE_TARGET, SECOND_SOURCE_ANYWHERE, SECOND_SOURCE_AT_STATEMENT_LEVEL, MERGE_STATEMENT, MERGE_TARGET, REFERENTIAL_ACTION;
22515
22686
  var init_write_gate = __esm(() => {
22516
22687
  init_permission_guard();
22517
22688
  init_sql_analysis();
@@ -22530,6 +22701,13 @@ var init_write_gate = __esm(() => {
22530
22701
  "CREATE"
22531
22702
  ]);
22532
22703
  DDL_TARGET = /\b(?:DROP\s+TABLE|TRUNCATE(?:\s+TABLE)?)\s+(?:IF\s+EXISTS\s+)?([`"[\]\w$.]+)\s*;?\s*$/i;
22704
+ EMPTY_SCAN = { owners: [], correlated: [], skippedSubquery: false };
22705
+ SINGLE_TARGET = /^\s*(?:UPDATE\s+(?:ONLY\s+)?[\w`"[\]$.]+\s+SET\b|DELETE\s+FROM\s+(?:ONLY\s+)?[\w`"[\]$.]+(?!\s*,)(?:\s|$))/i;
22706
+ SECOND_SOURCE_ANYWHERE = /\b(?:SELECT|TABLE|VALUES|JOIN|USING)\b/i;
22707
+ SECOND_SOURCE_AT_STATEMENT_LEVEL = [/\bWITH\b/i, /\bUPDATE\b[\s\S]*?\bFROM\b/i];
22708
+ MERGE_STATEMENT = /\bMERGE\s+INTO\b/i;
22709
+ MERGE_TARGET = /\bMERGE\s+INTO\s+(?:ONLY\s+)?([\w$.]+)/i;
22710
+ REFERENTIAL_ACTION = /\bON\s+(?:DELETE|UPDATE)\b/gi;
22533
22711
  });
22534
22712
 
22535
22713
  // src/commands/write-gate-guard.ts
@@ -22541,6 +22719,7 @@ __export(exports_write_gate_guard, {
22541
22719
  async function recordGateDecision(request) {
22542
22720
  await writeAuditEntry(request.config, request.command, request.options, {
22543
22721
  success: request.outcome === "allowed",
22722
+ sideEffectTier: "db-write",
22544
22723
  ...request.sql && { sql: request.sql },
22545
22724
  ...request.verdict.table && { target: request.verdict.table },
22546
22725
  metadata: {
@@ -25491,9 +25670,12 @@ async function qCommand(name, options, command) {
25491
25670
  console.error(`\u26A0 ${w2}`);
25492
25671
  if (prepared.warnings.length > 0)
25493
25672
  console.error("");
25673
+ const family = engineFamily(engine);
25674
+ const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
25675
+ const classification = family === "sql" ? enforcePermission(prepared.rewrittenSql, config.permission, sqlDialect) : undefined;
25494
25676
  if (options.dryRun) {
25495
25677
  console.log(formatDryRun({
25496
- family: engineFamily(engine),
25678
+ family,
25497
25679
  driverSql: prepared.driver.sql,
25498
25680
  values: prepared.driver.values,
25499
25681
  execHints: prepared.execHints,
@@ -25514,8 +25696,6 @@ async function qCommand(name, options, command) {
25514
25696
  }
25515
25697
  const blacklistManager = new BlacklistManager(config);
25516
25698
  const blacklistValidator = new BlacklistValidator(blacklistManager);
25517
- const family = engineFamily(engine);
25518
- const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
25519
25699
  const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
25520
25700
  ...sqlDialect ? { dialect: sqlDialect } : {}
25521
25701
  }) : family === "es" ? [prepared.execHints?.index ?? ""] : [];
@@ -25523,8 +25703,8 @@ async function qCommand(name, options, command) {
25523
25703
  targetNameForAudit = targetName || name;
25524
25704
  if (family === "es") {
25525
25705
  blacklistValidator.checkIndexBlacklist("SELECT", targetName);
25526
- } else if (family !== "redis" && targets.length > 0) {
25527
- blacklistValidator.checkTablesBlacklist("SELECT", targets);
25706
+ } else if (classification && targets.length > 0) {
25707
+ blacklistValidator.checkTablesBlacklist(classification.type, targets);
25528
25708
  }
25529
25709
  const adapter = AdapterFactory.createAdapter(config.connection);
25530
25710
  await adapter.connect();
@@ -36541,6 +36721,81 @@ Verification artifact: ${verificationArtifactPath}`);
36541
36721
  });
36542
36722
  });
36543
36723
 
36724
+ // src/core/audit/write-gate-summary.ts
36725
+ function metadataOf(entry) {
36726
+ const metadata = entry.metadata;
36727
+ return metadata && typeof metadata === "object" ? metadata : {};
36728
+ }
36729
+ function readString(source, key) {
36730
+ const value = source[key];
36731
+ return typeof value === "string" && value.length > 0 ? value : undefined;
36732
+ }
36733
+
36734
+ class Span {
36735
+ from;
36736
+ to;
36737
+ add(ts) {
36738
+ if (this.from === undefined || ts < this.from)
36739
+ this.from = ts;
36740
+ if (this.to === undefined || ts > this.to)
36741
+ this.to = ts;
36742
+ }
36743
+ value() {
36744
+ return this.from === undefined || this.to === undefined ? null : { from: this.from, to: this.to };
36745
+ }
36746
+ }
36747
+ function summarizeWriteGate(entries) {
36748
+ const outcomes = Object.fromEntries(GATE_OUTCOMES.map((o) => [o, 0]));
36749
+ const reasons = Object.fromEntries(GATE_REASONS.map((r) => [r, 0]));
36750
+ const tierOneOutcomes = {};
36751
+ const window = new Span;
36752
+ const range = new Span;
36753
+ let total = 0;
36754
+ let tierOneTotal = 0;
36755
+ const tally = (into, key) => {
36756
+ into[key] = (into[key] ?? 0) + 1;
36757
+ };
36758
+ for (const entry of entries) {
36759
+ window.add(entry.ts);
36760
+ const metadata = metadataOf(entry);
36761
+ const tier = readString(metadata, "write_gate_tier");
36762
+ if (!tier)
36763
+ continue;
36764
+ const outcome = readString(metadata, "write_gate_outcome") ?? "unknown";
36765
+ if (tier !== "two") {
36766
+ tierOneTotal += 1;
36767
+ tally(tierOneOutcomes, outcome);
36768
+ continue;
36769
+ }
36770
+ total += 1;
36771
+ range.add(entry.ts);
36772
+ tally(outcomes, outcome);
36773
+ tally(reasons, readString(metadata, "write_gate_reason") ?? "unknown");
36774
+ }
36775
+ return {
36776
+ scanned: entries.length,
36777
+ window: window.value(),
36778
+ total,
36779
+ outcomes,
36780
+ reasons,
36781
+ tierOne: { total: tierOneTotal, outcomes: tierOneOutcomes },
36782
+ range: range.value()
36783
+ };
36784
+ }
36785
+ var GATE_OUTCOMES, GATE_REASONS;
36786
+ var init_write_gate_summary = __esm(() => {
36787
+ GATE_OUTCOMES = ["allowed", "declined", "refused"];
36788
+ GATE_REASONS = [
36789
+ "no_where",
36790
+ "ddl_destruction",
36791
+ "unparseable",
36792
+ "multiple_statements",
36793
+ "non_unique_where",
36794
+ "multi_table",
36795
+ "nested_write"
36796
+ ];
36797
+ });
36798
+
36544
36799
  // src/commands/audit.ts
36545
36800
  var exports_audit = {};
36546
36801
  __export(exports_audit, {
@@ -36726,6 +36981,53 @@ function readLineFromStdinWithStderrPrompt(prompt) {
36726
36981
  process.stdin.resume();
36727
36982
  });
36728
36983
  }
36984
+ function formatOutcomeCounts(outcomes) {
36985
+ const seen = Object.entries(outcomes).filter(([, count]) => count > 0);
36986
+ if (seen.length === 0)
36987
+ return MISSING_PLACEHOLDER;
36988
+ return seen.map(([name, count]) => `${name} ${count}`).join(", ");
36989
+ }
36990
+ function renderWriteGateSummary(summary, connections) {
36991
+ const lines = [];
36992
+ lines.push(t_vars("audit.write_gate.header", {
36993
+ conn: connections.length > 0 ? connections.join(", ") : MISSING_PLACEHOLDER
36994
+ }));
36995
+ if (summary.window) {
36996
+ lines.push(t_vars("audit.write_gate.window", {
36997
+ scanned: String(summary.scanned),
36998
+ from: summary.window.from,
36999
+ to: summary.window.to
37000
+ }));
37001
+ }
37002
+ if (summary.total === 0) {
37003
+ lines.push("");
37004
+ lines.push(summary.scanned === 0 ? t("audit.write_gate.no_entries") : t("audit.write_gate.no_decisions"));
37005
+ if (summary.tierOne.total > 0) {
37006
+ lines.push(t_vars("audit.write_gate.tier_one", {
37007
+ count: String(summary.tierOne.total),
37008
+ outcomes: formatOutcomeCounts(summary.tierOne.outcomes)
37009
+ }));
37010
+ }
37011
+ return lines.join(`
37012
+ `);
37013
+ }
37014
+ lines.push(t_vars("audit.write_gate.decisions", {
37015
+ count: String(summary.total),
37016
+ from: summary.range?.from ?? MISSING_PLACEHOLDER,
37017
+ to: summary.range?.to ?? MISSING_PLACEHOLDER
37018
+ }));
37019
+ lines.push("");
37020
+ lines.push(renderTable(Object.entries(summary.outcomes).map(([name, count]) => [name, String(count)]), ["outcome", "count"]));
37021
+ lines.push("");
37022
+ lines.push(renderTable(Object.entries(summary.reasons).map(([name, count]) => [name, String(count)]), ["reason", "count"]));
37023
+ lines.push("");
37024
+ lines.push(t_vars("audit.write_gate.tier_one", {
37025
+ count: String(summary.tierOne.total),
37026
+ outcomes: formatOutcomeCounts(summary.tierOne.outcomes)
37027
+ }));
37028
+ return lines.join(`
37029
+ `);
37030
+ }
36729
37031
  async function statAuditFile(file) {
36730
37032
  try {
36731
37033
  const s = await stat7(file);
@@ -36745,6 +37047,7 @@ var init_audit = __esm(() => {
36745
37047
  init_config_binding();
36746
37048
  init_reader2();
36747
37049
  init_integration_helper();
37050
+ init_write_gate_summary();
36748
37051
  ALLOWED_FORMATS14 = ["table", "json"];
36749
37052
  auditCommand = new Command("audit").description(t("audit.description"));
36750
37053
  auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
@@ -36952,6 +37255,33 @@ var init_audit = __esm(() => {
36952
37255
  `);
36953
37256
  process.exit(0);
36954
37257
  });
37258
+ auditCommand.command("write-gate").description(t("audit.write_gate.description")).option("--all", "Summarize across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--for-agent", "Shortcut for --format json", false).action(async (options, command) => {
37259
+ const format = options.forAgent === true ? "json" : options.format;
37260
+ validateFormat(format, ALLOWED_FORMATS14, "audit write-gate");
37261
+ const configPath = resolveConfigPath(command, options);
37262
+ const config = await configModule.read(configPath);
37263
+ if (isAuditDisabled(config))
37264
+ emitDisabledAndExit0();
37265
+ const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
37266
+ let entries = [];
37267
+ let connections = [connectionName];
37268
+ if (options.all === true) {
37269
+ const discovered = await discoverConnections(auditDir);
37270
+ connections = discovered.map((c) => c.connection);
37271
+ for (const c of discovered) {
37272
+ for (const file of c.files)
37273
+ entries.push(...await readEntries(file));
37274
+ }
37275
+ } else {
37276
+ entries = await readEntries(auditFile, { include_rotated: true });
37277
+ }
37278
+ const summary = summarizeWriteGate(entries);
37279
+ if (format === "json") {
37280
+ console.log(JSON.stringify({ connections, summary }, null, 2));
37281
+ return;
37282
+ }
37283
+ console.log(renderWriteGateSummary(summary, connections));
37284
+ });
36955
37285
  auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
36956
37286
  const forAgent = options.forAgent === true;
36957
37287
  const format = forAgent ? "json" : options.format;
@@ -41196,20 +41526,39 @@ var init_grammar = __esm(() => {
41196
41526
  });
41197
41527
 
41198
41528
  // src/core/assert/evaluator.ts
41529
+ function numeric(value) {
41530
+ if (typeof value === "number")
41531
+ return Number.isNaN(value) ? null : value;
41532
+ const trimmed = value.trim();
41533
+ if (trimmed === "")
41534
+ return null;
41535
+ const parsed = Number(trimmed);
41536
+ return Number.isNaN(parsed) ? null : parsed;
41537
+ }
41199
41538
  function compare(a, op, b) {
41539
+ let left = a;
41540
+ let right = b;
41541
+ if (typeof a !== typeof b) {
41542
+ const na = numeric(a);
41543
+ const nb = numeric(b);
41544
+ if (na !== null && nb !== null) {
41545
+ left = na;
41546
+ right = nb;
41547
+ }
41548
+ }
41200
41549
  switch (op) {
41201
41550
  case ">":
41202
- return a > b;
41551
+ return left > right;
41203
41552
  case ">=":
41204
- return a >= b;
41553
+ return left >= right;
41205
41554
  case "<":
41206
- return a < b;
41555
+ return left < right;
41207
41556
  case "<=":
41208
- return a <= b;
41557
+ return left <= right;
41209
41558
  case "==":
41210
- return a === b;
41559
+ return left === right;
41211
41560
  case "!=":
41212
- return a !== b;
41561
+ return left !== right;
41213
41562
  }
41214
41563
  }
41215
41564
  function firstScalar(result) {
@@ -41262,7 +41611,10 @@ function evaluateExpect(node, result) {
41262
41611
  };
41263
41612
  }
41264
41613
  if (pred.type === "between") {
41265
- const bad2 = nonNull.filter((v) => typeof v !== "number" || v < pred.low || v > pred.high);
41614
+ const bad2 = nonNull.filter((v) => {
41615
+ const n = numeric(v);
41616
+ return n === null || n < pred.low || n > pred.high;
41617
+ });
41266
41618
  return {
41267
41619
  name: `col:${column} between ${pred.low} and ${pred.high}`,
41268
41620
  expected: "0 out of range",
@@ -41293,7 +41645,7 @@ function compareVs(a, b, mode) {
41293
41645
  name: "vs:value",
41294
41646
  expected: String(bv),
41295
41647
  actual: String(av),
41296
- pass: av !== null && bv !== null && av === bv
41648
+ pass: av !== null && bv !== null && compare(av, "==", bv)
41297
41649
  };
41298
41650
  }
41299
41651
  var AssertShapeError;
@@ -41366,7 +41718,7 @@ function canonicalizeEvidenceReceiptCommand(command, operation = "assert") {
41366
41718
  function sha2563(value) {
41367
41719
  return `sha256:${createHash5("sha256").update(value).digest("hex")}`;
41368
41720
  }
41369
- function verdictFingerprint(verdict2) {
41721
+ function verdictObservation(verdict2) {
41370
41722
  if (!record3(verdict2) || typeof verdict2.pass !== "boolean" || !Array.isArray(verdict2.checks) || verdict2.checks.length > 1000) {
41371
41723
  throw new EvidenceReceiptValidationError("verdict must contain bounded pass bits");
41372
41724
  }
@@ -41376,26 +41728,30 @@ function verdictFingerprint(verdict2) {
41376
41728
  }
41377
41729
  return check.pass;
41378
41730
  });
41379
- return sha2563(JSON.stringify({ pass: verdict2.pass, checks: bits }));
41731
+ return {
41732
+ kind: "assert-verdict",
41733
+ checksPassed: bits.filter(Boolean).length,
41734
+ checksTotal: bits.length
41735
+ };
41380
41736
  }
41381
- function verificationOutcomeFingerprint(status2, artifactPersisted) {
41737
+ function verificationObservation(status2, artifactPersisted) {
41382
41738
  if (!isVerificationStatus(status2)) {
41383
41739
  throw new EvidenceReceiptValidationError("verificationStatus must be a verification status");
41384
41740
  }
41385
41741
  if (typeof artifactPersisted !== "boolean") {
41386
41742
  throw new EvidenceReceiptValidationError("verificationArtifactPersisted must be a boolean");
41387
41743
  }
41388
- return sha2563(JSON.stringify({ status: status2, artifactPersisted }));
41744
+ return { kind: "verify-outcome", status: status2 };
41389
41745
  }
41390
41746
  function buildEvidenceReceipt(input, options = {}) {
41391
41747
  const operation = input.operation === "verify" ? "verify" : "assert";
41392
41748
  const command = canonicalizeEvidenceReceiptCommand(input.command, operation);
41393
41749
  const context = parseContext2(input.context);
41394
- const { observationFingerprint, outcome } = input.operation === "verify" ? {
41395
- observationFingerprint: verificationOutcomeFingerprint(input.verificationStatus, input.verificationArtifactPersisted),
41750
+ const { observation, outcome } = input.operation === "verify" ? {
41751
+ observation: verificationObservation(input.verificationStatus, input.verificationArtifactPersisted),
41396
41752
  outcome: input.verificationStatus === "verified" && input.verificationArtifactPersisted ? "succeeded" : "failed"
41397
41753
  } : {
41398
- observationFingerprint: verdictFingerprint(input.verdict),
41754
+ observation: verdictObservation(input.verdict),
41399
41755
  outcome: input.verdict.pass ? "succeeded" : "failed"
41400
41756
  };
41401
41757
  const receipt = {
@@ -41412,10 +41768,7 @@ function buildEvidenceReceipt(input, options = {}) {
41412
41768
  verificationArtifactRef: input.verificationArtifactRef === undefined ? null : nullableId(input.verificationArtifactRef, "verificationArtifactRef")
41413
41769
  },
41414
41770
  replay: { status: "context-required" },
41415
- observation: {
41416
- kind: operation === "assert" ? "assert-verdict" : "verify-outcome",
41417
- fingerprint: observationFingerprint
41418
- }
41771
+ observation
41419
41772
  };
41420
41773
  return receipt;
41421
41774
  }
@@ -41460,9 +41813,7 @@ function parseEvidenceReceipt(raw) {
41460
41813
  throw new EvidenceReceiptValidationError("command provenance hash mismatch");
41461
41814
  if (!record3(raw.replay) || raw.replay.status !== "context-required" || Object.keys(raw.replay).length !== 1)
41462
41815
  throw new EvidenceReceiptValidationError("replay must be context-required");
41463
- if (!record3(raw.observation) || raw.observation.kind !== (operation === "assert" ? "assert-verdict" : "verify-outcome") || Object.keys(raw.observation).length !== 2)
41464
- throw new EvidenceReceiptValidationError("observation must match receipt operation");
41465
- const observationKind = operation === "assert" ? "assert-verdict" : "verify-outcome";
41816
+ const observation = parseObservation(raw.observation, operation);
41466
41817
  return {
41467
41818
  version: EVIDENCE_RECEIPT_VERSION,
41468
41819
  id: safeId(raw.id, "id"),
@@ -41477,10 +41828,26 @@ function parseEvidenceReceipt(raw) {
41477
41828
  verificationArtifactRef: nullableId(raw.provenance.verificationArtifactRef, "provenance.verificationArtifactRef")
41478
41829
  },
41479
41830
  replay: { status: "context-required" },
41480
- observation: {
41481
- kind: observationKind,
41482
- fingerprint: fingerprint(raw.observation.fingerprint, "observation.fingerprint")
41483
- }
41831
+ observation
41832
+ };
41833
+ }
41834
+ function parseObservation(raw, operation) {
41835
+ if (!record3(raw))
41836
+ throw new EvidenceReceiptValidationError("observation must be an object");
41837
+ if (operation === "verify") {
41838
+ exact(raw, ["kind", "status"], "observation");
41839
+ if (raw.kind !== "verify-outcome" || !isVerificationStatus(raw.status))
41840
+ throw new EvidenceReceiptValidationError("observation must match receipt operation");
41841
+ return { kind: "verify-outcome", status: raw.status };
41842
+ }
41843
+ exact(raw, ["kind", "checksPassed", "checksTotal"], "observation");
41844
+ const { kind, checksPassed, checksTotal } = raw;
41845
+ if (kind !== "assert-verdict" || !Number.isSafeInteger(checksPassed) || !Number.isSafeInteger(checksTotal) || checksPassed < 0 || checksTotal < checksPassed)
41846
+ throw new EvidenceReceiptValidationError("observation must match receipt operation");
41847
+ return {
41848
+ kind: "assert-verdict",
41849
+ checksPassed,
41850
+ checksTotal
41484
41851
  };
41485
41852
  }
41486
41853
  function inside(root, target) {
@@ -44242,6 +44609,9 @@ function classifyInput(raw) {
44242
44609
  return { type: "meta", raw, normalized: normalized2 };
44243
44610
  }
44244
44611
  }
44612
+ if (normalized2.startsWith(COMMAND_PREFIX)) {
44613
+ return { type: "command", raw, normalized: normalized2 };
44614
+ }
44245
44615
  const firstWord = (normalized2.split(/\s+/)[0] ?? "").toUpperCase();
44246
44616
  if (sqlKeywordSet.has(firstWord)) {
44247
44617
  return { type: "sql", raw, normalized: normalized2 };
@@ -44251,7 +44621,7 @@ function classifyInput(raw) {
44251
44621
  }
44252
44622
  return { type: "command", raw, normalized: normalized2 };
44253
44623
  }
44254
- var META_PREFIX = ".", SQL_TERMINATOR = ";", sqlKeywordSet, metaCommandNames;
44624
+ var META_PREFIX = ".", COMMAND_PREFIX = "\\", SQL_TERMINATOR = ";", sqlKeywordSet, metaCommandNames;
44255
44625
  var init_input_classifier = __esm(() => {
44256
44626
  init_types11();
44257
44627
  sqlKeywordSet = new Set(SQL_KEYWORDS_FOR_DETECTION);
@@ -44275,6 +44645,10 @@ class MultilineBuffer {
44275
44645
  isActive() {
44276
44646
  return this.lines.length > 0;
44277
44647
  }
44648
+ isInsideLiteral() {
44649
+ return insideLiteral(this.lines.join(`
44650
+ `));
44651
+ }
44278
44652
  getPartial() {
44279
44653
  return this.lines.join(`
44280
44654
  `);
@@ -44284,9 +44658,17 @@ class MultilineBuffer {
44284
44658
  }
44285
44659
  }
44286
44660
  function hasUnquotedSemicolon(sql) {
44661
+ return scan(sql).semicolon;
44662
+ }
44663
+ function insideLiteral(sql) {
44664
+ const { inSingleQuote, inDoubleQuote } = scan(sql);
44665
+ return inSingleQuote || inDoubleQuote;
44666
+ }
44667
+ function scan(sql) {
44287
44668
  let inSingleQuote = false;
44288
44669
  let inDoubleQuote = false;
44289
44670
  let escaped = false;
44671
+ let semicolon = false;
44290
44672
  for (let i = 0;i < sql.length; i++) {
44291
44673
  const ch = sql[i];
44292
44674
  if (escaped) {
@@ -44306,10 +44688,10 @@ function hasUnquotedSemicolon(sql) {
44306
44688
  continue;
44307
44689
  }
44308
44690
  if (ch === ";" && !inSingleQuote && !inDoubleQuote) {
44309
- return true;
44691
+ semicolon = true;
44310
44692
  }
44311
44693
  }
44312
- return false;
44694
+ return { semicolon, inSingleQuote, inDoubleQuote };
44313
44695
  }
44314
44696
 
44315
44697
  // src/core/repl/meta-commands.ts
@@ -44353,7 +44735,8 @@ function helpText() {
44353
44735
  "",
44354
44736
  import_picocolors2.default.bold("Usage:"),
44355
44737
  ` SQL statements Execute directly (end with ${import_picocolors2.default.cyan(";")})`,
44356
- ` dbcli commands Run without ${import_picocolors2.default.dim("dbcli")} prefix (e.g. ${import_picocolors2.default.cyan("schema users")})`,
44738
+ ` dbcli commands Run without the ${import_picocolors2.default.dim("dbcli")} prefix (e.g. ${import_picocolors2.default.cyan("schema users")})`,
44739
+ ` ${import_picocolors2.default.cyan("\\<command>")} Force the dbcli subcommand \u2014 required for ${import_picocolors2.default.dim("delete")}, ${import_picocolors2.default.dim("update")}, ${import_picocolors2.default.dim("insert")} and other SQL keywords (e.g. ${import_picocolors2.default.cyan("\\delete users --where id=1")})`,
44357
44740
  ` Ctrl+D Exit`
44358
44741
  ];
44359
44742
  return lines.join(`
@@ -44457,24 +44840,26 @@ function splitRespectingQuotes(input) {
44457
44840
  let current = "";
44458
44841
  let inDoubleQuote = false;
44459
44842
  let inSingleQuote = false;
44843
+ let quoted = false;
44460
44844
  for (let i = 0;i < input.length; i++) {
44461
44845
  const ch = input[i];
44462
44846
  if (ch === '"' && !inSingleQuote) {
44463
44847
  inDoubleQuote = !inDoubleQuote;
44464
- current += ch;
44848
+ quoted = true;
44465
44849
  } else if (ch === "'" && !inDoubleQuote) {
44466
44850
  inSingleQuote = !inSingleQuote;
44467
- current += ch;
44851
+ quoted = true;
44468
44852
  } else if (ch === " " && !inDoubleQuote && !inSingleQuote) {
44469
- if (current.length > 0) {
44853
+ if (current.length > 0 || quoted) {
44470
44854
  tokens.push(current);
44471
44855
  current = "";
44856
+ quoted = false;
44472
44857
  }
44473
44858
  } else {
44474
44859
  current += ch;
44475
44860
  }
44476
44861
  }
44477
- if (current.length > 0) {
44862
+ if (current.length > 0 || quoted) {
44478
44863
  tokens.push(current);
44479
44864
  }
44480
44865
  return tokens;
@@ -44540,14 +44925,16 @@ var init_history_manager = () => {};
44540
44925
  class ReplEngine {
44541
44926
  adapter;
44542
44927
  context;
44928
+ writeGate;
44543
44929
  state;
44544
44930
  buffer;
44545
44931
  history;
44546
44932
  formatter;
44547
44933
  config;
44548
- constructor(adapter, context, historyPath, config = null) {
44934
+ constructor(adapter, context, historyPath, config = null, writeGate = null) {
44549
44935
  this.adapter = adapter;
44550
44936
  this.context = context;
44937
+ this.writeGate = writeGate;
44551
44938
  this.state = { format: "table", timing: false, connected: true, noLimit: false };
44552
44939
  this.buffer = new MultilineBuffer;
44553
44940
  this.history = new HistoryManager(historyPath);
@@ -44560,6 +44947,9 @@ class ReplEngine {
44560
44947
  isMultiline() {
44561
44948
  return this.buffer.isActive();
44562
44949
  }
44950
+ cancelMultiline() {
44951
+ this.buffer.reset();
44952
+ }
44563
44953
  getHistory() {
44564
44954
  return this.history.getAll();
44565
44955
  }
@@ -44568,6 +44958,13 @@ class ReplEngine {
44568
44958
  }
44569
44959
  async processInput(line) {
44570
44960
  if (this.buffer.isActive()) {
44961
+ const classified2 = classifyInput(line);
44962
+ if (classified2.type === "meta" && !this.buffer.isInsideLiteral()) {
44963
+ const result = this.handleMeta(classified2.normalized);
44964
+ if (result.action === "quit" || result.action === "clear")
44965
+ this.buffer.reset();
44966
+ return result;
44967
+ }
44571
44968
  const bufResult = this.buffer.append(line);
44572
44969
  if (bufResult.complete) {
44573
44970
  this.history.add(bufResult.sql);
@@ -44604,14 +45001,27 @@ class ReplEngine {
44604
45001
  this.history.add(bufResult.sql);
44605
45002
  return this.executeSql(bufResult.sql);
44606
45003
  }
44607
- return { action: "multiline" };
45004
+ const hint = this.subcommandHint(input);
45005
+ return hint ? { action: "multiline", output: hint } : { action: "multiline" };
45006
+ }
45007
+ subcommandHint(input) {
45008
+ const words = input.trim().split(/\s+/);
45009
+ const firstWord = (words[0] ?? "").toLowerCase();
45010
+ if (!this.context.commandNames.includes(firstWord))
45011
+ return;
45012
+ if (!/\s--[a-z]/.test(input))
45013
+ return;
45014
+ if (STATEMENT_CONTINUATION.has((words[1] ?? "").toUpperCase()))
45015
+ return;
45016
+ return import_picocolors3.default.dim(t_vars("shell.subcommand_prefix_hint", { command: `${COMMAND_PREFIX}${firstWord}` }));
44608
45017
  }
44609
45018
  async handleCommand(input) {
44610
- const parsed = parseCommandLine(input);
45019
+ const invocation = input.startsWith(COMMAND_PREFIX) ? input.slice(COMMAND_PREFIX.length) : input;
45020
+ const parsed = parseCommandLine(invocation);
44611
45021
  if (!isKnownCommand(parsed.command, this.context.commandNames)) {
44612
45022
  if (this.context.system === "redis") {
44613
45023
  this.history.add(input);
44614
- return this.executeSql(input);
45024
+ return this.executeSql(invocation);
44615
45025
  }
44616
45026
  return {
44617
45027
  action: "continue",
@@ -44621,11 +45031,11 @@ class ReplEngine {
44621
45031
  this.history.add(input);
44622
45032
  try {
44623
45033
  const cliPath = new URL("../../cli.ts", import.meta.url).pathname;
44624
- const argv = [parsed.command, "--config", this.context.configPath, ...parsed.args];
45034
+ const argv = ["--config", this.context.configPath, parsed.command, ...parsed.args];
44625
45035
  const proc = Bun.spawn(["bun", "run", cliPath, ...argv], {
44626
45036
  stdout: "pipe",
44627
45037
  stderr: "pipe",
44628
- env: { ...process.env }
45038
+ env: { ...process.env, DBCLI_SHELL_SUBCOMMAND: "1" }
44629
45039
  });
44630
45040
  const [stdoutBuf, stderrBuf] = await Promise.all([
44631
45041
  new Response(proc.stdout).text(),
@@ -44681,6 +45091,11 @@ class ReplEngine {
44681
45091
  };
44682
45092
  }
44683
45093
  }
45094
+ if (this.writeGate && !await this.writeGate(sql))
45095
+ return { action: "continue" };
45096
+ return this.runStatement(sql, blacklistValidator, referencedTables);
45097
+ }
45098
+ async runStatement(sql, blacklistValidator, referencedTables) {
44684
45099
  const startTime = Date.now();
44685
45100
  try {
44686
45101
  const result = await this.adapter.execute(sql, undefined, {
@@ -44716,7 +45131,7 @@ class ReplEngine {
44716
45131
  await this.adapter.connect();
44717
45132
  this.state = { ...this.state, connected: true };
44718
45133
  console.error(import_picocolors3.default.green(t("shell.error_reconnect_success")));
44719
- return this.executeSql(sql);
45134
+ return this.runStatement(sql, blacklistValidator, referencedTables);
44720
45135
  } catch (reconnectError) {
44721
45136
  return {
44722
45137
  action: "continue",
@@ -44747,7 +45162,7 @@ class ReplEngine {
44747
45162
  return isTransportFailure(error);
44748
45163
  }
44749
45164
  }
44750
- var import_picocolors3;
45165
+ var import_picocolors3, STATEMENT_CONTINUATION;
44751
45166
  var init_repl_engine = __esm(() => {
44752
45167
  init_connection_error_message();
44753
45168
  init_input_classifier();
@@ -44761,6 +45176,15 @@ var init_repl_engine = __esm(() => {
44761
45176
  init_blacklist_validator();
44762
45177
  init_blacklist();
44763
45178
  import_picocolors3 = __toESM(require_picocolors(), 1);
45179
+ STATEMENT_CONTINUATION = new Set([
45180
+ "FROM",
45181
+ "INTO",
45182
+ "SET",
45183
+ "TABLE",
45184
+ "INDEX",
45185
+ "VIEW",
45186
+ "DATABASE"
45187
+ ]);
44764
45188
  });
44765
45189
 
44766
45190
  // src/core/repl/completer.ts
@@ -44803,6 +45227,11 @@ function createCompleter(ctx) {
44803
45227
  }
44804
45228
  const words = trimmed.split(/\s+/);
44805
45229
  if (words.length === 1) {
45230
+ if (lastWord.startsWith(COMMAND_PREFIX)) {
45231
+ const bare = lastWord.slice(COMMAND_PREFIX.length);
45232
+ const [hits] = matchWithSuffix([...ctx.commandNames], bare);
45233
+ return [hits.map((hit) => `${COMMAND_PREFIX}${hit}`), lastWord];
45234
+ }
44806
45235
  const cmdHits = matchWithSuffix([...ctx.commandNames], lastWord);
44807
45236
  const sqlHits = matchWithSuffix([...SQL_KEYWORDS_FOR_COMPLETION], lastWord);
44808
45237
  return [[...cmdHits[0], ...sqlHits[0]], lastWord];
@@ -44856,6 +45285,7 @@ function matchWithSuffix(candidates, prefix) {
44856
45285
  var REDIS_COMMANDS, TABLE_POSITION_KEYWORDS, COMMANDS_TAKING_TABLE_ARG;
44857
45286
  var init_completer = __esm(() => {
44858
45287
  init_types11();
45288
+ init_input_classifier();
44859
45289
  init_command_metadata();
44860
45290
  REDIS_COMMANDS = Object.keys(REDIS_COMMAND_TABLE);
44861
45291
  TABLE_POSITION_KEYWORDS = new Set([
@@ -45153,6 +45583,94 @@ var init_command_registry = __esm(() => {
45153
45583
  REPL_DENYLIST = new Set(["shell"]);
45154
45584
  });
45155
45585
 
45586
+ // src/commands/shell-prompt-asker.ts
45587
+ var exports_shell_prompt_asker = {};
45588
+ __export(exports_shell_prompt_asker, {
45589
+ createPromptAsker: () => createPromptAsker
45590
+ });
45591
+ function createPromptAsker(getInterface) {
45592
+ let pending = null;
45593
+ return {
45594
+ async ask(question) {
45595
+ const rl = getInterface();
45596
+ if (!rl)
45597
+ throw new Error("Write gate asked for confirmation before the shell started");
45598
+ return new Promise((resolve10) => {
45599
+ const controller = new AbortController;
45600
+ const settle = (answer) => {
45601
+ pending = null;
45602
+ rl.pause();
45603
+ resolve10(answer);
45604
+ };
45605
+ pending = {
45606
+ abort: () => {
45607
+ controller.abort();
45608
+ settle(null);
45609
+ }
45610
+ };
45611
+ try {
45612
+ rl.question(`${question}: `, { signal: controller.signal }, settle);
45613
+ } catch (error) {
45614
+ pending = null;
45615
+ throw error;
45616
+ }
45617
+ });
45618
+ },
45619
+ cancel() {
45620
+ if (!pending)
45621
+ return false;
45622
+ pending.abort();
45623
+ return true;
45624
+ }
45625
+ };
45626
+ }
45627
+
45628
+ // src/commands/shell-write-gate.ts
45629
+ var exports_shell_write_gate = {};
45630
+ __export(exports_shell_write_gate, {
45631
+ createShellWriteGate: () => createShellWriteGate
45632
+ });
45633
+ function createShellWriteGate(options) {
45634
+ const record4 = options.record ?? ((decision) => recordGateDecision({
45635
+ config: options.config,
45636
+ command: "shell",
45637
+ options: { config: options.configPath },
45638
+ verdict: decision.verdict,
45639
+ outcome: decision.outcome,
45640
+ tier: decision.tier,
45641
+ sql: decision.sql
45642
+ }));
45643
+ return async (sql) => {
45644
+ const verdict2 = await classifySqlWriteGate(sql, { dialect: options.dialect });
45645
+ if (verdict2.tier !== "two")
45646
+ return true;
45647
+ const decide5 = (outcome) => record4({ verdict: verdict2, outcome, tier: "two", sql });
45648
+ try {
45649
+ const proceed = await enforceWriteGate({
45650
+ verdict: verdict2,
45651
+ statement: sql,
45652
+ operation: verdict2.operation ?? "WRITE",
45653
+ human: { isTTY: process.stdin.isTTY === true },
45654
+ ...options.ask && { ask: options.ask }
45655
+ });
45656
+ await decide5(proceed ? "allowed" : "declined");
45657
+ return proceed;
45658
+ } catch (error) {
45659
+ if (!(error instanceof WriteGateRefusal))
45660
+ throw error;
45661
+ await decide5("refused");
45662
+ process.stderr.write(`${error.message}
45663
+ `);
45664
+ return false;
45665
+ }
45666
+ };
45667
+ }
45668
+ var init_shell_write_gate = __esm(() => {
45669
+ init_write_gate();
45670
+ init_write_gate_prompt();
45671
+ init_write_gate_guard();
45672
+ });
45673
+
45156
45674
  // src/commands/shell.ts
45157
45675
  var exports_shell = {};
45158
45676
  __export(exports_shell, {
@@ -45259,7 +45777,18 @@ async function runShell(options, configPath) {
45259
45777
  columnsByTable,
45260
45778
  commandNames
45261
45779
  };
45262
- const engine = new ReplEngine(adapter, context, HISTORY_PATH, config);
45780
+ let replInterface = null;
45781
+ const { createPromptAsker: createPromptAsker2 } = await Promise.resolve().then(() => exports_shell_prompt_asker);
45782
+ const asker = createPromptAsker2(() => replInterface);
45783
+ const { toSqlDialect: toSqlDialect2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
45784
+ const dialect = toSqlDialect2(config.connection.system);
45785
+ const writeGate = dialect ? (await Promise.resolve().then(() => (init_shell_write_gate(), exports_shell_write_gate))).createShellWriteGate({
45786
+ config,
45787
+ configPath,
45788
+ dialect,
45789
+ ask: asker.ask
45790
+ }) : null;
45791
+ const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate);
45263
45792
  const complete = createCompleter(context);
45264
45793
  console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
45265
45794
  system: config.connection.system,
@@ -45293,10 +45822,22 @@ async function runShell(options, configPath) {
45293
45822
  completer: (line) => complete(line),
45294
45823
  terminal: process.stdin.isTTY ?? false
45295
45824
  });
45825
+ replInterface = rl;
45296
45826
  const continuationPrompt = import_picocolors5.default.dim(t("shell.continuation_prompt") + "> ");
45297
45827
  rl.prompt();
45298
- rl.on("line", async (line) => {
45299
- const result = await engine.processInput(line);
45828
+ const handleLine = async (line) => {
45829
+ rl.pause();
45830
+ let result;
45831
+ try {
45832
+ result = await engine.processInput(line);
45833
+ } catch (error) {
45834
+ console.error(import_picocolors5.default.red(t_vars("shell.error_sql_failed", { message: error.message })));
45835
+ rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
45836
+ rl.prompt();
45837
+ return;
45838
+ } finally {
45839
+ rl.resume();
45840
+ }
45300
45841
  switch (result.action) {
45301
45842
  case "quit":
45302
45843
  if (result.output)
@@ -45307,16 +45848,21 @@ async function runShell(options, configPath) {
45307
45848
  console.clear();
45308
45849
  break;
45309
45850
  case "multiline":
45310
- rl.setPrompt(continuationPrompt);
45851
+ if (result.output)
45852
+ console.error(result.output);
45311
45853
  break;
45312
45854
  case "continue":
45313
45855
  if (result.output) {
45314
45856
  console.log(result.output);
45315
45857
  }
45316
- rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
45317
45858
  break;
45318
45859
  }
45860
+ rl.setPrompt(engine.isMultiline() ? continuationPrompt : import_picocolors5.default.cyan(t("shell.prompt") + "> "));
45319
45861
  rl.prompt();
45862
+ };
45863
+ let pending = Promise.resolve();
45864
+ rl.on("line", (line) => {
45865
+ pending = pending.then(() => handleLine(line));
45320
45866
  });
45321
45867
  rl.on("close", async () => {
45322
45868
  console.error(import_picocolors5.default.dim(t("shell.goodbye")));
@@ -45325,7 +45871,10 @@ async function runShell(options, configPath) {
45325
45871
  process.exit(0);
45326
45872
  });
45327
45873
  rl.on("SIGINT", () => {
45874
+ if (asker.cancel())
45875
+ return;
45328
45876
  if (engine.isMultiline()) {
45877
+ engine.cancelMultiline();
45329
45878
  console.error(import_picocolors5.default.dim(t("shell.multiline_cancelled")));
45330
45879
  rl.setPrompt(import_picocolors5.default.cyan(t("shell.prompt") + "> "));
45331
45880
  }
@@ -45341,7 +45890,10 @@ async function runBatchSession(engine, input) {
45341
45890
  continue;
45342
45891
  const result = await engine.processInput(line);
45343
45892
  if (result.output) {
45344
- console.log(result.output);
45893
+ if (result.action === "multiline")
45894
+ console.error(result.output);
45895
+ else
45896
+ console.log(result.output);
45345
45897
  }
45346
45898
  if (result.action === "quit") {
45347
45899
  return;
@@ -49354,11 +49906,19 @@ function assertVerificationSubjects(subject, claims) {
49354
49906
  throw new EvidencePackValidationError("verification evidence must match the claims subject kind");
49355
49907
  }
49356
49908
  }
49357
- function canonicalizeWithoutDigest(pack) {
49358
- return JSON.stringify(pack);
49909
+ function canonicalize(value) {
49910
+ if (value === null || typeof value !== "object")
49911
+ return JSON.stringify(value) ?? "null";
49912
+ if (Array.isArray(value))
49913
+ return `[${value.map(canonicalize).join(",")}]`;
49914
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, v]) => `${JSON.stringify(key)}:${canonicalize(v)}`);
49915
+ return `{${entries.join(",")}}`;
49916
+ }
49917
+ function digest(content) {
49918
+ return createHash8("sha256").update(canonicalize(content)).digest("hex");
49359
49919
  }
49360
- function digest(pack) {
49361
- return createHash8("sha256").update(canonicalizeWithoutDigest(pack)).digest("hex");
49920
+ function packId(contentDigest) {
49921
+ return `evp_${contentDigest.slice(0, 32)}`;
49362
49922
  }
49363
49923
  function parseEvidenceClaimsInput(raw) {
49364
49924
  if (!isRecord4(raw))
@@ -49393,49 +49953,52 @@ function buildEvidencePack(input, references, options = {}) {
49393
49953
  if (evidence2.some((reference) => reference.kind === "verification-artifact" && reference.subjectKind !== normalized2.subject.kind)) {
49394
49954
  throw new EvidencePackValidationError("verification evidence must match the claims subject kind");
49395
49955
  }
49396
- const base = {
49956
+ const content = {
49397
49957
  version: EVIDENCE_PACK_VERSION,
49398
- id: id(options.idFactory?.() ?? `evp_${randomUUID6()}`, "pack.id"),
49399
- createdAt: (options.now?.() ?? new Date).toISOString(),
49400
49958
  subject: normalized2.subject,
49401
- claims: normalized2.claims.map((claim) => ({ ...claim, evidence: evidence2 })),
49402
- coverage: { completeForDeclaredEvidence: true, gaps: [] }
49959
+ claims: normalized2.claims.map((claim) => ({ ...claim, evidence: evidence2 }))
49960
+ };
49961
+ const contentDigest = digest(content);
49962
+ return {
49963
+ ...content,
49964
+ id: packId(contentDigest),
49965
+ createdAt: (options.now?.() ?? new Date).toISOString(),
49966
+ integrity: { algorithm: "sha256", digest: contentDigest }
49403
49967
  };
49404
- return { ...base, integrity: { algorithm: "sha256", digest: digest(base) } };
49405
49968
  }
49406
49969
  function parseEvidencePack(raw) {
49407
49970
  if (!isRecord4(raw))
49408
49971
  throw new EvidencePackValidationError("evidence pack must be an object");
49409
- requireExactKeys(raw, ["version", "id", "createdAt", "subject", "claims", "coverage", "integrity"], "evidence pack");
49972
+ requireExactKeys(raw, ["version", "id", "createdAt", "subject", "claims", "integrity"], "evidence pack");
49410
49973
  if (raw.version !== EVIDENCE_PACK_VERSION) {
49411
49974
  throw new EvidencePackValidationError(`evidence pack version must be ${EVIDENCE_PACK_VERSION}`);
49412
49975
  }
49413
- if (!isRecord4(raw.coverage))
49414
- throw new EvidencePackValidationError("coverage must be an object");
49415
- requireExactKeys(raw.coverage, ["completeForDeclaredEvidence", "gaps"], "coverage");
49416
- if (raw.coverage.completeForDeclaredEvidence !== true || !Array.isArray(raw.coverage.gaps) || raw.coverage.gaps.length !== 0) {
49417
- throw new EvidencePackValidationError("v1 coverage must be complete for declared evidence with no gaps");
49418
- }
49419
49976
  if (!isRecord4(raw.integrity))
49420
49977
  throw new EvidencePackValidationError("integrity must be an object");
49421
49978
  requireExactKeys(raw.integrity, ["algorithm", "digest"], "integrity");
49422
49979
  if (raw.integrity.algorithm !== "sha256")
49423
49980
  throw new EvidencePackValidationError("integrity.algorithm must be sha256");
49424
- const base = {
49981
+ const content = {
49425
49982
  version: EVIDENCE_PACK_VERSION,
49426
- id: id(raw.id, "pack.id"),
49427
- createdAt: iso(raw.createdAt, "pack.createdAt"),
49428
49983
  subject: parseSubject(raw.subject),
49429
- claims: parseClaims(raw.claims, true),
49430
- coverage: { completeForDeclaredEvidence: true, gaps: [] }
49984
+ claims: parseClaims(raw.claims, true)
49431
49985
  };
49432
- assertVerificationSubjects(base.subject, base.claims);
49986
+ assertVerificationSubjects(content.subject, content.claims);
49433
49987
  const actualDigest = text4(raw.integrity.digest, "integrity.digest", 128);
49434
49988
  if (!/^[a-f0-9]{64}$/.test(actualDigest))
49435
49989
  throw new EvidencePackValidationError("integrity.digest must be sha256 hex");
49436
- if (actualDigest !== digest(base))
49990
+ const expectedDigest = digest(content);
49991
+ if (actualDigest !== expectedDigest)
49437
49992
  throw new EvidencePackValidationError("evidence pack digest mismatch");
49438
- return { ...base, integrity: { algorithm: "sha256", digest: actualDigest } };
49993
+ const packIdentity = id(raw.id, "pack.id");
49994
+ if (packIdentity !== packId(expectedDigest))
49995
+ throw new EvidencePackValidationError("evidence pack id does not match its content digest");
49996
+ return {
49997
+ ...content,
49998
+ id: packIdentity,
49999
+ createdAt: iso(raw.createdAt, "pack.createdAt"),
50000
+ integrity: { algorithm: "sha256", digest: actualDigest }
50001
+ };
49439
50002
  }
49440
50003
  function isInside(root, target) {
49441
50004
  const relativePath = relative5(root, target);
@@ -49558,7 +50121,9 @@ var init_evidence_pack = __esm(() => {
49558
50121
  // src/commands/evidence.ts
49559
50122
  var exports_evidence = {};
49560
50123
  __export(exports_evidence, {
49561
- evidenceCommand: () => evidenceCommand
50124
+ evidenceCommand: () => evidenceCommand,
50125
+ containsBlockedTerm: () => containsBlockedTerm,
50126
+ blockedTerms: () => blockedTerms
49562
50127
  });
49563
50128
  import { join as join42, relative as relative6, resolve as resolve12, sep as sep8 } from "path";
49564
50129
  import { realpath as realpath3 } from "fs/promises";
@@ -49581,24 +50146,34 @@ async function readClaimsInput(filePath) {
49581
50146
  function blockedTerms(config) {
49582
50147
  return [
49583
50148
  ...config.blacklist?.tables ?? [],
50149
+ ...Object.keys(config.blacklist?.columns ?? {}),
49584
50150
  ...Object.values(config.blacklist?.columns ?? {}).flat()
49585
50151
  ].map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0);
49586
50152
  }
49587
- function assertNoBlockedText(value, terms) {
50153
+ function containsBlockedTerm(value, terms) {
50154
+ const normalized2 = value.toLowerCase();
50155
+ return terms.some((term) => {
50156
+ const trimmed = term.trim().toLowerCase();
50157
+ if (trimmed.length === 0)
50158
+ return false;
50159
+ const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50160
+ return new RegExp(`(?<![A-Za-z0-9_])${escaped}(?![A-Za-z0-9_])`).test(normalized2);
50161
+ });
50162
+ }
50163
+ function assertNoBlockedText(value, terms, field) {
49588
50164
  if (!value)
49589
50165
  return;
49590
- const normalized2 = value.toLowerCase();
49591
- if (terms.some((term) => normalized2.includes(term))) {
49592
- throw new EvidencePackValidationError("evidence content contains a blocked identifier");
50166
+ if (containsBlockedTerm(value, terms)) {
50167
+ throw new EvidencePackValidationError(`${field} contains a blocked identifier`);
49593
50168
  }
49594
50169
  }
49595
50170
  function assertClaimsSafe(raw, terms) {
49596
50171
  const input = parseEvidenceClaimsInput(raw);
49597
- assertNoBlockedText(input.subject.kind, terms);
49598
- assertNoBlockedText(input.subject.name, terms);
49599
- for (const claim of input.claims) {
49600
- assertNoBlockedText(claim.id, terms);
49601
- assertNoBlockedText(claim.text, terms);
50172
+ assertNoBlockedText(input.subject.kind, terms, "subject.kind");
50173
+ assertNoBlockedText(input.subject.name, terms, "subject.name");
50174
+ for (const [index, claim] of input.claims.entries()) {
50175
+ assertNoBlockedText(claim.id, terms, `claim ${index + 1} id`);
50176
+ assertNoBlockedText(claim.text, terms, `claim ${index + 1} text`);
49602
50177
  }
49603
50178
  }
49604
50179
  function sameReference(left, right) {
@@ -50788,7 +51363,7 @@ var init_change_set = __esm(() => {
50788
51363
  import { createReadStream } from "fs";
50789
51364
  import { lstat as lstat7 } from "fs/promises";
50790
51365
  import { createInterface as createInterface3 } from "readline";
50791
- async function loadWorkloadEvidence(options) {
51366
+ async function loadWorkloadSource(options) {
50792
51367
  const now = options.now ?? new Date;
50793
51368
  let info;
50794
51369
  try {
@@ -51169,7 +51744,7 @@ function semanticSchema(schema, blocked) {
51169
51744
  }
51170
51745
  return result;
51171
51746
  }
51172
- async function loadObservedWorkloadEvidence(path4, workspaceRoot, blocked) {
51747
+ async function loadObservedWorkloadSource(path4, workspaceRoot, blocked) {
51173
51748
  if (path4 === undefined) {
51174
51749
  return {
51175
51750
  state: "absent",
@@ -51179,7 +51754,7 @@ async function loadObservedWorkloadEvidence(path4, workspaceRoot, blocked) {
51179
51754
  issues: []
51180
51755
  };
51181
51756
  }
51182
- return loadWorkloadEvidence({ path: resolve14(workspaceRoot, path4), blockedIdentifiers: blocked });
51757
+ return loadWorkloadSource({ path: resolve14(workspaceRoot, path4), blockedIdentifiers: blocked });
51183
51758
  }
51184
51759
  function shouldFail(report, failOn) {
51185
51760
  if (failOn === "never")
@@ -51317,7 +51892,7 @@ var init_impact2 = __esm(() => {
51317
51892
  const contracts = await loadContractEvidence(workspaceRoot, semantic, baseline, savedQueries, blockedIdentifiers);
51318
51893
  const dataAccess = await loadDataAccessEvidence(workspaceRoot, semantic, baseline, savedQueries, blockedIdentifiers);
51319
51894
  const verifications = await loadVerificationEvidence(workspaceRoot, blockedIdentifiers);
51320
- const observedWorkload = await loadObservedWorkloadEvidence(options.events, workspaceRoot, blockedIdentifiers);
51895
+ const observedWorkload = await loadObservedWorkloadSource(options.events, workspaceRoot, blockedIdentifiers);
51321
51896
  const report = assessImpact({
51322
51897
  changes,
51323
51898
  semantic,