{
  "name": "Check an RFP response before you submit it",
  "nodes": [
    {
      "parameters": {
        "content": "## Check an RFP response before you submit it\nCheck a response to a request for proposals against every mandatory item, every judged criterion and every attachment, test its figures against your own records, and get a ready, ready after fixes or not ready verdict with a fix list that you decide on.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Each quote in the Item (quote) column appears in the call for proposals, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Each quote in the Where It Is Answered (quote) column appears in the your draft response, with shortened fragments in the same order. If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Every row of the table has one of \"covered\", \"partly covered\" or \"not covered\" in the Coverage column. If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): Every row of the table has one of \"matches\", \"differs\" or \"no record\" in the Verdict column. If it fails: try once more, then flag.\n- Check 5 (a plain rule, run automatically with no AI): Every number in the answer also appears somewhere in at least one of \"the call for proposals\", \"your draft response\" and \"your records\"; this does not check what each number means. If it fails: try once more, then flag.\n- Check 6 (a plain rule, run automatically with no AI): Every row of the table has one of \"must\", \"judged\", \"format\", \"form\" or \"deadline\" in the Type column. If it fails: try once more, then flag.\n- Check 7 (a second AI prompt answers pass or fail): Read the call below. Find every sentence that holds must, shall, required, attached, a weight or a due date. Does each one have a row in the table? Answer FAIL and quote each sentence with no row. If it fails: try once more, then flag.\n- Check 8 (a second AI prompt answers pass or fail): For each row marked None found, name the key word or number the item needs, then look in the response below for a line that holds it. Answer FAIL and quote each line that answers an item marked None found. If it fails: try once more, then flag.\n- Check 9 (a plain rule, run automatically with no AI): Every row of the table has one of \"record\", \"claim only\" or \"not needed\" in the Backed By column. If it fails: try once more, then flag.\n- Check 10 (a second AI prompt answers pass or fail): For each row marked covered, quote from the response below the words that give each thing the item asks for. Answer FAIL and name each covered row where a thing the item asks for has no words. If it fails: try once more, then flag.\n- Check 11 (a second AI prompt answers pass or fail): Look at the response below. Count the totals it states, such as a fixed price made of parts. Does the table show a sum for each one, and is each sum right? Answer FAIL and quote each total with no sum or a wrong sum. If it fails: try once more, then flag.\n- Check 12 (a second AI prompt answers pass or fail): For each row of the table whose Record is not No record, is that passage in the records below, word for word? Answer FAIL and quote each passage that is not. If it fails: try once more, then flag.\n- Check 13 (a plain rule, run automatically with no AI): The answer contains the text fragment \"This verdict comes from the checks above. An AI tool can miss things, so read the flagged parts yourself.\" (including inside a longer word). If it fails: try once more, then flag.\n- Check 14 (a second AI prompt answers pass or fail): Count the problems in the two tables below: each must, judged or form row that is not covered or is partly covered, each Backed By of claim only, and each verdict of differs. Count a problem raised in both tables once. Does the fix table have one row for each? Show both numbers. Answer FAIL and name each problem with no row. If it fails: try once more, then flag.\n- Check 15 (a second AI prompt answers pass or fail): The rule is: Not ready if any must item is not covered, any figure differs, or a form is not covered. Ready after fixes if any other row was raised. Ready by these checks if none was. Use the two tables below. Does the verdict follow the rule? Answer FAIL and say which line of the rule it breaks. If it fails: try once more, then flag.\n- Check 16 (you check it by hand): Check the deadline and the length rule against the response yourself, count the pages, and confirm the forms are attached and signed. For each figure that differs, decide which number is right. A person who can sign for your organization must read the call and the response together before anything is submitted. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 1320,
        "width": 760,
        "color": 5
      },
      "id": "1d15b8da-1341-4d2f-a9fc-42b067a4ff1d",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -1280
      ]
    },
    {
      "parameters": {},
      "id": "83d2bd5e-391c-45cf-a78d-23eccff8b955",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "813d7ce1-7cec-44cc-af6c-a7d76f23b6da",
              "name": "rfp",
              "value": "Sample: Request for Proposals, Riverside Park Grounds Care. Responses are due by 5 pm on June 14, as one PDF of no more than 8 pages. The response must name a lead contact. It must show a fixed annual price in dollars. It must describe the weekly work plan. It must list three similar jobs from the last five years, each with a client contact. It must include proof of liability cover of at least 2 million dollars. The Signed Form A must be attached. Responses are judged on price (40 percent), the work plan (35 percent) and past work (25 percent).",
              "type": "string"
            },
            {
              "id": "ca92c054-5413-41ab-a429-86aa902ef369",
              "name": "response",
              "value": "Sample: Response to the Riverside Park Grounds Care RFP, from Greenline Services. Lead contact: Sam Ortiz, 555-0166. Fixed annual price: 41,500 dollars (mowing 24,000, beds and paths 11,500, litter and bins 6,000). Work plan: a crew of 3 visits every Tuesday and Friday; beds are weeded monthly. Past work: Eastgate Business Park, 2022 to now (contact: Lee Park, 555-0120); Harbor View Apartments, 2021 to 2024 (contact: Jo Marsh, 555-0145). Insurance: we hold liability cover of 2 million dollars. Attached: Form A.",
              "type": "string"
            },
            {
              "id": "6bbb0e27-6cfd-442e-a137-b2a995e7f9d0",
              "name": "records",
              "value": "Sample: Price sheet, May: mowing 24,000; beds and paths 11,500; litter and bins 5,200. Job list: Eastgate Business Park, 2022 to now; Harbor View Apartments, 2021 to 2024; Cedar School, 2019 to 2021. Insurance letter: liability cover of 1.5 million dollars, renewed January 1.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "9de07bb3-8bbc-4128-a9c5-6545422245ba",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: rfp = The Call For Proposals; response = Your Draft Response; records = Your Records (optional).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful bid reviewer who reads a call for proposals line by line.\n\nYour task is to list every item in the call below.\n\nThe call:\n{{ $(\"Inputs\").first().json[\"rfp\"] }}\n\nRules:\n- One row for each item. Split a sentence that holds two items.\n- Quote each item from the call word for word, as short as you can while keeping its numbers and conditions. Do not correct it and do not reword it.\n- In the Type column, write exactly one of: must, judged, format, form, deadline.\n- Must is something the response has to contain. Judged is a criterion with a weight. Format is a length or file rule. Form is a document to attach. Deadline is a date or time.\n- Do not add an item the call does not have. If the text does not look like a call for proposals, return no table and say so in one line.\n\nReturn one markdown table and nothing else:\n| # | Type | Item (quote) |{{ $(\"Check 1: Every Item Quotes The Call\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Item Quotes The Call. What was wrong: \" + $(\"Check 1: Every Item Quotes The Call\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Every Type Is One Of The Five Allowed Words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Type Is One Of The Five Allowed Words. What was wrong: \" + $(\"Check 6: Every Type Is One Of The Five Allowed Words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: No Requirement In The Call Was Skipped\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No Requirement In The Call Was Skipped. What was wrong: \" + $(\"Check 7: No Requirement In The Call Was Skipped\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "3404acbd-2e6e-405a-ad6e-64cb23f94f00",
      "name": "1. List Every Item The Call Asks For",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Split the call into separate items, each quoted word for word and marked as a must, a judged criterion, a format rule, a form or a deadline. Returns: A numbered table of every item the call asks for, each quoted from the call and typed.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Item Quotes The Call\n// Rule: Each quote in the Item (quote) column appears in the call for proposals, with shortened fragments in the same order.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List Every Item The Call Asks For\").first().json.text ?? '');\nconst sources = {\n  \"rfp\": String($(\"Inputs\").first().json[\"rfp\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Item (quote)\",\"input\":\"rfp\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "df57001f-bacf-419a-a4c4-99e950b9dcdc",
      "name": "Check 1: Every Item Quotes The Call",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Each quote in the Item (quote) column appears in the call for proposals, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "33e61605-6d19-4e22-a1b8-6967606736e4",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ac5d1746-fa4d-4a67-a808-632c4079397d",
      "name": "Passed check 1?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6a89a9e2-43eb-48bd-a4ed-2b544eb636af",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7780dbfe-4a2a-4ddd-a7a5-dd04a0c29d8b",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. List Every Item The Call Asks For\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7a45eaf7-c26e-4458-ab73-b885afddc90e",
              "name": "flag",
              "value": "=Every Item Quotes The Call: did not pass. {{ $(\"Check 1: Every Item Quotes The Call\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c428a83b-02ee-457c-a627-bab99f1c9c7a",
      "name": "Flag check 1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1130,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful bid reviewer who searches a response for the words that answer each item.\n\nYour task is to find, for each item in the table below, where the response answers it.\n\nThe items:\n{{ $(\"1. List Every Item The Call Asks For\").first().json.text }}\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nRules:\n- One row for each item. Keep its number and its quote.\n- In the Where It Is Answered (quote) column, quote the words in the response that answer the item, word for word and as short as you can, or write exactly None found. A passage that only names the topic does not count. Do not join two passages.\n- In the Your Check column, write Count the pages yourself for a format row measured in pages, Check the date yourself for a deadline row, and for a format row measured in words, show the count of words in the response against the limit. For any other row, write Nothing.\n- For a form row, quote where the response says the form is attached, or write None found.\n- Use only the response above. Do not guess an answer from what is usual.\n\nReturn one markdown table and nothing else:\n| # | Item (quote) | Where It Is Answered (quote) | Your Check |{{ $(\"Check 2: Every Answer Quotes The Response\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Answer Quotes The Response. What was wrong: \" + $(\"Check 2: Every Answer Quotes The Response\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 8: Each None Found Was Searched For\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each None Found Was Searched For. What was wrong: \" + $(\"Check 8: Each None Found Was Searched For\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "f25c8353-18d7-4718-aa80-c5412477c746",
      "name": "2. Find Where The Response Answers Each Item",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "For each item, quote the words in the response that answer it, or say that none do. Returns: A table that puts the words of the response beside each item, or None found, with the checks left to you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Answer Quotes The Response\n// Rule: Each quote in the Where It Is Answered (quote) column appears in the your draft response, with shortened fragments in the same order.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"2. Find Where The Response Answers Each Item\").first().json.text ?? '');\nconst sources = {\n  \"response\": String($(\"Inputs\").first().json[\"response\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Where It Is Answered (quote)\",\"input\":\"response\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "31575398-28d8-4b79-a33c-49b6831e8627",
      "name": "Check 2: Every Answer Quotes The Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Each quote in the Where It Is Answered (quote) column appears in the your draft response, with shortened fragments in the same order.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "32e61472-6e19-4fb5-a0b8-67d461673877",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2c559201-fa4b-4bd0-a80a-a1c3c080bec2",
      "name": "Passed check 2?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6b89ab75-42eb-472a-a5ed-2ce74db6351c",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9f83598d-6228-450e-a79e-57bfc0c98bb0",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Find Where The Response Answers Each Item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7945e964-c36e-45eb-aa73-b6f2b0ddcaa1",
              "name": "flag",
              "value": "=Every Answer Quotes The Response: did not pass. {{ $(\"Check 2: Every Answer Quotes The Response\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c528a9ce-05ee-4a35-a327-b6009e1c9ae7",
      "name": "Flag check 2",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1910,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful bid reviewer who decides whether a passage gives everything an item asks for.\n\nYour task is to judge each must, judged and form item below against the words found for it.\n\nThe items and the passages found:\n{{ $(\"2. Find Where The Response Answers Each Item\").first().json.text }}\n\nThe items by type:\n{{ $(\"1. List Every Item The Call Asks For\").first().json.text }}\n\nRules:\n- One row for each item whose Type is must, judged or form. Leave out the format and deadline rows; a person checks those.\n- In the Coverage column, write exactly one of: covered, partly covered, not covered. Covered means the passage gives everything the item asks for. Partly covered means it gives some of it. Not covered means None found.\n- In the Backed By column, write exactly one of: record, claim only, not needed. Record means the response gives a figure, a named job, a named person or a named document. An item that asks for proof, such as cover or a certificate, is claim only unless the response attaches or names the proof. Claim only means it states something with nothing behind it. Not needed is for a form row.\n- In the What Is Missing column, name the one thing that is absent, or write Nothing.\n- Do not give credit for a passage that only names the topic.\n\nReturn one markdown table and nothing else:\n| # | Item (quote) | Type | Coverage | Backed By | What Is Missing |{{ $(\"Check 3: Every Coverage Cell Is One Of The Three Allowed Words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Coverage Cell Is One Of The Three Allowed Words. What was wrong: \" + $(\"Check 3: Every Coverage Cell Is One Of The Three Allowed Words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Backed By Cell Is One Of The Three Allowed Phrases. What was wrong: \" + $(\"Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 10: Every Covered Row Is Backed By Words In The Response\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Covered Row Is Backed By Words In The Response. What was wrong: \" + $(\"Check 10: Every Covered Row Is Backed By Words In The Response\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "6e5d3224-a65e-435b-a8a3-5c727ad320e9",
      "name": "3. Judge Each Item",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Say whether each must, judged and form item is covered, partly covered or not covered, and whether the response backs it with a record. Returns: A table that judges each item as covered, partly covered or not covered, with what backs it and what is missing.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Coverage Cell Is One Of The Three Allowed Words\n// Rule: Every row of the table has one of \"covered\", \"partly covered\" or \"not covered\" in the Coverage column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Judge Each Item\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Coverage\",\"allowed\":[\"covered\",\"partly covered\",\"not covered\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "8874a37d-f4f0-4cfa-ab1c-ede387468310",
      "name": "Check 3: Every Coverage Cell Is One Of The Three Allowed Words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Every row of the table has one of \"covered\", \"partly covered\" or \"not covered\" in the Coverage column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "31e612df-6b19-4afc-a3b8-6c8d62673a0a",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2c57d098-7a48-43b9-a80d-a9da407db6ab",
      "name": "Passed check 3?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6889a6bc-41eb-4597-a6ed-2e7a50b639d5",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f3861c60-3e26-4dcb-aba0-cf02acc72d9d",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Judge Each Item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7c45ee1d-c46e-477e-a973-b55fadddc5e8",
              "name": "flag",
              "value": "=Every Coverage Cell Is One Of The Three Allowed Words: did not pass. {{ $(\"Check 3: Every Coverage Cell Is One Of The Three Allowed Words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c628ab61-04ee-48a2-a427-b7939d1c9954",
      "name": "Flag check 3",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2690,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful bid reviewer who matches a response's claims to the records behind them.\n\nYour task is to tie each price, total, date range, job and certificate in the response below to the records.\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe records:\n{{ $(\"Inputs\").first().json[\"records\"] }}\n\nRules:\n- One row for each price, total, date range, named job and certificate the response states.\n- In the Response States column, quote it word for word, as short as you can.\n- In the Record column, quote the matching passage from the records word for word, as short as you can. If no passage speaks to it, write exactly No record.\n- In the Verdict column, write exactly one of: matches, differs, no record.\n- In the Sum column, recompute every total from its parts and show the sum. For any other row, write Not a total.\n- If the records are empty or say none given, write no record in every Verdict cell and say so in one line below the table.\n- Do not choose which figure is right when they differ. Show both.\n\nReturn one markdown table and nothing else, apart from the one line if the records are empty:\n| # | Response States | Record | Verdict | Sum |{{ $(\"Check 4: Every Verdict Is One Of The Three Allowed Words\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Verdict Is One Of The Three Allowed Words. What was wrong: \" + $(\"Check 4: Every Verdict Is One Of The Three Allowed Words\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 11: Every Total Was Recomputed\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Total Was Recomputed. What was wrong: \" + $(\"Check 11: Every Total Was Recomputed\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 12: Every Record Quote Is In Your Records\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Record Quote Is In Your Records. What was wrong: \" + $(\"Check 12: Every Record Quote Is In Your Records\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "c874b082-c852-4b2b-ae2d-6b108670f249",
      "name": "4. Tie The Response To Your Records",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Match each price, total, date range, job and certificate in the response to your records and recompute every total. Returns: A table that sets each price, job and certificate beside your records, with a verdict and the sum for every total.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Verdict Is One Of The Three Allowed Words\n// Rule: Every row of the table has one of \"matches\", \"differs\" or \"no record\" in the Verdict column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"4. Tie The Response To Your Records\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Verdict\",\"allowed\":[\"matches\",\"differs\",\"no record\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "1f1d9d49-de63-4b5e-aa1e-8a6f7f2fc66c",
      "name": "Check 4: Every Verdict Is One Of The Three Allowed Words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "Every row of the table has one of \"matches\", \"differs\" or \"no record\" in the Verdict column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "30e6114c-6c19-4c8f-a2b8-6afa63673b9d",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ac504b53-7a46-4522-a80f-e871c0853bf0",
      "name": "Passed check 4?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6989a84f-40eb-4404-a7ed-300d4fb63842",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "1b8899ef-4637-4384-ab99-175d4ccee542",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Tie The Response To Your Records\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7b45ec8a-c56e-4911-a873-b3ccaeddc77b",
              "name": "flag",
              "value": "=Every Verdict Is One Of The Three Allowed Words: did not pass. {{ $(\"Check 4: Every Verdict Is One Of The Three Allowed Words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c728acf4-ffee-40c3-a927-bf729c1c97c1",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3470,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful bid reviewer who gives a clear verdict and a short fix list and leaves the decision to the person.\n\nYour task is to give the verdict and the fix list for the response below.\n\nThe call:\n{{ $(\"Inputs\").first().json[\"rfp\"] }}\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe records:\n{{ $(\"Inputs\").first().json[\"records\"] }}\n\nThe items:\n{{ $(\"1. List Every Item The Call Asks For\").first().json.text }}\n\nThe judgment of each item:\n{{ $(\"3. Judge Each Item\").first().json.text }}\n\nThe response tied to the records:\n{{ $(\"4. Tie The Response To Your Records\").first().json.text }}\n\nRules:\n- Make one row for each problem: a must or judged item that is not covered or is partly covered, a form that is not covered or is partly covered, a Backed By of claim only, and a verdict of differs. A problem raised in two tables gets one row.\n- The row has four cells. Where quotes the words in the response, or says The whole response if nothing answers the item. Problem says in plain words what is wrong. Evidence quotes the call, the record or the missing item. Suggested Wording uses only facts and numbers from the response and the records.\n- If no safe wording exists for a claim, write exactly Cut it or find a record first. For an item with no passage, write exactly Add it from your own records; the response gives nothing to draw on.\n- Do not write a number of your own in the table, and do not write a step number. Do not work out a new total.\n- Verdict by this rule only: Not ready if any must item is not covered, any figure differs, or a form is not covered. Ready after fixes if any other row was raised. Ready by these checks if none was.\n- If there are no problems, write one row with None found in every cell.\n- Write the verdict line first, in words only, as \"Verdict:\" then the verdict then one deciding reason. On the next line write exactly: This verdict comes from the checks above. An AI tool can miss things, so read the flagged parts yourself.\n- On the third line write \"Check Yourself:\" and quote the deadline and format items of the call, then say these are for the person to confirm.\n- Do not rewrite the response, and do not choose to submit, hold or fix. The person decides.\n\nReturn the three lines, the fix table and, under the heading How Each Item Was Judged, the judging table from the judge step, and nothing else:\n| Where | Problem | Evidence | Suggested Wording |{{ $(\"Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No Number Appears That The Call, The Response Or The Records Do Not Have. What was wrong: \" + $(\"Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 13: The Line About Reading The Flagged Parts Is There\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The Line About Reading The Flagged Parts Is There. What was wrong: \" + $(\"Check 13: The Line About Reading The Flagged Parts Is There\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 14: Every Problem Has A Row\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every Problem Has A Row. What was wrong: \" + $(\"Check 14: Every Problem Has A Row\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 15: The Verdict Follows The Written Rule\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The Verdict Follows The Written Rule. What was wrong: \" + $(\"Check 15: The Verdict Follows The Written Rule\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "bc4b5963-8375-48c4-aae7-e6a1d0f33d72",
      "name": "5. Give The Verdict And The Fix List",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Turn every gap and mismatch into one fix row, then give one verdict by a fixed rule and stop. Returns: A verdict, a Check Yourself line for the deadline and format, and a fix table with a row for every problem.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// No Number Appears That The Call, The Response Or The Records Do Not Have\n// Rule: Every number in the answer also appears somewhere in at least one of \"the call for proposals\", \"your draft response\" and \"your records\"; this does not check what each number means.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"5. Give The Verdict And The Fix List\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"numbers_in_source\", {\"inputs\":[\"rfp\",\"response\",\"records\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "ce6a6dd7-b686-4cba-a793-dfdd19913848",
      "name": "Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3860,
        320
      ],
      "notes": "Every number in the answer also appears somewhere in at least one of \"the call for proposals\", \"your draft response\" and \"your records\"; this does not check what each number means.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2fe60fb9-6919-47d6-adb8-631b5c673098",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2c53536a-7a43-468b-a812-2708c082fd59",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6689a396-3feb-4271-a0ed-25084ab63063",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ff8aac72-e234-4781-af9b-757028cc6dff",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        520
      ],
      "notes": "True = first failure: send back to \"5. Give The Verdict And The Fix List\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7645e4ab-c66e-4aa4-a773-b239b3ddcf5a",
              "name": "flag",
              "value": "=No Number Appears That The Call, The Response Or The Records Do Not Have: did not pass. {{ $(\"Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c828ae87-feee-4f30-aa27-c1059b1c962e",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4250,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Type Is One Of The Five Allowed Words\n// Rule: Every row of the table has one of \"must\", \"judged\", \"format\", \"form\" or \"deadline\" in the Type column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List Every Item The Call Asks For\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Type\",\"allowed\":[\"must\",\"judged\",\"format\",\"form\",\"deadline\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "c4a55cde-937b-4a59-a820-67f8496b888b",
      "name": "Check 6: Every Type Is One Of The Five Allowed Words",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4380,
        320
      ],
      "notes": "Every row of the table has one of \"must\", \"judged\", \"format\", \"form\" or \"deadline\" in the Type column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2ee60e26-6a19-4969-acb8-61885d67322b",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ac4bce25-7a41-47f4-a814-659f408a829e",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4640,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6789a529-3eeb-40de-a1ed-269b49b62ed0",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "778da7f1-ea32-4582-af94-874ba8d3f344",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4640,
        520
      ],
      "notes": "True = first failure: send back to \"1. List Every Item The Call Asks For\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7545e318-c76e-4c37-a673-b0a6b4ddd0ed",
              "name": "flag",
              "value": "=Every Type Is One Of The Five Allowed Words: did not pass. {{ $(\"Check 6: Every Type Is One Of The Five Allowed Words\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "c928b01a-01ee-43e9-a727-bc4c9a1c949b",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4770,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: No Requirement In The Call Was Skipped\nRead the call below. Find every sentence that holds must, shall, required, attached, a weight or a due date. Does each one have a row in the table? Answer FAIL and quote each sentence with no row.\n\nThe call:\n{{ $(\"Inputs\").first().json[\"rfp\"] }}\n\nThe work to check (the answer from Step 1 (List Every Item The Call Asks For)):\n{{ $(\"1. List Every Item The Call Asks For\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "cf016334-d2a6-43cd-a379-0d122ecfec0b",
      "name": "Check 7: No Requirement In The Call Was Skipped",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4900,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the call below. Find every sentence that holds must, shall, required, attached, a weight or a due date. Does each one have a row in the table? Answer FAIL and quote each sentence with no row.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2de60c93-6719-44b0-afb8-66415e6733be",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ac4e0cbc-fa3e-4fdd-a817-6db640884407",
      "name": "Passed check 7?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5160,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6489a070-3deb-4f4b-a2ed-282e4cb63389",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9b901f34-c62f-4e3f-a396-fe8e44d11741",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5160,
        520
      ],
      "notes": "True = first failure: send back to \"1. List Every Item The Call Asks For\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7845e7d1-c86e-4dca-a573-af13b1ddcc34",
              "name": "flag",
              "value": "=No Requirement In The Call Was Skipped: did not pass. {{ $(\"Check 7: No Requirement In The Call Was Skipped\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ca28b1ad-00ee-4256-a827-bddf991c9308",
      "name": "Flag check 7",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5290,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Each None Found Was Searched For\nFor each row marked None found, name the key word or number the item needs, then look in the response below for a line that holds it. Answer FAIL and quote each line that answers an item marked None found.\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe work to check (the answer from Step 2 (Find Where The Response Answers Each Item)):\n{{ $(\"2. Find Where The Response Answers Each Item\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "06813372-ecd6-4f0f-aa26-794c44066189",
      "name": "Check 8: Each None Found Was Searched For",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        5420,
        320
      ],
      "notes": "Answers PASS or FAIL. For each row marked None found, name the key word or number the item needs, then look in the response below for a line that holds it. Answer FAIL and quote each line that answers an item marked None found.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2ce60b00-6819-4643-aeb8-64ae5f673551",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2c468777-7a64-4a36-a7f1-d35d408effcc",
      "name": "Passed check 8?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5680,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6589a203-3ceb-4db8-a3ed-29c14bb631f6",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "73921ed3-de41-4d28-a3b7-1fd9a4d86a26",
      "name": "Try again? (check 8)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5680,
        520
      ],
      "notes": "True = first failure: send back to \"2. Find Where The Response Answers Each Item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7745e63e-c96e-4f5d-a473-ad80b2ddcdc7",
              "name": "flag",
              "value": "=Each None Found Was Searched For: did not pass. {{ $(\"Check 8: Each None Found Was Searched For\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "bb289a10-0bee-43a7-ad27-c5be981c9175",
      "name": "Flag check 8",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5810,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every Backed By Cell Is One Of The Three Allowed Phrases\n// Rule: Every row of the table has one of \"record\", \"claim only\" or \"not needed\" in the Backed By column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Judge Each Item\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Backed By\",\"allowed\":[\"record\",\"claim only\",\"not needed\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "74fd5933-a00e-4cf2-a40f-2ba99c272ef8",
      "name": "Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5940,
        320
      ],
      "notes": "Every row of the table has one of \"record\", \"claim only\" or \"not needed\" in the Backed By column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "3be6229d-7519-4aba-a9b8-5ccf58672a4c",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ac498f8e-fa61-421f-a7f4-db74408cc135",
      "name": "Passed check 9?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6200,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "7289b67a-3beb-4c25-aced-1ebc56b64347",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "97949616-6a3e-47f5-a7b9-971cc0d657a3",
      "name": "Try again? (check 9)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6200,
        520
      ],
      "notes": "True = first failure: send back to \"3. Judge Each Item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7245de5f-ba6e-47c0-a373-c51da7ddbc76",
              "name": "flag",
              "value": "=Every Backed By Cell Is One Of The Three Allowed Phrases: did not pass. {{ $(\"Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "bc289ba3-0aee-4214-ae27-c751971c8fe2",
      "name": "Flag check 9",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6330,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every Covered Row Is Backed By Words In The Response\nFor each row marked covered, quote from the response below the words that give each thing the item asks for. Answer FAIL and name each covered row where a thing the item asks for has no words.\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe work to check (the answer from Step 3 (Judge Each Item)):\n{{ $(\"3. Judge Each Item\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "26ebbe9f-4f7a-4d24-aabc-fd8155d118a6",
      "name": "Check 10: Every Covered Row Is Backed By Words In The Response",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        6460,
        320
      ],
      "notes": "Answers PASS or FAIL. For each row marked covered, quote from the response below the words that give each thing the item asks for. Answer FAIL and name each covered row where a thing the item asks for has no words.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "3ae6210a-7619-4c4d-a8b8-5b3c59672bdf",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4fa70078-e00e-4b8b-a845-400ac0b0520d",
      "name": "Passed check 10?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6720,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "7389b80d-3aeb-4a92-aded-204f55b641b4",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4af05e00-915a-4791-ab30-fa72d06f9b3b",
      "name": "Try again? (check 10)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6720,
        520
      ],
      "notes": "True = first failure: send back to \"3. Judge Each Item\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7145dccc-bb6e-4953-a273-c38aa8ddbe09",
              "name": "flag",
              "value": "=Every Covered Row Is Backed By Words In The Response: did not pass. {{ $(\"Check 10: Every Covered Row Is Backed By Words In The Response\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d7008951-e8e4-46a4-a98a-a5abc40a087e",
      "name": "Flag check 10",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6850,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every Total Was Recomputed\nLook at the response below. Count the totals it states, such as a fixed price made of parts. Does the table show a sum for each one, and is each sum right? Answer FAIL and quote each total with no sum or a wrong sum.\n\nThe response:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe work to check (the answer from Step 4 (Tie The Response To Your Records)):\n{{ $(\"4. Tie The Response To Your Records\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "f46fc27c-2cec-48e3-ab12-837ea42fb1f5",
      "name": "Check 11: Every Total Was Recomputed",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        6980,
        320
      ],
      "notes": "Answers PASS or FAIL. Look at the response below. Count the totals it states, such as a fixed price made of parts. Does the table show a sum for each one, and is each sum right? Answer FAIL and quote each total with no sum or a wrong sum.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6231e3e6-d73c-4e5f-a64b-8bec9c7d97c5",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4fa4c1e1-e011-4a22-a843-0173c0b290a4",
      "name": "Passed check 11?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        7240,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "708bf1eb-3de9-40b4-aaef-735dc8b32526",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "76ee64ad-b55c-4ed4-a72e-e7efb471adbe",
      "name": "Try again? (check 11)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        7240,
        520
      ],
      "notes": "True = first failure: send back to \"4. Tie The Response To Your Records\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3d0e4f3c-81b1-47c1-a628-b366fe25e243",
              "name": "flag",
              "value": "=Every Total Was Recomputed: did not pass. {{ $(\"Check 11: Every Total Was Recomputed\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d60087be-e9e4-4837-a88a-a418c50a0a11",
      "name": "Flag check 11",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        7370,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every Record Quote Is In Your Records\nFor each row of the table whose Record is not No record, is that passage in the records below, word for word? Answer FAIL and quote each passage that is not.\n\nThe records:\n{{ $(\"Inputs\").first().json[\"records\"] }}\n\nThe work to check (the answer from Step 4 (Tie The Response To Your Records)):\n{{ $(\"4. Tie The Response To Your Records\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "03fa7855-1dfe-438c-a964-805f6b4a43e6",
      "name": "Check 12: Every Record Quote Is In Your Records",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        7500,
        320
      ],
      "notes": "Answers PASS or FAIL. For each row of the table whose Record is not No record, is that passage in the records below, word for word? Answer FAIL and quote each passage that is not.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6331e579-d63c-4ccc-a74b-8d7f9b7d9632",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4fab7da6-6009-44dd-a840-c2dc40b598bb",
      "name": "Passed check 12?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        7760,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6f8bf058-3ee9-4247-a9ef-71cac9b326b9",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4eebe71e-3555-498f-a736-6d34dc742b4d",
      "name": "Try again? (check 12)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        7760,
        520
      ],
      "notes": "True = first failure: send back to \"4. Tie The Response To Your Records\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3e0e50cf-80b1-462e-a728-b4f9fd25e0b0",
              "name": "flag",
              "value": "=Every Record Quote Is In Your Records: did not pass. {{ $(\"Check 12: Every Record Quote Is In Your Records\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d500862b-eae4-49ca-ab8a-a8d1c20a0558",
      "name": "Flag check 12",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        7890,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The Line About Reading The Flagged Parts Is There\n// Rule: The answer contains the text fragment \"This verdict comes from the checks above. An AI tool can miss things, so read the flagged parts yourself.\" (including inside a longer word).\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                if (f.length === 0) {\n                    problems.push(`The table has no data rows to check in \"${col}\".`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            if (f.length === 0)\n                return { pass: false, reason: `The table has no data rows to check in \"${value.column}\".` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                let cursor = 0;\n                const ordered = pieces.every((p) => {\n                    const found = src.indexOf(p, cursor);\n                    if (found < 0)\n                        return false;\n                    cursor = found + p.length;\n                    return true;\n                });\n                if (!pieces.length || !ordered)\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source in that order.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const ids = Array.isArray(value.inputs) ? value.inputs : [value.input];\n            const src = new Set(ids.flatMap((id) => nums(sources[id] ?? '')));\n            // With a column named, only the numbers in that column of a table are compared (a rank column is not a claim).\n            let target = work;\n            if (value.column) {\n                const cells = findColumn(String(value.column));\n                if (cells === null)\n                    return { pass: false, reason: `The answer has no table with a ${value.column} column.` };\n                target = cells.join(' ');\n            }\n            const missing = [...new Set(nums(target))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"5. Give The Verdict And The Fix List\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"This verdict comes from the checks above. An AI tool can miss things, so read the flagged parts yourself.\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "e8047ab9-56a8-4d68-a897-5b17fc82378e",
      "name": "Check 13: The Line About Reading The Flagged Parts Is There",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        8020,
        320
      ],
      "notes": "The answer contains the text fragment \"This verdict comes from the checks above. An AI tool can miss things, so read the flagged parts yourself.\" (including inside a longer word).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6031e0c0-d93c-4185-a84b-8f129a7d949f",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4fa93f0f-e00c-4cf4-a83d-bac540b7d752",
      "name": "Passed check 13?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        8280,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "728bf511-3fe9-43da-a8ef-7037c6b32200",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "eae90b1b-9957-4592-a333-f5f13076ee20",
      "name": "Try again? (check 13)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        8280,
        520
      ],
      "notes": "True = first failure: send back to \"5. Give The Verdict And The Fix List\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3f0e5262-7fb1-449b-a428-b0400025e569",
              "name": "flag",
              "value": "=The Line About Reading The Flagged Parts Is There: did not pass. {{ $(\"Check 13: The Line About Reading The Flagged Parts Is There\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d4008498-ebe4-4b5d-aa8a-a73ec30a06eb",
      "name": "Flag check 13",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        8410,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every Problem Has A Row\nCount the problems in the two tables below: each must, judged or form row that is not covered or is partly covered, each Backed By of claim only, and each verdict of differs. Count a problem raised in both tables once. Does the fix table have one row for each? Show both numbers. Answer FAIL and name each problem with no row.\n\nThe judgment of each item:\n{{ $(\"3. Judge Each Item\").first().json.text }}\n\nThe response tied to the records:\n{{ $(\"4. Tie The Response To Your Records\").first().json.text }}\n\nThe work to check (the answer from Step 5 (Give The Verdict And The Fix List)):\n{{ $(\"5. Give The Verdict And The Fix List\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "47ef4ff8-c2d3-4761-a3d8-5372b400d9d3",
      "name": "Check 14: Every Problem Has A Row",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        8540,
        320
      ],
      "notes": "Answers PASS or FAIL. Count the problems in the two tables below: each must, judged or form row that is not covered or is partly covered, each Backed By of claim only, and each verdict of differs. Count a problem raised in both tables once. Does the fix table have one row for each? Show both numbers. Answer FAIL and name each problem with no row.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6131e253-d83c-4ff2-a94b-90a5997d930c",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cf9d3c9c-6018-4f67-a84f-03e640ba15e9",
      "name": "Passed check 14?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        8800,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "718bf37e-40e9-456d-a7ef-6ea4c7b32393",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f2fa60d4-3950-42ad-a327-29fe58796baf",
      "name": "Try again? (check 14)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        8800,
        520
      ],
      "notes": "True = first failure: send back to \"5. Give The Verdict And The Fix List\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "400e53f5-7eb1-4308-a528-b1d3ff25e3d6",
              "name": "flag",
              "value": "=Every Problem Has A Row: did not pass. {{ $(\"Check 14: Every Problem Has A Row\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "db008f9d-e4e4-4058-ad8a-abf7c80a0eca",
      "name": "Flag check 14",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        8930,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: The Verdict Follows The Written Rule\nThe rule is: Not ready if any must item is not covered, any figure differs, or a form is not covered. Ready after fixes if any other row was raised. Ready by these checks if none was. Use the two tables below. Does the verdict follow the rule? Answer FAIL and say which line of the rule it breaks.\n\nThe judgment of each item:\n{{ $(\"3. Judge Each Item\").first().json.text }}\n\nThe response tied to the records:\n{{ $(\"4. Tie The Response To Your Records\").first().json.text }}\n\nThe work to check (the answer from Step 5 (Give The Verdict And The Fix List)):\n{{ $(\"5. Give The Verdict And The Fix List\").first().json.text }}\n\nReply with exactly PASS or FAIL on the first line. On the second line, say in one sentence why. If it fails, name the exact item or line that fails. Do not add anything else."
      },
      "id": "df10f838-ee78-4e97-a49b-0e1236b15c51",
      "name": "Check 15: The Verdict Follows The Written Rule",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        9060,
        320
      ],
      "notes": "Answers PASS or FAIL. The rule is: Not ready if any must item is not covered, any figure differs, or a form is not covered. Ready after fixes if any other row was raised. Ready by these checks if none was. Use the two tables below. Does the verdict follow the rule? Answer FAIL and say which line of the rule it breaks.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "6631ea32-d33c-4813-a24b-85a0987d9179",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4f9a3485-601a-4dfe-a84c-c54f40bc5480",
      "name": "Passed check 15?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        9320,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "748bf837-39e9-4a68-a6ef-6d11ccb32b72",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cef7e991-0d52-4c00-af25-177b3c7b7e32",
      "name": "Try again? (check 15)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        9320,
        520
      ],
      "notes": "True = first failure: send back to \"5. Give The Verdict And The Fix List\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "390e48f0-85b1-4e0d-aa28-b9b20225e88f",
              "name": "flag",
              "value": "=The Verdict Follows The Written Rule: did not pass. {{ $(\"Check 15: The Verdict Follows The Written Rule\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "da008e0a-e5e4-41eb-ac8a-aa64c90a105d",
      "name": "Flag check 15",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        9450,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 16: check by hand**\nCheck the deadline and the length rule against the response yourself, count the pages, and confirm the forms are attached and signed. For each figure that differs, decide which number is right. A person who can sign for your organization must read the call and the response together before anything is submitted.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "62f0898c-b809-4ae3-a8bf-13a698b54e9d",
      "name": "Check 16 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        9300,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a7da8712-f976-4f53-a802-4d44f7afe77d",
              "name": "result",
              "value": "={{ $(\"5. Give The Verdict And The Fix List\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "3f9dcacb-9269-4c18-a940-60edf46d5de2",
              "name": "flagged",
              "value": "={{ [$(\"Flag check 1\").isExecuted ? $(\"Flag check 1\").first().json.flag : \"\", $(\"Flag check 2\").isExecuted ? $(\"Flag check 2\").first().json.flag : \"\", $(\"Flag check 3\").isExecuted ? $(\"Flag check 3\").first().json.flag : \"\", $(\"Flag check 4\").isExecuted ? $(\"Flag check 4\").first().json.flag : \"\", $(\"Flag check 5\").isExecuted ? $(\"Flag check 5\").first().json.flag : \"\", $(\"Flag check 6\").isExecuted ? $(\"Flag check 6\").first().json.flag : \"\", $(\"Flag check 7\").isExecuted ? $(\"Flag check 7\").first().json.flag : \"\", $(\"Flag check 8\").isExecuted ? $(\"Flag check 8\").first().json.flag : \"\", $(\"Flag check 9\").isExecuted ? $(\"Flag check 9\").first().json.flag : \"\", $(\"Flag check 10\").isExecuted ? $(\"Flag check 10\").first().json.flag : \"\", $(\"Flag check 11\").isExecuted ? $(\"Flag check 11\").first().json.flag : \"\", $(\"Flag check 12\").isExecuted ? $(\"Flag check 12\").first().json.flag : \"\", $(\"Flag check 13\").isExecuted ? $(\"Flag check 13\").first().json.flag : \"\", $(\"Flag check 14\").isExecuted ? $(\"Flag check 14\").first().json.flag : \"\", $(\"Flag check 15\").isExecuted ? $(\"Flag check 15\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "072a2961-3032-4716-a32d-d7e38b31c0d0",
              "name": "check_by_hand",
              "value": "- Check the deadline and the length rule against the response yourself, count the pages, and confirm the forms are attached and signed. For each figure that differs, decide which number is right. A person who can sign for your organization must read the call and the response together before anything is submitted.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d3304047-e309-48f4-a82c-9829cf1267a6",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        9580,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "3fc6651d-63a7-4fd6-a537-430f48ee49f0",
      "name": "Chat model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        480,
        840
      ],
      "notes": "Attach your credential here, or swap in any chat model node.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inputs": {
      "main": [
        [
          {
            "node": "1. List Every Item The Call Asks For",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List Every Item The Call Asks For": {
      "main": [
        [
          {
            "node": "Check 1: Every Item Quotes The Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every Item Quotes The Call": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Find Where The Response Answers Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List Every Item The Call Asks For",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Find Where The Response Answers Each Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Find Where The Response Answers Each Item": {
      "main": [
        [
          {
            "node": "Check 2: Every Answer Quotes The Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every Answer Quotes The Response": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Judge Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Find Where The Response Answers Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Judge Each Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Judge Each Item": {
      "main": [
        [
          {
            "node": "Check 3: Every Coverage Cell Is One Of The Three Allowed Words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: Every Coverage Cell Is One Of The Three Allowed Words": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Tie The Response To Your Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Judge Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Tie The Response To Your Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Tie The Response To Your Records": {
      "main": [
        [
          {
            "node": "Check 4: Every Verdict Is One Of The Three Allowed Words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: Every Verdict Is One Of The Three Allowed Words": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Tie The Response To Your Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Give The Verdict And The Fix List": {
      "main": [
        [
          {
            "node": "Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: No Number Appears That The Call, The Response Or The Records Do Not Have": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Every Type Is One Of The Five Allowed Words",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Every Type Is One Of The Five Allowed Words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Every Type Is One Of The Five Allowed Words": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: No Requirement In The Call Was Skipped",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "1. List Every Item The Call Asks For",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: No Requirement In The Call Was Skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: No Requirement In The Call Was Skipped": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Check 8: Each None Found Was Searched For",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "1. List Every Item The Call Asks For",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Check 8: Each None Found Was Searched For",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 8: Each None Found Was Searched For": {
      "main": [
        [
          {
            "node": "Passed check 8?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 8?": {
      "main": [
        [
          {
            "node": "Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 8)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 8)": {
      "main": [
        [
          {
            "node": "2. Find Where The Response Answers Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 8": {
      "main": [
        [
          {
            "node": "Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 9: Every Backed By Cell Is One Of The Three Allowed Phrases": {
      "main": [
        [
          {
            "node": "Passed check 9?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 9?": {
      "main": [
        [
          {
            "node": "Check 10: Every Covered Row Is Backed By Words In The Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 9)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 9)": {
      "main": [
        [
          {
            "node": "3. Judge Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 9": {
      "main": [
        [
          {
            "node": "Check 10: Every Covered Row Is Backed By Words In The Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 10: Every Covered Row Is Backed By Words In The Response": {
      "main": [
        [
          {
            "node": "Passed check 10?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 10?": {
      "main": [
        [
          {
            "node": "Check 11: Every Total Was Recomputed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 10)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 10)": {
      "main": [
        [
          {
            "node": "3. Judge Each Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 10",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 10": {
      "main": [
        [
          {
            "node": "Check 11: Every Total Was Recomputed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 11: Every Total Was Recomputed": {
      "main": [
        [
          {
            "node": "Passed check 11?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 11?": {
      "main": [
        [
          {
            "node": "Check 12: Every Record Quote Is In Your Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 11)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 11)": {
      "main": [
        [
          {
            "node": "4. Tie The Response To Your Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 11",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 11": {
      "main": [
        [
          {
            "node": "Check 12: Every Record Quote Is In Your Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 12: Every Record Quote Is In Your Records": {
      "main": [
        [
          {
            "node": "Passed check 12?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 12?": {
      "main": [
        [
          {
            "node": "Check 13: The Line About Reading The Flagged Parts Is There",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 12)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 12)": {
      "main": [
        [
          {
            "node": "4. Tie The Response To Your Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 12",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 12": {
      "main": [
        [
          {
            "node": "Check 13: The Line About Reading The Flagged Parts Is There",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 13: The Line About Reading The Flagged Parts Is There": {
      "main": [
        [
          {
            "node": "Passed check 13?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 13?": {
      "main": [
        [
          {
            "node": "Check 14: Every Problem Has A Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 13)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 13)": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 13",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 13": {
      "main": [
        [
          {
            "node": "Check 14: Every Problem Has A Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 14: Every Problem Has A Row": {
      "main": [
        [
          {
            "node": "Passed check 14?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 14?": {
      "main": [
        [
          {
            "node": "Check 15: The Verdict Follows The Written Rule",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 14)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 14)": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 14",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 14": {
      "main": [
        [
          {
            "node": "Check 15: The Verdict Follows The Written Rule",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 15: The Verdict Follows The Written Rule": {
      "main": [
        [
          {
            "node": "Passed check 15?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 15?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 15)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 15)": {
      "main": [
        [
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 15",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 15": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List Every Item The Call Asks For",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Find Where The Response Answers Each Item",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Judge Each Item",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Tie The Response To Your Records",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "5. Give The Verdict And The Fix List",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 7: No Requirement In The Call Was Skipped",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 8: Each None Found Was Searched For",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 10: Every Covered Row Is Backed By Words In The Response",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 11: Every Total Was Recomputed",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 12: Every Record Quote Is In Your Records",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 14: Every Problem Has A Row",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 15: The Verdict Follows The Written Rule",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "check-rfp-response-before-submitting"
  }
}
