@titan-design/hitl 0.3.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sqlite.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BaseGateStore,
3
3
  GateStoreSchemaOutdated
4
- } from "./chunk-4GAZKT6Z.js";
4
+ } from "./chunk-TE2NXWWK.js";
5
5
 
6
6
  // src/sqlite-store.ts
7
7
  import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
@@ -48,72 +48,143 @@ function gateResolverMigration(version, name = DEFAULT_GATE_TABLE) {
48
48
  up: (db) => {
49
49
  if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);
50
50
  db.exec(resolverRequiredTriggerDdl(name));
51
+ if (hasColumn(db, name, "rule")) db.exec(ruleTriggerDdl(db, name));
51
52
  }
52
53
  };
53
54
  }
54
55
  function hasResolverColumn(db, table) {
55
- return db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = 'resolved_by'").get(table) !== void 0;
56
+ return hasColumn(db, table, "resolved_by");
57
+ }
58
+ function hasColumn(db, table, column) {
59
+ return db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = ?").get(table, column) !== void 0;
60
+ }
61
+ var CANONICAL_STATUSES = "'pending', 'resolved', 'cancelled', 'expired'";
62
+ function ruleTriggerDdl(db, name) {
63
+ const hasResolver = hasResolverColumn(db, name);
64
+ const update = ruleTrigger(name, "UPDATE", "OLD.rule", hasResolver, "NEW.rule IS NOT OLD.rule");
65
+ const insert = ruleTrigger(name, "INSERT", "NEW.rule", hasResolver, "0");
66
+ return `${update}
67
+ ${insert}
68
+ ${replaceGuard(name)}`;
69
+ }
70
+ function replaceGuard(name) {
71
+ const trigger = quoteIdent(`${name}_rule_replace`);
72
+ return `
73
+ DROP TRIGGER IF EXISTS ${trigger};
74
+ CREATE TRIGGER ${trigger}
75
+ BEFORE INSERT ON ${quoteIdent(name)}
76
+ FOR EACH ROW WHEN EXISTS (
77
+ SELECT 1 FROM ${quoteIdent(name)} WHERE id = NEW.id AND status = 'pending' AND rule IS NOT NULL AND rule IS NOT NEW.rule
78
+ )
79
+ BEGIN
80
+ SELECT RAISE(ABORT, 'hitl: a pending rule-bound gate cannot be replaced');
81
+ END;
82
+ `;
83
+ }
84
+ function ruleTrigger(name, event, rule, hasResolver, ruleChanged) {
85
+ const trigger = quoteIdent(event === "UPDATE" ? `${name}_rule_resolver` : `${name}_rule_resolver_insert`);
86
+ const outsideRule = hasResolver ? `COALESCE(json_extract(NEW.resolved_by, '$.class'), '') NOT IN (SELECT value FROM json_each(${rule}, '$.resolvers'))` : "1";
87
+ return `
88
+ DROP TRIGGER IF EXISTS ${trigger};
89
+ CREATE TRIGGER ${trigger}
90
+ BEFORE ${event} ON ${quoteIdent(name)}
91
+ FOR EACH ROW WHEN ${rule} IS NOT NULL OR ${ruleChanged}
92
+ BEGIN
93
+ SELECT RAISE(ABORT, 'hitl: status outside the canonical set') WHERE ${rule} IS NOT NULL AND NEW.status NOT IN (${CANONICAL_STATUSES});
94
+ SELECT RAISE(ABORT, 'hitl: resolver outside the gate rule')
95
+ WHERE ${ruleChanged} OR (NEW.status = 'resolved' AND ${outsideRule});
96
+ END;
97
+ `;
98
+ }
99
+ function gateRuleMigration(version, name = DEFAULT_GATE_TABLE) {
100
+ return {
101
+ version,
102
+ name: `hitl:rule:${name}`,
103
+ up: (db) => {
104
+ if (!hasColumn(db, name, "rule")) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN rule TEXT`);
105
+ db.exec(ruleTriggerDdl(db, name));
106
+ }
107
+ };
108
+ }
109
+ var BRIEF_COLUMNS = ["summary", "evidence_ref", "questions"];
110
+ function gateBriefMigration(version, name = DEFAULT_GATE_TABLE) {
111
+ return {
112
+ version,
113
+ name: `hitl:brief:${name}`,
114
+ up: (db) => {
115
+ for (const column of BRIEF_COLUMNS) {
116
+ if (!hasColumn(db, name, column)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN ${column} TEXT`);
117
+ }
118
+ }
119
+ };
56
120
  }
