@titan-design/hitl 0.6.0 → 0.7.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/README.md +5 -1
- package/dist/{base-store-B-IPO0UN.d.ts → base-store-imx8181g.d.ts} +7 -1
- package/dist/{chunk-TE2NXWWK.js → chunk-GZ6IE2SD.js} +23 -10
- package/dist/chunk-GZ6IE2SD.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/sqlite.d.ts +2 -2
- package/dist/sqlite.js +4 -3
- package/dist/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-TE2NXWWK.js.map +0 -1
package/README.md
CHANGED
|
@@ -267,7 +267,11 @@ attribution.
|
|
|
267
267
|
|
|
268
268
|
A gate is `pending`, then exactly one of `resolved`, `cancelled`, or `expired`.
|
|
269
269
|
A second `resolve` or `cancel` throws `GateAlreadySettled` and leaves the first
|
|
270
|
-
answer intact.
|
|
270
|
+
answer intact. That holds across stores on one file: the settle write only lands
|
|
271
|
+
on a row that is still pending, so a store that loses the race throws
|
|
272
|
+
`GateAlreadySettled` (or `GateExpired`) instead of overwriting. When the winner
|
|
273
|
+
wrote the same answer (same status, payload and reason), the loser gets the
|
|
274
|
+
settled gate back instead, so a retry that raced its own first attempt succeeds.
|
|
271
275
|
|
|
272
276
|
Expiry is **lazy**: nothing sweeps the table, so a read is what notices the
|
|
273
277
|
deadline passed and flips the row to `expired`. The instant named by `expiresAt`
|
|
@@ -178,7 +178,8 @@ declare abstract class BaseGateStore implements GateStore {
|
|
|
178
178
|
private readonly allowances;
|
|
179
179
|
protected abstract insert(record: GateRecord): void;
|
|
180
180
|
protected abstract read(id: string): GateRecord | undefined;
|
|
181
|
-
|
|
181
|
+
/** Writes only while the stored row is still pending; false means another writer settled it first. */
|
|
182
|
+
protected abstract update(record: GateRecord): boolean;
|
|
182
183
|
protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
|
|
183
184
|
create(input: GateInput): GateRecord;
|
|
184
185
|
get(id: string): GateRecord | undefined;
|
|
@@ -186,6 +187,11 @@ declare abstract class BaseGateStore implements GateStore {
|
|
|
186
187
|
cancel(id: string, reason: string): GateRecord;
|
|
187
188
|
listPending(): GateRecord[];
|
|
188
189
|
private requirePending;
|
|
190
|
+
/**
|
|
191
|
+
* `authorize` runs between the pending check and the write, and another store may settle the row in that gap.
|
|
192
|
+
* A writer that got there first with this same answer is a retry, not a conflict, so its row comes back.
|
|
193
|
+
*/
|
|
194
|
+
private settle;
|
|
189
195
|
/** The default check (or a listed allowance) runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */
|
|
190
196
|
private requireAuthorized;
|
|
191
197
|
/** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
|
|
@@ -380,25 +380,31 @@ var BaseGateStore = class {
|
|
|
380
380
|
if (issues.length > 0) throw new GatePayloadInvalid(id, issues);
|
|
381
381
|
}
|
|
382
382
|
const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso(), resolvedBy: resolver };
|
|
383
|
-
this.
|
|
384
|
-
return resolved;
|
|
383
|
+
return this.settle(resolved);
|
|
385
384
|
}
|
|
386
385
|
cancel(id, reason) {
|
|
387
386
|
const record = this.requirePending(id);
|
|
388
387
|
const cancelled = { ...record, status: "cancelled", reason, resolvedAt: this.nowIso() };
|
|
389
|
-
this.
|
|
390
|
-
return cancelled;
|
|
388
|
+
return this.settle(cancelled);
|
|
391
389
|
}
|
|
392
390
|
listPending() {
|
|
393
391
|
return this.readByStatus("pending").map((record) => this.lapseIfExpired(record)).filter((record) => record.status === "pending").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
394
392
|
}
|
|
395
393
|
requirePending(id) {
|
|
396
394
|
const record = this.get(id);
|
|
397
|
-
if (
|
|
398
|
-
if (record.status === "expired") throw new GateExpired(id);
|
|
399
|
-
if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
|
|
395
|
+
if (record?.status !== "pending") throw notPending(id, record);
|
|
400
396
|
return record;
|
|
401
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* `authorize` runs between the pending check and the write, and another store may settle the row in that gap.
|
|
400
|
+
* A writer that got there first with this same answer is a retry, not a conflict, so its row comes back.
|
|
401
|
+
*/
|
|
402
|
+
settle(record) {
|
|
403
|
+
if (this.update(record)) return record;
|
|
404
|
+
const current = this.read(record.id);
|
|
405
|
+
if (current && sameAnswer(current, record)) return current;
|
|
406
|
+
throw notPending(record.id, current);
|
|
407
|
+
}
|
|
402
408
|
/** The default check (or a listed allowance) runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */
|
|
403
409
|
requireAuthorized(record, resolver, payload) {
|
|
404
410
|
const refusal = resolverRefusal(record.id, resolver, payload, this.allowances);
|
|
@@ -414,13 +420,20 @@ var BaseGateStore = class {
|
|
|
414
420
|
if (record.status !== "pending" || !record.expiresAt) return record;
|
|
415
421
|
if (Date.parse(record.expiresAt) > this.clock()) return record;
|
|
416
422
|
const expired = { ...record, status: "expired", resolvedAt: this.nowIso() };
|
|
417
|
-
this.update(expired);
|
|
418
|
-
return expired;
|
|
423
|
+
return this.update(expired) ? expired : this.read(record.id) ?? expired;
|
|
419
424
|
}
|
|
420
425
|
nowIso() {
|
|
421
426
|
return new Date(this.clock()).toISOString();
|
|
422
427
|
}
|
|
423
428
|
};
|
|
429
|
+
function sameAnswer(stored, attempted) {
|
|
430
|
+
return stored.status === attempted.status && stored.reason === attempted.reason && jsonEqual(stored.payload, attempted.payload);
|
|
431
|
+
}
|
|
432
|
+
function notPending(id, record) {
|
|
433
|
+
if (!record) return new GateNotFound(id);
|
|
434
|
+
if (record.status === "expired") return new GateExpired(id);
|
|
435
|
+
return new GateAlreadySettled(id, record.status);
|
|
436
|
+
}
|
|
424
437
|
function toIso(value) {
|
|
425
438
|
if (value === void 0) return void 0;
|
|
426
439
|
return typeof value === "string" ? value : value.toISOString();
|
|
@@ -457,4 +470,4 @@ export {
|
|
|
457
470
|
* `recommended` keeps only `true`.
|
|
458
471
|
* @license Apache-2.0
|
|
459
472
|
*/
|
|
460
|
-
//# sourceMappingURL=chunk-
|
|
473
|
+
//# sourceMappingURL=chunk-GZ6IE2SD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/gate-brief.ts","../src/json-schema.ts","../src/resolver-policy.ts","../src/base-store.ts"],"sourcesContent":["import type { ActorClass, ResolverClass } 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 /** The authority rule that opened the gate; unset for a gate no rule governs. */\n rule: GateRule | undefined;\n /** One line for the owner: the decision and the recommendation. Unset for a gate opened without a brief. */\n summary: string | undefined;\n /** Where the owner checks the facts: an https URL, an absolute path, or `$ <command>`. */\n evidenceRef: string | undefined;\n /** A fixed menu a surface can render as buttons; the gate's `schema` still validates the answer. */\n questions: GateQuestion[] | undefined;\n}\n\nexport interface GateQuestionOption {\n /** 1-40 letters, digits, `-` or `_`; unique within its question. */\n id: string;\n /** 1-75 characters, the Slack button limit. */\n label: string;\n /** At most one option per question; omitted means false. */\n recommended?: true;\n}\n\nexport interface GateQuestion {\n /** 1-40 letters, digits, `-` or `_`; unique within the gate. */\n id: string;\n /** 1-500 characters. */\n question: string;\n /** 2-4 options. */\n options: GateQuestionOption[];\n}\n\n/** What the owner reads instead of the machine-facing prompt. A store with `requireBrief` refuses a gate without one. */\nexport interface GateBrief {\n /** 1-280 characters on one line. */\n summary: string;\n /** 1-500 characters on one line. */\n evidenceRef: string;\n /** 1-4 questions; absent means the gate's schema alone defines the answer. */\n questions?: GateQuestion[];\n}\n\n/** Binds a gate to the authority rule that opened it, so only that rule's resolver classes may answer. */\nexport interface GateRule {\n table: string;\n version: string;\n ruleId: string;\n resolvers: ResolverClass[];\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\n/**\n * One answer a non-owner class may give, declared by the product when it builds the store.\n * It admits exactly: `resolverClass`, a gate whose step is `stepId` (a bare id or `<id>:<n>` after\n * the last `/`), and a payload deep-equal to `payload`. Evaluated in place of the default refusal for\n * those gates only; the gate's rule and `authorize` still run after it. The resolver's id must be non-blank.\n */\nexport interface GateAnswerAllowance {\n resolverClass: ActorClass;\n stepId: string;\n payload: Readonly<Record<string, unknown>>;\n}\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 rule?: GateRule;\n /** Required, with `evidenceRef`, when the store has `requireBrief`. */\n summary?: string;\n evidenceRef?: string;\n questions?: GateQuestion[];\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/**\n * The store cannot record a resolver or a rule because its table predates the migration that adds the column.\n * `gateId` is empty when the store refuses at construction, before any gate is named.\n */\nexport class GateStoreSchemaOutdated extends GateError {\n constructor(\n gateId: string,\n readonly table: string,\n readonly migration: string,\n ) {\n const message =\n gateId === \"\"\n ? `cannot open a gate store on table ${table}: it needs ${migration}`\n : `gate ${gateId} cannot be recorded: table ${table} needs ${migration}`;\n super(message, gateId);\n }\n}\n\nexport class GateRuleInvalid extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} has an invalid rule: ${reason}`, gateId);\n }\n}\n\nexport class GateBriefInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`gate ${gateId} brief is invalid: ${issues.join(\"; \")}`, 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","/*!\n * The question bounds and `questionsIssue` are adapted from openrig,\n * https://github.com/mvschwarz/openrig, packages/daemon/src/domain/human-questions.ts\n * (parseHumanQuestions). Copyright 2026 Mike Schwarz, licensed under the Apache License 2.0,\n * http://www.apache.org/licenses/LICENSE-2.0.\n * Changed here: the checks report through GateBriefInvalid instead of a result union, and\n * `recommended` keeps only `true`.\n * @license Apache-2.0\n */\nimport { GateBriefInvalid, type GateQuestion, type GateQuestionOption } from \"./types.js\";\n\nconst MAX_SUMMARY = 280;\nconst MAX_EVIDENCE_REF = 500;\nconst MAX_QUESTIONS = 4;\nconst MIN_OPTIONS = 2;\nconst MAX_OPTIONS = 4;\n/** Slack's button text limit, so every option fits on a button. */\nconst MAX_OPTION_LABEL = 75;\nconst MAX_QUESTION_TEXT = 500;\n\nconst ID_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\nconst ID_RULE = 'id must be 1-40 letters, digits, \"-\" or \"_\"';\n// eslint-disable-next-line no-control-regex -- a control character is exactly what a one-line field refuses\nconst CONTROL_CHAR = /[\\u0000-\\u001f\\u007f]/;\n\nexport interface GateBriefSnapshot {\n summary: string | undefined;\n evidenceRef: string | undefined;\n questions: GateQuestion[] | undefined;\n}\n\n/**\n * Checks the brief fields and returns a copy holding only the declared ones, so a caller\n * cannot change the stored questions after `create`. `required` makes `summary` and `evidenceRef` mandatory; without it they are still checked when present.\n */\nexport function snapshotBrief(gateId: string, raw: unknown, required: boolean): Readonly<GateBriefSnapshot> {\n const { summary, evidenceRef, questions } = (raw ?? {}) as Record<string, unknown>;\n const issues = [\n ...lineIssues(\"summary\", summary, MAX_SUMMARY, required),\n ...lineIssues(\"evidenceRef\", evidenceRef, MAX_EVIDENCE_REF, required),\n ];\n const questionIssue = questions === undefined ? undefined : questionsIssue(questions);\n if (questionIssue) issues.push(questionIssue);\n if (issues.length > 0) throw new GateBriefInvalid(gateId, issues);\n return Object.freeze({\n summary: summary as string | undefined,\n evidenceRef: evidenceRef as string | undefined,\n questions: questions === undefined ? undefined : copyQuestions(questions as GateQuestion[]),\n });\n}\n\nfunction lineIssues(field: string, value: unknown, max: number, required: boolean): string[] {\n if (value === undefined) return required ? [`${field} is required`] : [];\n if (typeof value !== \"string\") return [`${field} must be a string`];\n if (value.trim() === \"\") return [`${field} is blank`];\n if (value.length > max) return [`${field} is over ${max} characters`];\n if (CONTROL_CHAR.test(value)) return [`${field} must be one line with no control characters`];\n return [];\n}\n\nfunction questionsIssue(value: unknown): string | undefined {\n if (!Array.isArray(value)) return \"questions must be an array\";\n if (value.length < 1 || value.length > MAX_QUESTIONS) return `ask 1 to ${MAX_QUESTIONS} questions (got ${value.length})`;\n const ids = new Set<string>();\n for (const [index, raw] of value.entries()) {\n const question = (raw ?? {}) as Record<string, unknown>;\n const where = `question ${index + 1}`;\n if (typeof question.id !== \"string\" || !ID_PATTERN.test(question.id)) return `${where}: ${ID_RULE}`;\n if (ids.has(question.id)) return `${where}: duplicate question id \"${question.id}\"`;\n ids.add(question.id);\n const issue = questionTextIssue(question.question) ?? optionsIssue(question.options);\n if (issue) return `${where}: ${issue}`;\n }\n return undefined;\n}\n\nfunction questionTextIssue(text: unknown): string | undefined {\n if (typeof text !== \"string\" || text.trim() === \"\") return \"question text is required\";\n if (text.length > MAX_QUESTION_TEXT) return `question text is over ${MAX_QUESTION_TEXT} characters`;\n return undefined;\n}\n\nfunction optionsIssue(options: unknown): string | undefined {\n if (!Array.isArray(options) || options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {\n return `give ${MIN_OPTIONS} to ${MAX_OPTIONS} options`;\n }\n const ids = new Set<string>();\n let recommended = 0;\n for (const [index, raw] of options.entries()) {\n const option = (raw ?? {}) as Record<string, unknown>;\n const issue = optionIssue(option, ids);\n if (issue) return `option ${index + 1}: ${issue}`;\n ids.add(option.id as string);\n if (option.recommended === true) recommended++;\n }\n return recommended > 1 ? \"mark at most one option as recommended\" : undefined;\n}\n\nfunction optionIssue(option: Record<string, unknown>, seen: ReadonlySet<string>): string | undefined {\n if (typeof option.id !== \"string\" || !ID_PATTERN.test(option.id)) return ID_RULE;\n if (seen.has(option.id)) return `duplicate option id \"${option.id}\"`;\n if (typeof option.label !== \"string\" || option.label.trim() === \"\") return \"label is required\";\n if (option.label.length > MAX_OPTION_LABEL) return `label is over ${MAX_OPTION_LABEL} characters`;\n if (option.recommended !== undefined && typeof option.recommended !== \"boolean\") return \"recommended must be true or false\";\n return undefined;\n}\n\n/** Keeps only the declared fields, so nothing a caller attached rides along into the row. */\nfunction copyQuestions(questions: readonly GateQuestion[]): GateQuestion[] {\n return questions.map((question) => ({\n id: question.id,\n question: question.question,\n options: question.options.map(copyOption),\n }));\n}\n\nfunction copyOption(option: GateQuestionOption): GateQuestionOption {\n return option.recommended === true ? { id: option.id, label: option.label, recommended: true } : { id: option.id, label: option.label };\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, type ResolverClass } from \"@titan-design/authority\";\nimport {\n GateAuthorizeInvalid,\n GateResolverRefused,\n GateRuleInvalid,\n type GateAnswerAllowance,\n type GateAuthorization,\n type GateRecord,\n type GateResolver,\n type GateRule,\n} 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/** Reads each allowance once into a frozen deep copy, so a caller cannot widen the list after the store is built. */\nexport function snapshotAllowances(raw: readonly GateAnswerAllowance[] | undefined): readonly GateAnswerAllowance[] {\n return Object.freeze(\n (raw ?? []).map(({ resolverClass, stepId, payload }) =>\n Object.freeze({ resolverClass, stepId, payload: Object.freeze(structuredClone(payload)) }),\n ),\n );\n}\n\n/**\n * Like `defaultResolverRefusal`, but a listed (class, step, payload) triple is admitted. Anything not\n * listed gets the default refusal unchanged.\n */\nexport function resolverRefusal(\n gateId: string,\n resolver: GateResolver,\n payload: unknown,\n allowances: readonly GateAnswerAllowance[],\n): string | undefined {\n const refusal = defaultResolverRefusal(resolver);\n if (refusal === undefined) return undefined;\n if (!matchesAllowance(allowances, gateId, resolver, payload)) return refusal;\n return resolver.id.trim() === \"\" ? `actor class ${resolver.class} must name itself to resolve this gate` : undefined;\n}\n\n/** True when an allowance names this resolver's class, the gate's step and exactly this payload. The store and its callers share this test. */\nexport function matchesAllowance(allowances: readonly GateAnswerAllowance[], gateId: string, resolver: GateResolver, payload: unknown): boolean {\n return allowances.some((a) => a.resolverClass === resolver.class && stepOf(gateId) === a.stepId && jsonEqual(payload, a.payload));\n}\n\n/** Deep equality over plain JSON data (objects, arrays, primitives); anything else, such as a class instance, is unequal. */\nexport function jsonEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const proto = Object.getPrototypeOf(a) as unknown;\n if (proto !== Object.getPrototypeOf(b) || (proto !== Object.prototype && proto !== Array.prototype && proto !== null)) return false;\n const keys = Object.keys(a);\n if (keys.length !== Object.keys(b).length) return false;\n return keys.every((key) => Object.hasOwn(b, key) && jsonEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));\n}\n\n/** The step id of a gate id: the text after the last `/`, without a repeat suffix `:<n>`. */\nfunction stepOf(gateId: string): string {\n return gateId.slice(gateId.lastIndexOf(\"/\") + 1).replace(/:\\d+$/, \"\");\n}\n\n/** A rule-bound gate admits only its rule's resolver classes. Runs after the default refusal, so it only narrows. */\nexport function ruleResolverRefusal(record: GateRecord, resolver: GateResolver): string | undefined {\n if (!record.rule) return undefined;\n if ((record.rule.resolvers as readonly string[]).includes(resolver.class)) return undefined;\n return `rule ${record.rule.ruleId} does not let ${resolver.class} resolve this gate`;\n}\n\n/** Reads each declared rule field once into a frozen copy, so a caller cannot widen the rule after `create`. */\nexport function snapshotRule(gateId: string, raw: unknown): Readonly<GateRule> {\n if (typeof raw !== \"object\" || raw === null) throw new GateRuleInvalid(gateId, \"the rule is not an object\");\n const { table, version, ruleId, resolvers } = raw as Record<string, unknown>;\n if (typeof table !== \"string\" || typeof version !== \"string\" || typeof ruleId !== \"string\") {\n throw new GateRuleInvalid(gateId, \"the rule's table, version and ruleId must be strings\");\n }\n const classes = Array.isArray(resolvers) ? [...(resolvers as unknown[])] : [];\n if (classes.length === 0 || !classes.every(isResolverClass)) {\n throw new GateRuleInvalid(gateId, \"the rule's resolvers must be a non-empty list of resolver classes\");\n }\n return Object.freeze({ table, version, ruleId, resolvers: Object.freeze(classes) as ResolverClass[] });\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 isResolverClass(value: unknown): value is ResolverClass {\n return typeof value === \"string\" && (RESOLVER_CLASSES as readonly string[]).includes(value);\n}\n\nfunction isActorClass(value: unknown): value is ActorClass {\n return typeof value === \"string\" && (ACTOR_CLASSES as readonly string[]).includes(value);\n}\n","import { snapshotBrief } from \"./gate-brief.js\";\nimport { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport { jsonEqual, readDecision, resolverRefusal, ruleResolverRefusal, snapshotAllowances, snapshotResolver, snapshotRule } from \"./resolver-policy.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n GateResolverRefused,\n type GateAnswerAllowance,\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 private readonly requireBrief = false,\n allowances?: readonly GateAnswerAllowance[],\n ) {\n this.allowances = snapshotAllowances(allowances);\n }\n\n private readonly allowances: readonly GateAnswerAllowance[];\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n /** Writes only while the stored row is still pending; false means another writer settled it first. */\n protected abstract update(record: GateRecord): boolean;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? globalThis.crypto.randomUUID();\n const rule = input.rule === undefined ? undefined : snapshotRule(id, input.rule);\n const brief = snapshotBrief(id, input, this.requireBrief);\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 rule,\n ...brief,\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 if (!resolvedBy) throw new GateResolverRefused(id, undefined, \"a resolver is required\");\n const resolver = snapshotResolver(id, resolvedBy);\n const record = this.requirePending(id);\n this.requireAuthorized(record, resolver, payload);\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 return this.settle(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 return this.settle(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?.status !== \"pending\") throw notPending(id, record);\n return record;\n }\n\n /**\n * `authorize` runs between the pending check and the write, and another store may settle the row in that gap.\n * A writer that got there first with this same answer is a retry, not a conflict, so its row comes back.\n */\n private settle(record: GateRecord): GateRecord {\n if (this.update(record)) return record;\n const current = this.read(record.id);\n if (current && sameAnswer(current, record)) return current;\n throw notPending(record.id, current);\n }\n\n /** The default check (or a listed allowance) runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */\n private requireAuthorized(record: GateRecord, resolver: Readonly<GateResolver>, payload: unknown): void {\n const refusal = resolverRefusal(record.id, resolver, payload, this.allowances);\n if (refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);\n const ruleRefusal = ruleResolverRefusal(record, resolver);\n if (ruleRefusal) throw new GateResolverRefused(record.id, resolver.class, ruleRefusal);\n if (!this.authorize) return;\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 return this.update(expired) ? expired : (this.read(record.id) ?? expired);\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction sameAnswer(stored: GateRecord, attempted: GateRecord): boolean {\n return stored.status === attempted.status && stored.reason === attempted.reason && jsonEqual(stored.payload, attempted.payload);\n}\n\nfunction notPending(id: string, record: GateRecord | undefined): Error {\n if (!record) return new GateNotFound(id);\n if (record.status === \"expired\") return new GateExpired(id);\n return new GateAlreadySettled(id, record.status);\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":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodType } from 'zod';
|
|
2
|
-
import { G as GateRecord, a as GateQuestion, b as GateStore, c as GateResolver, B as BaseGateStore, d as GateAuthorize, e as GateAnswerAllowance, J as JsonSchema } from './base-store-
|
|
3
|
-
export { f as GateAborted, g as GateAlreadyExists, h as GateAlreadySettled, i as GateAuthorization, j as GateAuthorizeInvalid, k as GateBrief, l as GateBriefInvalid, m as GateCancelled, n as GateError, o as GateExpired, p as GateInput, q as GateNotFound, r as GatePayloadInvalid, s as GateQuestionOption, t as GateResolverRefused, u as GateRule, v as GateRuleInvalid, w as GateStatus, x as GateStoreSchemaOutdated } from './base-store-
|
|
2
|
+
import { G as GateRecord, a as GateQuestion, b as GateStore, c as GateResolver, B as BaseGateStore, d as GateAuthorize, e as GateAnswerAllowance, J as JsonSchema } from './base-store-imx8181g.js';
|
|
3
|
+
export { f as GateAborted, g as GateAlreadyExists, h as GateAlreadySettled, i as GateAuthorization, j as GateAuthorizeInvalid, k as GateBrief, l as GateBriefInvalid, m as GateCancelled, n as GateError, o as GateExpired, p as GateInput, q as GateNotFound, r as GatePayloadInvalid, s as GateQuestionOption, t as GateResolverRefused, u as GateRule, v as GateRuleInvalid, w as GateStatus, x as GateStoreSchemaOutdated } from './base-store-imx8181g.js';
|
|
4
4
|
import '@titan-design/authority';
|
|
5
5
|
|
|
6
6
|
declare const DEFAULT_POLL_MS = 250;
|
|
@@ -61,7 +61,7 @@ declare class MemoryGateStore extends BaseGateStore {
|
|
|
61
61
|
constructor(options?: MemoryGateStoreOptions);
|
|
62
62
|
protected insert(record: GateRecord): void;
|
|
63
63
|
protected read(id: string): GateRecord | undefined;
|
|
64
|
-
protected update(record: GateRecord):
|
|
64
|
+
protected update(record: GateRecord): boolean;
|
|
65
65
|
protected readByStatus(status: GateRecord["status"]): GateRecord[];
|
|
66
66
|
}
|
|
67
67
|
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
ruleResolverRefusal,
|
|
20
20
|
snapshotBrief,
|
|
21
21
|
snapshotResolver
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-GZ6IE2SD.js";
|
|
23
23
|
|
|
24
24
|
// src/gate.ts
|
|
25
25
|
import { toJSONSchema } from "zod";
|
|
@@ -99,7 +99,9 @@ var MemoryGateStore = class extends BaseGateStore {
|
|
|
99
99
|
return row ? copy(row) : void 0;
|
|
100
100
|
}
|
|
101
101
|
update(record) {
|
|
102
|
+
if (this.rows.get(record.id)?.status !== "pending") return false;
|
|
102
103
|
this.rows.set(record.id, copy(record));
|
|
104
|
+
return true;
|
|
103
105
|
}
|
|
104
106
|
readByStatus(status) {
|
|
105
107
|
return [...this.rows.values()].filter((row) => row.status === status).map(copy);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gate.ts","../src/memory-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateQuestion,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n /** Required, with `evidenceRef`, when the store has `requireBrief`. */\n summary?: string;\n evidenceRef?: string;\n questions?: GateQuestion[];\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.summary !== undefined) input.summary = options.summary;\n if (options.evidenceRef !== undefined) input.evidenceRef = options.evidenceRef;\n if (options.questions !== undefined) input.questions = options.questions;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy: GateResolver): GateRecord {\n return store.resolve(id, payload, resolvedBy);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateAnswerAllowance, GateAuthorize, GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\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. */\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\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now, options.authorize, options.requireBrief, options.allowances);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, copy(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? copy(row) : undefined;\n }\n\n protected update(record: GateRecord):
|
|
1
|
+
{"version":3,"sources":["../src/gate.ts","../src/memory-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateQuestion,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n /** Required, with `evidenceRef`, when the store has `requireBrief`. */\n summary?: string;\n evidenceRef?: string;\n questions?: GateQuestion[];\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.summary !== undefined) input.summary = options.summary;\n if (options.evidenceRef !== undefined) input.evidenceRef = options.evidenceRef;\n if (options.questions !== undefined) input.questions = options.questions;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy: GateResolver): GateRecord {\n return store.resolve(id, payload, resolvedBy);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateAnswerAllowance, GateAuthorize, GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\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. */\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\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now, options.authorize, options.requireBrief, options.allowances);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, copy(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? copy(row) : undefined;\n }\n\n protected update(record: GateRecord): boolean {\n if (this.rows.get(record.id)?.status !== \"pending\") return false;\n this.rows.set(record.id, copy(record));\n return true;\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map(copy);\n }\n}\n\n/** `resolvedBy`, `rule` and `questions` are nested, so a shallow spread would let a caller rewrite what the store holds. */\nfunction copy(record: GateRecord): GateRecord {\n return {\n ...record,\n resolvedBy: record.resolvedBy && { ...record.resolvedBy },\n rule: record.rule && { ...record.rule, resolvers: [...record.rule.resolvers] },\n questions: record.questions?.map((question) => ({ ...question, options: question.options.map((option) => ({ ...option })) })),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Db, Migration } from '@titan-design/store-sqlite';
|
|
2
|
-
import { B as BaseGateStore, d as GateAuthorize, e as GateAnswerAllowance, G as GateRecord, w as GateStatus } from './base-store-
|
|
2
|
+
import { B as BaseGateStore, d as GateAuthorize, e as GateAnswerAllowance, G as GateRecord, w as GateStatus } from './base-store-imx8181g.js';
|
|
3
3
|
import '@titan-design/authority';
|
|
4
4
|
|
|
5
5
|
declare const DEFAULT_GATE_TABLE = "hitl_gate";
|
|
@@ -46,7 +46,7 @@ declare class SqliteGateStore extends BaseGateStore {
|
|
|
46
46
|
protected insert(record: GateRecord): void;
|
|
47
47
|
private optionalColumns;
|
|
48
48
|
protected read(id: string): GateRecord | undefined;
|
|
49
|
-
protected update(record: GateRecord):
|
|
49
|
+
protected update(record: GateRecord): boolean;
|
|
50
50
|
/** Only a present column is cached: a migration can add one while the store is open, never remove one. */
|
|
51
51
|
private requireColumn;
|
|
52
52
|
protected readByStatus(status: GateStatus): GateRecord[];
|
package/dist/sqlite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BaseGateStore,
|
|
3
3
|
GateStoreSchemaOutdated
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GZ6IE2SD.js";
|
|
5
5
|
|
|
6
6
|
// src/sqlite-store.ts
|
|
7
7
|
import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
|
|
@@ -156,11 +156,12 @@ var SqliteGateStore = class extends BaseGateStore {
|
|
|
156
156
|
}
|
|
157
157
|
update(record) {
|
|
158
158
|
const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;
|
|
159
|
-
this.db.prepare(
|
|
159
|
+
const result = this.db.prepare(
|
|
160
160
|
`UPDATE ${quoteIdent(this.table)}
|
|
161
161
|
SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?
|
|
162
|
-
WHERE id =
|
|
162
|
+
WHERE id = ? AND status = 'pending'`
|
|
163
163
|
).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);
|
|
164
|
+
return result.changes > 0;
|
|
164
165
|
}
|
|
165
166
|
/** Only a present column is cached: a migration can add one while the store is open, never remove one. */
|
|
166
167
|
requireColumn(gateId, column, migration) {
|
package/dist/sqlite.js.map
CHANGED
|
@@ -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 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":[]}
|
|
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): boolean {\n const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;\n const result = this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?\n WHERE id = ? AND status = 'pending'`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);\n return result.changes > 0;\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 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/gate-brief.ts","../src/json-schema.ts","../src/resolver-policy.ts","../src/base-store.ts"],"sourcesContent":["import type { ActorClass, ResolverClass } 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 /** The authority rule that opened the gate; unset for a gate no rule governs. */\n rule: GateRule | undefined;\n /** One line for the owner: the decision and the recommendation. Unset for a gate opened without a brief. */\n summary: string | undefined;\n /** Where the owner checks the facts: an https URL, an absolute path, or `$ <command>`. */\n evidenceRef: string | undefined;\n /** A fixed menu a surface can render as buttons; the gate's `schema` still validates the answer. */\n questions: GateQuestion[] | undefined;\n}\n\nexport interface GateQuestionOption {\n /** 1-40 letters, digits, `-` or `_`; unique within its question. */\n id: string;\n /** 1-75 characters, the Slack button limit. */\n label: string;\n /** At most one option per question; omitted means false. */\n recommended?: true;\n}\n\nexport interface GateQuestion {\n /** 1-40 letters, digits, `-` or `_`; unique within the gate. */\n id: string;\n /** 1-500 characters. */\n question: string;\n /** 2-4 options. */\n options: GateQuestionOption[];\n}\n\n/** What the owner reads instead of the machine-facing prompt. A store with `requireBrief` refuses a gate without one. */\nexport interface GateBrief {\n /** 1-280 characters on one line. */\n summary: string;\n /** 1-500 characters on one line. */\n evidenceRef: string;\n /** 1-4 questions; absent means the gate's schema alone defines the answer. */\n questions?: GateQuestion[];\n}\n\n/** Binds a gate to the authority rule that opened it, so only that rule's resolver classes may answer. */\nexport interface GateRule {\n table: string;\n version: string;\n ruleId: string;\n resolvers: ResolverClass[];\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\n/**\n * One answer a non-owner class may give, declared by the product when it builds the store.\n * It admits exactly: `resolverClass`, a gate whose step is `stepId` (a bare id or `<id>:<n>` after\n * the last `/`), and a payload deep-equal to `payload`. Evaluated in place of the default refusal for\n * those gates only; the gate's rule and `authorize` still run after it. The resolver's id must be non-blank.\n */\nexport interface GateAnswerAllowance {\n resolverClass: ActorClass;\n stepId: string;\n payload: Readonly<Record<string, unknown>>;\n}\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 rule?: GateRule;\n /** Required, with `evidenceRef`, when the store has `requireBrief`. */\n summary?: string;\n evidenceRef?: string;\n questions?: GateQuestion[];\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/**\n * The store cannot record a resolver or a rule because its table predates the migration that adds the column.\n * `gateId` is empty when the store refuses at construction, before any gate is named.\n */\nexport class GateStoreSchemaOutdated extends GateError {\n constructor(\n gateId: string,\n readonly table: string,\n readonly migration: string,\n ) {\n const message =\n gateId === \"\"\n ? `cannot open a gate store on table ${table}: it needs ${migration}`\n : `gate ${gateId} cannot be recorded: table ${table} needs ${migration}`;\n super(message, gateId);\n }\n}\n\nexport class GateRuleInvalid extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} has an invalid rule: ${reason}`, gateId);\n }\n}\n\nexport class GateBriefInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`gate ${gateId} brief is invalid: ${issues.join(\"; \")}`, 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","/*!\n * The question bounds and `questionsIssue` are adapted from openrig,\n * https://github.com/mvschwarz/openrig, packages/daemon/src/domain/human-questions.ts\n * (parseHumanQuestions). Copyright 2026 Mike Schwarz, licensed under the Apache License 2.0,\n * http://www.apache.org/licenses/LICENSE-2.0.\n * Changed here: the checks report through GateBriefInvalid instead of a result union, and\n * `recommended` keeps only `true`.\n * @license Apache-2.0\n */\nimport { GateBriefInvalid, type GateQuestion, type GateQuestionOption } from \"./types.js\";\n\nconst MAX_SUMMARY = 280;\nconst MAX_EVIDENCE_REF = 500;\nconst MAX_QUESTIONS = 4;\nconst MIN_OPTIONS = 2;\nconst MAX_OPTIONS = 4;\n/** Slack's button text limit, so every option fits on a button. */\nconst MAX_OPTION_LABEL = 75;\nconst MAX_QUESTION_TEXT = 500;\n\nconst ID_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;\nconst ID_RULE = 'id must be 1-40 letters, digits, \"-\" or \"_\"';\n// eslint-disable-next-line no-control-regex -- a control character is exactly what a one-line field refuses\nconst CONTROL_CHAR = /[\\u0000-\\u001f\\u007f]/;\n\nexport interface GateBriefSnapshot {\n summary: string | undefined;\n evidenceRef: string | undefined;\n questions: GateQuestion[] | undefined;\n}\n\n/**\n * Checks the brief fields and returns a copy holding only the declared ones, so a caller\n * cannot change the stored questions after `create`. `required` makes `summary` and `evidenceRef` mandatory; without it they are still checked when present.\n */\nexport function snapshotBrief(gateId: string, raw: unknown, required: boolean): Readonly<GateBriefSnapshot> {\n const { summary, evidenceRef, questions } = (raw ?? {}) as Record<string, unknown>;\n const issues = [\n ...lineIssues(\"summary\", summary, MAX_SUMMARY, required),\n ...lineIssues(\"evidenceRef\", evidenceRef, MAX_EVIDENCE_REF, required),\n ];\n const questionIssue = questions === undefined ? undefined : questionsIssue(questions);\n if (questionIssue) issues.push(questionIssue);\n if (issues.length > 0) throw new GateBriefInvalid(gateId, issues);\n return Object.freeze({\n summary: summary as string | undefined,\n evidenceRef: evidenceRef as string | undefined,\n questions: questions === undefined ? undefined : copyQuestions(questions as GateQuestion[]),\n });\n}\n\nfunction lineIssues(field: string, value: unknown, max: number, required: boolean): string[] {\n if (value === undefined) return required ? [`${field} is required`] : [];\n if (typeof value !== \"string\") return [`${field} must be a string`];\n if (value.trim() === \"\") return [`${field} is blank`];\n if (value.length > max) return [`${field} is over ${max} characters`];\n if (CONTROL_CHAR.test(value)) return [`${field} must be one line with no control characters`];\n return [];\n}\n\nfunction questionsIssue(value: unknown): string | undefined {\n if (!Array.isArray(value)) return \"questions must be an array\";\n if (value.length < 1 || value.length > MAX_QUESTIONS) return `ask 1 to ${MAX_QUESTIONS} questions (got ${value.length})`;\n const ids = new Set<string>();\n for (const [index, raw] of value.entries()) {\n const question = (raw ?? {}) as Record<string, unknown>;\n const where = `question ${index + 1}`;\n if (typeof question.id !== \"string\" || !ID_PATTERN.test(question.id)) return `${where}: ${ID_RULE}`;\n if (ids.has(question.id)) return `${where}: duplicate question id \"${question.id}\"`;\n ids.add(question.id);\n const issue = questionTextIssue(question.question) ?? optionsIssue(question.options);\n if (issue) return `${where}: ${issue}`;\n }\n return undefined;\n}\n\nfunction questionTextIssue(text: unknown): string | undefined {\n if (typeof text !== \"string\" || text.trim() === \"\") return \"question text is required\";\n if (text.length > MAX_QUESTION_TEXT) return `question text is over ${MAX_QUESTION_TEXT} characters`;\n return undefined;\n}\n\nfunction optionsIssue(options: unknown): string | undefined {\n if (!Array.isArray(options) || options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {\n return `give ${MIN_OPTIONS} to ${MAX_OPTIONS} options`;\n }\n const ids = new Set<string>();\n let recommended = 0;\n for (const [index, raw] of options.entries()) {\n const option = (raw ?? {}) as Record<string, unknown>;\n const issue = optionIssue(option, ids);\n if (issue) return `option ${index + 1}: ${issue}`;\n ids.add(option.id as string);\n if (option.recommended === true) recommended++;\n }\n return recommended > 1 ? \"mark at most one option as recommended\" : undefined;\n}\n\nfunction optionIssue(option: Record<string, unknown>, seen: ReadonlySet<string>): string | undefined {\n if (typeof option.id !== \"string\" || !ID_PATTERN.test(option.id)) return ID_RULE;\n if (seen.has(option.id)) return `duplicate option id \"${option.id}\"`;\n if (typeof option.label !== \"string\" || option.label.trim() === \"\") return \"label is required\";\n if (option.label.length > MAX_OPTION_LABEL) return `label is over ${MAX_OPTION_LABEL} characters`;\n if (option.recommended !== undefined && typeof option.recommended !== \"boolean\") return \"recommended must be true or false\";\n return undefined;\n}\n\n/** Keeps only the declared fields, so nothing a caller attached rides along into the row. */\nfunction copyQuestions(questions: readonly GateQuestion[]): GateQuestion[] {\n return questions.map((question) => ({\n id: question.id,\n question: question.question,\n options: question.options.map(copyOption),\n }));\n}\n\nfunction copyOption(option: GateQuestionOption): GateQuestionOption {\n return option.recommended === true ? { id: option.id, label: option.label, recommended: true } : { id: option.id, label: option.label };\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, type ResolverClass } from \"@titan-design/authority\";\nimport {\n GateAuthorizeInvalid,\n GateResolverRefused,\n GateRuleInvalid,\n type GateAnswerAllowance,\n type GateAuthorization,\n type GateRecord,\n type GateResolver,\n type GateRule,\n} 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/** Reads each allowance once into a frozen deep copy, so a caller cannot widen the list after the store is built. */\nexport function snapshotAllowances(raw: readonly GateAnswerAllowance[] | undefined): readonly GateAnswerAllowance[] {\n return Object.freeze(\n (raw ?? []).map(({ resolverClass, stepId, payload }) =>\n Object.freeze({ resolverClass, stepId, payload: Object.freeze(structuredClone(payload)) }),\n ),\n );\n}\n\n/**\n * Like `defaultResolverRefusal`, but a listed (class, step, payload) triple is admitted. Anything not\n * listed gets the default refusal unchanged.\n */\nexport function resolverRefusal(\n gateId: string,\n resolver: GateResolver,\n payload: unknown,\n allowances: readonly GateAnswerAllowance[],\n): string | undefined {\n const refusal = defaultResolverRefusal(resolver);\n if (refusal === undefined) return undefined;\n if (!matchesAllowance(allowances, gateId, resolver, payload)) return refusal;\n return resolver.id.trim() === \"\" ? `actor class ${resolver.class} must name itself to resolve this gate` : undefined;\n}\n\n/** True when an allowance names this resolver's class, the gate's step and exactly this payload. The store and its callers share this test. */\nexport function matchesAllowance(allowances: readonly GateAnswerAllowance[], gateId: string, resolver: GateResolver, payload: unknown): boolean {\n return allowances.some((a) => a.resolverClass === resolver.class && stepOf(gateId) === a.stepId && jsonEqual(payload, a.payload));\n}\n\n/** Deep equality over plain JSON data (objects, arrays, primitives); anything else, such as a class instance, is unequal. */\nfunction jsonEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== \"object\" || typeof b !== \"object\" || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const proto = Object.getPrototypeOf(a) as unknown;\n if (proto !== Object.getPrototypeOf(b) || (proto !== Object.prototype && proto !== Array.prototype && proto !== null)) return false;\n const keys = Object.keys(a);\n if (keys.length !== Object.keys(b).length) return false;\n return keys.every((key) => Object.hasOwn(b, key) && jsonEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));\n}\n\n/** The step id of a gate id: the text after the last `/`, without a repeat suffix `:<n>`. */\nfunction stepOf(gateId: string): string {\n return gateId.slice(gateId.lastIndexOf(\"/\") + 1).replace(/:\\d+$/, \"\");\n}\n\n/** A rule-bound gate admits only its rule's resolver classes. Runs after the default refusal, so it only narrows. */\nexport function ruleResolverRefusal(record: GateRecord, resolver: GateResolver): string | undefined {\n if (!record.rule) return undefined;\n if ((record.rule.resolvers as readonly string[]).includes(resolver.class)) return undefined;\n return `rule ${record.rule.ruleId} does not let ${resolver.class} resolve this gate`;\n}\n\n/** Reads each declared rule field once into a frozen copy, so a caller cannot widen the rule after `create`. */\nexport function snapshotRule(gateId: string, raw: unknown): Readonly<GateRule> {\n if (typeof raw !== \"object\" || raw === null) throw new GateRuleInvalid(gateId, \"the rule is not an object\");\n const { table, version, ruleId, resolvers } = raw as Record<string, unknown>;\n if (typeof table !== \"string\" || typeof version !== \"string\" || typeof ruleId !== \"string\") {\n throw new GateRuleInvalid(gateId, \"the rule's table, version and ruleId must be strings\");\n }\n const classes = Array.isArray(resolvers) ? [...(resolvers as unknown[])] : [];\n if (classes.length === 0 || !classes.every(isResolverClass)) {\n throw new GateRuleInvalid(gateId, \"the rule's resolvers must be a non-empty list of resolver classes\");\n }\n return Object.freeze({ table, version, ruleId, resolvers: Object.freeze(classes) as ResolverClass[] });\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 isResolverClass(value: unknown): value is ResolverClass {\n return typeof value === \"string\" && (RESOLVER_CLASSES as readonly string[]).includes(value);\n}\n\nfunction isActorClass(value: unknown): value is ActorClass {\n return typeof value === \"string\" && (ACTOR_CLASSES as readonly string[]).includes(value);\n}\n","import { snapshotBrief } from \"./gate-brief.js\";\nimport { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport { readDecision, resolverRefusal, ruleResolverRefusal, snapshotAllowances, snapshotResolver, snapshotRule } from \"./resolver-policy.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n GateResolverRefused,\n type GateAnswerAllowance,\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 private readonly requireBrief = false,\n allowances?: readonly GateAnswerAllowance[],\n ) {\n this.allowances = snapshotAllowances(allowances);\n }\n\n private readonly allowances: readonly GateAnswerAllowance[];\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 const rule = input.rule === undefined ? undefined : snapshotRule(id, input.rule);\n const brief = snapshotBrief(id, input, this.requireBrief);\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 rule,\n ...brief,\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 if (!resolvedBy) throw new GateResolverRefused(id, undefined, \"a resolver is required\");\n const resolver = snapshotResolver(id, resolvedBy);\n const record = this.requirePending(id);\n this.requireAuthorized(record, resolver, payload);\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 (or a listed allowance) runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */\n private requireAuthorized(record: GateRecord, resolver: Readonly<GateResolver>, payload: unknown): void {\n const refusal = resolverRefusal(record.id, resolver, payload, this.allowances);\n if (refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);\n const ruleRefusal = ruleResolverRefusal(record, resolver);\n if (ruleRefusal) throw new GateResolverRefused(record.id, resolver.class, ruleRefusal);\n if (!this.authorize) return;\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":";AA0HO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YACE,SACS,QACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAJW;AAKb;AAEO,IAAM,eAAN,cAA2B,UAAU;AAAA,EAC1C,YAAY,QAAgB;AAC1B,UAAM,mBAAmB,MAAM,IAAI,MAAM;AAAA,EAC3C;AACF;AAEO,IAAM,oBAAN,cAAgC,UAAU;AAAA,EAC/C,YAAY,QAAgB;AAC1B,UAAM,kBAAkB,MAAM,mBAAmB,MAAM;AAAA,EACzD;AACF;AAEO,IAAM,qBAAN,cAAiC,UAAU;AAAA,EAChD,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,eAAe,MAAM,IAAI,MAAM;AAF1C;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,gBAAN,cAA4B,UAAU;AAAA,EAC3C,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,mBAAmB,MAAM,IAAI,MAAM;AAF9C;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,cAAN,cAA0B,UAAU;AAAA,EACzC,YAAY,QAAgB;AAC1B,UAAM,QAAQ,MAAM,sCAAsC,MAAM;AAAA,EAClE;AACF;AAEO,IAAM,qBAAN,cAAiC,UAAU;AAAA,EAChD,YACE,QACS,QACT;AACA,UAAM,oBAAoB,MAAM,+BAA+B,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM;AAFjF;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,cAAN,cAA0B,UAAU;AAAA,EACzC,YAAY,QAAgB,QAAgB;AAC1C,UAAM,2BAA2B,MAAM,KAAK,MAAM,IAAI,MAAM;AAAA,EAC9D;AACF;AAEO,IAAM,sBAAN,cAAkC,UAAU;AAAA,EACjD,YACE,QACS,YACA,QACT;AACA,UAAM,QAAQ,MAAM,4BAA4B,cAAc,qBAAqB,KAAK,MAAM,IAAI,MAAM;AAH/F;AACA;AAAA,EAGX;AAAA,EAJW;AAAA,EACA;AAIb;AAMO,IAAM,0BAAN,cAAsC,UAAU;AAAA,EACrD,YACE,QACS,OACA,WACT;AACA,UAAM,UACJ,WAAW,KACP,qCAAqC,KAAK,cAAc,SAAS,KACjE,QAAQ,MAAM,8BAA8B,KAAK,UAAU,SAAS;AAC1E,UAAM,SAAS,MAAM;AAPZ;AACA;AAAA,EAOX;AAAA,EARW;AAAA,EACA;AAQb;AAEO,IAAM,kBAAN,cAA8B,UAAU;AAAA,EAC7C,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,yBAAyB,MAAM,IAAI,MAAM;AAFpD;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,mBAAN,cAA+B,UAAU;AAAA,EAC9C,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,sBAAsB,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM;AAF5D;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,uBAAN,cAAmC,UAAU;AAAA,EAClD,YAAY,QAAgB;AAC1B,UAAM,sBAAsB,MAAM,8DAA8D,MAAM;AAAA,EACxG;AACF;;;AC9NA,IAAM,cAAc;AACpB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,cAAc;AAEpB,IAAM,mBAAmB;AACzB,IAAM,oBAAoB;AAE1B,IAAM,aAAa;AACnB,IAAM,UAAU;AAEhB,IAAM,eAAe;AAYd,SAAS,cAAc,QAAgB,KAAc,UAAgD;AAC1G,QAAM,EAAE,SAAS,aAAa,UAAU,IAAK,OAAO,CAAC;AACrD,QAAM,SAAS;AAAA,IACb,GAAG,WAAW,WAAW,SAAS,aAAa,QAAQ;AAAA,IACvD,GAAG,WAAW,eAAe,aAAa,kBAAkB,QAAQ;AAAA,EACtE;AACA,QAAM,gBAAgB,cAAc,SAAY,SAAY,eAAe,SAAS;AACpF,MAAI,cAAe,QAAO,KAAK,aAAa;AAC5C,MAAI,OAAO,SAAS,EAAG,OAAM,IAAI,iBAAiB,QAAQ,MAAM;AAChE,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA;AAAA,IACA,WAAW,cAAc,SAAY,SAAY,cAAc,SAA2B;AAAA,EAC5F,CAAC;AACH;AAEA,SAAS,WAAW,OAAe,OAAgB,KAAa,UAA6B;AAC3F,MAAI,UAAU,OAAW,QAAO,WAAW,CAAC,GAAG,KAAK,cAAc,IAAI,CAAC;AACvE,MAAI,OAAO,UAAU,SAAU,QAAO,CAAC,GAAG,KAAK,mBAAmB;AAClE,MAAI,MAAM,KAAK,MAAM,GAAI,QAAO,CAAC,GAAG,KAAK,WAAW;AACpD,MAAI,MAAM,SAAS,IAAK,QAAO,CAAC,GAAG,KAAK,YAAY,GAAG,aAAa;AACpE,MAAI,aAAa,KAAK,KAAK,EAAG,QAAO,CAAC,GAAG,KAAK,8CAA8C;AAC5F,SAAO,CAAC;AACV;AAEA,SAAS,eAAe,OAAoC;AAC1D,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,MAAI,MAAM,SAAS,KAAK,MAAM,SAAS,cAAe,QAAO,YAAY,aAAa,mBAAmB,MAAM,MAAM;AACrH,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,CAAC,OAAO,GAAG,KAAK,MAAM,QAAQ,GAAG;AAC1C,UAAM,WAAY,OAAO,CAAC;AAC1B,UAAM,QAAQ,YAAY,QAAQ,CAAC;AACnC,QAAI,OAAO,SAAS,OAAO,YAAY,CAAC,WAAW,KAAK,SAAS,EAAE,EAAG,QAAO,GAAG,KAAK,KAAK,OAAO;AACjG,QAAI,IAAI,IAAI,SAAS,EAAE,EAAG,QAAO,GAAG,KAAK,4BAA4B,SAAS,EAAE;AAChF,QAAI,IAAI,SAAS,EAAE;AACnB,UAAM,QAAQ,kBAAkB,SAAS,QAAQ,KAAK,aAAa,SAAS,OAAO;AACnF,QAAI,MAAO,QAAO,GAAG,KAAK,KAAK,KAAK;AAAA,EACtC;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,MAAmC;AAC5D,MAAI,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM,GAAI,QAAO;AAC3D,MAAI,KAAK,SAAS,kBAAmB,QAAO,yBAAyB,iBAAiB;AACtF,SAAO;AACT;AAEA,SAAS,aAAa,SAAsC;AAC1D,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,eAAe,QAAQ,SAAS,aAAa;AAC3F,WAAO,QAAQ,WAAW,OAAO,WAAW;AAAA,EAC9C;AACA,QAAM,MAAM,oBAAI,IAAY;AAC5B,MAAI,cAAc;AAClB,aAAW,CAAC,OAAO,GAAG,KAAK,QAAQ,QAAQ,GAAG;AAC5C,UAAM,SAAU,OAAO,CAAC;AACxB,UAAM,QAAQ,YAAY,QAAQ,GAAG;AACrC,QAAI,MAAO,QAAO,UAAU,QAAQ,CAAC,KAAK,KAAK;AAC/C,QAAI,IAAI,OAAO,EAAY;AAC3B,QAAI,OAAO,gBAAgB,KAAM;AAAA,EACnC;AACA,SAAO,cAAc,IAAI,2CAA2C;AACtE;AAEA,SAAS,YAAY,QAAiC,MAA+C;AACnG,MAAI,OAAO,OAAO,OAAO,YAAY,CAAC,WAAW,KAAK,OAAO,EAAE,EAAG,QAAO;AACzE,MAAI,KAAK,IAAI,OAAO,EAAE,EAAG,QAAO,wBAAwB,OAAO,EAAE;AACjE,MAAI,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,MAAM,GAAI,QAAO;AAC3E,MAAI,OAAO,MAAM,SAAS,iBAAkB,QAAO,iBAAiB,gBAAgB;AACpF,MAAI,OAAO,gBAAgB,UAAa,OAAO,OAAO,gBAAgB,UAAW,QAAO;AACxF,SAAO;AACT;AAGA,SAAS,cAAc,WAAoD;AACzE,SAAO,UAAU,IAAI,CAAC,cAAc;AAAA,IAClC,IAAI,SAAS;AAAA,IACb,UAAU,SAAS;AAAA,IACnB,SAAS,SAAS,QAAQ,IAAI,UAAU;AAAA,EAC1C,EAAE;AACJ;AAEA,SAAS,WAAW,QAAgD;AAClE,SAAO,OAAO,gBAAgB,OAAO,EAAE,IAAI,OAAO,IAAI,OAAO,OAAO,OAAO,aAAa,KAAK,IAAI,EAAE,IAAI,OAAO,IAAI,OAAO,OAAO,MAAM;AACxI;;;AC7GO,SAAS,uBAAuB,QAAoB,OAAgB,OAAO,KAAe;AAC/F,QAAM,SAAmB,CAAC;AAC1B,UAAQ,QAAQ,OAAO,MAAM,MAAM;AACnC,SAAO;AACT;AAEA,SAAS,QAAQ,QAAoB,OAAgB,MAAc,QAAwB;AACzF,MAAI,MAAM,QAAQ,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO,KAAK,GAAG;AAC9D,eAAY,OAAO,SAAS,OAAO,OAAwB,OAAO,MAAM,MAAM;AAC9E;AAAA,EACF;AACA,MAAI,WAAW,UAAU,CAAC,OAAO,GAAG,OAAO,OAAO,KAAK,GAAG;AACxD,WAAO,KAAK,GAAG,IAAI,cAAc,KAAK,UAAU,OAAO,KAAK,CAAC,EAAE;AAC/D;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,WAAW,OAAO,GAAG,QAAQ,KAAK,CAAC,GAAG;AACzF,WAAO,KAAK,GAAG,IAAI,qBAAqB,KAAK,UAAU,OAAO,IAAI,CAAC,EAAE;AACrE;AAAA,EACF;AACA,MAAI,OAAO,OAAO,SAAS,SAAU;AACrC,MAAI,CAAC,YAAY,OAAO,MAAM,KAAK,GAAG;AACpC,WAAO,KAAK,GAAG,IAAI,cAAc,OAAO,IAAI,cAAc,SAAS,KAAK,CAAC,EAAE;AAC3E;AAAA,EACF;AACA,MAAI,OAAO,SAAS,SAAU,aAAY,QAAQ,OAAkC,MAAM,MAAM;AAChG,MAAI,OAAO,SAAS,QAAS,YAAW,QAAQ,OAAoB,MAAM,MAAM;AAClF;AAEA,SAAS,WAAW,UAAwB,OAAgB,MAAc,QAAwB;AAChG,QAAM,UAAU,SAAS,KAAK,CAAC,WAAW,uBAAuB,QAAQ,OAAO,IAAI,EAAE,WAAW,CAAC;AAClG,MAAI,CAAC,QAAS,QAAO,KAAK,GAAG,IAAI,yBAAyB,SAAS,MAAM,iBAAiB;AAC5F;AAEA,SAAS,YAAY,QAAoB,OAAgC,MAAc,QAAwB;AAC7G,QAAM,aAAc,OAAO,cAAc,CAAC;AAC1C,aAAW,OAAQ,OAAO,YAAY,CAAC,GAAgB;AACrD,QAAI,EAAE,OAAO,OAAQ,QAAO,KAAK,GAAG,IAAI,IAAI,GAAG,YAAY;AAAA,EAC7D;AACA,MAAI,OAAO,yBAAyB,OAAO;AACzC,eAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAI,EAAE,OAAO,YAAa,QAAO,KAAK,GAAG,IAAI,IAAI,GAAG,uBAAuB;AAAA,IAC7E;AAAA,EACF;AACA,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,UAAU,GAAG;AACnD,QAAI,OAAO,MAAO,SAAQ,KAAK,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,GAAG,IAAI,MAAM;AAAA,EACrE;AACF;AAEA,SAAS,WAAW,QAAoB,OAAkB,MAAc,QAAwB;AAC9F,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAO;AACZ,QAAM,QAAQ,CAAC,OAAO,UAAU,QAAQ,OAAO,OAAO,GAAG,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AACpF;AAEA,SAAS,YAAY,MAAc,OAAyB;AAC1D,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAAA,IAC5E,KAAK;AACH,aAAO,MAAM,QAAQ,KAAK;AAAA,IAC5B,KAAK;AACH,aAAO,OAAO,UAAU,KAAK;AAAA,IAC/B,KAAK;AACH,aAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAAA,IAC3D,KAAK;AACH,aAAO,UAAU;AAAA,IACnB;AACE,aAAO,OAAO,UAAU;AAAA,EAC5B;AACF;AAEA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO,MAAM,QAAQ,KAAK,IAAI,UAAU,OAAO;AACjD;;;ACnFA,SAAS,eAAe,wBAA6D;AAgB9E,SAAS,uBAAuB,UAA4C;AACjF,MAAK,iBAAuC,SAAS,SAAS,KAAK,EAAG,QAAO;AAC7E,SAAO,eAAe,SAAS,KAAK;AACtC;AAGO,SAAS,mBAAmB,KAAiF;AAClH,SAAO,OAAO;AAAA,KACX,OAAO,CAAC,GAAG;AAAA,MAAI,CAAC,EAAE,eAAe,QAAQ,QAAQ,MAChD,OAAO,OAAO,EAAE,eAAe,QAAQ,SAAS,OAAO,OAAO,gBAAgB,OAAO,CAAC,EAAE,CAAC;AAAA,IAC3F;AAAA,EACF;AACF;AAMO,SAAS,gBACd,QACA,UACA,SACA,YACoB;AACpB,QAAM,UAAU,uBAAuB,QAAQ;AAC/C,MAAI,YAAY,OAAW,QAAO;AAClC,MAAI,CAAC,iBAAiB,YAAY,QAAQ,UAAU,OAAO,EAAG,QAAO;AACrE,SAAO,SAAS,GAAG,KAAK,MAAM,KAAK,eAAe,SAAS,KAAK,2CAA2C;AAC7G;AAGO,SAAS,iBAAiB,YAA4C,QAAgB,UAAwB,SAA2B;AAC9I,SAAO,WAAW,KAAK,CAAC,MAAM,EAAE,kBAAkB,SAAS,SAAS,OAAO,MAAM,MAAM,EAAE,UAAU,UAAU,SAAS,EAAE,OAAO,CAAC;AAClI;AAGA,SAAS,UAAU,GAAY,GAAqB;AAClD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,MAAM,QAAQ,MAAM,KAAM,QAAO;AACvF,MAAI,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAClD,QAAM,QAAQ,OAAO,eAAe,CAAC;AACrC,MAAI,UAAU,OAAO,eAAe,CAAC,KAAM,UAAU,OAAO,aAAa,UAAU,MAAM,aAAa,UAAU,KAAO,QAAO;AAC9H,QAAM,OAAO,OAAO,KAAK,CAAC;AAC1B,MAAI,KAAK,WAAW,OAAO,KAAK,CAAC,EAAE,OAAQ,QAAO;AAClD,SAAO,KAAK,MAAM,CAAC,QAAQ,OAAO,OAAO,GAAG,GAAG,KAAK,UAAW,EAA8B,GAAG,GAAI,EAA8B,GAAG,CAAC,CAAC;AACzI;AAGA,SAAS,OAAO,QAAwB;AACtC,SAAO,OAAO,MAAM,OAAO,YAAY,GAAG,IAAI,CAAC,EAAE,QAAQ,SAAS,EAAE;AACtE;AAGO,SAAS,oBAAoB,QAAoB,UAA4C;AAClG,MAAI,CAAC,OAAO,KAAM,QAAO;AACzB,MAAK,OAAO,KAAK,UAAgC,SAAS,SAAS,KAAK,EAAG,QAAO;AAClF,SAAO,QAAQ,OAAO,KAAK,MAAM,iBAAiB,SAAS,KAAK;AAClE;AAGO,SAAS,aAAa,QAAgB,KAAkC;AAC7E,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,OAAM,IAAI,gBAAgB,QAAQ,2BAA2B;AAC1G,QAAM,EAAE,OAAO,SAAS,QAAQ,UAAU,IAAI;AAC9C,MAAI,OAAO,UAAU,YAAY,OAAO,YAAY,YAAY,OAAO,WAAW,UAAU;AAC1F,UAAM,IAAI,gBAAgB,QAAQ,sDAAsD;AAAA,EAC1F;AACA,QAAM,UAAU,MAAM,QAAQ,SAAS,IAAI,CAAC,GAAI,SAAuB,IAAI,CAAC;AAC5E,MAAI,QAAQ,WAAW,KAAK,CAAC,QAAQ,MAAM,eAAe,GAAG;AAC3D,UAAM,IAAI,gBAAgB,QAAQ,mEAAmE;AAAA,EACvG;AACA,SAAO,OAAO,OAAO,EAAE,OAAO,SAAS,QAAQ,WAAW,OAAO,OAAO,OAAO,EAAqB,CAAC;AACvG;AAMO,SAAS,iBAAiB,QAAgB,KAAsC;AACrF,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,OAAM,IAAI,oBAAoB,QAAQ,QAAW,+BAA+B;AAC7H,QAAM,EAAE,OAAO,YAAY,IAAI,SAAS,aAAa,IAAI;AACzD,MAAI,CAAC,aAAa,UAAU,GAAG;AAC7B,UAAM,IAAI,oBAAoB,QAAQ,QAAW,iDAAiD;AAAA,EACpG;AACA,MAAI,OAAO,OAAO,YAAY,OAAO,YAAY,UAAU;AACzD,UAAM,IAAI,oBAAoB,QAAQ,YAAY,+CAA+C;AAAA,EACnG;AACA,MAAI,iBAAiB,OAAW,QAAO,OAAO,OAAO,EAAE,OAAO,YAAY,IAAI,QAAQ,CAAC;AACvF,MAAI,OAAO,iBAAiB,UAAU;AACpC,UAAM,IAAI,oBAAoB,QAAQ,YAAY,8CAA8C;AAAA,EAClG;AACA,SAAO,OAAO,OAAO,EAAE,OAAO,YAAY,IAAI,SAAS,aAAa,CAAC;AACvE;AAGO,SAAS,aAAa,QAAgB,UAAsC;AACjF,MAAI,OAAO,aAAa,YAAY,aAAa,KAAM,OAAM,IAAI,qBAAqB,MAAM;AAC5F,QAAM,EAAE,MAAM,SAAS,OAAO,IAAI;AAClC,MAAI,OAAO,SAAS,YAAY;AAC9B,IAAC,KAAqE,KAAK,UAAU,QAAW,MAAM;AAAA,IAAC,CAAC;AACxG,UAAM,IAAI,qBAAqB,MAAM;AAAA,EACvC;AACA,MAAI,OAAO,YAAY,UAAW,OAAM,IAAI,qBAAqB,MAAM;AACvE,MAAI,QAAS,QAAO,EAAE,SAAS,KAAK;AACpC,SAAO,EAAE,SAAS,OAAO,QAAQ,OAAO,WAAW,WAAW,SAAS,iCAAiC;AAC1G;AAEA,SAAS,gBAAgB,OAAwC;AAC/D,SAAO,OAAO,UAAU,YAAa,iBAAuC,SAAS,KAAK;AAC5F;AAEA,SAAS,aAAa,OAAqC;AACzD,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;;;AC1GO,IAAe,gBAAf,MAAkD;AAAA,EAC7C,YACW,OACF,WACA,eAAe,OAChC,YACA;AAJmB;AACF;AACA;AAGjB,SAAK,aAAa,mBAAmB,UAAU;AAAA,EACjD;AAAA,EANqB;AAAA,EACF;AAAA,EACA;AAAA,EAMF;AAAA,EAOjB,OAAO,OAA8B;AACnC,UAAM,KAAK,MAAM,MAAM,WAAW,OAAO,WAAW;AACpD,UAAM,OAAO,MAAM,SAAS,SAAY,SAAY,aAAa,IAAI,MAAM,IAAI;AAC/E,UAAM,QAAQ,cAAc,IAAI,OAAO,KAAK,YAAY;AACxD,QAAI,KAAK,KAAK,EAAE,EAAG,OAAM,IAAI,kBAAkB,EAAE;AACjD,UAAM,SAAqB;AAAA,MACzB;AAAA,MACA,QAAQ,MAAM;AAAA,MACd,QAAQ,MAAM;AAAA,MACd,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,WAAW,KAAK,OAAO;AAAA,MACvB,YAAY;AAAA,MACZ,WAAW,MAAM,MAAM,SAAS;AAAA,MAChC,YAAY;AAAA,MACZ;AAAA,MACA,GAAG;AAAA,IACL;AACA,SAAK,OAAO,MAAM;AAClB,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,IAAoC;AACtC,UAAM,SAAS,KAAK,KAAK,EAAE;AAC3B,WAAO,SAAS,KAAK,eAAe,MAAM,IAAI;AAAA,EAChD;AAAA,EAEA,QAAQ,IAAY,SAAkB,YAAsC;AAC1E,QAAI,CAAC,WAAY,OAAM,IAAI,oBAAoB,IAAI,QAAW,wBAAwB;AACtF,UAAM,WAAW,iBAAiB,IAAI,UAAU;AAChD,UAAM,SAAS,KAAK,eAAe,EAAE;AACrC,SAAK,kBAAkB,QAAQ,UAAU,OAAO;AAChD,QAAI,OAAO,QAAQ;AACjB,YAAM,SAAS,uBAAuB,OAAO,QAAQ,OAAO;AAC5D,UAAI,OAAO,SAAS,EAAG,OAAM,IAAI,mBAAmB,IAAI,MAAM;AAAA,IAChE;AACA,UAAM,WAAuB,EAAE,GAAG,QAAQ,QAAQ,YAAY,SAAS,YAAY,KAAK,OAAO,GAAG,YAAY,SAAS;AACvH,SAAK,OAAO,QAAQ;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,IAAY,QAA4B;AAC7C,UAAM,SAAS,KAAK,eAAe,EAAE;AACrC,UAAM,YAAwB,EAAE,GAAG,QAAQ,QAAQ,aAAa,QAAQ,YAAY,KAAK,OAAO,EAAE;AAClG,SAAK,OAAO,SAAS;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,cAA4B;AAC1B,WAAO,KAAK,aAAa,SAAS,EAC/B,IAAI,CAAC,WAAW,KAAK,eAAe,MAAM,CAAC,EAC3C,OAAO,CAAC,WAAW,OAAO,WAAW,SAAS,EAC9C,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;AAAA,EAC1D;AAAA,EAEQ,eAAe,IAAwB;AAC7C,UAAM,SAAS,KAAK,IAAI,EAAE;AAC1B,QAAI,CAAC,OAAQ,OAAM,IAAI,aAAa,EAAE;AACtC,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,YAAY,EAAE;AACzD,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,mBAAmB,IAAI,OAAO,MAAM;AAC/E,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,kBAAkB,QAAoB,UAAkC,SAAwB;AACtG,UAAM,UAAU,gBAAgB,OAAO,IAAI,UAAU,SAAS,KAAK,UAAU;AAC7E,QAAI,QAAS,OAAM,IAAI,oBAAoB,OAAO,IAAI,SAAS,OAAO,OAAO;AAC7E,UAAM,cAAc,oBAAoB,QAAQ,QAAQ;AACxD,QAAI,YAAa,OAAM,IAAI,oBAAoB,OAAO,IAAI,SAAS,OAAO,WAAW;AACrF,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,WAAW,aAAa,OAAO,IAAI,KAAK,UAAU,OAAO,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,QAAQ,CAAC;AAC/F,QAAI,CAAC,SAAS,QAAS,OAAM,IAAI,oBAAoB,OAAO,IAAI,SAAS,OAAO,SAAS,MAAM;AAAA,EACjG;AAAA;AAAA,EAGQ,eAAe,QAAgC;AACrD,QAAI,OAAO,WAAW,aAAa,CAAC,OAAO,UAAW,QAAO;AAC7D,QAAI,KAAK,MAAM,OAAO,SAAS,IAAI,KAAK,MAAM,EAAG,QAAO;AACxD,UAAM,UAAsB,EAAE,GAAG,QAAQ,QAAQ,WAAW,YAAY,KAAK,OAAO,EAAE;AACtF,SAAK,OAAO,OAAO;AACnB,WAAO;AAAA,EACT;AAAA,EAEU,SAAiB;AACzB,WAAO,IAAI,KAAK,KAAK,MAAM,CAAC,EAAE,YAAY;AAAA,EAC5C;AACF;AAEA,SAAS,MAAM,OAAsD;AACnE,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,UAAU,WAAW,QAAQ,MAAM,YAAY;AAC/D;","names":[]}
|