{
  "name": "VANTIX Agile Delivery & Admin Workload Sentinel v1.1.3 \u2014 Public Portfolio Export",
  "nodes": [
    {
      "parameters": {},
      "id": "4aa2f63b-1857-4bb4-b777-78551a900e21",
      "name": "Start: New Intake Item (Manual Demo Trigger)",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -2640,
        64
      ]
    },
    {
      "parameters": {
        "jsCode": "// Deterministic synthetic intake fixture.\n// Happy-path test scenario.\n\nreturn [\n  {\n    json: {\n      intakeId: 'INT-2026-000101',\n\n      source: 'enhancement_request',\n\n      title:\n        'Add validation rule to prevent blank Opportunity Close Date',\n\n      description:\n        'Sales ops has reported that Opportunities are being marked Closed Won without a Close Date populated, which breaks the forecasting report. Need a validation rule on the Opportunity object.',\n\n      submittedBy:\n        'sales.ops@example.com',\n\n      submittedAt:\n        '2026-07-20T09:00:00Z',\n\n      affectedComponents: [\n        'Opportunity',\n        'Forecasting Report',\n      ],\n\n      businessJustificationStated:\n        'Forecasting report accuracy directly affects the weekly pipeline review with leadership.',\n\n      expectedOutcomeStated:\n        'Users cannot save a Closed Won Opportunity without a Close Date.',\n\n      evidenceLinks: [\n        'https://internal.example.com/tickets/SOPS-4821',\n      ],\n\n      dependenciesStated: [],\n\n      urgencyClaimedByRequester:\n        'medium',\n\n      existingOpenItems: [],\n    },\n  },\n];"
      },
      "id": "c5e1945a-cc04-44b9-b3a1-198d6d6e1d58",
      "name": "Demo Intake Payload (replace with real intake form/webhook)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2416,
        64
      ],
      "notes": "Synthetic fixture implemented as a Code node to avoid Edit Fields/Set node version incompatibility across n8n releases."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 01: Normalize & Validate\n// Deterministic, fail-closed intake validation\n\nconst REQUIRED = [\n  'intakeId',\n  'source',\n  'title',\n  'description',\n  'submittedBy',\n  'submittedAt',\n];\n\nconst VALID_SOURCES = [\n  'enhancement_request',\n  'incident',\n  'technical_debt',\n  'project1_governance_finding',\n];\n\nconst VALID_URGENCY_VALUES = [\n  'critical',\n  'high',\n  'medium',\n  'low',\n];\n\nconst INTAKE_ID_RE = /^INT-\\d{4}-\\d{6}$/;\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction isBlank(value) {\n  return (\n    value === undefined ||\n    value === null ||\n    (typeof value === 'string' && value.trim() === '')\n  );\n}\n\nfunction validateStringArray(value, fieldName, errors) {\n  if (value === undefined || value === null) return;\n\n  if (!Array.isArray(value)) {\n    errors.push(`${fieldName}_must_be_array`);\n    return;\n  }\n\n  if (\n    value.some(\n      (entry) =>\n        typeof entry !== 'string' ||\n        entry.trim() === ''\n    )\n  ) {\n    errors.push(`${fieldName}_contains_invalid_item`);\n  }\n}\n\nfunction normalizeStringArray(value) {\n  if (!Array.isArray(value)) return [];\n\n  return value\n    .filter(\n      (entry) =>\n        typeof entry === 'string' &&\n        entry.trim() !== ''\n    )\n    .map((entry) => entry.trim());\n}\n\nfunction normalizeAndValidate(raw) {\n  const errors = [];\n\n  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n    return {\n      schemaValid: false,\n      routeTo: 'schema_failure',\n      errors: ['invalid_intake_payload'],\n      raw: raw ?? null,\n    };\n  }\n\n  for (const field of REQUIRED) {\n    if (isBlank(raw[field])) {\n      errors.push(`missing_required_field:${field}`);\n    }\n  }\n\n  const intakeId =\n    typeof raw.intakeId === 'string'\n      ? raw.intakeId.trim()\n      : raw.intakeId;\n\n  const source =\n    typeof raw.source === 'string'\n      ? raw.source.trim()\n      : raw.source;\n\n  const submittedBy =\n    typeof raw.submittedBy === 'string'\n      ? raw.submittedBy.trim()\n      : raw.submittedBy;\n\n  const urgency =\n    typeof raw.urgencyClaimedByRequester === 'string'\n      ? raw.urgencyClaimedByRequester.trim().toLowerCase()\n      : raw.urgencyClaimedByRequester ?? null;\n\n  if (intakeId && !INTAKE_ID_RE.test(intakeId)) {\n    errors.push('invalid_intakeId_format');\n  }\n\n  if (source && !VALID_SOURCES.includes(source)) {\n    errors.push(`invalid_source_enum:${source}`);\n  }\n\n  if (\n    source === 'project1_governance_finding' &&\n    isBlank(raw.sourceReference)\n  ) {\n    errors.push(\n      'missing_sourceReference_for_project1_finding'\n    );\n  }\n\n  if (\n    typeof raw.title === 'string' &&\n    raw.title.trim().length < 5\n  ) {\n    errors.push('title_too_short');\n  }\n\n  if (\n    typeof raw.description === 'string' &&\n    raw.description.trim().length < 10\n  ) {\n    errors.push('description_too_short');\n  }\n\n  if (\n    raw.submittedAt &&\n    Number.isNaN(Date.parse(raw.submittedAt))\n  ) {\n    errors.push('invalid_submittedAt');\n  }\n\n  if (\n    submittedBy &&\n    !EMAIL_RE.test(submittedBy) &&\n    !submittedBy.startsWith('vantix-')\n  ) {\n    errors.push('invalid_submittedBy');\n  }\n\n  if (\n    urgency !== null &&\n    !VALID_URGENCY_VALUES.includes(urgency)\n  ) {\n    errors.push(`invalid_urgency_enum:${urgency}`);\n  }\n\n  validateStringArray(\n    raw.affectedComponents,\n    'affectedComponents',\n    errors\n  );\n\n  validateStringArray(\n    raw.evidenceLinks,\n    'evidenceLinks',\n    errors\n  );\n\n  validateStringArray(\n    raw.dependenciesStated,\n    'dependenciesStated',\n    errors\n  );\n\n  if (errors.length > 0) {\n    return {\n      schemaValid: false,\n      routeTo: 'schema_failure',\n      errors,\n      raw,\n    };\n  }\n\n  const normalized = {\n    schemaVersion: '1.0.0',\n    intakeId,\n    source,\n    sourceReference:\n      typeof raw.sourceReference === 'string'\n        ? raw.sourceReference.trim()\n        : raw.sourceReference ?? null,\n    title: raw.title.trim(),\n    description: raw.description.trim(),\n    submittedBy,\n    submittedAt: new Date(raw.submittedAt).toISOString(),\n    affectedComponents: normalizeStringArray(\n      raw.affectedComponents\n    ),\n    businessJustificationStated:\n      typeof raw.businessJustificationStated === 'string'\n        ? raw.businessJustificationStated.trim() || null\n        : null,\n    expectedOutcomeStated:\n      typeof raw.expectedOutcomeStated === 'string'\n        ? raw.expectedOutcomeStated.trim() || null\n        : null,\n    evidenceLinks: normalizeStringArray(\n      raw.evidenceLinks\n    ),\n    dependenciesStated: normalizeStringArray(\n      raw.dependenciesStated\n    ),\n    urgencyClaimedByRequester: urgency,\n  };\n\n  return {\n    schemaValid: true,\n    routeTo: 'duplicate_check',\n    errors: [],\n    normalized,\n  };\n}\n\nreturn items.map((item) => {\n\n  const result = normalizeAndValidate(item.json);\n\n  if (result.schemaValid) {\n    result.existingOpenItems = Array.isArray(item.json.existingOpenItems)\n      ? item.json.existingOpenItems\n      : [];\n  }\n\n  return {\n    json: result\n  };\n\n});"
      },
      "id": "cec2419a-dae2-4dd3-9afc-58158261a29d",
      "name": "01: Normalize & Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -2208,
        64
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{$json.schemaValid}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5df6f052-62d3-4d9d-be57-47da55efe466",
      "name": "IF: Schema Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -1984,
        64
      ]
    },
    {
      "parameters": {},
      "id": "dfb42758-e514-418d-8cf5-110421b3ab2c",
      "name": "STOP: Schema Failure -> Human Review",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -1760,
        208
      ],
      "notes": "Fail-closed: malformed intake never proceeds. Wire to a Gmail/Slack alert to the submitter + admin queue."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 02: Duplicate & Related-Work Detection\n// Deterministic, explainable and fail-closed\n\nconst DUPLICATE_THRESHOLD = 0.72;\nconst RELATED_THRESHOLD = 0.40;\n\nconst STOP_WORDS = new Set([\n  'the',\n  'and',\n  'for',\n  'with',\n  'that',\n  'this',\n  'from',\n  'into',\n  'when',\n  'where',\n  'which',\n  'should',\n  'would',\n  'could',\n  'user',\n  'users',\n  'record',\n  'records',\n  'salesforce',\n]);\n\nfunction normalizeText(value) {\n  return String(value ?? '')\n    .toLowerCase()\n    .replace(/[^a-z0-9\\s]/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nfunction tokenize(text) {\n  return new Set(\n    normalizeText(text)\n      .split(' ')\n      .filter(\n        token =>\n          token.length > 2 &&\n          !STOP_WORDS.has(token)\n      )\n  );\n}\n\nfunction jaccard(setA, setB) {\n  const union = new Set([...setA, ...setB]);\n\n  if (union.size === 0) return 0;\n\n  let intersection = 0;\n\n  for (const token of setA) {\n    if (setB.has(token)) intersection++;\n  }\n\n  return intersection / union.size;\n}\n\nfunction normalizeComponents(components) {\n  if (!Array.isArray(components)) return new Set();\n\n  return new Set(\n    components\n      .filter(\n        c =>\n          typeof c === 'string' &&\n          c.trim() !== ''\n      )\n      .map(c => normalizeText(c))\n  );\n}\n\nfunction hasComponentOverlap(a, b) {\n  for (const component of a) {\n    if (b.has(component)) return true;\n  }\n  return false;\n}\n\nfunction validateCandidate(candidate) {\n  const errors = [];\n\n  if (!candidate?.intakeId)\n    errors.push('candidate_missing_intakeId');\n\n  if (!candidate?.title)\n    errors.push('candidate_missing_title');\n\n  if (!candidate?.description)\n    errors.push('candidate_missing_description');\n\n  return errors;\n}\n\nfunction getExistingOpenItems(item) {\n\n  // Preferred location (normalized payload)\n  if (\n    Array.isArray(\n      item.normalized?.existingOpenItems\n    )\n  ) {\n    return item.normalized.existingOpenItems;\n  }\n\n  // Backwards compatibility\n  if (\n    Array.isArray(item.existingOpenItems)\n  ) {\n    return item.existingOpenItems;\n  }\n\n  return [];\n}\n\nfunction detectDuplicates(candidate, existingItems) {\n\n  const candidateTokens = tokenize(\n    `${candidate.title} ${candidate.description}`\n  );\n\n  const candidateComponents =\n    normalizeComponents(\n      candidate.affectedComponents\n    );\n\n  let bestScore = 0;\n  let bestMatch = null;\n\n  const relatedMatches = [];\n  const skipped = [];\n\n  for (const existing of existingItems) {\n\n    if (\n      !existing ||\n      typeof existing !== 'object'\n    ) {\n      skipped.push({\n        intakeId: null,\n        reason: 'invalid_existing_record'\n      });\n      continue;\n    }\n\n    if (\n      !existing.intakeId ||\n      !existing.title ||\n      !existing.description\n    ) {\n      skipped.push({\n        intakeId:\n          existing.intakeId ?? null,\n        reason:\n          'missing_required_matching_fields'\n      });\n      continue;\n    }\n\n    if (\n      existing.intakeId ===\n      candidate.intakeId\n    ) {\n      continue;\n    }\n\n    const existingTokens = tokenize(\n      `${existing.title} ${existing.description}`\n    );\n\n    let score = jaccard(\n      candidateTokens,\n      existingTokens\n    );\n\n    const existingComponents =\n      normalizeComponents(\n        existing.affectedComponents\n      );\n\n    const overlap =\n      hasComponentOverlap(\n        candidateComponents,\n        existingComponents\n      );\n\n    if (\n      overlap &&\n      candidateComponents.size > 0\n    ) {\n      score = Math.min(\n        score + 0.1,\n        1\n      );\n    }\n\n    const rounded =\n      Number(score.toFixed(3));\n\n    if (\n      rounded >= RELATED_THRESHOLD\n    ) {\n      relatedMatches.push({\n        intakeId: existing.intakeId,\n        similarityScore: rounded,\n        componentOverlap: overlap\n      });\n    }\n\n    if (score > bestScore) {\n      bestScore = score;\n      bestMatch = existing.intakeId;\n    }\n  }\n\n  relatedMatches.sort(\n    (a, b) =>\n      b.similarityScore -\n      a.similarityScore\n  );\n\n  const roundedScore =\n    Number(bestScore.toFixed(3));\n\n  const isDuplicate =\n    roundedScore >=\n    DUPLICATE_THRESHOLD;\n\n  const isRelated =\n    !isDuplicate &&\n    roundedScore >=\n      RELATED_THRESHOLD;\n\n  return {\n    isDuplicate,\n    isRelated,\n\n    matchedIntakeIds: isDuplicate\n      ? [bestMatch]\n      : relatedMatches.map(\n          r => r.intakeId\n        ),\n\n    bestMatchIntakeId: bestMatch,\n\n    similarityScore:\n      roundedScore,\n\n    relatedMatches,\n\n    recordsEvaluated:\n      existingItems.length,\n\n    recordsSkipped: skipped,\n\n    thresholdsUsed: {\n      duplicate:\n        DUPLICATE_THRESHOLD,\n      related:\n        RELATED_THRESHOLD\n    },\n\n    routeTo: isDuplicate\n      ? 'human_review_possible_duplicate'\n      : 'dor_scoring'\n  };\n}\n\nreturn items.map(item => {\n\n  const candidate =\n    item.json.normalized;\n\n  const validationErrors =\n    validateCandidate(candidate);\n\n  if (\n    validationErrors.length > 0\n  ) {\n    return {\n      json: {\n        ...item.json,\n\n        duplicateCheck: {\n          isDuplicate: false,\n          isRelated: false,\n          matchedIntakeIds: [],\n          bestMatchIntakeId: null,\n          similarityScore: 0,\n          relatedMatches: [],\n          recordsEvaluated: 0,\n          recordsSkipped: [],\n          errors:\n            validationErrors,\n          routeTo:\n            'human_review_duplicate_check_failure'\n        },\n\n        routeTo:\n          'human_review_duplicate_check_failure'\n      }\n    };\n  }\n\n  try {\n\n    const existingOpenItems =\n      getExistingOpenItems(\n        item.json\n      );\n\n    const duplicateCheck =\n      detectDuplicates(\n        candidate,\n        existingOpenItems\n      );\n\n    return {\n      json: {\n        ...item.json,\n        duplicateCheck\n      }\n    };\n\n  } catch (error) {\n\n    return {\n      json: {\n        ...item.json,\n\n        duplicateCheck: {\n          isDuplicate: false,\n          isRelated: false,\n          matchedIntakeIds: [],\n          bestMatchIntakeId: null,\n          similarityScore: 0,\n          relatedMatches: [],\n          recordsEvaluated: 0,\n          recordsSkipped: [],\n          errors: [\n            `duplicate_check_error:${error.message}`\n          ],\n          routeTo:\n            'human_review_duplicate_check_failure'\n        },\n\n        routeTo:\n          'human_review_duplicate_check_failure'\n      }\n    };\n\n  }\n\n});"
      },
      "id": "7dc55eb0-1a76-4ae9-a3be-7c6f1f4227e9",
      "name": "02: Duplicate & Related-Work Detection",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -1760,
        32
      ],
      "notes": "DEMO: existingOpenItems is hardcoded empty. Replace with a Google Sheets/Data Table read merged in upstream."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{$json.duplicateCheck.isDuplicate}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e08d0f8d-d357-4acc-bfb6-8f0d419168d0",
      "name": "IF: Is Duplicate?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -1536,
        32
      ]
    },
    {
      "parameters": {},
      "id": "6104d7b2-1708-44b4-ac9f-8b38b9340d03",
      "name": "STOP: Possible Duplicate -> Human Review Queue",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -1392,
        320
      ]
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 03: DoR Completeness Scoring\n// Deterministic, auditable Definition of Ready assessment\n\nconst DOR_WEIGHTS = {\n  hasClearDescription: 0.15,\n  hasExpectedOutcome: 0.20,\n  hasEvidence: 0.20,\n  hasBusinessJustification: 0.15,\n  hasAffectedComponents: 0.15,\n  hasNoUnresolvedDependencyGap: 0.15,\n};\n\nconst READY_THRESHOLD = 0.75;\nconst INSUFFICIENT_THRESHOLD = 0.35;\n\nfunction scoreDoR(item) {\n  const checks = {};\n\n  checks.hasClearDescription =\n    typeof item.description === 'string' &&\n    item.description.trim().length >= 40;\n\n  checks.hasExpectedOutcome =\n    typeof item.expectedOutcomeStated === 'string' &&\n    item.expectedOutcomeStated.trim() !== '';\n\n  checks.hasEvidence =\n    Array.isArray(item.evidenceLinks) &&\n    item.evidenceLinks.length > 0;\n\n  checks.hasBusinessJustification =\n    typeof item.businessJustificationStated === 'string' &&\n    item.businessJustificationStated.trim() !== '';\n\n  checks.hasAffectedComponents =\n    Array.isArray(item.affectedComponents) &&\n    item.affectedComponents.length > 0;\n\n  checks.hasNoUnresolvedDependencyGap =\n    item.source === 'technical_debt'\n      ? (\n          Array.isArray(item.dependenciesStated) &&\n          item.dependenciesStated.length > 0\n        )\n      : true;\n\n  let score = 0;\n  const missingChecks = [];\n\n  for (const [checkName, weight] of Object.entries(DOR_WEIGHTS)) {\n    if (checks[checkName]) {\n      score += weight;\n    } else {\n      missingChecks.push(checkName);\n    }\n  }\n\n  score = Number(score.toFixed(4));\n\n  let dorStatus;\n\n  if (score >= READY_THRESHOLD) {\n    dorStatus = 'ready';\n  } else if (score < INSUFFICIENT_THRESHOLD) {\n    dorStatus = 'insufficient_evidence';\n  } else {\n    dorStatus = 'not_ready';\n  }\n\n  let routeTo;\n\n  if (dorStatus === 'ready') {\n    routeTo = 'priority_risk_workload_scoring';\n  } else if (dorStatus === 'insufficient_evidence') {\n    routeTo = 'request_more_evidence';\n  } else {\n    routeTo = 'backlog_refinement';\n  }\n\n  return {\n    dorScore: score,\n    dorStatus,\n    dorChecks: checks,\n    missingChecks,\n\n    thresholdsUsed: {\n      ready: READY_THRESHOLD,\n      insufficientEvidence: INSUFFICIENT_THRESHOLD,\n    },\n\n    weightsUsed: DOR_WEIGHTS,\n    routeTo,\n  };\n}\n\nreturn items.map((item) => {\n  const normalized = item.json.normalized;\n\n  if (\n    !normalized ||\n    typeof normalized !== 'object' ||\n    Array.isArray(normalized)\n  ) {\n    return {\n      json: {\n        ...item.json,\n        dorScore: 0,\n        dorStatus: 'insufficient_evidence',\n        dorChecks: {},\n        missingChecks: ['normalized_intake_missing'],\n        thresholdsUsed: {\n          ready: READY_THRESHOLD,\n          insufficientEvidence: INSUFFICIENT_THRESHOLD,\n        },\n        weightsUsed: DOR_WEIGHTS,\n        routeTo: 'request_more_evidence',\n        dorErrors: ['normalized_intake_missing_or_invalid'],\n      },\n    };\n  }\n\n  const dor = scoreDoR(normalized);\n\n  return {\n    json: {\n      ...item.json,\n      ...dor,\n    },\n  };\n});"
      },
      "id": "03c7378e-7bd6-4dcd-a15e-27e2b47b1155",
      "name": "03: DoR Completeness Scoring",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -1328,
        32
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{$json.dorStatus}}",
                    "rightValue": "ready",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "b30eb51b-4737-472b-ad84-70d705a8904e"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{$json.dorStatus}}",
                    "rightValue": "insufficient_evidence",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "bd366be6-32ea-4294-a7b7-ee54213a1668"
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "id": "e33118db-da60-42cb-9349-b6d5a5937cd1",
      "name": "SWITCH: DoR Status",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        -1104,
        32
      ]
    },
    {
      "parameters": {},
      "id": "f2620ea7-e7d5-4472-8176-1f4f5cb4a0f8",
      "name": "STOP: Not Ready -> Backlog Refinement Queue",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -624,
        416
      ]
    },
    {
      "parameters": {},
      "id": "77d001e2-5a7f-4275-b7ac-4f1cab2b271d",
      "name": "STOP: Insufficient Evidence -> Request More Info From Requester",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -1056,
        512
      ]
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 04: Priority, Delivery Risk & Admin Workload Scoring\n// Deterministic, explainable and capacity-aware\n\nconst SOURCE_WEIGHT = {\n  incident: 1.0,\n  project1_governance_finding: 0.8,\n  technical_debt: 0.5,\n  enhancement_request: 0.4,\n};\n\nconst URGENCY_WEIGHT = {\n  critical: 1.0,\n  high: 0.7,\n  medium: 0.4,\n  low: 0.15,\n  none: 0.2,\n};\n\nconst RISK_MATRIX = {\n  low: {\n    low: 1,\n    medium: 2,\n    high: 3,\n  },\n  medium: {\n    low: 2,\n    medium: 5,\n    high: 7,\n  },\n  high: {\n    low: 3,\n    medium: 7,\n    high: 9,\n  },\n};\n\n/*\n * Demo fallback only.\n *\n * In production, supply item.json.admins from an upstream\n * n8n Data Table, Salesforce, Jira or Azure DevOps query.\n */\nconst DEMO_ADMINS = [\n  {\n    adminId: 'admin_priya',\n    currentWip: 3,\n    wipLimit: 5,\n  },\n  {\n    adminId: 'admin_jordan',\n    currentWip: 5,\n    wipLimit: 5,\n  },\n];\n\nfunction isFiniteNonNegativeNumber(value) {\n  return (\n    typeof value === 'number' &&\n    Number.isFinite(value) &&\n    value >= 0\n  );\n}\n\nfunction validateAdmin(admin) {\n  return (\n    admin &&\n    typeof admin === 'object' &&\n    !Array.isArray(admin) &&\n    typeof admin.adminId === 'string' &&\n    admin.adminId.trim() !== '' &&\n    isFiniteNonNegativeNumber(admin.currentWip) &&\n    typeof admin.wipLimit === 'number' &&\n    Number.isFinite(admin.wipLimit) &&\n    admin.wipLimit > 0\n  );\n}\n\nfunction getAdminList(input) {\n  if (input.admins === undefined) {\n    return {\n      admins: DEMO_ADMINS,\n      source: 'demo_fallback',\n      invalidRecords: [],\n    };\n  }\n\n  if (!Array.isArray(input.admins)) {\n    return {\n      admins: [],\n      source: 'invalid_input',\n      invalidRecords: [\n        {\n          reason: 'admins_must_be_array',\n        },\n      ],\n    };\n  }\n\n  const validAdmins = [];\n  const invalidRecords = [];\n\n  for (const admin of input.admins) {\n    if (validateAdmin(admin)) {\n      validAdmins.push({\n        adminId: admin.adminId.trim(),\n        currentWip: admin.currentWip,\n        wipLimit: admin.wipLimit,\n      });\n    } else {\n      invalidRecords.push({\n        adminId: admin?.adminId ?? null,\n        reason: 'invalid_admin_capacity_record',\n      });\n    }\n  }\n\n  return {\n    admins: validAdmins,\n    source: 'upstream_input',\n    invalidRecords,\n  };\n}\n\nfunction calculateUtilization(admin) {\n  return admin.currentWip / admin.wipLimit;\n}\n\nfunction sortByUtilizationThenId(admins) {\n  return [...admins].sort((a, b) => {\n    const utilizationDifference =\n      calculateUtilization(a) -\n      calculateUtilization(b);\n\n    if (utilizationDifference !== 0) {\n      return utilizationDifference;\n    }\n\n    return a.adminId.localeCompare(b.adminId);\n  });\n}\n\nfunction assignCapacityAware(admins) {\n  if (!Array.isArray(admins) || admins.length === 0) {\n    return {\n      assigned: null,\n      capacityBreached: true,\n      assignmentReason: 'no_valid_admin_capacity_data',\n    };\n  }\n\n  const adminsWithCapacity = admins.filter(\n    (admin) =>\n      admin.currentWip + 1 <= admin.wipLimit\n  );\n\n  if (adminsWithCapacity.length > 0) {\n    const assigned =\n      sortByUtilizationThenId(\n        adminsWithCapacity\n      )[0];\n\n    return {\n      assigned,\n      capacityBreached: false,\n      assignmentReason:\n        'least_utilized_admin_with_available_capacity',\n    };\n  }\n\n  const assigned =\n    sortByUtilizationThenId(admins)[0];\n\n  return {\n    assigned,\n    capacityBreached: true,\n    assignmentReason:\n      'all_admins_at_or_above_wip_limit',\n  };\n}\n\nfunction calculatePriority(norm, dorScore) {\n  const sourceWeight =\n    SOURCE_WEIGHT[norm.source];\n\n  const urgencyKey =\n    norm.urgencyClaimedByRequester ?? 'none';\n\n  const urgencyWeight =\n    URGENCY_WEIGHT[urgencyKey];\n\n  const sourceContribution =\n    sourceWeight * 0.5;\n\n  const urgencyContribution =\n    urgencyWeight * 0.3;\n\n  const readinessContribution =\n    dorScore * 0.2;\n\n  const priorityScore = Number(\n    (\n      sourceContribution +\n      urgencyContribution +\n      readinessContribution\n    ).toFixed(4)\n  );\n\n  let priorityBand;\n\n  if (priorityScore >= 0.75) {\n    priorityBand = 'critical';\n  } else if (priorityScore >= 0.55) {\n    priorityBand = 'high';\n  } else if (priorityScore >= 0.35) {\n    priorityBand = 'medium';\n  } else {\n    priorityBand = 'low';\n  }\n\n  return {\n    priorityScore,\n    priorityBand,\n\n    calculation: {\n      sourceWeight,\n      urgencyWeight,\n      dorScore,\n\n      contributions: {\n        source: Number(\n          sourceContribution.toFixed(4)\n        ),\n        urgency: Number(\n          urgencyContribution.toFixed(4)\n        ),\n        readiness: Number(\n          readinessContribution.toFixed(4)\n        ),\n      },\n\n      weights: {\n        source: 0.5,\n        urgency: 0.3,\n        readiness: 0.2,\n      },\n    },\n  };\n}\n\nfunction calculateLikelihoodBand(norm, dorScore) {\n  let points = 0;\n  const factors = [];\n\n  if (norm.source === 'incident') {\n    points += 2;\n    factors.push('incident_source');\n  }\n\n  if (\n    norm.source ===\n    'project1_governance_finding'\n  ) {\n    points += 1;\n    factors.push('governance_finding_source');\n  }\n\n  if (dorScore < 0.85) {\n    points += 1;\n    factors.push('readiness_below_0_85');\n  }\n\n  if (\n    Array.isArray(norm.dependenciesStated) &&\n    norm.dependenciesStated.length > 0\n  ) {\n    points += 1;\n    factors.push('dependencies_present');\n  }\n\n  const band =\n    points >= 3\n      ? 'high'\n      : points >= 1\n        ? 'medium'\n        : 'low';\n\n  return {\n    band,\n    points,\n    factors,\n  };\n}\n\nfunction calculateImpactBand(norm) {\n  let points = 0;\n  const factors = [];\n\n  const componentCount =\n    Array.isArray(norm.affectedComponents)\n      ? norm.affectedComponents.length\n      : 0;\n\n  if (componentCount >= 3) {\n    points += 2;\n    factors.push(\n      'three_or_more_components_affected'\n    );\n  } else if (componentCount >= 1) {\n    points += 1;\n    factors.push(\n      'one_or_two_components_affected'\n    );\n  }\n\n  if (\n    typeof norm.businessJustificationStated ===\n      'string' &&\n    norm.businessJustificationStated.trim() !== ''\n  ) {\n    points += 1;\n    factors.push(\n      'business_impact_documented'\n    );\n  }\n\n  if (\n    norm.urgencyClaimedByRequester ===\n      'critical' ||\n    norm.urgencyClaimedByRequester === 'high'\n  ) {\n    points += 1;\n    factors.push(\n      'high_or_critical_requester_urgency'\n    );\n  }\n\n  const band =\n    points >= 3\n      ? 'high'\n      : points >= 1\n        ? 'medium'\n        : 'low';\n\n  return {\n    band,\n    points,\n    factors,\n  };\n}\n\nreturn items.map((item) => {\n  const norm = item.json.normalized;\n  const dorScore = item.json.dorScore;\n\n  if (\n    !norm ||\n    typeof norm !== 'object' ||\n    Array.isArray(norm)\n  ) {\n    throw new Error(\n      'priority_scoring_missing_normalized_intake'\n    );\n  }\n\n  if (\n    typeof dorScore !== 'number' ||\n    !Number.isFinite(dorScore) ||\n    dorScore < 0 ||\n    dorScore > 1\n  ) {\n    throw new Error(\n      'priority_scoring_invalid_dor_score'\n    );\n  }\n\n  if (\n    SOURCE_WEIGHT[norm.source] === undefined\n  ) {\n    throw new Error(\n      `priority_scoring_invalid_source:${norm.source}`\n    );\n  }\n\n  const urgencyKey =\n    norm.urgencyClaimedByRequester ?? 'none';\n\n  if (\n    URGENCY_WEIGHT[urgencyKey] === undefined\n  ) {\n    throw new Error(\n      `priority_scoring_invalid_urgency:${urgencyKey}`\n    );\n  }\n\n  const priority =\n    calculatePriority(norm, dorScore);\n\n  const likelihood =\n    calculateLikelihoodBand(\n      norm,\n      dorScore\n    );\n\n  const impact =\n    calculateImpactBand(norm);\n\n  const compositeScore =\n    RISK_MATRIX[likelihood.band][impact.band];\n\n  const adminData =\n    getAdminList(item.json);\n\n  const assignment =\n    assignCapacityAware(adminData.admins);\n\n  const assigned =\n    assignment.assigned;\n\n  return {\n    json: {\n      ...item.json,\n\n      priorityScore:\n        priority.priorityScore,\n\n      priorityBand:\n        priority.priorityBand,\n\n      priorityCalculation:\n        priority.calculation,\n\n      riskIndex: {\n        likelihoodBand:\n          likelihood.band,\n\n        impactBand:\n          impact.band,\n\n        compositeScore,\n\n        likelihoodPoints:\n          likelihood.points,\n\n        impactPoints:\n          impact.points,\n\n        likelihoodFactors:\n          likelihood.factors,\n\n        impactFactors:\n          impact.factors,\n\n        note:\n          'Deterministic delivery-risk likelihood \u00d7 impact matrix. It is a governed prioritisation aid, not a statistical probability or predictive model.',\n      },\n\n      workloadImpact: {\n        assignedAdminId:\n          assigned?.adminId ?? null,\n\n        currentWipForAdmin:\n          assigned?.currentWip ?? null,\n\n        projectedWipForAdmin:\n          assigned\n            ? assigned.currentWip + 1\n            : null,\n\n        wipLimit:\n          assigned?.wipLimit ?? null,\n\n        utilizationBefore:\n          assigned\n            ? Number(\n                calculateUtilization(\n                  assigned\n                ).toFixed(4)\n              )\n            : null,\n\n        utilizationAfter:\n          assigned\n            ? Number(\n                (\n                  (assigned.currentWip + 1) /\n                  assigned.wipLimit\n                ).toFixed(4)\n              )\n            : null,\n\n        capacityBreached:\n          assignment.capacityBreached,\n\n        assignmentReason:\n          assignment.assignmentReason,\n\n        capacityDataSource:\n          adminData.source,\n\n        invalidAdminRecords:\n          adminData.invalidRecords,\n      },\n\n      scoringVersion: '1.1.0',\n      routeTo: 'ai_story_draft',\n    },\n  };\n});"
      },
      "id": "16b51506-219d-44d4-98b5-a503b66f0600",
      "name": "04: Priority / Risk / Workload Scoring",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -848,
        -80
      ],
      "notes": "DEMO: admins list is hardcoded. Replace with a live Google Sheets/Data Table read of admin WIP."
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
        "jsonParameters": true,
        "options": {
          "timeout": 90000
        },
        "bodyParametersJson": "{\n  \"systemInstruction\": {\n    \"parts\": [\n      {\n        \"text\": \"You are the bounded story-drafting component of the VANTIX Salesforce Agile Delivery and Admin Workload Sentinel. Use only the supplied deterministic intake evidence. Draft one user story, evidence-supported acceptance criteria, clarification questions, clearly labelled assumptions requiring human confirmation, and a non-authoritative effort band. For this controlled positive-path test, acceptanceCriteria must contain exactly one criterion: The system prevents a user from saving an Opportunity record with a Stage of Closed Won when the Close Date field is blank. Do not add an error-message requirement. Do not add profile exemptions. Do not add other Opportunity stages. Do not add field-visibility requirements. Do not add new Salesforce fields. Do not add implementation details beyond the validation-rule requirement explicitly stated in the intake. Do not invent requirements, evidence, business value, dependencies, access rules or technical constraints. Clarification questions may identify unresolved matters, but they must not be converted into acceptance criteria or established facts. Assumptions must remain clearly labelled as assumptions. The evidenceReferenced array must contain only this exact value: https://internal.example.com/tickets/SOPS-4821. Do not include intakeId, storyId, title or any other identifier in evidenceReferenced. Return only valid JSON matching the required schema.\"\n      }\n    ]\n  },\n  \"contents\": [\n    {\n      \"role\": \"user\",\n      \"parts\": [\n        {\n          \"text\": \"Create a governed backlog-ready Salesforce story for intake INT-2026-000101. Source: enhancement_request. Title: Add validation rule to prevent blank Opportunity Close Date. Description: Sales ops has reported that Opportunities are being marked Closed Won without a Close Date populated, which breaks the forecasting report. Need a validation rule on the Opportunity object. Business justification: Forecasting report accuracy directly affects the weekly pipeline review with leadership. Expected outcome: Users cannot save a Closed Won Opportunity without a Close Date. Affected components: Opportunity and Forecasting Report. Allowed evidence reference: https://internal.example.com/tickets/SOPS-4821. The acceptanceCriteria array must contain exactly one item: The system prevents a user from saving an Opportunity record with a Stage of Closed Won when the Close Date field is blank.\"\n        }\n      ]\n    }\n  ],\n  \"generationConfig\": {\n    \"temperature\": 0,\n    \"responseMimeType\": \"application/json\",\n    \"responseSchema\": {\n      \"type\": \"OBJECT\",\n      \"required\": [\n        \"intakeId\",\n        \"storyId\",\n        \"userStory\",\n        \"acceptanceCriteria\",\n        \"clarificationQuestions\",\n        \"assumptionsMade\",\n        \"effortBandSuggested\",\n        \"confidence\",\n        \"evidenceReferenced\"\n      ],\n      \"properties\": {\n        \"intakeId\": {\n          \"type\": \"STRING\"\n        },\n        \"storyId\": {\n          \"type\": \"STRING\"\n        },\n        \"userStory\": {\n          \"type\": \"STRING\"\n        },\n        \"acceptanceCriteria\": {\n          \"type\": \"ARRAY\",\n          \"minItems\": 1,\n          \"maxItems\": 1,\n          \"items\": {\n            \"type\": \"STRING\"\n          }\n        },\n        \"clarificationQuestions\": {\n          \"type\": \"ARRAY\",\n          \"items\": {\n            \"type\": \"STRING\"\n          }\n        },\n        \"assumptionsMade\": {\n          \"type\": \"ARRAY\",\n          \"items\": {\n            \"type\": \"STRING\"\n          }\n        },\n        \"effortBandSuggested\": {\n          \"type\": \"STRING\",\n          \"enum\": [\n            \"S\",\n            \"M\",\n            \"L\",\n            \"XL\",\n            \"unknown\"\n          ]\n        },\n        \"confidence\": {\n          \"type\": \"NUMBER\"\n        },\n        \"evidenceReferenced\": {\n          \"type\": \"ARRAY\",\n          \"minItems\": 1,\n          \"maxItems\": 1,\n          \"items\": {\n            \"type\": \"STRING\",\n            \"enum\": [\n              \"https://internal.example.com/tickets/SOPS-4821\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}",
        "headerParametersJson": "\n{\n  \"x-goog-api-key\": \"={{$env.GEMINI_API_KEY}}\",\n  \"Content-Type\": \"application/json\"\n}"
      },
      "id": "ada6936d-792a-49d8-8c6e-b69747039cc4",
      "name": "AI: Story Draft (Gemini/Claude \u2014 see prompts/story_draft_prompt.md)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -656,
        32
      ],
      "notes": "Requires endpoint configuration and provider-specific response verification. Intentionally not part of deterministic baseline test."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Validate AI Story Draft \u2014 governed deterministic validator\n//\n// Responsibilities:\n// 1. Restore deterministic context from Node 04.\n// 2. Safely extract and parse the Gemini response.\n// 3. Validate the response contract independently of the prompt.\n// 4. Enforce deterministic identity and evidence boundaries.\n// 5. Enforce the controlled acceptance-criteria boundary.\n// 6. Assign the authoritative story ID.\n// 7. Fail closed without throwing an unhandled parsing error.\n\nconst REQUIRED_FIELDS = [\n  'intakeId',\n  'userStory',\n  'acceptanceCriteria',\n  'clarificationQuestions',\n  'assumptionsMade',\n  'effortBandSuggested',\n  'confidence',\n  'evidenceReferenced',\n];\n\nconst VALID_EFFORT_BANDS = [\n  'S',\n  'M',\n  'L',\n  'XL',\n  'unknown',\n];\n\n/*\n * Controlled positive-path acceptance criterion.\n *\n * In the production version, this should come from an upstream\n * deterministic requirements-authorisation object rather than\n * remaining hardcoded.\n */\nconst CONTROLLED_ACCEPTANCE_CRITERION =\n  'The system prevents a user from saving an Opportunity record with a Stage of Closed Won when the Close Date field is blank.';\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction cleanJsonText(value) {\n  return String(value)\n    .trim()\n    .replace(/^```(?:json)?\\s*/i, '')\n    .replace(/\\s*```$/i, '');\n}\n\nfunction parseJsonText(value) {\n  const parsed = JSON.parse(\n    cleanJsonText(value)\n  );\n\n  if (!isPlainObject(parsed)) {\n    throw new Error(\n      'parsed_ai_output_must_be_object'\n    );\n  }\n\n  return parsed;\n}\n\nfunction extractGeminiDraft(rawResponse) {\n  if (!isPlainObject(rawResponse)) {\n    throw new Error('missing_or_invalid_ai_response');\n  }\n\n  /*\n   * Gemini generateContent REST response:\n   * candidates[0].content.parts[0].text\n   */\n  const candidate =\n    rawResponse?.candidates?.[0];\n\n  const candidateText =\n    candidate?.content?.parts?.[0]?.text;\n\n  if (\n    typeof candidateText === 'string' &&\n    candidateText.trim() !== ''\n  ) {\n    return {\n      parsedDraft: parseJsonText(\n        candidateText\n      ),\n\n      responseMetadata: {\n        finishReason:\n          candidate?.finishReason ?? null,\n\n        candidateCount:\n          Array.isArray(rawResponse.candidates)\n            ? rawResponse.candidates.length\n            : 0,\n\n        promptFeedback:\n          rawResponse.promptFeedback ?? null,\n\n        usageMetadata:\n          rawResponse.usageMetadata ?? null,\n      },\n    };\n  }\n\n  /*\n   * Optional compatibility fallback when a future HTTP mapping\n   * outputs the inner governed draft directly.\n   */\n  if (\n    rawResponse.intakeId !== undefined &&\n    rawResponse.userStory !== undefined &&\n    rawResponse.acceptanceCriteria !== undefined\n  ) {\n    return {\n      parsedDraft: rawResponse,\n\n      responseMetadata: {\n        finishReason: 'direct_object_mapping',\n        candidateCount: null,\n        promptFeedback: null,\n        usageMetadata: null,\n      },\n    };\n  }\n\n  throw new Error(\n    'unsupported_ai_response_shape'\n  );\n}\n\nfunction validateStringArray(\n  value,\n  fieldName,\n  errors,\n  options = {}\n) {\n  const {\n    minItems = 0,\n    maxItems = null,\n  } = options;\n\n  if (!Array.isArray(value)) {\n    errors.push(\n      `${fieldName}_must_be_array`\n    );\n    return;\n  }\n\n  if (value.length < minItems) {\n    errors.push(\n      `${fieldName}_below_min_items:${minItems}`\n    );\n  }\n\n  if (\n    maxItems !== null &&\n    value.length > maxItems\n  ) {\n    errors.push(\n      `${fieldName}_above_max_items:${maxItems}`\n    );\n  }\n\n  const invalidItem = value.some(\n    (entry) =>\n      typeof entry !== 'string' ||\n      entry.trim() === ''\n  );\n\n  if (invalidItem) {\n    errors.push(\n      `${fieldName}_contains_invalid_item`\n    );\n  }\n}\n\nfunction containsDuplicates(values) {\n  if (!Array.isArray(values)) {\n    return false;\n  }\n\n  return (\n    new Set(\n      values.map((value) =>\n        typeof value === 'string'\n          ? value.trim()\n          : value\n      )\n    ).size !== values.length\n  );\n}\n\nfunction normalizeStringArray(values) {\n  if (!Array.isArray(values)) {\n    return [];\n  }\n\n  return values.map((value) =>\n    value.trim()\n  );\n}\n\nconst baseItems =\n  $('04: Priority / Risk / Workload Scoring').all();\n\nreturn items.map((item, index) => {\n  /*\n   * Match the AI response to the corresponding deterministic\n   * record. This remains safe if multiple intake items are\n   * processed in one execution.\n   */\n  const base =\n    baseItems[index]?.json ??\n    baseItems[0]?.json ??\n    null;\n\n  const rawResponse = item.json;\n  const errors = [];\n  const warnings = [];\n\n  let parsedDraft = null;\n  let responseMetadata = null;\n\n  if (!isPlainObject(base)) {\n    errors.push(\n      'missing_deterministic_context'\n    );\n  }\n\n  try {\n    const extracted =\n      extractGeminiDraft(rawResponse);\n\n    parsedDraft =\n      extracted.parsedDraft;\n\n    responseMetadata =\n      extracted.responseMetadata;\n  } catch (error) {\n    errors.push(\n      `ai_output_parse_error:${error.message}`\n    );\n  }\n\n  if (\n    responseMetadata?.finishReason &&\n    ![\n      'STOP',\n      'direct_object_mapping',\n    ].includes(\n      responseMetadata.finishReason\n    )\n  ) {\n    errors.push(\n      `unexpected_finish_reason:${responseMetadata.finishReason}`\n    );\n  }\n\n  if (parsedDraft) {\n    for (const field of REQUIRED_FIELDS) {\n      if (\n        parsedDraft[field] === undefined ||\n        parsedDraft[field] === null\n      ) {\n        errors.push(\n          `missing_field:${field}`\n        );\n      }\n    }\n\n    const expectedIntakeId =\n      base?.normalized?.intakeId;\n\n    if (!expectedIntakeId) {\n      errors.push(\n        'deterministic_intakeId_missing'\n      );\n    }\n\n    if (\n      parsedDraft.intakeId !== undefined &&\n      parsedDraft.intakeId !==\n        expectedIntakeId\n    ) {\n      errors.push(\n        'intakeId_mismatch'\n      );\n    }\n\n    if (\n      typeof parsedDraft.userStory !==\n        'string' ||\n      parsedDraft.userStory.trim().length <\n        10\n    ) {\n      errors.push(\n        'invalid_userStory'\n      );\n    }\n\n    validateStringArray(\n      parsedDraft.acceptanceCriteria,\n      'acceptanceCriteria',\n      errors,\n      {\n        minItems: 1,\n        maxItems: 1,\n      }\n    );\n\n    if (\n      Array.isArray(\n        parsedDraft.acceptanceCriteria\n      ) &&\n      parsedDraft.acceptanceCriteria.length ===\n        1 &&\n      parsedDraft.acceptanceCriteria[0]\n        ?.trim() !==\n        CONTROLLED_ACCEPTANCE_CRITERION\n    ) {\n      errors.push(\n        'acceptanceCriteria_not_authorised'\n      );\n    }\n\n    validateStringArray(\n      parsedDraft.clarificationQuestions,\n      'clarificationQuestions',\n      errors\n    );\n\n    validateStringArray(\n      parsedDraft.assumptionsMade,\n      'assumptionsMade',\n      errors\n    );\n\n    validateStringArray(\n      parsedDraft.evidenceReferenced,\n      'evidenceReferenced',\n      errors,\n      {\n        minItems: 1,\n        maxItems: 1,\n      }\n    );\n\n    if (\n      containsDuplicates(\n        parsedDraft.evidenceReferenced\n      )\n    ) {\n      errors.push(\n        'duplicate_evidence_reference'\n      );\n    }\n\n    if (\n      parsedDraft.effortBandSuggested !==\n        undefined &&\n      !VALID_EFFORT_BANDS.includes(\n        parsedDraft.effortBandSuggested\n      )\n    ) {\n      errors.push(\n        'invalid_effort_band'\n      );\n    }\n\n    if (\n      typeof parsedDraft.confidence !==\n        'number' ||\n      !Number.isFinite(\n        parsedDraft.confidence\n      ) ||\n      parsedDraft.confidence < 0 ||\n      parsedDraft.confidence > 1\n    ) {\n      errors.push(\n        'confidence_out_of_range'\n      );\n    }\n\n    /*\n     * AI may reference only evidence already present in the\n     * deterministic intake.\n     */\n    const allowedEvidence =\n      new Set(\n        base?.normalized?.evidenceLinks ??\n          []\n      );\n\n    if (\n      Array.isArray(\n        parsedDraft.evidenceReferenced\n      )\n    ) {\n      for (\n        const evidence of\n        parsedDraft.evidenceReferenced\n      ) {\n        if (\n          typeof evidence === 'string' &&\n          !allowedEvidence.has(\n            evidence.trim()\n          )\n        ) {\n          errors.push(\n            `unsupported_evidence_reference:${evidence}`\n          );\n        }\n      }\n    }\n\n    /*\n     * storyId is not authoritative when returned by AI.\n     * Record a warning for audit visibility, then overwrite it.\n     */\n    const deterministicStoryId =\n      `STORY-${\n        expectedIntakeId || 'UNKNOWN'\n      }`;\n\n    if (\n      parsedDraft.storyId !== undefined &&\n      parsedDraft.storyId !==\n        deterministicStoryId\n    ) {\n      warnings.push(\n        'ai_storyId_overwritten_by_deterministic_storyId'\n      );\n    }\n  }\n\n  const valid =\n    errors.length === 0;\n\n  const deterministicStoryId =\n    `STORY-${\n      base?.normalized?.intakeId ||\n      'UNKNOWN'\n    }`;\n\n  const governedDraft =\n    valid && parsedDraft\n      ? {\n          ...parsedDraft,\n\n          intakeId:\n            base.normalized.intakeId,\n\n          storyId:\n            deterministicStoryId,\n\n          userStory:\n            parsedDraft.userStory.trim(),\n\n          acceptanceCriteria:\n            normalizeStringArray(\n              parsedDraft.acceptanceCriteria\n            ),\n\n          clarificationQuestions:\n            normalizeStringArray(\n              parsedDraft.clarificationQuestions\n            ),\n\n          assumptionsMade:\n            normalizeStringArray(\n              parsedDraft.assumptionsMade\n            ),\n\n          evidenceReferenced:\n            normalizeStringArray(\n              parsedDraft.evidenceReferenced\n            ),\n        }\n      : null;\n\n  return {\n    json: {\n      ...(base ?? {}),\n\n      aiStoryDraftRaw:\n        rawResponse,\n\n      aiStoryResponseMetadata:\n        responseMetadata,\n\n      storyDraft:\n        governedDraft,\n\n      storyDraftValid:\n        valid,\n\n      storyDraftErrors:\n        errors,\n\n      storyDraftWarnings:\n        warnings,\n\n      storyValidationVersion:\n        '1.1.0',\n\n      routeTo: valid\n        ? 'ai_critique'\n        : 'human_review_invalid_ai_output',\n    },\n  };\n});"
      },
      "id": "f722de85-f930-4f15-92a5-0e53f93357d8",
      "name": "Validate AI Story Draft (schema check)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -448,
        32
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{$json.storyDraftValid}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "99f76f4a-5dd3-451c-8cc1-767faa03520d",
      "name": "IF: Draft Schema Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -224,
        32
      ]
    },
    {
      "parameters": {},
      "id": "5b287ab8-6a7d-4b1d-baa2-f44c05e14cd9",
      "name": "STOP: Invalid AI Output -> Human Review",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        0,
        208
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
        "jsonParameters": true,
        "options": {
          "timeout": 120000
        },
        "bodyParametersJson": "={{ $json.critiqueRequestBody }}",
        "headerParametersJson": "{\n  \"x-goog-api-key\": \"={{$env.GEMINI_API_KEY}}\",\n  \"Content-Type\": \"application/json\"\n}"
      },
      "id": "7b06085e-a796-4338-a37e-dc8d06da8100",
      "name": "AI: Independent Critique (see prompts/critique_prompt.md)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        304,
        -224
      ],
      "notes": "Separate critique call. Requires endpoint configuration and provider-specific response verification. Must remain fail-closed."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Validate AI Critique \u2014 deterministic, fail-closed validator\n//\n// Responsibilities:\n// 1. Restore the governed story and deterministic context.\n// 2. Extract and safely parse the Gemini critique response.\n// 3. Validate identity, types, arrays, routing and confidence.\n// 4. Prevent adverse or malformed critique output from advancing.\n// 5. Preserve raw AI output for audit evidence.\n// 6. Route only a clean critique to human approval.\n\nconst REQUIRED_FIELDS = [\n  'intakeId',\n  'storyId',\n  'inventedContentFlag',\n  'missingEvidenceFlag',\n  'unsupportedAcceptanceCriteria',\n  'routingRecommendation',\n  'critiqueNotes',\n  'overallConfidence',\n];\n\nconst VALID_ROUTES = [\n  'proceed_to_human_review',\n  'request_more_evidence',\n  'likely_duplicate',\n  'reject_out_of_scope',\n];\n\nconst CRITIQUE_VALIDATION_VERSION = '1.1.0';\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction cleanJsonText(value) {\n  return String(value)\n    .trim()\n    .replace(/^```(?:json)?\\s*/i, '')\n    .replace(/\\s*```$/i, '');\n}\n\nfunction parseJsonText(value) {\n  const parsed = JSON.parse(\n    cleanJsonText(value)\n  );\n\n  if (!isPlainObject(parsed)) {\n    throw new Error(\n      'parsed_critique_must_be_object'\n    );\n  }\n\n  return parsed;\n}\n\nfunction extractCritique(rawResponse) {\n  if (!isPlainObject(rawResponse)) {\n    throw new Error(\n      'missing_or_invalid_critique_response'\n    );\n  }\n\n  /*\n   * Standard Gemini generateContent REST response:\n   * candidates[0].content.parts[0].text\n   */\n  const candidate =\n    rawResponse?.candidates?.[0];\n\n  const candidateText =\n    candidate?.content?.parts?.[0]?.text;\n\n  if (\n    typeof candidateText === 'string' &&\n    candidateText.trim() !== ''\n  ) {\n    return {\n      critique: parseJsonText(\n        candidateText\n      ),\n\n      responseMetadata: {\n        finishReason:\n          candidate?.finishReason ?? null,\n\n        candidateCount:\n          Array.isArray(\n            rawResponse.candidates\n          )\n            ? rawResponse.candidates.length\n            : 0,\n\n        promptFeedback:\n          rawResponse.promptFeedback ?? null,\n\n        usageMetadata:\n          rawResponse.usageMetadata ?? null,\n      },\n    };\n  }\n\n  /*\n   * Compatibility paths for alternative HTTP mappings.\n   */\n  const possibleValues = [\n    rawResponse.body,\n    rawResponse.data,\n    rawResponse.output,\n    rawResponse.text,\n    rawResponse.content?.[0]?.text,\n    rawResponse.aiCritiqueRaw,\n  ];\n\n  for (const value of possibleValues) {\n    if (\n      typeof value === 'string' &&\n      value.trim() !== ''\n    ) {\n      try {\n        return {\n          critique: parseJsonText(value),\n\n          responseMetadata: {\n            finishReason:\n              'alternative_text_mapping',\n            candidateCount: null,\n            promptFeedback: null,\n            usageMetadata: null,\n          },\n        };\n      } catch (_) {\n        // Continue checking other supported shapes.\n      }\n    }\n\n    if (isPlainObject(value)) {\n      return {\n        critique: value,\n\n        responseMetadata: {\n          finishReason:\n            'alternative_object_mapping',\n          candidateCount: null,\n          promptFeedback: null,\n          usageMetadata: null,\n        },\n      };\n    }\n  }\n\n  /*\n   * Direct-object fallback.\n   */\n  if (\n    rawResponse.intakeId !== undefined &&\n    rawResponse.storyId !== undefined &&\n    rawResponse.routingRecommendation !==\n      undefined\n  ) {\n    return {\n      critique: rawResponse,\n\n      responseMetadata: {\n        finishReason:\n          'direct_object_mapping',\n        candidateCount: null,\n        promptFeedback: null,\n        usageMetadata: null,\n      },\n    };\n  }\n\n  throw new Error(\n    'unsupported_critique_response_shape'\n  );\n}\n\nfunction validateStringArray(\n  value,\n  fieldName,\n  errors\n) {\n  if (!Array.isArray(value)) {\n    errors.push(\n      `${fieldName}_must_be_array`\n    );\n    return;\n  }\n\n  const invalidItem = value.some(\n    (entry) =>\n      typeof entry !== 'string' ||\n      entry.trim() === ''\n  );\n\n  if (invalidItem) {\n    errors.push(\n      `${fieldName}_contains_invalid_item`\n    );\n  }\n}\n\nfunction normalizeStringArray(value) {\n  if (!Array.isArray(value)) {\n    return [];\n  }\n\n  return value.map((entry) =>\n    entry.trim()\n  );\n}\n\nconst baseItems =\n  $('Validate AI Story Draft (schema check)').all();\n\nreturn items.map((item, index) => {\n  /*\n   * Match each critique response with the corresponding\n   * governed story record.\n   */\n  const base =\n    baseItems[index]?.json ??\n    baseItems[0]?.json ??\n    null;\n\n  const rawResponse = item.json;\n\n  const errors = [];\n  const warnings = [];\n\n  let critique = null;\n  let responseMetadata = null;\n\n  if (!isPlainObject(base)) {\n    errors.push(\n      'missing_governed_story_context'\n    );\n  }\n\n  try {\n    const extracted =\n      extractCritique(rawResponse);\n\n    critique =\n      extracted.critique;\n\n    responseMetadata =\n      extracted.responseMetadata;\n  } catch (error) {\n    errors.push(\n      `critique_parse_error:${error.message}`\n    );\n  }\n\n  /*\n   * Fail closed for abnormal Gemini completion reasons.\n   */\n  const acceptableFinishReasons = [\n    'STOP',\n    'alternative_text_mapping',\n    'alternative_object_mapping',\n    'direct_object_mapping',\n    null,\n  ];\n\n  if (\n    responseMetadata &&\n    !acceptableFinishReasons.includes(\n      responseMetadata.finishReason\n    )\n  ) {\n    errors.push(\n      `unexpected_finish_reason:${responseMetadata.finishReason}`\n    );\n  }\n\n  if (critique) {\n    for (const field of REQUIRED_FIELDS) {\n      if (\n        critique[field] === undefined ||\n        critique[field] === null\n      ) {\n        errors.push(\n          `missing_field:${field}`\n        );\n      }\n    }\n\n    const expectedIntakeId =\n      base?.normalized?.intakeId;\n\n    const expectedStoryId =\n      base?.storyDraft?.storyId;\n\n    if (!expectedIntakeId) {\n      errors.push(\n        'deterministic_intakeId_missing'\n      );\n    }\n\n    if (!expectedStoryId) {\n      errors.push(\n        'deterministic_storyId_missing'\n      );\n    }\n\n    if (\n      critique.intakeId !== undefined &&\n      critique.intakeId !==\n        expectedIntakeId\n    ) {\n      errors.push(\n        'intakeId_mismatch'\n      );\n    }\n\n    if (\n      critique.storyId !== undefined &&\n      critique.storyId !==\n        expectedStoryId\n    ) {\n      errors.push(\n        'storyId_mismatch'\n      );\n    }\n\n    if (\n      critique.inventedContentFlag !==\n        undefined &&\n      typeof critique.inventedContentFlag !==\n        'boolean'\n    ) {\n      errors.push(\n        'inventedContentFlag_not_boolean'\n      );\n    }\n\n    if (\n      critique.missingEvidenceFlag !==\n        undefined &&\n      typeof critique.missingEvidenceFlag !==\n        'boolean'\n    ) {\n      errors.push(\n        'missingEvidenceFlag_not_boolean'\n      );\n    }\n\n    validateStringArray(\n      critique.unsupportedAcceptanceCriteria,\n      'unsupportedAcceptanceCriteria',\n      errors\n    );\n\n    validateStringArray(\n      critique.critiqueNotes,\n      'critiqueNotes',\n      errors\n    );\n\n    if (\n      critique.routingRecommendation !==\n        undefined &&\n      !VALID_ROUTES.includes(\n        critique.routingRecommendation\n      )\n    ) {\n      errors.push(\n        'invalid_routing_recommendation'\n      );\n    }\n\n    if (\n      critique.overallConfidence !==\n        undefined &&\n      (\n        typeof critique.overallConfidence !==\n          'number' ||\n        !Number.isFinite(\n          critique.overallConfidence\n        ) ||\n        critique.overallConfidence < 0 ||\n        critique.overallConfidence > 1\n      )\n    ) {\n      errors.push(\n        'overallConfidence_out_of_range'\n      );\n    }\n\n    /*\n     * Internal consistency controls.\n     */\n    if (\n      critique.routingRecommendation ===\n        'proceed_to_human_review' &&\n      critique.inventedContentFlag === true\n    ) {\n      errors.push(\n        'inconsistent_route_invented_content'\n      );\n    }\n\n    if (\n      critique.routingRecommendation ===\n        'proceed_to_human_review' &&\n      critique.missingEvidenceFlag === true\n    ) {\n      errors.push(\n        'inconsistent_route_missing_evidence'\n      );\n    }\n\n    if (\n      critique.routingRecommendation ===\n        'proceed_to_human_review' &&\n      Array.isArray(\n        critique.unsupportedAcceptanceCriteria\n      ) &&\n      critique.unsupportedAcceptanceCriteria\n        .length > 0\n    ) {\n      errors.push(\n        'inconsistent_route_unsupported_acceptance_criteria'\n      );\n    }\n\n    if (\n      critique.overallConfidence !==\n        undefined &&\n      critique.overallConfidence < 0.5\n    ) {\n      warnings.push(\n        'low_critique_confidence'\n      );\n    }\n  }\n\n  const valid =\n    errors.length === 0;\n\n  const normalizedCritique =\n    valid && critique\n      ? {\n          ...critique,\n\n          intakeId:\n            base.normalized.intakeId,\n\n          storyId:\n            base.storyDraft.storyId,\n\n          unsupportedAcceptanceCriteria:\n            normalizeStringArray(\n              critique.unsupportedAcceptanceCriteria\n            ),\n\n          critiqueNotes:\n            normalizeStringArray(\n              critique.critiqueNotes\n            ),\n        }\n      : null;\n\n  const safeToReview =\n    valid &&\n    normalizedCritique\n      ?.routingRecommendation ===\n      'proceed_to_human_review' &&\n    normalizedCritique\n      ?.inventedContentFlag === false &&\n    normalizedCritique\n      ?.missingEvidenceFlag === false &&\n    normalizedCritique\n      ?.unsupportedAcceptanceCriteria\n      ?.length === 0;\n\n  let routeTo;\n\n  if (!valid) {\n    routeTo =\n      'human_review_invalid_critique';\n  } else if (safeToReview) {\n    routeTo =\n      'human_approval';\n  } else {\n    routeTo =\n      normalizedCritique\n        .routingRecommendation;\n  }\n\n  return {\n    json: {\n      ...(base ?? {}),\n\n      // Preserve complete provider response for audit evidence.\n      aiCritiqueRaw:\n        rawResponse,\n\n      aiCritiqueResponseMetadata:\n        responseMetadata,\n\n      critiqueResult:\n        normalizedCritique,\n\n      critiqueValid:\n        valid,\n\n      critiqueSafeToReview:\n        safeToReview,\n\n      critiqueErrors:\n        errors,\n\n      critiqueWarnings:\n        warnings,\n\n      critiqueValidationVersion:\n        CRITIQUE_VALIDATION_VERSION,\n\n      routeTo,\n    },\n  };\n});"
      },
      "id": "8772849b-80af-4604-a2e2-e4176b59228d",
      "name": "Validate AI Critique (schema check)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        544,
        32
      ]
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 05: Human Approval Gate\n//\n// Fail-closed validator for the human approval response.\n\nconst APPROVAL_VALIDATION_VERSION = '1.1.0';\n\nconst ALLOWED_APPROVAL_STATUSES = [\n  'approved',\n  'modified_then_approved',\n];\n\nconst RECOGNISED_STATUSES = [\n  'approved',\n  'modified_then_approved',\n  'rejected',\n  'deferred',\n];\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction cleanString(value) {\n  return typeof value === 'string'\n    ? value.trim()\n    : null;\n}\n\nfunction isValidTimestamp(value) {\n  return (\n    typeof value === 'string' &&\n    value.trim() !== '' &&\n    !Number.isNaN(Date.parse(value))\n  );\n}\n\nfunction evaluateApproval(decision, governedContext) {\n\n  const errors = [];\n  const warnings = [];\n\n  if (!isPlainObject(decision)) {\n    return {\n      approvalGranted: false,\n      routeTo: 'no_backlog_write',\n      approvalReason: 'missing_or_invalid_decision_payload',\n      approvalErrors: [\n        'missing_or_invalid_decision_payload'\n      ],\n      approvalWarnings: [],\n      humanApproval: null\n    };\n  }\n\n  const status = cleanString(decision.status);\n  const decidedBy = cleanString(decision.decidedBy);\n  const decidedAt = cleanString(decision.decidedAt);\n\n  const overrideNotes =\n    decision.overrideNotes === undefined ||\n    decision.overrideNotes === null\n      ? null\n      : cleanString(decision.overrideNotes);\n\n  const reworkFlagged =\n    decision.reworkFlagged === true;\n\n  const submittedApprovalRequestId =\n    cleanString(decision.approvalRequestId);\n\n  const submittedIntakeId =\n    cleanString(decision.intakeId);\n\n  const submittedStoryId =\n    cleanString(decision.storyId);\n\n  const expectedApprovalRequestId =\n    governedContext?.approvalRequest?.approvalRequestId ?? null;\n\n  const expectedIntakeId =\n    governedContext?.approvalRequest?.intakeId ??\n    governedContext?.normalized?.intakeId ??\n    null;\n\n  const expectedStoryId =\n    governedContext?.approvalRequest?.storyId ??\n    governedContext?.storyDraft?.storyId ??\n    null;\n\n  if (!status) {\n    errors.push('status_missing');\n  } else if (!RECOGNISED_STATUSES.includes(status)) {\n    errors.push(`unrecognised_status:${status}`);\n  }\n\n  if (!decidedBy) {\n    errors.push('decidedBy_missing');\n  }\n\n  if (!decidedAt) {\n    errors.push('decidedAt_missing');\n  } else if (!isValidTimestamp(decidedAt)) {\n    errors.push('decidedAt_invalid');\n  }\n\n  if (\n    decision.overrideNotes !== undefined &&\n    decision.overrideNotes !== null &&\n    typeof decision.overrideNotes !== 'string'\n  ) {\n    errors.push('overrideNotes_invalid');\n  }\n\n  if (\n    decision.reworkFlagged !== undefined &&\n    typeof decision.reworkFlagged !== 'boolean'\n  ) {\n    errors.push('reworkFlagged_invalid');\n  }\n\n  if (\n    status === 'modified_then_approved' &&\n    !overrideNotes\n  ) {\n    errors.push(\n      'overrideNotes_required_for_modified_approval'\n    );\n  }\n\n  if (!submittedApprovalRequestId) {\n    errors.push('approvalRequestId_missing');\n  } else if (\n    expectedApprovalRequestId &&\n    submittedApprovalRequestId !==\n      expectedApprovalRequestId\n  ) {\n    errors.push('approvalRequestId_mismatch');\n  }\n\n  if (!submittedIntakeId) {\n    errors.push('intakeId_missing');\n  } else if (\n    expectedIntakeId &&\n    submittedIntakeId !== expectedIntakeId\n  ) {\n    errors.push('intakeId_mismatch');\n  }\n\n  if (!submittedStoryId) {\n    errors.push('storyId_missing');\n  } else if (\n    expectedStoryId &&\n    submittedStoryId !== expectedStoryId\n  ) {\n    errors.push('storyId_mismatch');\n  }\n\n  if (isValidTimestamp(decidedAt)) {\n\n    const decisionTime = Date.parse(decidedAt);\n\n    if (\n      decisionTime >\n      Date.now() + (5 * 60 * 1000)\n    ) {\n      errors.push('decision_in_future');\n    }\n  }\n\n  const approvalGranted =\n    errors.length === 0 &&\n    ALLOWED_APPROVAL_STATUSES.includes(status);\n\n  let approvalReason = null;\n\n  if (errors.length > 0) {\n    approvalReason = 'invalid_decision_payload';\n  } else if (!approvalGranted) {\n    approvalReason = `status_not_approved:${status}`;\n  }\n\n  if (\n    approvalGranted &&\n    reworkFlagged\n  ) {\n    warnings.push('approved_with_rework');\n  }\n\n  return {\n\n    approvalGranted,\n\n    routeTo: approvalGranted\n      ? 'backlog_write'\n      : 'no_backlog_write',\n\n    approvalReason,\n\n    approvalErrors: errors,\n\n    approvalWarnings: warnings,\n\n    humanApproval: {\n\n      approvalRequestId:\n        submittedApprovalRequestId,\n\n      intakeId:\n        submittedIntakeId,\n\n      storyId:\n        submittedStoryId,\n\n      status,\n\n      decidedBy,\n\n      decidedAt,\n\n      overrideNotes,\n\n      reworkFlagged\n\n    }\n\n  };\n\n}\n\nconst governedItems =\n  $('Prepare Human Approval').all();\n\nreturn items.map((item, index) => {\n\n  const webhookPayload =\n    item.json ?? {};\n\n  const governedContext =\n    governedItems[index]?.json ??\n    governedItems[0]?.json ??\n    {};\n\n  const decision =\n    webhookPayload?.body?.humanDecision ??\n    webhookPayload?.humanDecision ??\n    null;\n\n  const approvalResult =\n    evaluateApproval(\n      decision,\n      governedContext\n    );\n\n  const {\n    approvalRequest,\n    ...safeContext\n  } = governedContext;\n\n  const safeApprovalRequest =\n    approvalRequest\n      ? {\n          approvalRequestId:\n            approvalRequest.approvalRequestId,\n\n          version:\n            approvalRequest.version,\n\n          intakeId:\n            approvalRequest.intakeId,\n\n          storyId:\n            approvalRequest.storyId,\n\n          requestedAt:\n            approvalRequest.requestedAt,\n\n          allowedDecisions:\n            approvalRequest.allowedDecisions ?? [],\n\n          requiredDecisionFields:\n            approvalRequest.requiredDecisionFields ?? [],\n\n          instructions:\n            approvalRequest.instructions ?? null\n        }\n      : null;\n\n  return {\n\n    json: {\n\n      ...safeContext,\n\n      approvalRequest:\n        safeApprovalRequest,\n\n      ...approvalResult,\n\n      approvalReceipt: {\n\n        receivedAt:\n          new Date().toISOString(),\n\n        source:\n          'wait_webhook',\n\n        validationVersion:\n          APPROVAL_VALIDATION_VERSION\n\n      }\n\n    }\n\n  };\n\n});"
      },
      "id": "d5c5d144-35c9-4a61-97c8-215146adcfa4",
      "name": "05: Human Approval Gate (fail-closed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        1104,
        592
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{$json.approvalGranted}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "6003cf7f-3068-464d-baa9-b8fea3744570",
      "name": "IF: Approval Granted?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1328,
        0
      ]
    },
    {
      "parameters": {},
      "id": "a98998d9-76ff-4677-ac36-e597a5f07914",
      "name": "STOP: Not Approved -> No Backlog Write",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1552,
        208
      ]
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Backlog Write \u2014 idempotent simulated store\n//\n// Portfolio/demo behaviour:\n// Uses workflow static data as a stand-in for a real n8n Data Table,\n// Jira, Azure DevOps or Salesforce backlog object.\n//\n// Production behaviour:\n// Replace only the persistence layer while keeping the validation,\n// idempotency and governed-record construction logic.\n\nconst BACKLOG_WRITE_VERSION = '1.1.0';\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction stableStringify(value) {\n  if (Array.isArray(value)) {\n    return `[${value\n      .map((entry) => stableStringify(entry))\n      .join(',')}]`;\n  }\n\n  if (isPlainObject(value)) {\n    const keys = Object.keys(value).sort();\n\n    return `{${keys\n      .map(\n        (key) =>\n          `${JSON.stringify(key)}:${stableStringify(\n            value[key]\n          )}`\n      )\n      .join(',')}}`;\n  }\n\n  return JSON.stringify(value);\n}\n\n/*\n * Lightweight deterministic checksum for change detection.\n * This is not a security or cryptographic hash.\n */\nfunction calculateChecksum(value) {\n  const text = stableStringify(value);\n\n  let hash = 2166136261;\n\n  for (let index = 0; index < text.length; index++) {\n    hash ^= text.charCodeAt(index);\n\n    hash = Math.imul(\n      hash,\n      16777619\n    );\n  }\n\n  return (\n    hash >>> 0\n  )\n    .toString(16)\n    .padStart(8, '0');\n}\n\nfunction validateGovernedRecord(record) {\n  const errors = [];\n\n  if (!record.approvalGranted) {\n    errors.push(\n      'backlog_write_without_approval'\n    );\n  }\n\n  if (!isPlainObject(record.normalized)) {\n    errors.push(\n      'normalized_intake_missing'\n    );\n  }\n\n  if (!record.normalized?.intakeId) {\n    errors.push(\n      'intakeId_missing'\n    );\n  }\n\n  if (!isPlainObject(record.storyDraft)) {\n    errors.push(\n      'governed_story_missing'\n    );\n  }\n\n  if (!record.storyDraft?.storyId) {\n    errors.push(\n      'storyId_missing'\n    );\n  }\n\n  if (!record.storyDraftValid) {\n    errors.push(\n      'story_draft_not_valid'\n    );\n  }\n\n  if (!record.critiqueValid) {\n    errors.push(\n      'critique_not_valid'\n    );\n  }\n\n  if (!record.critiqueSafeToReview) {\n    errors.push(\n      'critique_not_safe'\n    );\n  }\n\n  if (!isPlainObject(record.humanApproval)) {\n    errors.push(\n      'human_approval_missing'\n    );\n  }\n\n  if (\n    ![\n      'approved',\n      'modified_then_approved',\n    ].includes(\n      record.humanApproval?.status\n    )\n  ) {\n    errors.push(\n      'human_approval_status_not_approved'\n    );\n  }\n\n  if (\n    record.storyDraft?.intakeId !==\n    record.normalized?.intakeId\n  ) {\n    errors.push(\n      'story_intakeId_mismatch'\n    );\n  }\n\n  return errors;\n}\n\nfunction buildGovernedBacklogRecord(input) {\n  return {\n    intakeId:\n      input.normalized.intakeId,\n\n    storyId:\n      input.storyDraft.storyId,\n\n    source:\n      input.normalized.source,\n\n    sourceReference:\n      input.normalized.sourceReference ??\n      null,\n\n    title:\n      input.normalized.title,\n\n    userStory:\n      input.storyDraft.userStory,\n\n    acceptanceCriteria:\n      input.storyDraft\n        .acceptanceCriteria ?? [],\n\n    clarificationQuestions:\n      input.storyDraft\n        .clarificationQuestions ?? [],\n\n    assumptionsMade:\n      input.storyDraft\n        .assumptionsMade ?? [],\n\n    effortBandSuggested:\n      input.storyDraft\n        .effortBandSuggested ?? null,\n\n    storyConfidence:\n      input.storyDraft.confidence ??\n      null,\n\n    evidenceReferenced:\n      input.storyDraft\n        .evidenceReferenced ?? [],\n\n    dorScore:\n      input.dorScore ?? null,\n\n    dorStatus:\n      input.dorStatus ?? null,\n\n    priorityScore:\n      input.priorityScore ?? null,\n\n    priorityBand:\n      input.priorityBand ?? null,\n\n    riskIndex:\n      input.riskIndex ?? null,\n\n    workloadImpact:\n      input.workloadImpact ?? null,\n\n    critiqueSummary: {\n      routingRecommendation:\n        input.critiqueResult\n          ?.routingRecommendation ?? null,\n\n      inventedContentFlag:\n        input.critiqueResult\n          ?.inventedContentFlag ?? null,\n\n      missingEvidenceFlag:\n        input.critiqueResult\n          ?.missingEvidenceFlag ?? null,\n\n      unsupportedAcceptanceCriteria:\n        input.critiqueResult\n          ?.unsupportedAcceptanceCriteria ??\n        [],\n\n      overallConfidence:\n        input.critiqueResult\n          ?.overallConfidence ?? null,\n    },\n\n    humanApproval: {\n      approvalRequestId:\n        input.humanApproval\n          ?.approvalRequestId ?? null,\n\n      status:\n        input.humanApproval?.status ??\n        null,\n\n      decidedBy:\n        input.humanApproval\n          ?.decidedBy ?? null,\n\n      decidedAt:\n        input.humanApproval\n          ?.decidedAt ?? null,\n\n      overrideNotes:\n        input.humanApproval\n          ?.overrideNotes ?? null,\n\n      reworkFlagged:\n        input.humanApproval\n          ?.reworkFlagged === true,\n    },\n\n    governance: {\n      storyValidationVersion:\n        input.storyValidationVersion ??\n        null,\n\n      critiqueValidationVersion:\n        input.critiqueValidationVersion ??\n        null,\n\n      approvalValidationVersion:\n        input.approvalReceipt\n          ?.validationVersion ?? null,\n\n      backlogWriteVersion:\n        BACKLOG_WRITE_VERSION,\n\n      executionId:\n        $execution.id ?? null,\n    },\n  };\n}\n\nconst staticData =\n  $getWorkflowStaticData('global');\n\nif (\n  !isPlainObject(staticData.backlog)\n) {\n  staticData.backlog = {};\n}\n\nreturn items.map((item) => {\n  const input = item.json;\n\n  const errors =\n    validateGovernedRecord(input);\n\n  if (errors.length > 0) {\n    return {\n      json: {\n        ...input,\n\n        backlogWriteValid: false,\n\n        backlogWriteErrors:\n          errors,\n\n        backlogAction: {\n          actionTaken:\n            'not_written',\n\n          reason:\n            'governance_validation_failed',\n\n          backlogItemId:\n            null,\n\n          idempotencyKey:\n            null,\n        },\n\n        routeTo:\n          'human_review_backlog_write_failure',\n      },\n    };\n  }\n\n  const intakeId =\n    input.normalized.intakeId;\n\n  const idempotencyKey =\n    `agile-delivery:${intakeId}`;\n\n  const now =\n    new Date().toISOString();\n\n  const governedRecord =\n    buildGovernedBacklogRecord(input);\n\n  const recordChecksum =\n    calculateChecksum(governedRecord);\n\n  const existingRecord =\n    staticData.backlog[\n      idempotencyKey\n    ] ?? null;\n\n  const existed =\n    isPlainObject(existingRecord);\n\n  const unchanged =\n    existed &&\n    existingRecord.recordChecksum ===\n      recordChecksum;\n\n  let actionTaken;\n\n  if (!existed) {\n    actionTaken = 'created';\n  } else if (unchanged) {\n    actionTaken = 'unchanged';\n  } else {\n    actionTaken = 'updated';\n  }\n\n  const storedRecord = {\n    ...governedRecord,\n\n    idempotencyKey,\n\n    recordChecksum,\n\n    createdAt:\n      existingRecord?.createdAt ??\n      now,\n\n    updatedAt:\n      now,\n\n    revision:\n      existed\n        ? (\n            Number(\n              existingRecord.revision\n            ) || 1\n          ) +\n          (unchanged ? 0 : 1)\n        : 1,\n  };\n\n  staticData.backlog[\n    idempotencyKey\n  ] = storedRecord;\n\n  return {\n    json: {\n      ...input,\n\n      backlogWriteValid: true,\n\n      backlogWriteErrors: [],\n\n      backlogAction: {\n        actionTaken,\n\n        backlogItemId:\n          idempotencyKey,\n\n        idempotencyKey,\n\n        recordChecksum,\n\n        revision:\n          storedRecord.revision,\n\n        createdAt:\n          storedRecord.createdAt,\n\n        updatedAt:\n          storedRecord.updatedAt,\n\n        storeType:\n          'n8n_workflow_static_data_demo',\n      },\n\n      routeTo:\n        'raid_and_audit',\n    },\n  };\n});"
      },
      "id": "c4260a2b-bae0-4a05-9027-e9d1c05c1ecd",
      "name": "Backlog Write (idempotent, simulated store)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        1552,
        0
      ],
      "notes": "DEMO: uses n8n workflow static data as a stand-in backlog store. Replace with a real Data Table, Jira, or Azure DevOps write for production use."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 08: RAID Register, Audit Trail & Final Processing Result\n//\n// Purpose:\n// 1. Create a governed RAID record.\n// 2. Build an auditable sequence of workflow decisions.\n// 3. Add run and correlation identifiers.\n// 4. Record cycle time and final processing status.\n// 5. Avoid reporting success when the backlog write failed.\n\nconst AUDIT_VERSION = '1.1.0';\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction cleanString(value) {\n  return typeof value === 'string'\n    ? value.trim()\n    : null;\n}\n\nfunction buildAuditEvent({\n  actorType,\n  actorId,\n  action,\n  outcome,\n  details = null,\n  timestamp = null,\n}) {\n  return {\n    timestamp:\n      timestamp ??\n      new Date().toISOString(),\n\n    actorType,\n    actorId:\n      actorId ?? 'unknown',\n\n    action,\n    outcome,\n    details,\n  };\n}\n\nfunction calculateCycleTimeHours(\n  submittedAt,\n  completedAt\n) {\n  if (\n    typeof submittedAt !== 'string' ||\n    typeof completedAt !== 'string'\n  ) {\n    return null;\n  }\n\n  const submittedTime =\n    Date.parse(submittedAt);\n\n  const completedTime =\n    Date.parse(completedAt);\n\n  if (\n    Number.isNaN(submittedTime) ||\n    Number.isNaN(completedTime) ||\n    completedTime < submittedTime\n  ) {\n    return null;\n  }\n\n  return Number(\n    (\n      (completedTime - submittedTime) /\n      (1000 * 60 * 60)\n    ).toFixed(4)\n  );\n}\n\nfunction createRaidId(\n  type,\n  intakeId,\n  index\n) {\n  return `${type}-${intakeId}-${String(\n    index + 1\n  ).padStart(2, '0')}`;\n}\n\nfunction deriveRaidEntry(record) {\n  const intakeId =\n    record.normalized?.intakeId ??\n    'UNKNOWN';\n\n  const risks = [];\n  const assumptions = [];\n  const issues = [];\n\n  const dependencies =\n    Array.isArray(\n      record.normalized\n        ?.dependenciesStated\n    )\n      ? record.normalized\n          .dependenciesStated\n          .map((dependency, index) => ({\n            dependencyId:\n              createRaidId(\n                'DEP',\n                intakeId,\n                index\n              ),\n\n            description:\n              dependency,\n\n            owner:\n              record.workloadImpact\n                ?.assignedAdminId ??\n              'unassigned',\n\n            status:\n              'open',\n          }))\n      : [];\n\n  if (\n    record.riskIndex\n      ?.compositeScore >= 7\n  ) {\n    risks.push({\n      riskId:\n        createRaidId(\n          'RSK',\n          intakeId,\n          risks.length\n        ),\n\n      description:\n        `Elevated deterministic delivery-risk score: ${record.riskIndex.compositeScore}.`,\n\n      owner:\n        record.workloadImpact\n          ?.assignedAdminId ??\n        'unassigned',\n\n      response:\n        'escalate_for_review',\n\n      trigger:\n        'risk_composite_score_greater_than_or_equal_to_7',\n\n      status:\n        'open',\n\n      severity:\n        'high',\n    });\n  }\n\n  if (\n    record.workloadImpact\n      ?.capacityBreached === true\n  ) {\n    risks.push({\n      riskId:\n        createRaidId(\n          'RSK',\n          intakeId,\n          risks.length\n        ),\n\n      description:\n        record.workloadImpact\n          ?.assignedAdminId\n          ? `Assignment to ${record.workloadImpact.assignedAdminId} would exceed the configured WIP limit.`\n          : 'No valid administrator capacity was available.',\n\n      owner:\n        record.workloadImpact\n          ?.assignedAdminId ??\n        'delivery_owner',\n\n      response:\n        'reassign_or_defer',\n\n      trigger:\n        'admin_capacity_breached',\n\n      status:\n        'open',\n\n      severity:\n        'high',\n    });\n  }\n\n  if (\n    Array.isArray(\n      record.storyDraft\n        ?.assumptionsMade\n    )\n  ) {\n    for (\n      const assumption of\n      record.storyDraft\n        .assumptionsMade\n    ) {\n      assumptions.push({\n        assumptionId:\n          createRaidId(\n            'ASM',\n            intakeId,\n            assumptions.length\n          ),\n\n        description:\n          assumption,\n\n        owner:\n          record.humanApproval\n            ?.decidedBy ??\n          'product_owner',\n\n        validationStatus:\n          'requires_confirmation',\n      });\n    }\n  }\n\n  if (\n    record.duplicateCheck\n      ?.isRelated === true\n  ) {\n    issues.push({\n      issueId:\n        createRaidId(\n          'ISS',\n          intakeId,\n          issues.length\n        ),\n\n      description:\n        `Related backlog work detected: ${\n          Array.isArray(\n            record.duplicateCheck\n              ?.matchedIntakeIds\n          )\n            ? record.duplicateCheck\n                .matchedIntakeIds\n                .join(', ')\n            : 'unspecified'\n        }`,\n\n      severity:\n        'low',\n\n      status:\n        'informational',\n\n      owner:\n        record.workloadImpact\n          ?.assignedAdminId ??\n        'backlog_owner',\n    });\n  }\n\n  if (\n    record.critiqueResult\n      ?.inventedContentFlag === true\n  ) {\n    issues.push({\n      issueId:\n        createRaidId(\n          'ISS',\n          intakeId,\n          issues.length\n        ),\n\n      description:\n        'Independent critique flagged possible invented content.',\n\n      severity:\n        'high',\n\n      status:\n        'requires_human_resolution',\n\n      owner:\n        record.humanApproval\n          ?.decidedBy ??\n        'reviewer',\n    });\n  }\n\n  if (\n    record.backlogWriteValid ===\n      false\n  ) {\n    issues.push({\n      issueId:\n        createRaidId(\n          'ISS',\n          intakeId,\n          issues.length\n        ),\n\n      description:\n        'Governed backlog persistence failed or was blocked.',\n\n      severity:\n        'high',\n\n      status:\n        'open',\n\n      owner:\n        'workflow_owner',\n    });\n  }\n\n  return {\n    risks,\n    assumptions,\n    issues,\n    dependencies,\n\n    summary: {\n      riskCount:\n        risks.length,\n\n      assumptionCount:\n        assumptions.length,\n\n      issueCount:\n        issues.length,\n\n      dependencyCount:\n        dependencies.length,\n    },\n  };\n}\n\nfunction determineProcessingResult(\n  record\n) {\n  const action =\n    record.backlogAction\n      ?.actionTaken ?? null;\n\n  const validActions = [\n    'created',\n    'updated',\n    'unchanged',\n  ];\n\n  if (\n    record.approvalGranted ===\n      true &&\n    record.backlogWriteValid ===\n      true &&\n    validActions.includes(action)\n  ) {\n    return {\n      status:\n        'completed',\n\n      outcome:\n        'approved_backlog_record_persisted',\n\n      successful:\n        true,\n\n      backlogAction:\n        action,\n\n      routeTo:\n        'item_fully_processed',\n    };\n  }\n\n  return {\n    status:\n      'controlled_failure',\n\n    outcome:\n      'backlog_record_not_persisted',\n\n    successful:\n      false,\n\n    backlogAction:\n      action,\n\n    routeTo:\n      'human_review_processing_failure',\n  };\n}\n\nreturn items.map((item) => {\n  const record =\n    item.json ?? {};\n\n  const completedAt =\n    new Date().toISOString();\n\n  const intakeId =\n    record.normalized\n      ?.intakeId ??\n    null;\n\n  const storyId =\n    record.storyDraft\n      ?.storyId ??\n    null;\n\n  const executionId =\n    $execution.id ?? null;\n\n  const runId =\n    record.runId ??\n    `VAD-${executionId ?? Date.now()}`;\n\n  const correlationId =\n    record.correlationId ??\n    intakeId ??\n    runId;\n\n  const cycleTimeHours =\n    calculateCycleTimeHours(\n      record.normalized\n        ?.submittedAt,\n      completedAt\n    );\n\n  const raidEntry =\n    deriveRaidEntry(record);\n\n  const processingResult =\n    determineProcessingResult(\n      record\n    );\n\n  const reviewer =\n    cleanString(\n      record.humanApproval\n        ?.decidedBy\n    ) ?? 'unknown';\n\n  const auditTrail = [\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'intake-validator',\n\n      action:\n        'intake_normalized',\n\n      outcome:\n        record.schemaValid === true\n          ? 'passed'\n          : 'failed',\n\n      details: {\n        schemaValid:\n          record.schemaValid ??\n          null,\n\n        validationErrors:\n          record.errors ?? [],\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'duplicate-detector',\n\n      action:\n        'duplicate_check_completed',\n\n      outcome:\n        record.duplicateCheck\n          ?.isDuplicate\n          ? 'possible_duplicate'\n          : 'no_duplicate_block',\n\n      details:\n        record.duplicateCheck ??\n        null,\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'definition-of-ready-engine',\n\n      action:\n        'definition_of_ready_scored',\n\n      outcome:\n        record.dorStatus ??\n        'unknown',\n\n      details: {\n        dorScore:\n          record.dorScore ??\n          null,\n\n        dorStatus:\n          record.dorStatus ??\n          null,\n\n        missingChecks:\n          record.missingChecks ??\n          [],\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'priority-risk-workload-engine',\n\n      action:\n        'priority_risk_workload_scored',\n\n      outcome:\n        'completed',\n\n      details: {\n        priorityScore:\n          record.priorityScore ??\n          null,\n\n        priorityBand:\n          record.priorityBand ??\n          null,\n\n        riskIndex:\n          record.riskIndex ??\n          null,\n\n        workloadImpact:\n          record.workloadImpact ??\n          null,\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'ai',\n\n      actorId:\n        'story-drafting-model',\n\n      action:\n        'story_draft_generated_and_validated',\n\n      outcome:\n        record.storyDraftValid\n          ? 'passed'\n          : 'failed',\n\n      details: {\n        storyId,\n        storyDraftValid:\n          record.storyDraftValid ??\n          null,\n\n        validationErrors:\n          record.storyDraftErrors ??\n          [],\n\n        validationWarnings:\n          record.storyDraftWarnings ??\n          [],\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'ai',\n\n      actorId:\n        'independent-critique-model',\n\n      action:\n        'independent_critique_completed_and_validated',\n\n      outcome:\n        record.critiqueSafeToReview\n          ? 'safe_for_human_review'\n          : 'not_safe_for_human_review',\n\n      details: {\n        critiqueValid:\n          record.critiqueValid ??\n          null,\n\n        critiqueSafeToReview:\n          record.critiqueSafeToReview ??\n          null,\n\n        routingRecommendation:\n          record.critiqueResult\n            ?.routingRecommendation ??\n          null,\n\n        overallConfidence:\n          record.critiqueResult\n            ?.overallConfidence ??\n          null,\n\n        validationErrors:\n          record.critiqueErrors ??\n          [],\n\n        validationWarnings:\n          record.critiqueWarnings ??\n          [],\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'human',\n\n      actorId:\n        reviewer,\n\n      action:\n        'human_approval_decision_recorded',\n\n      outcome:\n        record.humanApproval\n          ?.status ??\n        'missing',\n\n      details:\n        record.humanApproval ??\n        null,\n\n      timestamp:\n        record.humanApproval\n          ?.decidedAt ??\n        null,\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'backlog-writer',\n\n      action:\n        'backlog_persistence_attempted',\n\n      outcome:\n        record.backlogAction\n          ?.actionTaken ??\n        'unknown',\n\n      details: {\n        backlogWriteValid:\n          record.backlogWriteValid ??\n          null,\n\n        backlogWriteErrors:\n          record.backlogWriteErrors ??\n          [],\n\n        backlogAction:\n          record.backlogAction ??\n          null,\n      },\n    }),\n\n    buildAuditEvent({\n      actorType:\n        'system',\n\n      actorId:\n        'workflow-controller',\n\n      action:\n        'item_processing_completed',\n\n      outcome:\n        processingResult.outcome,\n\n      details: {\n        processingStatus:\n          processingResult.status,\n\n        successful:\n          processingResult.successful,\n\n        runId,\n        correlationId,\n        cycleTimeHours,\n      },\n\n      timestamp:\n        completedAt,\n    }),\n  ];\n\n  return {\n    json: {\n      ...record,\n\n      runId,\n      correlationId,\n\n      workflowVersion:\n        '1.1.3',\n\n      completedAt,\n      cycleTimeHours,\n\n      raidEntry,\n      auditTrail,\n\n      auditSummary: {\n        version:\n          AUDIT_VERSION,\n\n        eventCount:\n          auditTrail.length,\n\n        startedAt:\n          record.normalized\n            ?.submittedAt ??\n          null,\n\n        completedAt,\n\n        cycleTimeHours,\n\n        finalActor:\n          reviewer,\n      },\n\n      processingResult,\n\n      routeTo:\n        processingResult.routeTo,\n    },\n  };\n});"
      },
      "id": "c1d23b7d-99a7-4e20-9007-fdfa10ffdaab",
      "name": "08: RAID Register + Audit Trail",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        1760,
        0
      ]
    },
    {
      "parameters": {},
      "id": "b104e118-1729-4b4a-a44e-070f73ddb3fb",
      "name": "END: Item Fully Processed",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1984,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Deterministic synthetic Project 1 adapter fixture.\nreturn [{ json: {\n  runContext: {\n    runId: 'VOS-20260802041538-PUJR38',\n    schemaVersion: 'vantix-flow-integrity-final-output-1.4.0',\n    validatedAt: '2026-08-02T04:15:51.639Z'\n  },\n  issue: {\n    issueId: 'ISSUE-B7AD267D',\n    relatedFlow: 'Create_Task_for_High_Value_Practice_Deals',\n    relatedFlowLabel: 'Create Task for High Value Practice Deals',\n    defectsFound: ['Missing Description'],\n    defectCount: 1,\n    severity: 'Minor',\n    confidence: 0.9,\n    routingDecision: 'Minor',\n    lifecycleState: 'AI_ASSESSED',\n    validationFlag: 'OK'\n  }\n} }];"
      },
      "id": "583d0ae8-10f9-4151-a308-6e34bdf1f42b",
      "name": "Demo Flow Integrity Issue (real issue from v1.4.0 evidence run)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2416,
        240
      ],
      "notes": "Synthetic Project 1 fixture implemented as a Code node to avoid Edit Fields/Set node version incompatibility."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel \u2014 Node 00: Project 1 (Flow Integrity) Adapter\n// Built against the REAL v1.4.0 schema (final-output.schema.json) from the\n// Flow Integrity handoff, not an assumed one. Fail-closed on version drift\n// or missing fields \u2014 never guesses at a mapping.\nconst FLOW_INTEGRITY_SCHEMA_VERSION = 'vantix-flow-integrity-final-output-1.4.0';\nconst SEVERITY_TO_URGENCY = { 'Critical': 'critical', 'Review Required': 'high', 'Minor': 'medium', 'Unassessed': null };\nconst REQUIRED_ISSUE_FIELDS = ['issueId', 'relatedFlow', 'defectsFound', 'severity', 'routingDecision', 'lifecycleState'];\nconst ISSUE_ID_RE = /^ISSUE-[A-F0-9]{8}$/;\n\nfunction adaptFlowIntegrityIssue(issue, runContext, intakeId) {\n  const missing = REQUIRED_ISSUE_FIELDS.filter(f => issue?.[f] === undefined || issue?.[f] === null);\n  if (runContext?.schemaVersion && runContext.schemaVersion !== FLOW_INTEGRITY_SCHEMA_VERSION) {\n    return { ok: false, routeTo: 'human_review_schema_mismatch', reason: `flow_integrity_schema_version_mismatch: expected ${FLOW_INTEGRITY_SCHEMA_VERSION}, got ${runContext.schemaVersion}` };\n  }\n  if (missing.length > 0) return { ok: false, routeTo: 'human_review_schema_mismatch', reason: `missing_required_fields: ${missing.join(', ')}` };\n  if (!ISSUE_ID_RE.test(issue.issueId)) return { ok: false, routeTo: 'human_review_schema_mismatch', reason: `invalid_issueId_format: ${issue.issueId}` };\n\n  const flowLabel = issue.relatedFlowLabel || issue.relatedFlow;\n  const defectsList = issue.defectsFound.join(', ');\n  const businessJustificationStated = issue.impact?.rationale ?? issue.exposure?.rationale ?? null;\n  const expectedOutcomeStated = `Flow '${flowLabel}' shows zero findings for [${defectsList}] on the next Flow Integrity re-scan.`;\n\n  const normalized = {\n    schemaVersion: '1.0.0', intakeId, source: 'project1_governance_finding', sourceReference: issue.issueId,\n    title: `Flow governance defect: ${flowLabel} (${defectsList})`,\n    description: `VANTIX Flow Integrity (v1.4.0, run ${runContext?.runId ?? 'unknown'}) flagged ${issue.defectCount ?? issue.defectsFound.length} defect(s) \u2014 [${defectsList}] \u2014 on Salesforce Flow '${flowLabel}'. Routing decision: ${issue.routingDecision}. Lifecycle state: ${issue.lifecycleState}.`,\n    submittedBy: 'vantix-flow-integrity-adapter-v1', submittedAt: runContext?.validatedAt || runContext?.startedAt || new Date().toISOString(),\n    affectedComponents: [flowLabel], businessJustificationStated, expectedOutcomeStated,\n    evidenceLinks: [`project1://flow-integrity/issue/${issue.issueId}`], dependenciesStated: [],\n    urgencyClaimedByRequester: SEVERITY_TO_URGENCY[issue.severity] ?? null\n  };\n  return { ok: true, routeTo: 'normalize_validate', normalized };\n}\n\nreturn items.map(item => {\n  const result = adaptFlowIntegrityIssue(item.json.issue, item.json.runContext, `INT-2026-${String(parseInt(item.json.issue?.issueId?.slice(-6) || '0', 16) % 1000000).padStart(6, '0')}`);\n  // Output in the SAME shape as \"01: Normalize & Validate\" ({schemaValid,\n  // routeTo, errors, normalized}) so this branch can feed directly into the\n  // shared \"IF: Schema Valid?\" gate downstream, rather than needing its own\n  // parallel set of gates.\n  return { json: result.ok\n    ? { schemaValid: true, routeTo: 'duplicate_check', errors: [], normalized: result.normalized }\n    : { schemaValid: false, routeTo: 'schema_failure', errors: [result.reason] } };\n});\n"
      },
      "id": "3df826a5-184a-4aa7-922a-7c94ce2e6b95",
      "name": "00: Project 1 (Flow Integrity) Adapter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -2208,
        240
      ],
      "notes": "Fail-closed on Flow Integrity schema version drift or missing required fields \u2014 routes to human_review_schema_mismatch rather than guessing. See docs/project1-adapter-mapping.md."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 06: Verify Completed Work Against Acceptance Criteria\n//\n// Purpose:\n// 1. Validate the approved acceptance criteria.\n// 2. Validate submitted completion evidence.\n// 3. Match evidence deterministically to each criterion.\n// 4. Distinguish incomplete, unverifiable, matched and mismatched outcomes.\n// 5. Produce auditable verification output for RAID and Six Sigma rollup.\n\nconst VERIFICATION_VERSION = '1.1.0';\n\nconst ALLOWED_EVIDENCE_TYPES = [\n  'test_result',\n  'deployment_log',\n  'salesforce_record',\n  'screenshot',\n  'admin_attestation',\n  'reviewer_attestation',\n  'other',\n];\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction cleanString(value) {\n  return typeof value === 'string'\n    ? value.trim()\n    : null;\n}\n\nfunction normalizeAcceptanceCriteria(value) {\n  if (!Array.isArray(value)) {\n    return {\n      valid: false,\n      criteria: [],\n      errors: [\n        'acceptanceCriteria_must_be_array',\n      ],\n    };\n  }\n\n  const errors = [];\n\n  const criteria = value\n    .map((criterion) =>\n      cleanString(criterion)\n    )\n    .filter((criterion) => {\n      if (!criterion) {\n        errors.push(\n          'acceptanceCriteria_contains_blank_item'\n        );\n        return false;\n      }\n\n      return true;\n    });\n\n  if (criteria.length === 0) {\n    errors.push(\n      'acceptanceCriteria_empty'\n    );\n  }\n\n  const uniqueCriteria =\n    [...new Set(criteria)];\n\n  if (\n    uniqueCriteria.length !==\n    criteria.length\n  ) {\n    errors.push(\n      'acceptanceCriteria_contains_duplicates'\n    );\n  }\n\n  return {\n    valid: errors.length === 0,\n    criteria: uniqueCriteria,\n    errors,\n  };\n}\n\nfunction validateEvidenceRecord(\n  evidence,\n  index\n) {\n  const errors = [];\n\n  if (!isPlainObject(evidence)) {\n    return {\n      valid: false,\n      normalized: null,\n      errors: [\n        `evidence_${index}_must_be_object`,\n      ],\n    };\n  }\n\n  const evidenceId =\n    cleanString(evidence.evidenceId);\n\n  const linkedAcceptanceCriterion =\n    cleanString(\n      evidence.linkedAcceptanceCriterion\n    );\n\n  const evidenceType =\n    cleanString(evidence.evidenceType);\n\n  const evidenceReference =\n    cleanString(\n      evidence.evidenceReference\n    );\n\n  const verifiedBy =\n    cleanString(evidence.verifiedBy);\n\n  const verifiedAt =\n    cleanString(evidence.verifiedAt);\n\n  const verificationResult =\n    cleanString(\n      evidence.verificationResult\n    );\n\n  const notes =\n    evidence.notes === undefined ||\n    evidence.notes === null\n      ? null\n      : cleanString(evidence.notes);\n\n  if (!evidenceId) {\n    errors.push(\n      `evidence_${index}_evidenceId_missing`\n    );\n  }\n\n  if (!linkedAcceptanceCriterion) {\n    errors.push(\n      `evidence_${index}_linkedAcceptanceCriterion_missing`\n    );\n  }\n\n  if (!evidenceType) {\n    errors.push(\n      `evidence_${index}_evidenceType_missing`\n    );\n  } else if (\n    !ALLOWED_EVIDENCE_TYPES.includes(\n      evidenceType\n    )\n  ) {\n    errors.push(\n      `evidence_${index}_invalid_evidenceType:${evidenceType}`\n    );\n  }\n\n  if (!evidenceReference) {\n    errors.push(\n      `evidence_${index}_evidenceReference_missing`\n    );\n  }\n\n  if (!verifiedBy) {\n    errors.push(\n      `evidence_${index}_verifiedBy_missing`\n    );\n  }\n\n  if (!verifiedAt) {\n    errors.push(\n      `evidence_${index}_verifiedAt_missing`\n    );\n  } else if (\n    Number.isNaN(\n      Date.parse(verifiedAt)\n    )\n  ) {\n    errors.push(\n      `evidence_${index}_verifiedAt_invalid`\n    );\n  }\n\n  if (!verificationResult) {\n    errors.push(\n      `evidence_${index}_verificationResult_missing`\n    );\n  } else if (\n    ![\n      'pass',\n      'fail',\n      'inconclusive',\n    ].includes(\n      verificationResult\n    )\n  ) {\n    errors.push(\n      `evidence_${index}_invalid_verificationResult:${verificationResult}`\n    );\n  }\n\n  return {\n    valid:\n      errors.length === 0,\n\n    normalized: {\n      evidenceId,\n      linkedAcceptanceCriterion,\n      evidenceType,\n      evidenceReference,\n      verifiedBy,\n      verifiedAt,\n      verificationResult,\n      notes,\n    },\n\n    errors,\n  };\n}\n\nfunction verifyAgainstAcceptanceCriteria(\n  acceptanceCriteria,\n  evidence\n) {\n  const criterionResult =\n    normalizeAcceptanceCriteria(\n      acceptanceCriteria\n    );\n\n  if (!criterionResult.valid) {\n    return {\n      status:\n        'unverifiable_no_valid_acceptance_criteria',\n\n      outcomeMatch:\n        null,\n\n      checkedAgainst:\n        criterionResult.criteria,\n\n      matchedCriteria: [],\n      unmatchedCriteria: [],\n      failedCriteria: [],\n      inconclusiveCriteria: [],\n\n      evidenceEvaluated: [],\n      invalidEvidence: [],\n\n      errors:\n        criterionResult.errors,\n\n      notes:\n        'Verification could not proceed because valid approved acceptance criteria were not available.',\n    };\n  }\n\n  if (!Array.isArray(evidence)) {\n    return {\n      status:\n        'invalid_completion_evidence',\n\n      outcomeMatch:\n        null,\n\n      checkedAgainst:\n        criterionResult.criteria,\n\n      matchedCriteria: [],\n      unmatchedCriteria:\n        criterionResult.criteria,\n\n      failedCriteria: [],\n      inconclusiveCriteria: [],\n\n      evidenceEvaluated: [],\n      invalidEvidence: [],\n\n      errors: [\n        'completionEvidence_must_be_array',\n      ],\n\n      notes:\n        'Completion evidence was not supplied as an array.',\n    };\n  }\n\n  if (evidence.length === 0) {\n    return {\n      status:\n        'not_yet_completed',\n\n      outcomeMatch:\n        null,\n\n      checkedAgainst:\n        criterionResult.criteria,\n\n      matchedCriteria: [],\n      unmatchedCriteria:\n        criterionResult.criteria,\n\n      failedCriteria: [],\n      inconclusiveCriteria: [],\n\n      evidenceEvaluated: [],\n      invalidEvidence: [],\n\n      errors: [],\n\n      notes:\n        'No completion evidence has been submitted.',\n    };\n  }\n\n  const validEvidence = [];\n  const invalidEvidence = [];\n\n  evidence.forEach(\n    (record, index) => {\n      const result =\n        validateEvidenceRecord(\n          record,\n          index\n        );\n\n      if (result.valid) {\n        validEvidence.push(\n          result.normalized\n        );\n      } else {\n        invalidEvidence.push({\n          index,\n          errors:\n            result.errors,\n        });\n      }\n    }\n  );\n\n  if (validEvidence.length === 0) {\n    return {\n      status:\n        'invalid_completion_evidence',\n\n      outcomeMatch:\n        null,\n\n      checkedAgainst:\n        criterionResult.criteria,\n\n      matchedCriteria: [],\n      unmatchedCriteria:\n        criterionResult.criteria,\n\n      failedCriteria: [],\n      inconclusiveCriteria: [],\n\n      evidenceEvaluated: [],\n      invalidEvidence,\n\n      errors: [\n        'no_valid_completion_evidence',\n      ],\n\n      notes:\n        'Completion evidence was submitted, but none of the records passed validation.',\n    };\n  }\n\n  const matchedCriteria = [];\n  const unmatchedCriteria = [];\n  const failedCriteria = [];\n  const inconclusiveCriteria = [];\n\n  for (\n    const criterion of\n    criterionResult.criteria\n  ) {\n    const linkedEvidence =\n      validEvidence.filter(\n        (record) =>\n          record\n            .linkedAcceptanceCriterion ===\n          criterion\n      );\n\n    if (linkedEvidence.length === 0) {\n      unmatchedCriteria.push(\n        criterion\n      );\n      continue;\n    }\n\n    const hasFail =\n      linkedEvidence.some(\n        (record) =>\n          record.verificationResult ===\n          'fail'\n      );\n\n    const hasPass =\n      linkedEvidence.some(\n        (record) =>\n          record.verificationResult ===\n          'pass'\n      );\n\n    const hasInconclusive =\n      linkedEvidence.some(\n        (record) =>\n          record.verificationResult ===\n          'inconclusive'\n      );\n\n    if (hasFail) {\n      failedCriteria.push(\n        criterion\n      );\n    } else if (hasPass) {\n      matchedCriteria.push(\n        criterion\n      );\n    } else if (hasInconclusive) {\n      inconclusiveCriteria.push(\n        criterion\n      );\n    } else {\n      unmatchedCriteria.push(\n        criterion\n      );\n    }\n  }\n\n  let status;\n  let outcomeMatch;\n\n  if (\n    failedCriteria.length > 0\n  ) {\n    status =\n      'verified_mismatch';\n\n    outcomeMatch =\n      false;\n  } else if (\n    inconclusiveCriteria.length > 0 ||\n    unmatchedCriteria.length > 0\n  ) {\n    status =\n      'verification_incomplete';\n\n    outcomeMatch =\n      null;\n  } else {\n    status =\n      'verified_match';\n\n    outcomeMatch =\n      true;\n  }\n\n  return {\n    status,\n    outcomeMatch,\n\n    checkedAgainst:\n      criterionResult.criteria,\n\n    matchedCriteria,\n    unmatchedCriteria,\n    failedCriteria,\n    inconclusiveCriteria,\n\n    evidenceEvaluated:\n      validEvidence,\n\n    invalidEvidence,\n\n    errors:\n      invalidEvidence.length > 0\n        ? [\n            'some_evidence_records_invalid',\n          ]\n        : [],\n\n    notes:\n      status ===\n      'verified_match'\n        ? 'All approved acceptance criteria have valid passing completion evidence.'\n        : status ===\n          'verified_mismatch'\n          ? 'At least one approved acceptance criterion has failing completion evidence.'\n          : status ===\n            'verification_incomplete'\n            ? 'Verification is incomplete because one or more criteria are unmatched or supported only by inconclusive evidence.'\n            : 'Verification outcome recorded.',\n  };\n}\n\nreturn items.map((item) => {\n  const input =\n    item.json ?? {};\n\n  /*\n   * Do not use a hardcoded acceptance criterion.\n   * Read only from the governed backlog or approved story.\n   */\n  const acceptanceCriteria =\n    input.backlogRecord\n      ?.acceptanceCriteria ??\n    input.storyDraft\n      ?.acceptanceCriteria ??\n    input.acceptanceCriteria ??\n    null;\n\n  const completionEvidence =\n    input.completionEvidence ??\n    [];\n\n  const verification =\n    verifyAgainstAcceptanceCriteria(\n      acceptanceCriteria,\n      completionEvidence\n    );\n\n  return {\n    json: {\n      ...input,\n\n      verification: {\n        ...verification,\n\n        version:\n          VERIFICATION_VERSION,\n\n        verifiedAt:\n          new Date().toISOString(),\n      },\n\n      routeTo:\n        verification.status ===\n        'verified_match'\n          ? 'verification_recorded'\n          : verification.status ===\n            'verified_mismatch'\n            ? 'human_review_outcome_mismatch'\n            : verification.status ===\n              'verification_incomplete'\n              ? 'request_more_completion_evidence'\n              : 'human_review_verification_failure',\n    },\n  };\n});"
      },
      "id": "272cace5-850d-43a5-a1d7-da20e75ef78b",
      "name": "06: Verify Completed Work Against Acceptance Criteria",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -2416,
        464
      ],
      "notes": "DEMO payload hardcoded. Real deployment reads the actual backlog item's AC + submitted completion evidence."
    },
    {
      "parameters": {},
      "id": "895a0674-64d6-469e-bf02-7e01af2dd6f3",
      "name": "END: Verification Recorded (feeds RAID + Six Sigma rollup)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -2208,
        464
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 2 * * *"
            }
          ]
        }
      },
      "id": "13839b9b-b241-4019-b926-da3f2b6bf243",
      "name": "Schedule: Nightly Six Sigma Rollup",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        -2640,
        768
      ]
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Node 07: Six Sigma Measurement Rollup\n//\n// Purpose:\n// 1. Calculate governed defect and process-quality metrics.\n// 2. Keep defective-unit and total-defect measures separate.\n// 3. Calculate DPU, DPO and DPMO correctly.\n// 4. Record cycle-time observations.\n// 5. Prevent unsupported control-chart claims when sample size is too small.\n\nconst MEASUREMENT_VERSION = '1.1.0';\n\nconst OPPORTUNITIES_PER_UNIT = 4;\nconst MIN_OBSERVATIONS_FOR_CONTROL_CHART = 20;\n\nconst DEFECT_DEFINITIONS = {\n  D1_REJECTED: {\n    description:\n      'Human reviewer rejected the generated story.',\n    opportunity:\n      'human_approval_quality',\n  },\n\n  D2_VERIFIED_MISMATCH: {\n    description:\n      'Completed work failed one or more approved acceptance criteria.',\n    opportunity:\n      'outcome_verification_quality',\n  },\n\n  D3_REWORK_FLAGGED: {\n    description:\n      'Human reviewer approved the story only after modification and flagged rework.',\n    opportunity:\n      'first_pass_story_quality',\n  },\n\n  D4_DUPLICATE_MISSED: {\n    description:\n      'Duplicate work was not identified before human review.',\n    opportunity:\n      'duplicate_detection_quality',\n  },\n};\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nfunction finiteNonNegativeNumber(value) {\n  return (\n    typeof value === 'number' &&\n    Number.isFinite(value) &&\n    value >= 0\n  );\n}\n\nfunction classifyDefects(record) {\n  const defectCodes = [];\n\n  if (\n    record.humanApproval?.status ===\n    'rejected'\n  ) {\n    defectCodes.push(\n      'D1_REJECTED'\n    );\n  }\n\n  if (\n    record.verification?.status ===\n    'verified_mismatch'\n  ) {\n    defectCodes.push(\n      'D2_VERIFIED_MISMATCH'\n    );\n  }\n\n  if (\n    record.humanApproval?.status ===\n      'modified_then_approved' &&\n    record.humanApproval\n      ?.reworkFlagged === true\n  ) {\n    defectCodes.push(\n      'D3_REWORK_FLAGGED'\n    );\n  }\n\n  if (\n    record.duplicateCheck\n      ?.isDuplicate === true &&\n    record\n      ._duplicateCaughtPreReview ===\n      false\n  ) {\n    defectCodes.push(\n      'D4_DUPLICATE_MISSED'\n    );\n  }\n\n  return {\n    defectiveUnit:\n      defectCodes.length > 0,\n\n    defectCodes,\n\n    defectCount:\n      defectCodes.length,\n  };\n}\n\nfunction extractRecords(items) {\n  const validRecords = [];\n  const invalidInputs = [];\n\n  items.forEach(\n    (item, index) => {\n      const candidate =\n        item.json?.record ??\n        item.json;\n\n      if (\n        isPlainObject(candidate)\n      ) {\n        validRecords.push(\n          candidate\n        );\n      } else {\n        invalidInputs.push({\n          index,\n          reason:\n            'record_missing_or_invalid',\n        });\n      }\n    }\n  );\n\n  return {\n    validRecords,\n    invalidInputs,\n  };\n}\n\nconst inputItems =\n  $input.all();\n\nconst {\n  validRecords: records,\n  invalidInputs,\n} = extractRecords(inputItems);\n\nconst unitsProcessed =\n  records.length;\n\nlet defectiveUnits = 0;\nlet totalDefects = 0;\n\nconst defectReasonBreakdown = {};\n\nconst cycleTimes = [];\n\nfor (const record of records) {\n  const classification =\n    classifyDefects(record);\n\n  if (\n    classification.defectiveUnit\n  ) {\n    defectiveUnits++;\n  }\n\n  totalDefects +=\n    classification.defectCount;\n\n  for (\n    const defectCode of\n    classification.defectCodes\n  ) {\n    defectReasonBreakdown[\n      defectCode\n    ] =\n      (\n        defectReasonBreakdown[\n          defectCode\n        ] ?? 0\n      ) + 1;\n  }\n\n  const cycleTime =\n    record.cycleTimeHours ??\n    record._cycleTimeHours ??\n    null;\n\n  if (\n    finiteNonNegativeNumber(\n      cycleTime\n    )\n  ) {\n    cycleTimes.push(\n      cycleTime\n    );\n  }\n}\n\nconst totalOpportunities =\n  unitsProcessed *\n  OPPORTUNITIES_PER_UNIT;\n\nconst defectiveUnitRate =\n  unitsProcessed > 0\n    ? defectiveUnits /\n      unitsProcessed\n    : 0;\n\nconst firstPassYield =\n  unitsProcessed > 0\n    ? (\n        unitsProcessed -\n        defectiveUnits\n      ) /\n      unitsProcessed\n    : 0;\n\nconst defectsPerUnit =\n  unitsProcessed > 0\n    ? totalDefects /\n      unitsProcessed\n    : 0;\n\nconst defectsPerOpportunity =\n  totalOpportunities > 0\n    ? totalDefects /\n      totalOpportunities\n    : 0;\n\nconst dpmo =\n  defectsPerOpportunity *\n  1000000;\n\nconst averageCycleTimeHours =\n  cycleTimes.length > 0\n    ? cycleTimes.reduce(\n        (sum, value) =>\n          sum + value,\n        0\n      ) /\n      cycleTimes.length\n    : null;\n\nconst sortedCycleTimes =\n  [...cycleTimes].sort(\n    (a, b) => a - b\n  );\n\nlet medianCycleTimeHours =\n  null;\n\nif (\n  sortedCycleTimes.length > 0\n) {\n  const midpoint =\n    Math.floor(\n      sortedCycleTimes.length / 2\n    );\n\n  medianCycleTimeHours =\n    sortedCycleTimes.length % 2 ===\n    0\n      ? (\n          sortedCycleTimes[\n            midpoint - 1\n          ] +\n          sortedCycleTimes[\n            midpoint\n          ]\n        ) / 2\n      : sortedCycleTimes[\n          midpoint\n        ];\n}\n\nconst controlChartEligible =\n  cycleTimes.length >=\n  MIN_OBSERVATIONS_FOR_CONTROL_CHART;\n\nconst computedAt =\n  new Date().toISOString();\n\nreturn [\n  {\n    json: {\n      measurementVersion:\n        MEASUREMENT_VERSION,\n\n      measurementWindow: {\n        recordsReceived:\n          inputItems.length,\n\n        validRecords:\n          unitsProcessed,\n\n        invalidRecords:\n          invalidInputs.length,\n\n        computedAt,\n      },\n\n      defectModel: {\n        opportunitiesPerUnit:\n          OPPORTUNITIES_PER_UNIT,\n\n        defectDefinitions:\n          DEFECT_DEFINITIONS,\n      },\n\n      qualityMetrics: {\n        unitsProcessed,\n\n        defectiveUnits,\n\n        nonDefectiveUnits:\n          unitsProcessed -\n          defectiveUnits,\n\n        totalDefects,\n\n        totalOpportunities,\n\n        defectiveUnitRate:\n          Number(\n            defectiveUnitRate.toFixed(\n              6\n            )\n          ),\n\n        firstPassYield:\n          Number(\n            firstPassYield.toFixed(\n              6\n            )\n          ),\n\n        defectsPerUnit:\n          Number(\n            defectsPerUnit.toFixed(\n              6\n            )\n          ),\n\n        defectsPerOpportunity:\n          Number(\n            defectsPerOpportunity.toFixed(\n              6\n            )\n          ),\n\n        dpmo:\n          Number(\n            dpmo.toFixed(2)\n          ),\n\n        defectReasonBreakdown,\n      },\n\n      cycleTimeMetrics: {\n        observationCount:\n          cycleTimes.length,\n\n        averageCycleTimeHours:\n          averageCycleTimeHours ===\n          null\n            ? null\n            : Number(\n                averageCycleTimeHours.toFixed(\n                  4\n                )\n              ),\n\n        medianCycleTimeHours:\n          medianCycleTimeHours ===\n          null\n            ? null\n            : Number(\n                medianCycleTimeHours.toFixed(\n                  4\n                )\n              ),\n      },\n\n      controlChart: {\n        eligible:\n          controlChartEligible,\n\n        observationCount:\n          cycleTimes.length,\n\n        minimumRequired:\n          MIN_OBSERVATIONS_FOR_CONTROL_CHART,\n\n        status:\n          controlChartEligible\n            ? 'eligible_for_control_chart_analysis'\n            : 'insufficient_observations',\n\n        note:\n          controlChartEligible\n            ? 'The minimum observation threshold has been met. Control limits must still be calculated using an appropriate chart type and stable process assumptions.'\n            : 'No control-chart claim should be made until the minimum number of valid cycle-time observations is available.',\n      },\n\n      dataQuality: {\n        invalidInputs,\n\n        warnings:\n          unitsProcessed === 0\n            ? [\n                'no_valid_records_in_measurement_window',\n              ]\n            : [],\n      },\n\n      routeTo:\n        'metrics_stored',\n    },\n  },\n];"
      },
      "id": "aa9b88e4-0e0a-46d4-9f90-1f01455cfe64",
      "name": "07: Six Sigma Measurement Rollup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        -2416,
        768
      ],
      "notes": "DEMO: reads $input.all() which will be empty unless fed by an upstream Data Table read of the rollup window's records. Wire a real read node before this in production."
    },
    {
      "parameters": {},
      "id": "4b2e4b79-617e-4c25-ad65-b0b446c30c29",
      "name": "END: Metrics Stored (dashboard/report source)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -2208,
        768
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{$json.critiqueSafeToReview}}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "25725a13-f913-4117-95e3-ba6cb9d3a4d2",
      "name": "IF: Critique Safe to Human Review?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        768,
        -224
      ]
    },
    {
      "parameters": {},
      "id": "37cb0fe7-f532-429b-a023-63fad89124c9",
      "name": "STOP: Critique Invalid or Adverse -> Human Resolution Queue",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        784,
        384
      ],
      "notes": "Fail-closed. Invalid critique, missing evidence, invented content, unsupported acceptance criteria, likely duplicate or out-of-scope recommendations cannot advance to approval."
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Prepare Independent Critique Request\n//\n// Purpose:\n// Build a bounded critique request using only the governed story\n// and the deterministic intake evidence needed to assess it.\n\nconst CRITIQUE_REQUEST_VERSION = '1.1.0';\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nreturn items.map((item) => {\n  const input = item.json;\n  const errors = [];\n\n  if (!input.storyDraftValid) {\n    errors.push('story_draft_not_valid');\n  }\n\n  if (!isPlainObject(input.storyDraft)) {\n    errors.push('governed_story_draft_missing');\n  }\n\n  if (!isPlainObject(input.normalized)) {\n    errors.push('normalized_intake_missing');\n  }\n\n  const intakeId =\n    input.normalized?.intakeId ?? null;\n\n  const storyId =\n    input.storyDraft?.storyId ?? null;\n\n  if (!intakeId) {\n    errors.push('intakeId_missing');\n  }\n\n  if (!storyId) {\n    errors.push('storyId_missing');\n  }\n\n  if (\n    input.storyDraft?.intakeId &&\n    input.storyDraft.intakeId !== intakeId\n  ) {\n    errors.push('story_intakeId_mismatch');\n  }\n\n  if (errors.length > 0) {\n    throw new Error(\n      `cannot_prepare_critique_request:${errors.join('|')}`\n    );\n  }\n\n  /*\n   * Deliberately exclude priority, delivery risk, workload,\n   * admin assignment and other operational decisions.\n   *\n   * The critique AI only needs the authorised intake evidence\n   * and governed draft to detect unsupported content.\n   */\n  const authorisedEvidence = {\n    intakeId,\n    source:\n      input.normalized.source ?? null,\n    sourceReference:\n      input.normalized.sourceReference ?? null,\n    title:\n      input.normalized.title ?? null,\n    description:\n      input.normalized.description ?? null,\n    affectedComponents:\n      input.normalized.affectedComponents ?? [],\n    businessJustificationStated:\n      input.normalized.businessJustificationStated ?? null,\n    expectedOutcomeStated:\n      input.normalized.expectedOutcomeStated ?? null,\n    evidenceLinks:\n      input.normalized.evidenceLinks ?? [],\n    dependenciesStated:\n      input.normalized.dependenciesStated ?? [],\n  };\n\n  const critiqueRequestBody = {\n    systemInstruction: {\n      parts: [\n        {\n          text:\n            'You are the independent critique component of the VANTIX ' +\n            'Salesforce Agile Delivery and Admin Workload Sentinel. ' +\n            'Compare the governed story draft only against the supplied ' +\n            'deterministic intake evidence. Flag invented roles, requirements, ' +\n            'business value, technical solutions, dependencies, unsupported ' +\n            'acceptance criteria, missing evidence, and assumptions presented ' +\n            'as established facts. Do not approve the work, rewrite the story, ' +\n            'create new requirements, alter identifiers, change deterministic ' +\n            'scores, or recommend an unapproved Salesforce change. The supplied ' +\n            'intakeId and storyId must be returned unchanged. Return only valid ' +\n            'JSON matching the required response schema.',\n        },\n      ],\n    },\n\n    contents: [\n      {\n        role: 'user',\n        parts: [\n          {\n            text: JSON.stringify({\n              task:\n                'Independently critique the governed story draft against the authorised deterministic intake evidence.',\n\n              requiredIdentity: {\n                intakeId,\n                storyId,\n              },\n\n              authorisedEvidence,\n\n              draftToCritique:\n                input.storyDraft,\n            }),\n          },\n        ],\n      },\n    ],\n\n    generationConfig: {\n      temperature: 0,\n      responseMimeType: 'application/json',\n\n      responseSchema: {\n        type: 'OBJECT',\n\n        required: [\n          'intakeId',\n          'storyId',\n          'inventedContentFlag',\n          'missingEvidenceFlag',\n          'unsupportedAcceptanceCriteria',\n          'routingRecommendation',\n          'critiqueNotes',\n          'overallConfidence',\n        ],\n\n        properties: {\n          intakeId: {\n            type: 'STRING',\n          },\n\n          storyId: {\n            type: 'STRING',\n          },\n\n          inventedContentFlag: {\n            type: 'BOOLEAN',\n          },\n\n          missingEvidenceFlag: {\n            type: 'BOOLEAN',\n          },\n\n          unsupportedAcceptanceCriteria: {\n            type: 'ARRAY',\n            items: {\n              type: 'STRING',\n            },\n          },\n\n          routingRecommendation: {\n            type: 'STRING',\n            enum: [\n              'proceed_to_human_review',\n              'request_more_evidence',\n              'likely_duplicate',\n              'reject_out_of_scope',\n            ],\n          },\n\n          critiqueNotes: {\n            type: 'ARRAY',\n            items: {\n              type: 'STRING',\n            },\n          },\n\n          overallConfidence: {\n            type: 'NUMBER',\n          },\n        },\n      },\n    },\n  };\n\n  return {\n    json: {\n      ...input,\n\n      critiqueRequestBody,\n\n      critiqueRequestMetadata: {\n        version:\n          CRITIQUE_REQUEST_VERSION,\n        intakeId,\n        storyId,\n        preparedAt:\n          new Date().toISOString(),\n        contextPolicy:\n          'minimum_authorised_intake_evidence',\n      },\n    },\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        80,
        -240
      ],
      "id": "df88f4dc-8691-49cd-be25-98fd5e727ff3",
      "name": "Prepare Critique Request"
    },
    {
      "parameters": {
        "jsCode": "// VANTIX Agile Delivery & Admin Workload Sentinel\n// Prepare Human Approval\n//\n// Purpose:\n// 1. Confirm that a governed story and safe critique exist.\n// 2. Generate the execution-specific approval resume URL.\n// 3. Build a bounded approval request.\n// 4. Fail closed if critical approval context is missing.\n\nconst APPROVAL_REQUEST_VERSION = '1.1.0';\n\nconst ALLOWED_DECISIONS = [\n  'approved',\n  'modified_then_approved',\n  'rejected',\n  'deferred',\n];\n\nfunction isPlainObject(value) {\n  return (\n    value !== null &&\n    typeof value === 'object' &&\n    !Array.isArray(value)\n  );\n}\n\nreturn items.map((item) => {\n  const input = item.json;\n  const errors = [];\n\n  const intakeId =\n    input.normalized?.intakeId ?? null;\n\n  const storyId =\n    input.storyDraft?.storyId ?? null;\n\n  const resumeUrl =\n    $execution.resumeUrl ?? null;\n\n  if (!isPlainObject(input.normalized)) {\n    errors.push('normalized_intake_missing');\n  }\n\n  if (!isPlainObject(input.storyDraft)) {\n    errors.push('governed_story_draft_missing');\n  }\n\n  if (!input.storyDraftValid) {\n    errors.push('story_draft_not_valid');\n  }\n\n  if (!isPlainObject(input.critiqueResult)) {\n    errors.push('validated_critique_missing');\n  }\n\n  if (!input.critiqueValid) {\n    errors.push('critique_not_valid');\n  }\n\n  if (!input.critiqueSafeToReview) {\n    errors.push('critique_not_safe_for_human_review');\n  }\n\n  if (!intakeId) {\n    errors.push('intakeId_missing');\n  }\n\n  if (!storyId) {\n    errors.push('storyId_missing');\n  }\n\n  if (\n    input.storyDraft?.intakeId &&\n    input.storyDraft.intakeId !== intakeId\n  ) {\n    errors.push('story_intakeId_mismatch');\n  }\n\n  if (!resumeUrl) {\n    errors.push('execution_resume_url_missing');\n  }\n\n  /*\n   * Fail closed. The workflow must never enter the approval\n   * wait state without a governed story, validated critique,\n   * deterministic identity and execution-specific resume URL.\n   */\n  if (errors.length > 0) {\n    throw new Error(\n      `cannot_prepare_human_approval:${errors.join('|')}`\n    );\n  }\n\n  const requestedAt =\n    new Date().toISOString();\n\n  // Deterministic approval request ID\n  const approvalRequestId =\n    `APR-${intakeId}`;\n\n  return {\n    json: {\n      ...input,\n\n      approvalRequest: {\n        approvalRequestId,\n        version:\n          APPROVAL_REQUEST_VERSION,\n\n        intakeId,\n        storyId,\n\n        requestedAt,\n\n        /*\n         * This URL behaves like a temporary approval token.\n         * It must not be published, logged publicly or stored\n         * in the final GitHub evidence package.\n         */\n        resumeUrl,\n\n        allowedDecisions:\n          ALLOWED_DECISIONS,\n\n        reviewerContext: {\n          userStory:\n            input.storyDraft.userStory,\n\n          acceptanceCriteria:\n            input.storyDraft\n              .acceptanceCriteria ?? [],\n\n          effortBandSuggested:\n            input.storyDraft\n              .effortBandSuggested ?? null,\n\n          priorityBand:\n            input.priorityBand ?? null,\n\n          riskCompositeScore:\n            input.riskIndex\n              ?.compositeScore ?? null,\n\n          assignedAdminId:\n            input.workloadImpact\n              ?.assignedAdminId ?? null,\n\n          critiqueConfidence:\n            input.critiqueResult\n              ?.overallConfidence ?? null,\n        },\n\n        instructions:\n          'POST a JSON payload containing humanDecision to the execution-specific resumeUrl.',\n\n        requiredDecisionFields: [\n          'status',\n          'decidedBy',\n          'decidedAt',\n        ],\n\n        expectedPayloadExample: {\n          humanDecision: {\n            status: 'approved',\n            decidedBy:\n              'reviewer@example.com',\n            decidedAt:\n              '2026-08-06T00:00:00.000Z',\n            overrideNotes: null,\n            reworkFlagged: false,\n          },\n        },\n      },\n\n      approvalPreparationValid: true,\n      approvalPreparationErrors: [],\n      routeTo: 'wait_for_human_approval',\n    },\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1008,
        -352
      ],
      "id": "52a723a0-9d87-4787-b989-d97e0ce4b970",
      "name": "Prepare Human Approval"
    },
    {
      "parameters": {
        "jsCode": "return items.map(item => {\n\n  const approval = item.json.approvalRequest;\n\n  return {\n\n    json: {\n\n      body: {\n\n        humanDecision: {\n\n          approvalRequestId:\n            approval.approvalRequestId,\n\n          intakeId:\n            approval.intakeId,\n\n          storyId:\n            approval.storyId,\n\n          status: \"approved\",\n\n          decidedBy:\n            \"portfolio-reviewer@example.com\",\n\n          decidedAt:\n            new Date().toISOString(),\n\n          overrideNotes: null,\n\n          reworkFlagged: false\n\n        }\n\n      }\n\n    }\n\n  };\n\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1232,
        -224
      ],
      "id": "2208d2a5-88a9-453e-a4c1-f56290f6d41f",
      "name": "Mock Human Approval"
    }
  ],
  "connections": {
    "Start: New Intake Item (Manual Demo Trigger)": {
      "main": [
        [
          {
            "node": "Demo Intake Payload (replace with real intake form/webhook)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Demo Intake Payload (replace with real intake form/webhook)": {
      "main": [
        [
          {
            "node": "01: Normalize & Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "01: Normalize & Validate": {
      "main": [
        [
          {
            "node": "IF: Schema Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Demo Flow Integrity Issue (real issue from v1.4.0 evidence run)": {
      "main": [
        [
          {
            "node": "00: Project 1 (Flow Integrity) Adapter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "00: Project 1 (Flow Integrity) Adapter": {
      "main": [
        [
          {
            "node": "IF: Schema Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Schema Valid?": {
      "main": [
        [
          {
            "node": "02: Duplicate & Related-Work Detection",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Schema Failure -> Human Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "02: Duplicate & Related-Work Detection": {
      "main": [
        [
          {
            "node": "IF: Is Duplicate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Is Duplicate?": {
      "main": [
        [
          {
            "node": "STOP: Possible Duplicate -> Human Review Queue",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "03: DoR Completeness Scoring",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "03: DoR Completeness Scoring": {
      "main": [
        [
          {
            "node": "SWITCH: DoR Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SWITCH: DoR Status": {
      "main": [
        [
          {
            "node": "04: Priority / Risk / Workload Scoring",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Insufficient Evidence -> Request More Info From Requester",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Not Ready -> Backlog Refinement Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "04: Priority / Risk / Workload Scoring": {
      "main": [
        [
          {
            "node": "AI: Story Draft (Gemini/Claude \u2014 see prompts/story_draft_prompt.md)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Story Draft (Gemini/Claude \u2014 see prompts/story_draft_prompt.md)": {
      "main": [
        [
          {
            "node": "Validate AI Story Draft (schema check)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate AI Story Draft (schema check)": {
      "main": [
        [
          {
            "node": "IF: Draft Schema Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Draft Schema Valid?": {
      "main": [
        [
          {
            "node": "Prepare Critique Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Invalid AI Output -> Human Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Independent Critique (see prompts/critique_prompt.md)": {
      "main": [
        [
          {
            "node": "Validate AI Critique (schema check)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate AI Critique (schema check)": {
      "main": [
        [
          {
            "node": "IF: Critique Safe to Human Review?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "05: Human Approval Gate (fail-closed)": {
      "main": [
        [
          {
            "node": "IF: Approval Granted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Approval Granted?": {
      "main": [
        [
          {
            "node": "Backlog Write (idempotent, simulated store)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Not Approved -> No Backlog Write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Backlog Write (idempotent, simulated store)": {
      "main": [
        [
          {
            "node": "08: RAID Register + Audit Trail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "08: RAID Register + Audit Trail": {
      "main": [
        [
          {
            "node": "END: Item Fully Processed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "06: Verify Completed Work Against Acceptance Criteria": {
      "main": [
        [
          {
            "node": "END: Verification Recorded (feeds RAID + Six Sigma rollup)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: Nightly Six Sigma Rollup": {
      "main": [
        [
          {
            "node": "07: Six Sigma Measurement Rollup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "07: Six Sigma Measurement Rollup": {
      "main": [
        [
          {
            "node": "END: Metrics Stored (dashboard/report source)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Critique Safe to Human Review?": {
      "main": [
        [
          {
            "node": "Prepare Human Approval",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "STOP: Critique Invalid or Adverse -> Human Resolution Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Critique Request": {
      "main": [
        [
          {
            "node": "AI: Independent Critique (see prompts/critique_prompt.md)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Human Approval": {
      "main": [
        [
          {
            "node": "Mock Human Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Human Approval": {
      "main": [
        [
          {
            "node": "05: Human Approval Gate (fail-closed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "nodeGroups": [],
  "tags": []
}