From 0d464a67eb7738c6d28f6e7a32dc6b86a2643ae6 Mon Sep 17 00:00:00 2001 From: Amal Date: Fri, 17 Jul 2026 00:27:19 -0700 Subject: [PATCH] feat: language and jurisdictions metadata in .mikeworkflow.json packs Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC --- backend/package.json | 3 +- backend/scripts/generate-workflow-schema.ts | 19 ++ backend/src/lib/workflowFormat.ts | 182 +++++++++++++++++ backend/src/routes/workflows.ts | 152 ++++++++++++++ backend/tests/workflowFormat.test.ts | 148 ++++++++++++++ .../tests/workflows.import-metadata.test.ts | 67 +++++++ schemas/workflow.schema.json | 188 ++++++++++++++++++ 7 files changed, 758 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/generate-workflow-schema.ts create mode 100644 backend/src/lib/workflowFormat.ts create mode 100644 backend/tests/workflowFormat.test.ts create mode 100644 backend/tests/workflows.import-metadata.test.ts create mode 100644 schemas/workflow.schema.json diff --git a/backend/package.json b/backend/package.json index 195a6acfcd..2affb1ed3b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "generate:workflow-schema": "tsx scripts/generate-workflow-schema.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.90.0", diff --git a/backend/scripts/generate-workflow-schema.ts b/backend/scripts/generate-workflow-schema.ts new file mode 100644 index 0000000000..f81e8a833e --- /dev/null +++ b/backend/scripts/generate-workflow-schema.ts @@ -0,0 +1,19 @@ +// Regenerates schemas/workflow.schema.json from the zod schema in +// src/lib/workflowFormat.ts — the single source of truth for the +// .mikeworkflow.json format. +// +// Run from backend: npm run generate:workflow-schema +// +// The drift test (workflowFormat.test.ts) fails CI whenever the committed +// file differs from what this script would write, so a format change is +// always a two-file commit: the zod schema and the regenerated JSON. + +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { buildWorkflowPackJsonSchema } from "../src/lib/workflowFormat"; + +const outPath = resolve(__dirname, "../../schemas/workflow.schema.json"); +const json = `${JSON.stringify(buildWorkflowPackJsonSchema(), null, 2)}\n`; + +writeFileSync(outPath, json); +console.log(`wrote ${outPath}`); diff --git a/backend/src/lib/workflowFormat.ts b/backend/src/lib/workflowFormat.ts new file mode 100644 index 0000000000..16726757c5 --- /dev/null +++ b/backend/src/lib/workflowFormat.ts @@ -0,0 +1,182 @@ +// Single source of truth for the .mikeworkflow.json interchange format. +// +// The zod schema below is THE definition of the format. Everything else is +// derived from it: +// - `importWorkflow` (routes/workflows.ts) validates uploads with it, so +// the API can never accept a file the published schema rejects. +// - `schemas/workflow.schema.json` (the schema we publish for external +// tooling) is GENERATED from it via `npm run generate:workflow-schema` +// in backend. Never edit that file by hand. +// - A drift test (workflowFormat.test.ts) fails CI if the generated file +// and this schema ever disagree, so the two cannot drift apart silently. +// +// If you change the format: edit this file, run the generator, and commit +// both files together. Breaking changes must bump WORKFLOW_PACK_FORMAT_VERSION. + +import { z } from "zod/v4"; + +export const WORKFLOW_PACK_FORMAT_VERSION = 1; + +const columnConfigSchema = z + .looseObject({ + name: z.string().describe("Column heading shown in the UI."), + prompt: z + .string() + .describe("The prompt sent to the LLM for each cell in this column."), + type: z + .enum(["text", "flag", "yesno"]) + .optional() + .describe( + "Optional cell rendering hint. 'flag' renders a coloured badge; 'yesno' renders Yes/No; 'text' (default) renders plain text.", + ), + }) + .describe( + "One column definition in a 'tabular' workflow's review table. Extra keys are allowed so newer exports keep importing into older deployments.", + ); + +export const workflowPackSchema = z.strictObject({ + formatVersion: z + .literal(WORKFLOW_PACK_FORMAT_VERSION) + .describe( + "Schema version. Always 1 for files produced by the current export endpoint. Future breaking changes will increment this value.", + ), + exportedAt: z.iso + .datetime() + .optional() + .describe( + "ISO 8601 timestamp of when the file was exported. Informational only — not used during import.", + ), + workflow: z.strictObject({ + title: z + .string() + .min(1) + .max(255) + .describe("Human-readable name shown in the workflow picker."), + type: z + .enum(["assistant", "tabular"]) + .describe( + "Determines where the workflow appears. 'assistant' workflows appear in the chat sidebar; 'tabular' workflows appear in the tabular review column picker.", + ), + prompt_md: z + .string() + .nullable() + .optional() + .describe( + "The full workflow prompt in Markdown. For 'assistant' workflows, this is injected into the system prompt when the workflow is activated. For 'tabular' workflows, this describes the analysis task for each cell.", + ), + columns_config: z + .array(columnConfigSchema) + .nullable() + .optional() + .describe( + "Column definitions for 'tabular' workflows. Each entry defines one column in the review table. Null for 'assistant' workflows.", + ), + practice: z + .string() + .nullable() + .optional() + .describe( + "Optional legal practice area tag (e.g. 'corporate', 'ip', 'employment'). Used for filtering in the workflow picker.", + ), + language: z + .string() + .nullable() + .optional() + .describe( + "Optional drafting/analysis language (e.g. 'English', 'French'). Defaults to 'English' on import when omitted.", + ), + jurisdictions: z + .array(z.string()) + .nullable() + .optional() + .describe( + "Optional governing-law jurisdiction tags (e.g. ['England and Wales', 'Singapore']). Defaults to ['General'] on import when omitted.", + ), + }), +}); + +export type WorkflowPack = z.infer; + +// Turns zod validation issues into the single human-readable `detail` string +// the import endpoint returns. Kept here so route code never needs to know +// zod's issue format. +export function describeWorkflowPackIssues(error: z.ZodError): string { + return error.issues + .map((issue) => { + const path = issue.path.length ? issue.path.join(".") : "(root)"; + return `${path}: ${issue.message}`; + }) + .join("; "); +} + +// Builds the exact JSON value published as schemas/workflow.schema.json. +// The zod schema converts to draft-07; the envelope ($id, title, examples) +// is metadata for external consumers and lives here so the generator and the +// drift test share one definition. +export function buildWorkflowPackJsonSchema(): Record { + const converted = z.toJSONSchema(workflowPackSchema, { + target: "draft-7", + }) as Record; + + return { + ...converted, + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://github.com/willchen96/mike/schemas/workflow.schema.json", + title: "Mike Workflow Pack", + description: + "Schema for .mikeworkflow.json files exported from and imported into Mike. GENERATED from backend/src/lib/workflowFormat.ts by `npm run generate:workflow-schema` — do not edit by hand.", + examples: [ + { + formatVersion: 1, + exportedAt: "2026-05-24T12:00:00.000Z", + workflow: { + title: "NDA Quick Review", + type: "assistant", + prompt_md: + "Review the provided NDA and identify:\n1. Key definitions and their scope\n2. Exclusions from confidential information\n3. Duration of confidentiality obligations\n4. Any unusual or unfair clauses\n\nProvide a structured summary with a risk rating (Low / Medium / High).", + columns_config: null, + practice: "corporate", + language: "English", + jurisdictions: ["General"], + }, + }, + { + formatVersion: 1, + exportedAt: "2026-05-24T12:00:00.000Z", + workflow: { + title: "Contract Risk Matrix", + type: "tabular", + prompt_md: null, + columns_config: [ + { + name: "Governing Law", + prompt: + "What jurisdiction's law governs this agreement? Return only the jurisdiction name.", + type: "text", + }, + { + name: "Liability Cap", + prompt: + "Is there a liability cap? If yes, state the amount or formula. If no, say 'None'.", + type: "text", + }, + { + name: "Auto-Renewal", + prompt: "Does this contract auto-renew? Answer Yes or No.", + type: "yesno", + }, + { + name: "Red Flag", + prompt: + "Does this contract contain any clauses that are unusual, unfair, or potentially unenforceable? If yes, flag as RED and briefly explain. If no, flag as GREEN.", + type: "flag", + }, + ], + practice: "corporate", + language: "English", + jurisdictions: ["England and Wales"], + }, + }, + ], + }; +} diff --git a/backend/src/routes/workflows.ts b/backend/src/routes/workflows.ts index 62b28d4c8a..9e566f0e37 100644 --- a/backend/src/routes/workflows.ts +++ b/backend/src/routes/workflows.ts @@ -7,6 +7,11 @@ import { type SystemWorkflow, } from "../lib/systemWorkflows"; import { findMissingUserEmails } from "../lib/userLookup"; +import { + WORKFLOW_PACK_FORMAT_VERSION, + describeWorkflowPackIssues, + workflowPackSchema, +} from "../lib/workflowFormat"; export const workflowsRouter = Router(); @@ -771,6 +776,153 @@ workflowsRouter.post("/:workflowId/share", requireAuth, asyncRoute(async (req, r res.status(204).send(); })); +// --------------------------------------------------------------------------- +// Import / export (.mikeworkflow.json) +// --------------------------------------------------------------------------- + +export async function exportWorkflow( + db: Db, + params: { workflowId: string; userId: string }, +): Promise< + | { ok: true; payload: Record; filename: string } + | { ok: false } +> { + const { workflowId, userId } = params; + + const { data: wf } = await db + .from("workflows") + .select( + "title, type, prompt_md, columns_config, practice, language, jurisdictions", + ) + .eq("id", workflowId) + .eq("user_id", userId) + .single(); + + if (!wf) return { ok: false }; + + const payload = { + formatVersion: WORKFLOW_PACK_FORMAT_VERSION, + exportedAt: new Date().toISOString(), + workflow: { + title: wf.title, + type: wf.type, + prompt_md: wf.prompt_md ?? null, + columns_config: wf.columns_config ?? null, + practice: wf.practice ?? null, + language: wf.language ?? null, + jurisdictions: wf.jurisdictions ?? null, + }, + }; + + // Produce a safe filename from the workflow title. + const safeName = String(wf.title ?? "workflow") + .replace(/[^a-zA-Z0-9 _-]/g, "") + .trim() + .replace(/\s+/g, "-") + .slice(0, 80) || "workflow"; + + return { ok: true, payload, filename: `${safeName}.mikeworkflow.json` }; +} + +export type ImportWorkflowResult = + | { ok: true; workflow: Record } + | { ok: false; kind: "validation"; detail: string } + | { ok: false; kind: "db_error"; detail: string }; + +export async function importWorkflow( + db: Db, + params: { userId: string; body: Record }, +): Promise { + const { userId, body } = params; + + // Validate against the same schema we publish as + // schemas/workflow.schema.json — one definition of the format, so the API + // can never accept a file the published schema rejects (or vice versa). + const parsed = workflowPackSchema.safeParse(body); + if (!parsed.success) { + return { + ok: false, + kind: "validation", + detail: `Invalid workflow file: ${describeWorkflowPackIssues(parsed.error)}`, + }; + } + const wf = parsed.data.workflow; + const title = wf.title.trim(); + if (!title) + return { ok: false, kind: "validation", detail: "workflow.title is required." }; + + const { data, error } = await db + .from("workflows") + .insert({ + user_id: userId, + title, + type: wf.type, + prompt_md: wf.prompt_md ?? null, + columns_config: wf.columns_config ?? null, + practice: wf.practice ?? null, + // Imported files may predate these fields — normalize to the defaults. + language: + normalizeOptionalString(wf.language) ?? DEFAULT_WORKFLOW_LANGUAGE, + jurisdictions: + normalizeJurisdictions(wf.jurisdictions) ?? + DEFAULT_WORKFLOW_JURISDICTIONS, + }) + .select("*") + .single(); + + if (error || !data) { + return { + ok: false, + kind: "db_error", + detail: error?.message ?? "Failed to import workflow.", + }; + } + + return { ok: true, workflow: withDatabaseWorkflow(data as WorkflowRecord) }; +} + +// GET /workflows/:workflowId/export +// Returns the workflow as a downloadable .mikeworkflow.json file. +// Only the owner can export — the exported file contains the full prompt +// content which may be proprietary. +workflowsRouter.get("/:workflowId/export", requireAuth, asyncRoute(async (req, res) => { + const userId = res.locals.userId as string; + const { workflowId } = req.params; + const db = createServerSupabase(); + + const result = await exportWorkflow(db, { workflowId, userId }); + if (!result.ok) + return void res.status(404).json({ detail: "Workflow not found" }); + + res.setHeader("Content-Type", "application/json"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${result.filename}"`, + ); + res.json(result.payload); +})); + +// POST /workflows/import +// Accepts a .mikeworkflow.json payload (the body, not a file upload) and +// creates a new workflow owned by the authenticated user. The imported +// workflow always gets a fresh ID — it is never merged with an existing one. +workflowsRouter.post("/import", requireAuth, asyncRoute(async (req, res) => { + const userId = res.locals.userId as string; + const db = createServerSupabase(); + + const result = await importWorkflow(db, { + userId, + body: req.body as Record, + }); + if (!result.ok) { + if (result.kind === "validation") + return void res.status(400).json({ detail: result.detail }); + return void res.status(500).json({ detail: result.detail }); + } + + res.status(201).json(result.workflow); +})); + workflowsRouter.use( (err: unknown, _req: Request, res: Response, next: NextFunction) => { if (res.headersSent) return next(err); diff --git a/backend/tests/workflowFormat.test.ts b/backend/tests/workflowFormat.test.ts new file mode 100644 index 0000000000..8de4929c7e --- /dev/null +++ b/backend/tests/workflowFormat.test.ts @@ -0,0 +1,148 @@ +// Guards the single-source-of-truth contract for the .mikeworkflow.json +// format: the zod schema in workflowFormat.ts is the definition, the +// published schemas/workflow.schema.json is generated from it, and this test +// fails whenever the two disagree — so the published contract can never +// drift from what the import endpoint actually enforces. + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + buildWorkflowPackJsonSchema, + describeWorkflowPackIssues, + workflowPackSchema, +} from "../src/lib/workflowFormat"; + +const publishedSchemaPath = resolve( + __dirname, + "../../schemas/workflow.schema.json", +); + +describe("workflow pack schema — drift check", () => { + it("schemas/workflow.schema.json matches the zod source of truth", () => { + const published = JSON.parse(readFileSync(publishedSchemaPath, "utf8")); + // Deep equality, not just key presence: any change to the zod schema + // must be accompanied by `npm run generate:workflow-schema`. + expect(published).toEqual(buildWorkflowPackJsonSchema()); + }); + + it("the published examples validate against the schema they document", () => { + const examples = buildWorkflowPackJsonSchema().examples as unknown[]; + expect(examples.length).toBeGreaterThan(0); + for (const example of examples) { + const result = workflowPackSchema.safeParse(example); + expect( + result.success, + result.success ? "" : describeWorkflowPackIssues(result.error), + ).toBe(true); + } + }); +}); + +describe("workflow pack schema — validation behavior", () => { + const validPack = { + formatVersion: 1, + workflow: { title: "NDA Review", type: "assistant" }, + }; + + it("accepts a minimal valid pack", () => { + expect(workflowPackSchema.safeParse(validPack).success).toBe(true); + }); + + it("accepts and preserves language and jurisdiction metadata", () => { + const result = workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { + title: "Cross-border NDA", + type: "assistant", + language: "French", + jurisdictions: ["France", "England and Wales"], + }, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.workflow.language).toBe("French"); + expect(result.data.workflow.jurisdictions).toEqual([ + "France", + "England and Wales", + ]); + } + }); + + it("rejects non-string jurisdiction entries", () => { + expect( + workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { + title: "Invalid metadata", + type: "assistant", + jurisdictions: ["France", 42], + }, + }).success, + ).toBe(false); + }); + + it("rejects a wrong formatVersion", () => { + const result = workflowPackSchema.safeParse({ + ...validPack, + formatVersion: 2, + }); + expect(result.success).toBe(false); + }); + + it("rejects an unknown workflow type", () => { + const result = workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { title: "x", type: "spreadsheet" }, + }); + expect(result.success).toBe(false); + }); + + it("rejects unknown top-level keys (additionalProperties: false)", () => { + const result = workflowPackSchema.safeParse({ + ...validPack, + injected: "payload", + }); + expect(result.success).toBe(false); + }); + + it("allows extra keys inside a column (forward compatibility)", () => { + const result = workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { + title: "Risk Matrix", + type: "tabular", + columns_config: [ + { name: "Law", prompt: "Which law governs?", future_field: 42 }, + ], + }, + }); + expect(result.success).toBe(true); + }); + + it("rejects a column missing its prompt", () => { + const result = workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { + title: "Risk Matrix", + type: "tabular", + columns_config: [{ name: "Law" }], + }, + }); + expect(result.success).toBe(false); + }); + + it("describeWorkflowPackIssues names the offending path", () => { + const result = workflowPackSchema.safeParse({ + formatVersion: 1, + workflow: { title: "", type: "assistant" }, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(describeWorkflowPackIssues(result.error)).toContain( + "workflow.title", + ); + } + }); +}); diff --git a/backend/tests/workflows.import-metadata.test.ts b/backend/tests/workflows.import-metadata.test.ts new file mode 100644 index 0000000000..3a3ae374bb --- /dev/null +++ b/backend/tests/workflows.import-metadata.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { importWorkflow } from "../src/routes/workflows"; + +function database() { + const inserts: Record[] = []; + const db = { + from(table: string) { + expect(table).toBe("workflows"); + return { + insert(row: Record) { + inserts.push(row); + return { + select() { + return { + single: async () => ({ + data: { id: "wf-1", ...row }, + error: null, + }), + }; + }, + }; + }, + }; + }, + }; + return { db: db as Parameters[0], inserts }; +} + +describe("workflow import metadata", () => { + it("stores explicit language and jurisdictions", async () => { + const { db, inserts } = database(); + const result = await importWorkflow(db, { + userId: "user-1", + body: { + formatVersion: 1, + workflow: { + title: "Cross-border review", + type: "assistant", + language: "French", + jurisdictions: ["France"], + }, + }, + }); + + expect(result.ok).toBe(true); + expect(inserts[0]).toMatchObject({ + language: "French", + jurisdictions: ["France"], + }); + }); + + it("applies stable defaults to legacy version-one packs", async () => { + const { db, inserts } = database(); + await importWorkflow(db, { + userId: "user-1", + body: { + formatVersion: 1, + workflow: { title: "Legacy review", type: "assistant" }, + }, + }); + + expect(inserts[0]).toMatchObject({ + language: "English", + jurisdictions: ["General"], + }); + }); +}); diff --git a/schemas/workflow.schema.json b/schemas/workflow.schema.json new file mode 100644 index 0000000000..b0b75cbab5 --- /dev/null +++ b/schemas/workflow.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "formatVersion": { + "description": "Schema version. Always 1 for files produced by the current export endpoint. Future breaking changes will increment this value.", + "type": "number", + "const": 1 + }, + "exportedAt": { + "description": "ISO 8601 timestamp of when the file was exported. Informational only — not used during import.", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "workflow": { + "type": "object", + "properties": { + "title": { + "description": "Human-readable name shown in the workflow picker.", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "type": { + "description": "Determines where the workflow appears. 'assistant' workflows appear in the chat sidebar; 'tabular' workflows appear in the tabular review column picker.", + "type": "string", + "enum": [ + "assistant", + "tabular" + ] + }, + "prompt_md": { + "description": "The full workflow prompt in Markdown. For 'assistant' workflows, this is injected into the system prompt when the workflow is activated. For 'tabular' workflows, this describes the analysis task for each cell.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "columns_config": { + "description": "Column definitions for 'tabular' workflows. Each entry defines one column in the review table. Null for 'assistant' workflows.", + "anyOf": [ + { + "type": "array", + "items": { + "description": "One column definition in a 'tabular' workflow's review table. Extra keys are allowed so newer exports keep importing into older deployments.", + "type": "object", + "properties": { + "name": { + "description": "Column heading shown in the UI.", + "type": "string" + }, + "prompt": { + "description": "The prompt sent to the LLM for each cell in this column.", + "type": "string" + }, + "type": { + "description": "Optional cell rendering hint. 'flag' renders a coloured badge; 'yesno' renders Yes/No; 'text' (default) renders plain text.", + "type": "string", + "enum": [ + "text", + "flag", + "yesno" + ] + } + }, + "required": [ + "name", + "prompt" + ], + "additionalProperties": {} + } + }, + { + "type": "null" + } + ] + }, + "practice": { + "description": "Optional legal practice area tag (e.g. 'corporate', 'ip', 'employment'). Used for filtering in the workflow picker.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "language": { + "description": "Optional drafting/analysis language (e.g. 'English', 'French'). Defaults to 'English' on import when omitted.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "jurisdictions": { + "description": "Optional governing-law jurisdiction tags (e.g. ['England and Wales', 'Singapore']). Defaults to ['General'] on import when omitted.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "title", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "formatVersion", + "workflow" + ], + "additionalProperties": false, + "$id": "https://github.com/willchen96/mike/schemas/workflow.schema.json", + "title": "Mike Workflow Pack", + "description": "Schema for .mikeworkflow.json files exported from and imported into Mike. GENERATED from backend/src/lib/workflowFormat.ts by `npm run generate:workflow-schema` — do not edit by hand.", + "examples": [ + { + "formatVersion": 1, + "exportedAt": "2026-05-24T12:00:00.000Z", + "workflow": { + "title": "NDA Quick Review", + "type": "assistant", + "prompt_md": "Review the provided NDA and identify:\n1. Key definitions and their scope\n2. Exclusions from confidential information\n3. Duration of confidentiality obligations\n4. Any unusual or unfair clauses\n\nProvide a structured summary with a risk rating (Low / Medium / High).", + "columns_config": null, + "practice": "corporate", + "language": "English", + "jurisdictions": [ + "General" + ] + } + }, + { + "formatVersion": 1, + "exportedAt": "2026-05-24T12:00:00.000Z", + "workflow": { + "title": "Contract Risk Matrix", + "type": "tabular", + "prompt_md": null, + "columns_config": [ + { + "name": "Governing Law", + "prompt": "What jurisdiction's law governs this agreement? Return only the jurisdiction name.", + "type": "text" + }, + { + "name": "Liability Cap", + "prompt": "Is there a liability cap? If yes, state the amount or formula. If no, say 'None'.", + "type": "text" + }, + { + "name": "Auto-Renewal", + "prompt": "Does this contract auto-renew? Answer Yes or No.", + "type": "yesno" + }, + { + "name": "Red Flag", + "prompt": "Does this contract contain any clauses that are unusual, unfair, or potentially unenforceable? If yes, flag as RED and briefly explain. If no, flag as GREEN.", + "type": "flag" + } + ], + "practice": "corporate", + "language": "English", + "jurisdictions": [ + "England and Wales" + ] + } + } + ] +}