{
  "name": "Check every requirement is covered",
  "nodes": [
    {
      "parameters": {
        "content": "## Check every requirement is covered\nGo through a list of requirements one at a time, find where your document answers each one, and get a short list of the gaps to close before you submit, without inventing any missing detail.\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): Every quote in the Requirement (quote) column appears word for word in the requirements. If it fails: try once more, then flag.\n- Check 2 (a plain rule, run automatically with no AI): Every quote in the Where it is answered (quote) column appears word for word in the your document. 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 something written in \"Requirement (quote)\", \"What is missing\" and \"Where to add it\" (a blank or a dash does not count). If it fails: try once more, then flag.\n- Check 5 (a plain rule, run automatically with no AI): Every quote in the Requirement (quote) column appears word for word in the requirements. If it fails: try once more, then flag.\n- Check 6 (a second AI prompt answers pass or fail): Count the judgments in the table below. Do the numbers on the Result line of the report match those counts, and does the number of rows under \"Gaps to close\" equal the number of requirements that are not fully covered? Answer FAIL and say which count is wrong. If it fails: try once more, then flag.\n- Check 7 (a second AI prompt answers pass or fail): Read the \"Gaps to close\" table. Does any row state or suggest a figure, name, date or promise that is in neither the requirements nor the document below? Answer FAIL and quote it. If it fails: try once more, then flag.\n- Check 8 (you check it by hand): Read each row under \"Gaps to close\". For a partly covered requirement, decide whether the missing part really matters to the person who set the requirements, then add the detail yourself from facts you hold. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 780,
        "width": 760,
        "color": 5
      },
      "id": "f288cf62-d14b-4a7b-aa9e-9c1cbeab585d",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -740
      ]
    },
    {
      "parameters": {},
      "id": "7ececa96-a2fc-486b-a247-4cc883c68495",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "351be88b-d3bb-4af8-af15-a641d3e86d9e",
              "name": "requirements",
              "value": "Sample: The quote must name the person who will lead the work and give their phone number. It must give a total price in dollars, with the cost of materials shown separately. It must say how many days the work will take and the earliest start date. It must include a plan for removing waste. It must list two earlier jobs, with a contact for each. It must say how long the work is guaranteed.",
              "type": "string"
            },
            {
              "id": "68f287cc-8563-409f-a2b2-1ede9c7d6e29",
              "name": "response",
              "value": "Sample: Quote for the Maple Street community garden. Our crew of four, led by Priya Nair (phone 555-0142), will build the raised beds, lay the paths and connect the water line. Price: the total is 6,400 dollars, which includes soil, lumber and the water line fittings. Timing: the work will take 10 working days, and the earliest we can start is July 6. Past work: we built the Eastside school garden last year (contact: Dana Ruiz, 555-0177) and a rooftop garden for a downtown cafe. All our work is guaranteed.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "e612531b-b652-4714-ac88-32619d1512fa",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: requirements = The requirements; response = Your document.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reader who turns a list of requirements into items that can be checked one by one.\n\nYour task is to list every requirement and say what the document must show to meet it.\n\nThe requirements:\n{{ $(\"Inputs\").first().json[\"requirements\"] }}\n\nRules:\n- One row for each requirement. If one sentence asks for two things, keep it as one row and name both things in the last column.\n- Quote the requirement from the text above word for word, as short as you can while keeping its numbers and conditions. Do not reword it.\n- In the last column, write what a reader could look for in the document: a number, a name, a date, a method or a promise. Use only what the requirement asks for.\n- Do not add a requirement the text does not make. Do not look at any document yet.\n\nReturn one markdown table and nothing else:\n| # | Requirement (quote) | What the document must show |{{ $(\"Check 1: Every requirement is quoted word for word\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every requirement is quoted word for word. What was wrong: \" + $(\"Check 1: Every requirement is quoted word for word\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "f799c8c6-a0f7-47ef-aab8-9a10addc9239",
      "name": "1. List each requirement and what it asks for",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Split the requirements into separate items, quoted word for word, and say what the document must show for each. Returns: A numbered table of the requirements, each quoted and with what the document must show.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every requirement is quoted word for word\n// Rule: Every quote in the Requirement (quote) column appears word for word in the requirements.\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                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            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            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                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\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 src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].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 each requirement and what it asks for\").first().json.text ?? '');\nconst sources = {\n  \"requirements\": String($(\"Inputs\").first().json[\"requirements\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Requirement (quote)\",\"input\":\"requirements\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "a91d1484-4258-4367-a37f-e19a1a90878d",
      "name": "Check 1: Every requirement is quoted word for word",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every quote in the Requirement (quote) column appears word for word in the requirements.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2429295d-c9bb-4ba6-a203-47fb55e32424",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4908cdce-2b2a-4313-a5a3-71a8ee4a4ebd",
      "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": "a4e2d51a-499a-4981-a68e-15a8681b05ef",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7ec6dfe6-4659-4ac9-afd6-b0f0eb276a4b",
      "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 each requirement and what it asks for\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "341b859f-9001-4e2c-a7b1-4a69e4c53e4e",
              "name": "flag",
              "value": "=Every requirement is quoted word for word: did not pass. {{ $(\"Check 1: Every requirement is quoted word for word\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7b649bb3-e0fa-4870-a11f-147d127b253a",
      "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 research assistant who finds the exact words in a document that answer a requirement.\n\nYour task is to find, for each requirement below, the passage in the document that answers it.\n\nThe requirements:\n{{ $(\"1. List each requirement and what it asks for\").first().json.text }}\n\nThe document:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nRules:\n- Search the whole document for each requirement, not just the part where you would expect it.\n- Quote the passage from the document word for word, as short as you can while keeping the facts that answer the requirement. The same passage may serve two requirements.\n- If nothing in the document speaks to the requirement, write exactly None found. Do not count a passage that only names the topic without giving what the requirement asks for.\n- Use only the document above. Do not guess and do not join two passages into one quote.\n\nReturn one markdown table and nothing else:\n| # | Requirement (quote) | Where it is answered (quote) |{{ $(\"Check 2: Every passage is quoted word for word from the document\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every passage is quoted word for word from the document. What was wrong: \" + $(\"Check 2: Every passage is quoted word for word from the document\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "0ee6ec40-5515-48df-ae71-c2d6bfed09ad",
      "name": "2. Find where each one is answered",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "For each requirement, quote the words in your document that answer it, or say that none do. Returns: A table that puts a quoted passage from the document beside each requirement, or None found.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every passage is quoted word for word from the document\n// Rule: Every quote in the Where it is answered (quote) column appears word for word in the your document.\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                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            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            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                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\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 src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].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 each one is answered\").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": "a02a8238-8d84-466f-a7f6-2ef2448dc961",
      "name": "Check 2: Every passage is quoted word for word from the document",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every quote in the Where it is answered (quote) column appears word for word in the your document.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "232927ca-cabb-4d39-a103-466856e325b7",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c9014889-2b28-447c-a5a5-b03f6e51d402",
      "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": "a5e2d6ad-489a-47ee-a78e-173b671b045c",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a6c95d75-2e57-466a-afcf-c2cb0b2e5870",
      "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 each one is answered\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "331b840c-9101-4fbf-a6b1-48d6e5c53fe1",
              "name": "flag",
              "value": "=Every passage is quoted word for word from the document: did not pass. {{ $(\"Check 2: Every passage is quoted word for word from the document\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7c649d46-e3fa-4d29-ae1f-0fc4117b23a7",
      "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 strict assessor who decides whether a document gives what a requirement asks for.\n\nYour task is to judge each requirement as covered, partly covered or not covered.\n\nThe requirements and the passages found:\n{{ $(\"2. Find where each one is answered\").first().json.text }}\n\nWhat each requirement asks the document to show:\n{{ $(\"1. List each requirement and what it asks for\").first().json.text }}\n\nRules:\n- Use exactly these words in the Coverage column: covered, partly covered, not covered.\n- covered: the passage gives everything the requirement asks for. A passage that only mentions the topic does not count.\n- partly covered: the passage gives some of what the requirement asks for but not all, or it names the topic without giving the detail.\n- not covered: the passage is None found.\n- If a requirement depends on a fact neither text gives, such as today's date, mark it partly covered and say what is needed to settle it.\n- In \"What is missing\", name the one thing that is absent, or write Nothing for a covered row. Do not write the missing content itself, because you do not have the facts. Do not be generous.\n\nReturn one markdown table and nothing else:\n| # | Requirement (quote) | Coverage | What is missing |\nThe Coverage column must say exactly covered, partly covered or not covered.{{ $(\"Check 3: Each requirement is marked covered, partly covered or not covered\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each requirement is marked covered, partly covered or not covered. What was wrong: \" + $(\"Check 3: Each requirement is marked covered, partly covered or not covered\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "95a16986-1023-4bb3-a810-8be0d8c91755",
      "name": "3. Judge each requirement",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Compare what each requirement asks for with the passage found, and say what is missing. Returns: A table that marks each requirement covered, partly covered or not covered and names what is missing.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Each requirement is marked covered, partly covered or not covered\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                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            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            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                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\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 src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].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 requirement\").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": "a3ae3ded-9ff3-493e-a689-ac276707d3a8",
      "name": "Check 3: Each requirement is marked covered, partly covered or not covered",
      "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": "22292637-c7bb-4880-a403-4b2157e3274a",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c9038720-2b26-45e5-a5a7-eed6ee4ecbeb",
      "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": "a2e2d1f4-479a-465b-a88e-18ce6a1b0915",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "1acc52a8-ca54-4a67-a3d2-3a0ef72bfa5d",
      "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 requirement\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "361b88c5-9201-4152-a5b1-4743e2c53b28",
              "name": "flag",
              "value": "=Each requirement is marked covered, partly covered or not covered: did not pass. {{ $(\"Check 3: Each requirement is marked covered, partly covered or not covered\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7d649ed9-e2fa-4b96-af1f-1157107b2214",
      "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 an editor who turns a requirements check into a short list of gaps the writer can close.\n\nYour task is to write the gap report from the judgments below.\n\nThe judgments:\n{{ $(\"3. Judge each requirement\").first().json.text }}\n\nThe document:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nRules:\n- Start with one line that begins \"Result:\", gives the number of requirements checked and says how many are covered, partly covered and not covered.\n- Under \"Gaps to close\", write one row for each requirement that is not fully covered, in the same order as the judgments. If every requirement is covered, write one row with None found in every cell.\n- In \"What is missing\", repeat the gap from the judgments in your own plain words.\n- In \"Where to add it\", name the paragraph or line of the document where the missing part belongs, or say it needs a new paragraph and where.\n- Do not write the missing content, and do not suggest a figure, name or date. Do not change the document.\n- Under \"Covered\", list each covered requirement as a quote, one line each, with nothing else.\n\nReturn markdown in this form and nothing else:\nResult: (the counts)\n## Gaps to close\n| # | Requirement (quote) | What is missing | Where to add it |\n## Covered\n(one quoted requirement per line){{ $(\"Check 4: Every gap row is filled in\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every gap row is filled in. What was wrong: \" + $(\"Check 4: Every gap row is filled in\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: Every requirement in the gap list is in the requirements word for word\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every requirement in the gap list is in the requirements word for word. What was wrong: \" + $(\"Check 5: Every requirement in the gap list is in the requirements word for word\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: The counts add up to the judgments\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The counts add up to the judgments. What was wrong: \" + $(\"Check 6: The counts add up to the judgments\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: No gap is filled with a detail nobody gave\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No gap is filled with a detail nobody gave. What was wrong: \" + $(\"Check 7: No gap is filled with a detail nobody gave\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "3570c762-13dd-4c37-aef7-8938e9bf059d",
      "name": "4. Write the gap report",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Turn the judgments into a count, a list of gaps with where each should be added, and a list of what is covered. Returns: A report with a count line, a table of gaps and a list of the requirements that are covered.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every gap row is filled in\n// Rule: Every row of the table has something written in \"Requirement (quote)\", \"What is missing\" and \"Where to add it\" (a blank or a dash does not count).\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                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            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            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                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\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 src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].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. Write the gap report\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"table_cells_filled\", [\"Requirement (quote)\",\"What is missing\",\"Where to add it\"], work, sources);\nreturn [{ json: result }];"
      },
      "id": "571c5ad6-e729-49e3-acb0-32d427db1709",
      "name": "Check 4: Every gap row is filled in",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "Every row of the table has something written in \"Requirement (quote)\", \"What is missing\" and \"Where to add it\" (a blank or a dash does not count).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "212924a4-c8bb-4a13-a303-498e58e328dd",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "48fc01db-2b37-4f06-a596-a5b56e565130",
      "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": "a3e2d387-469a-44c8-a98e-1a61691b0782",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "02ce6b77-a251-4cd8-a3de-3c819733b202",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the gap report\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "351b8732-9301-42e5-a4b1-45b0e3c53cbb",
              "name": "flag",
              "value": "=Every gap row is filled in: did not pass. {{ $(\"Check 4: Every gap row is filled in\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7e64a06c-e5fa-404f-ac1f-0c9e0f7b2081",
      "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": {
        "jsCode": "// Every requirement in the gap list is in the requirements word for word\n// Rule: Every quote in the Requirement (quote) column appears word for word in the requirements.\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                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            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            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                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\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 src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].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. Write the gap report\").first().json.text ?? '');\nconst sources = {\n  \"requirements\": String($(\"Inputs\").first().json[\"requirements\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Requirement (quote)\",\"input\":\"requirements\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "b6a45c0e-5402-47c7-a129-1e14f6ceb65d",
      "name": "Check 5: Every requirement in the gap list is in the requirements word for word",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3600,
        320
      ],
      "notes": "Every quote in the Requirement (quote) column appears word for word in the requirements.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "20292311-cdbb-41f2-a603-4e4751e31dd8",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "48fe4072-2b35-406f-a598-e44c6e541299",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "a0e2cece-4d9a-4fcd-aa8e-1bf4641affa3",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "26d0e2ba-ae4f-4125-a7e1-188473313abf",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the gap report\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "301b7f53-8c01-47e0-abb1-50b5e8c5449a",
              "name": "flag",
              "value": "=Every requirement in the gap list is in the requirements word for word: did not pass. {{ $(\"Check 5: Every requirement in the gap list is in the requirements word for word\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7f64a1ff-e4fa-4ebc-ad1f-0e310e7b1eee",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3990,
        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 counts add up to the judgments\nCount the judgments in the table below. Do the numbers on the Result line of the report match those counts, and does the number of rows under \"Gaps to close\" equal the number of requirements that are not fully covered? Answer FAIL and say which count is wrong.\n\nThe judgments:\n{{ $(\"3. Judge each requirement\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Write the gap report)):\n{{ $(\"4. Write the gap report\").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": "47c7ab50-a48e-4f15-a976-5b32c67bbf7f",
      "name": "Check 6: The counts add up to the judgments",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4120,
        320
      ],
      "notes": "Answers PASS or FAIL. Count the judgments in the table below. Do the numbers on the Result line of the report match those counts, and does the number of rows under \"Gaps to close\" equal the number of requirements that are not fully covered? Answer FAIL and say which count is wrong.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "1f29217e-cebb-4385-a503-4cb452e31f6b",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "48f784ad-ab32-4858-a59b-ec63ee5b97de",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "a1e2d061-4c9a-4e3a-ab8e-1d87631afe10",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7ed3abd9-464c-4ed6-a7d9-933ff338c004",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the gap report\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2f1b7dc0-8d01-4973-aab1-4f22e9c5462d",
              "name": "flag",
              "value": "=The counts add up to the judgments: did not pass. {{ $(\"Check 6: The counts add up to the judgments\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8064a392-e7fa-4375-aa1f-09780d7b1d5b",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4510,
        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 gap is filled with a detail nobody gave\nRead the \"Gaps to close\" table. Does any row state or suggest a figure, name, date or promise that is in neither the requirements nor the document below? Answer FAIL and quote it.\n\nThe requirements:\n{{ $(\"Inputs\").first().json[\"requirements\"] }}\n\nThe document:\n{{ $(\"Inputs\").first().json[\"response\"] }}\n\nThe work to check (the answer from Step 4 (Write the gap report)):\n{{ $(\"4. Write the gap report\").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": "2b48a70a-f5e3-4e4f-a143-b598b62a257d",
      "name": "Check 7: No gap is filled with a detail nobody gave",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4640,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the \"Gaps to close\" table. Does any row state or suggest a figure, name, date or promise that is in neither the requirements nor the document below? Answer FAIL and quote it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "1e291feb-cbbb-4ecc-a803-516d53e320fe",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "48f9c344-ab30-49c1-a59e-2afaee595947",
      "name": "Passed check 7?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "9ee2cba8-4b9a-4ca7-ac8e-1f1a661b02c9",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a2d6231c-224a-4793-abdc-0a828f35e401",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4900,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the gap report\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "321b8279-8e01-4b06-a9b1-4d8fe6c54174",
              "name": "flag",
              "value": "=No gap is filled with a detail nobody gave: did not pass. {{ $(\"Check 7: No gap is filled with a detail nobody gave\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8164a525-e6fa-41e2-ab1f-0b0b0c7b1bc8",
      "name": "Flag check 7",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5030,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 8: check by hand**\nRead each row under \"Gaps to close\". For a partly covered requirement, decide whether the missing part really matters to the person who set the requirements, then add the detail yourself from facts you hold.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "d1e08343-b168-4dea-a4b6-14c120a37e88",
      "name": "Check 8 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        4880,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d575beda-7ec7-403f-adcb-1650e5e2bb3d",
              "name": "result",
              "value": "={{ $(\"4. Write the gap report\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "3a6e06a3-cf49-4e0c-ab95-3ef1a2b1a3a2",
              "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 : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "e706e8d9-e9bb-445a-ad12-4af728fe1390",
              "name": "check_by_hand",
              "value": "- Read each row under \"Gaps to close\". For a partly covered requirement, decide whether the missing part really matters to the person who set the requirements, then add the detail yourself from facts you hold.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "1541217f-9c82-4168-a8a3-15bd82bd71e6",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5160,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "10199e45-7d0e-402a-a132-dbd318c7d3b0",
      "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 each requirement and what it asks for",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List each requirement and what it asks for": {
      "main": [
        [
          {
            "node": "Check 1: Every requirement is quoted word for word",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every requirement is quoted word for word": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Find where each one is answered",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List each requirement and what it asks for",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Find where each one is answered",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Find where each one is answered": {
      "main": [
        [
          {
            "node": "Check 2: Every passage is quoted word for word from the document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every passage is quoted word for word from the document": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Judge each requirement",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Find where each one is answered",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Judge each requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Judge each requirement": {
      "main": [
        [
          {
            "node": "Check 3: Each requirement is marked covered, partly covered or not covered",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: Each requirement is marked covered, partly covered or not covered": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Judge each requirement",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Write the gap report": {
      "main": [
        [
          {
            "node": "Check 4: Every gap row is filled in",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: Every gap row is filled in": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: Every requirement in the gap list is in the requirements word for word",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: Every requirement in the gap list is in the requirements word for word",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: Every requirement in the gap list is in the requirements word for word": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: The counts add up to the judgments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: The counts add up to the judgments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: The counts add up to the judgments": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: No gap is filled with a detail nobody gave",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: No gap is filled with a detail nobody gave",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: No gap is filled with a detail nobody gave": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "4. Write the gap report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List each requirement and what it asks for",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Find where each one is answered",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Judge each requirement",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Write the gap report",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 6: The counts add up to the judgments",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 7: No gap is filled with a detail nobody gave",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "check-every-requirement-is-covered"
  }
}