57
121
  var SqliteGateStore = class extends BaseGateStore {
58
122
  constructor(db, options = {}) {
59
- super(options.now ?? Date.now, options.authorize);
123
+ super(options.now ?? Date.now, options.authorize, options.requireBrief, options.allowances);
60
124
  this.db = db;
61
125
  this.table = options.table ?? DEFAULT_GATE_TABLE;
62
- if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);
126
+ if (options.migrate ?? true) runMigrations(db, defaultMigrations(this.table));
127
+ const missing = missingMigration(db, this.table, options.requireBrief ?? false);
128
+ if (missing) throw new GateStoreSchemaOutdated("", this.table, missing);
63
129
  }
64
130
  db;
65
131
  table;
66
- resolverColumnSeen = false;
132
+ columnsSeen = /* @__PURE__ */ new Set();
133
+ /** Refuses rather than dropping a rule or a brief when the table has no column to hold it. */
67
134
  insert(record) {
68
- this.db.prepare(
69
- `INSERT INTO ${quoteIdent(this.table)}
70
- (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)
71
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
72
- ).run(...columns(record));
135
+ const optional = this.optionalColumns(record);
136
+ const names = [...BASE_COLUMNS, ...optional.keys()];
137
+ this.db.prepare(`INSERT INTO ${quoteIdent(this.table)} (${names.join(", ")}) VALUES (${names.map(() => "?").join(", ")})`).run(...columns(record), ...optional.values());
138
+ }
139
+ optionalColumns(record) {
140
+ const optional = /* @__PURE__ */ new Map();
141
+ if (record.rule) {
142
+ this.requireColumn(record.id, "rule", "gateRuleMigration");
143
+ optional.set("rule", JSON.stringify(record.rule));
144
+ }
145
+ if (hasBrief(record)) {
146
+ this.requireColumn(record.id, "questions", "gateBriefMigration");
147
+ optional.set("summary", record.summary ?? null);
148
+ optional.set("evidence_ref", record.evidenceRef ?? null);
149
+ optional.set("questions", record.questions ? JSON.stringify(record.questions) : null);
150
+ }
151
+ return optional;
73
152
  }
74
153
  read(id) {
75
154
  const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id);
76
155
  return row ? toRecord(row) : void 0;
77
156
  }
78
157
  update(record) {
79
- if (record.resolvedBy) {
80
- this.updateWithResolver(record, record.resolvedBy);
81
- return;
82
- }
83
- this.db.prepare(
84
- `UPDATE ${quoteIdent(this.table)}
85
- SET status = ?, payload = ?, reason = ?, resolved_at = ?
86
- WHERE id = ?`
87
- ).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
88
- }
89
- /** Refuses rather than dropping the resolver when the table has no column to hold it. */
90
- updateWithResolver(record, resolvedBy) {
91
- if (!this.resolverColumnPresent()) {
92
- throw new GateStoreSchemaOutdated(record.id, this.table, "gateResolverMigration");
93
- }
158
+ const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;
94
159
  this.db.prepare(
95
160
  `UPDATE ${quoteIdent(this.table)}
96
161
  SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?
97
162
  WHERE id = ?`
98
- ).run(
99
- record.status,
100
- toJson(record.payload),
101
- record.reason ?? null,
102
- record.resolvedAt ?? null,
103
- JSON.stringify(resolvedBy),
104
- record.id
105
- );
163
+ ).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);
106
164
  }
107
- /** Only a positive probe is cached, so a migration run after construction is still noticed. */
108
- resolverColumnPresent() {
109
- if (!this.resolverColumnSeen) this.resolverColumnSeen = hasResolverColumn(this.db, this.table);
110
- return this.resolverColumnSeen;
165
+ /** Only a present column is cached: a migration can add one while the store is open, never remove one. */
166
+ requireColumn(gateId, column, migration) {
167
+ if (this.columnsSeen.has(column)) return;
168
+ if (!hasColumn(this.db, this.table, column)) throw new GateStoreSchemaOutdated(gateId, this.table, migration);
169
+ this.columnsSeen.add(column);
111
170
  }
112
171
  readByStatus(status) {
113
172
  const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
114
173
  return rows.map(toRecord);
115
174
  }
116
175
  };
176
+ function missingMigration(db, table, requireBrief) {
177
+ if (!hasColumn(db, table, "id")) return "gateMigration";
178
+ if (!hasResolverColumn(db, table)) return "gateResolverMigration";
179
+ return requireBrief && !hasColumn(db, table, "questions") ? "gateBriefMigration" : void 0;
180
+ }
181
+ function defaultMigrations(table) {
182
+ return [gateMigration(1, table), gateResolverMigration(2, table), gateRuleMigration(3, table), gateBriefMigration(4, table)];
183
+ }
184
+ var BASE_COLUMNS = ["id", "prompt", "schema", "status", "payload", "reason", "created_at", "resolved_at", "expires_at"];
185
+ function hasBrief(record) {
186
+ return record.summary !== void 0 || record.evidenceRef !== void 0 || record.questions !== void 0;
187
+ }
117
188
  function columns(record) {
118
189
  return [
119
190
  record.id,
@@ -141,14 +212,20 @@ function toRecord(row) {
141
212
  createdAt: row.created_at,
142
213
  resolvedAt: row.resolved_at ?? void 0,
143
214
  expiresAt: row.expires_at ?? void 0,
144
- resolvedBy: row.resolved_by ? JSON.parse(row.resolved_by) : void 0
215
+ resolvedBy: row.resolved_by ? JSON.parse(row.resolved_by) : void 0,
216
+ rule: row.rule ? JSON.parse(row.rule) : void 0,
217
+ summary: row.summary ?? void 0,
218
+ evidenceRef: row.evidence_ref ?? void 0,
219
+ questions: row.questions ? JSON.parse(row.questions) : void 0
145
220
  };
146
221
  }
147
222
  export {
148
223
  DEFAULT_GATE_TABLE,
149
224
  SqliteGateStore,
225
+ gateBriefMigration,
150
226
  gateMigration,
151
227
  gateResolverMigration,
228
+ gateRuleMigration,
152
229
  gateTableDdl,
153
230
  resolverRequiredTriggerDdl
154
231
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sqlite-store.ts"],"sourcesContent":["import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport {\n GateStoreSchemaOutdated,\n type GateAuthorize,\n type GateRecord,\n type GateResolver,\n type GateStatus,\n type JsonSchema,\n} from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\n/**\n * The triggers that refuse a resolved row naming no resolver, on update and on\n * insert, so a writer that predates `resolved_by` fails loudly instead of resolving anonymously.\n */\nexport function resolverRequiredTriggerDdl(name: string = DEFAULT_GATE_TABLE): string {\n return ([\"UPDATE\", \"INSERT\"] as const).map((event) => resolverTrigger(name, event)).join(\"\\n\");\n}\n\nfunction resolverTrigger(name: string, event: \"UPDATE\" | \"INSERT\"): string {\n const suffix = event === \"UPDATE\" ? \"resolver_required\" : \"resolver_required_insert\";\n return `\n CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${name}_${suffix}`)}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN NEW.status = 'resolved' AND NEW.resolved_by IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'hitl: resolvedBy required');\n END;\n `;\n}\n\n/**\n * Adds `resolved_by` and the resolver trigger. Idempotent and backfill-free:\n * gates resolved before it keep an unknown resolver rather than a guessed one.\n */\nexport function gateResolverMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:resolver:${name}`,\n up: (db) => {\n if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);\n db.exec(resolverRequiredTriggerDdl(name));\n },\n };\n}\n\nfunction hasResolverColumn(db: Db, table: string): boolean {\n return db.prepare(\"SELECT 1 FROM pragma_table_info(?) WHERE name = 'resolved_by'\").get(table) !== undefined;\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n /** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */\n authorize?: GateAuthorize;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n /** Absent entirely on a table that has not run `gateResolverMigration`. */\n resolved_by?: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n private resolverColumnSeen = false;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now, options.authorize);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);\n }\n\n protected insert(record: GateRecord): void {\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n if (record.resolvedBy) {\n this.updateWithResolver(record, record.resolvedBy);\n return;\n }\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);\n }\n\n /** Refuses rather than dropping the resolver when the table has no column to hold it. */\n private updateWithResolver(record: GateRecord, resolvedBy: GateResolver): void {\n if (!this.resolverColumnPresent()) {\n throw new GateStoreSchemaOutdated(record.id, this.table, \"gateResolverMigration\");\n }\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?\n WHERE id = ?`,\n )\n .run(\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.resolvedAt ?? null,\n JSON.stringify(resolvedBy),\n record.id,\n );\n }\n\n /** Only a positive probe is cached, so a migration run after construction is still noticed. */\n private resolverColumnPresent(): boolean {\n if (!this.resolverColumnSeen) this.resolverColumnSeen = hasResolverColumn(this.db, this.table);\n return this.resolverColumnSeen;\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n resolvedBy: row.resolved_by ? (JSON.parse(row.resolved_by) as GateResolver) : undefined,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sqlite-store.ts"],"sourcesContent":["import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport {\n GateStoreSchemaOutdated,\n type GateAnswerAllowance,\n type GateAuthorize,\n type GateQuestion,\n type GateRecord,\n type GateResolver,\n type GateRule,\n type GateStatus,\n type JsonSchema,\n} from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\n/**\n * The triggers that refuse a resolved row naming no resolver, on update and on\n * insert, so a writer that predates `resolved_by` fails loudly instead of resolving anonymously.\n */\nexport function resolverRequiredTriggerDdl(name: string = DEFAULT_GATE_TABLE): string {\n return ([\"UPDATE\", \"INSERT\"] as const).map((event) => resolverTrigger(name, event)).join(\"\\n\");\n}\n\nfunction resolverTrigger(name: string, event: \"UPDATE\" | \"INSERT\"): string {\n const suffix = event === \"UPDATE\" ? \"resolver_required\" : \"resolver_required_insert\";\n return `\n CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${name}_${suffix}`)}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN NEW.status = 'resolved' AND NEW.resolved_by IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'hitl: resolvedBy required');\n END;\n `;\n}\n\n/**\n * Adds `resolved_by` and the resolver trigger. Idempotent and backfill-free:\n * gates resolved before it keep an unknown resolver rather than a guessed one.\n */\nexport function gateResolverMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:resolver:${name}`,\n up: (db) => {\n if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);\n db.exec(resolverRequiredTriggerDdl(name));\n if (hasColumn(db, name, \"rule\")) db.exec(ruleTriggerDdl(db, name));\n },\n };\n}\n\nfunction hasResolverColumn(db: Db, table: string): boolean {\n return hasColumn(db, table, \"resolved_by\");\n}\n\nfunction hasColumn(db: Db, table: string, column: string): boolean {\n return db.prepare(\"SELECT 1 FROM pragma_table_info(?) WHERE name = ?\").get(table, column) !== undefined;\n}\n\nconst CANONICAL_STATUSES = \"'pending', 'resolved', 'cancelled', 'expired'\";\n\n/**\n * The triggers that refuse a resolve by a class outside the row's rule, and any\n * change to the rule, so a writer that predates `rule` cannot widen it. The\n * INSERT twin closes REPLACE INTO and DELETE then INSERT, which an UPDATE trigger never sees.\n * A rule-bound row also refuses a status outside the canonical set, so `RESOLVED` cannot slip past the `resolved` test.\n * Without `resolved_by` no resolver can be recorded, so they refuse every resolve\n * of a rule-bound row; whichever migration runs second installs the class-aware form.\n */\nfunction ruleTriggerDdl(db: Db, name: string): string {\n const hasResolver = hasResolverColumn(db, name);\n const update = ruleTrigger(name, \"UPDATE\", \"OLD.rule\", hasResolver, \"NEW.rule IS NOT OLD.rule\");\n const insert = ruleTrigger(name, \"INSERT\", \"NEW.rule\", hasResolver, \"0\");\n return `${update}\\n${insert}\\n${replaceGuard(name)}`;\n}\n\n/** REPLACE deletes the old row before any delete trigger fires without recursive_triggers, so refuse it up front. */\nfunction replaceGuard(name: string): string {\n const trigger = quoteIdent(`${name}_rule_replace`);\n return `\n DROP TRIGGER IF EXISTS ${trigger};\n CREATE TRIGGER ${trigger}\n BEFORE INSERT ON ${quoteIdent(name)}\n FOR EACH ROW WHEN EXISTS (\n SELECT 1 FROM ${quoteIdent(name)} WHERE id = NEW.id AND status = 'pending' AND rule IS NOT NULL AND rule IS NOT NEW.rule\n )\n BEGIN\n SELECT RAISE(ABORT, 'hitl: a pending rule-bound gate cannot be replaced');\n END;\n `;\n}\n\nfunction ruleTrigger(name: string, event: \"UPDATE\" | \"INSERT\", rule: string, hasResolver: boolean, ruleChanged: string): string {\n const trigger = quoteIdent(event === \"UPDATE\" ? `${name}_rule_resolver` : `${name}_rule_resolver_insert`);\n const outsideRule = hasResolver\n ? `COALESCE(json_extract(NEW.resolved_by, '$.class'), '') NOT IN (SELECT value FROM json_each(${rule}, '$.resolvers'))`\n : \"1\";\n return `\n DROP TRIGGER IF EXISTS ${trigger};\n CREATE TRIGGER ${trigger}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN ${rule} IS NOT NULL OR ${ruleChanged}\n BEGIN\n SELECT RAISE(ABORT, 'hitl: status outside the canonical set') WHERE ${rule} IS NOT NULL AND NEW.status NOT IN (${CANONICAL_STATUSES});\n SELECT RAISE(ABORT, 'hitl: resolver outside the gate rule')\n WHERE ${ruleChanged} OR (NEW.status = 'resolved' AND ${outsideRule});\n END;\n `;\n}\n\n/** Adds `rule` and its trigger. Idempotent and backfill-free: gates opened before it carry no rule. */\nexport function gateRuleMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:rule:${name}`,\n up: (db) => {\n if (!hasColumn(db, name, \"rule\")) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN rule TEXT`);\n db.exec(ruleTriggerDdl(db, name));\n },\n };\n}\n\nconst BRIEF_COLUMNS = [\"summary\", \"evidence_ref\", \"questions\"] as const;\n\n/** Adds `summary`, `evidence_ref` and `questions`. Idempotent and backfill-free: gates opened before it carry no brief. */\nexport function gateBriefMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:brief:${name}`,\n up: (db) => {\n for (const column of BRIEF_COLUMNS) {\n if (!hasColumn(db, name, column)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN ${column} TEXT`);\n }\n },\n };\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration`, `gateResolverMigration`, `gateRuleMigration` and `gateBriefMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n /** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */\n authorize?: GateAuthorize;\n /** Refuse `create` without a `summary` and an `evidenceRef`. Off by default; needs `gateBriefMigration`. */\n requireBrief?: boolean;\n /** Answers a non-owner class may give, each exact in class, step and payload. Nothing else widens the default class check. */\n allowances?: readonly GateAnswerAllowance[];\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n resolved_by: string | null;\n /** Absent entirely on a table that has not run `gateRuleMigration`. */\n rule?: string | null;\n /** These three are absent entirely on a table that has not run `gateBriefMigration`. */\n summary?: string | null;\n evidence_ref?: string | null;\n questions?: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n private readonly columnsSeen = new Set<string>();\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now, options.authorize, options.requireBrief, options.allowances);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, defaultMigrations(this.table));\n const missing = missingMigration(db, this.table, options.requireBrief ?? false);\n if (missing) throw new GateStoreSchemaOutdated(\"\", this.table, missing);\n }\n\n /** Refuses rather than dropping a rule or a brief when the table has no column to hold it. */\n protected insert(record: GateRecord): void {\n const optional = this.optionalColumns(record);\n const names = [...BASE_COLUMNS, ...optional.keys()];\n this.db\n .prepare(`INSERT INTO ${quoteIdent(this.table)} (${names.join(\", \")}) VALUES (${names.map(() => \"?\").join(\", \")})`)\n .run(...columns(record), ...optional.values());\n }\n\n private optionalColumns(record: GateRecord): Map<string, string | null> {\n const optional = new Map<string, string | null>();\n if (record.rule) {\n this.requireColumn(record.id, \"rule\", \"gateRuleMigration\");\n optional.set(\"rule\", JSON.stringify(record.rule));\n }\n if (hasBrief(record)) {\n this.requireColumn(record.id, \"questions\", \"gateBriefMigration\");\n optional.set(\"summary\", record.summary ?? null);\n optional.set(\"evidence_ref\", record.evidenceRef ?? null);\n optional.set(\"questions\", record.questions ? JSON.stringify(record.questions) : null);\n }\n return optional;\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);\n }\n\n /** Only a present column is cached: a migration can add one while the store is open, never remove one. */\n private requireColumn(gateId: string, column: string, migration: string): void {\n if (this.columnsSeen.has(column)) return;\n if (!hasColumn(this.db, this.table, column)) throw new GateStoreSchemaOutdated(gateId, this.table, migration);\n this.columnsSeen.add(column);\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\n/** Names the earliest migration the table lacks, so the error points at the step to add. */\nfunction missingMigration(db: Db, table: string, requireBrief: boolean): string | undefined {\n if (!hasColumn(db, table, \"id\")) return \"gateMigration\";\n if (!hasResolverColumn(db, table)) return \"gateResolverMigration\";\n return requireBrief && !hasColumn(db, table, \"questions\") ? \"gateBriefMigration\" : undefined;\n}\n\nfunction defaultMigrations(table: string): Migration[] {\n return [gateMigration(1, table), gateResolverMigration(2, table), gateRuleMigration(3, table), gateBriefMigration(4, table)];\n}\n\nconst BASE_COLUMNS = [\"id\", \"prompt\", \"schema\", \"status\", \"payload\", \"reason\", \"created_at\", \"resolved_at\", \"expires_at\"];\n\nfunction hasBrief(record: GateRecord): boolean {\n return record.summary !== undefined || record.evidenceRef !== undefined || record.questions !== undefined;\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n resolvedBy: row.resolved_by ? (JSON.parse(row.resolved_by) as GateResolver) : undefined,\n rule: row.rule ? (JSON.parse(row.rule) as GateRule) : undefined,\n summary: row.summary ?? undefined,\n evidenceRef: row.evidence_ref ?? undefined,\n questions: row.questions ? (JSON.parse(row.questions) as GateQuestion[]) : undefined,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@titan-design/hitl",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
4
4
  "description": "Human-in-the-loop gate()/resolve() primitive",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
- "README.md"
20
+ "README.md",
21
+ "NOTICE"
21
22
  ],
22
23
  "author": "Henry Jewkes",
23
24
  "license": "MIT",
@@ -35,8 +36,8 @@
35
36
  "access": "public"
36
37
  },
37
38
  "dependencies": {
38
- "@titan-design/authority": "^0.2.0",
39
- "@titan-design/store-sqlite": "0.3.1"
39
+ "@titan-design/authority": "^0.3.0",
40
+ "@titan-design/store-sqlite": "0.3.3"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "zod": "^4.0.0"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/types.ts","../src/json-schema.ts","../src/resolver-policy.ts","../src/base-store.ts"],"sourcesContent":["import type { ActorClass } from \"@titan-design/authority\";\n\n/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n /** Who resolved the gate, as claimed by the caller; unset for gates resolved before stores recorded it. */\n resolvedBy: GateResolver | undefined;\n}\n\n/** A claim about who answered a gate. hitl records it and checks its class; it cannot prove it. */\nexport interface GateResolver {\n class: ActorClass;\n id: string;\n channel: string;\n /** The channel's own id for the confirming event, such as a chat reaction. */\n confirmEvent?: string;\n}\n\nexport type GateAuthorization = { allowed: true } | { allowed: false; reason: string };\n\n/**\n * Runs after the default resolver-class check, so it can refuse a resolver but\n * never admit one the default refused. Must return synchronously.\n */\nexport type GateAuthorize = (gate: Readonly<GateRecord>, resolver: Readonly<GateResolver>) => GateAuthorization;\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n\nexport class GateResolverRefused extends GateError {\n constructor(\n gateId: string,\n readonly actorClass: string | undefined,\n readonly reason: string,\n ) {\n super(`gate ${gateId} refused a resolution by ${actorClass ?? \"an unnamed resolver\"}: ${reason}`, gateId);\n }\n}\n\n/** The store cannot record a resolver because its table predates the resolver migration. */\nexport class GateStoreSchemaOutdated extends GateError {\n constructor(\n gateId: string,\n readonly table: string,\n readonly migration: string,\n ) {\n super(`gate ${gateId} cannot record its resolver: table ${table} needs ${migration}`, gateId);\n }\n}\n\nexport class GateAuthorizeInvalid extends GateError {\n constructor(gateId: string) {\n super(`authorize for gate ${gateId} must return a decision { allowed: boolean } synchronously`, gateId);\n }\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { ACTOR_CLASSES, RESOLVER_CLASSES, type ActorClass } from \"@titan-design/authority\";\nimport { GateAuthorizeInvalid, GateResolverRefused, type GateAuthorization, type GateResolver } from \"./types.js\";\n\n/**\n * The default refusal: only an owner class may resolve a gate, so an agent or\n * automation never answers its own gate. Every store applies it before `authorize`.\n */\nexport function defaultResolverRefusal(resolver: GateResolver): string | undefined {\n if ((RESOLVER_CLASSES as readonly string[]).includes(resolver.class)) return undefined;\n return `actor class ${resolver.class} may not resolve a gate`;\n}\n\n/**\n * Reads each declared field exactly once into a frozen plain object, so the\n * checks and the stored row see the same resolver and nothing undeclared is kept.\n */\nexport function snapshotResolver(gateId: string, raw: unknown): Readonly<GateResolver> {\n if (typeof raw !== \"object\" || raw === null) throw new GateResolverRefused(gateId, undefined, \"the resolver is not an object\");\n const { class: actorClass, id, channel, confirmEvent } = raw as Record<string, unknown>;\n if (!isActorClass(actorClass)) {\n throw new GateResolverRefused(gateId, undefined, \"the resolver's class is not a known actor class\");\n }\n if (typeof id !== \"string\" || typeof channel !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's id and channel must be strings\");\n }\n if (confirmEvent === undefined) return Object.freeze({ class: actorClass, id, channel });\n if (typeof confirmEvent !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's confirmEvent must be a string\");\n }\n return Object.freeze({ class: actorClass, id, channel, confirmEvent });\n}\n\n/** Accepts only a synchronous `{ allowed: boolean }`; a promise is refused because the row cannot wait on it. */\nexport function readDecision(gateId: string, decision: unknown): GateAuthorization {\n if (typeof decision !== \"object\" || decision === null) throw new GateAuthorizeInvalid(gateId);\n const { then, allowed, reason } = decision as Record<string, unknown>;\n if (typeof then === \"function\") {\n (then as (onFulfilled: undefined, onRejected: () => void) => unknown).call(decision, undefined, () => {});\n throw new GateAuthorizeInvalid(gateId);\n }\n if (typeof allowed !== \"boolean\") throw new GateAuthorizeInvalid(gateId);\n if (allowed) return { allowed: true };\n return { allowed: false, reason: typeof reason === \"string\" ? reason : \"authorize refused the resolver\" };\n}\n\nfunction isActorClass(value: unknown): value is ActorClass {\n return typeof value === \"string\" && (ACTOR_CLASSES as readonly string[]).includes(value);\n}\n","import { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport { defaultResolverRefusal, readDecision, snapshotResolver } from \"./resolver-policy.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n GateResolverRefused,\n type GateAuthorize,\n type GateInput,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(\n protected readonly clock: () => number,\n private readonly authorize?: GateAuthorize,\n ) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? globalThis.crypto.randomUUID();\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n resolvedBy: undefined,\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord {\n const resolver = resolvedBy === undefined ? undefined : snapshotResolver(id, resolvedBy);\n const record = this.requirePending(id);\n this.requireAuthorized(record, resolver);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso(), resolvedBy: resolver };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** The default check runs first and `authorize` second, so `authorize` can only narrow who may resolve. */\n private requireAuthorized(record: GateRecord, resolver: Readonly<GateResolver> | undefined): void {\n const refusal = resolver ? defaultResolverRefusal(resolver) : undefined;\n if (resolver && refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);\n if (!this.authorize) return;\n if (!resolver) throw new GateResolverRefused(record.id, undefined, \"a resolver is required when authorize is installed\");\n const decision = readDecision(record.id, this.authorize(Object.freeze({ ...record }), resolver));\n if (!decision.allowed) throw new GateResolverRefused(record.id, resolver.class, decision.reason);\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n"],"mappings":"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","names":[]}