{
  "id": "syntheticDemo001",
  "name": "Civico Due - evidence-bound household preview (synthetic, no delivery)",
  "active": false,
  "nodes": [
    {
      "id": "manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "synthetic-fixture",
      "name": "Synthetic note + provided extraction (NOT AI)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ],
      "notesInFlow": true,
      "notes": "All data is synthetic. Structured candidates are provided, not extracted by an LLM. One intentional duplicate exercises idempotent preparation.",
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {\n  \"note\": {\n    \"id\": \"synthetic-household-planning\",\n    \"revision\": 1,\n    \"capturedAt\": \"2030-05-01T12:00:00Z\",\n    \"synthetic\": true,\n    \"text\": \"SYNTHETIC HOUSEHOLD PLANNING NOTE. No real people or appointments.\\n\\nAction A1\\nKind: calendar\\nTitle: Filter reminder\\nOwner: Alex Demo\\nStart: 2030-05-06T09:00:00-07:00\\nEnd: 2030-05-06T09:30:00-07:00\\nRecipient: unknown\\nDetails: Replace the hallway air filter.\\n\\nAction A2\\nKind: email_draft\\nTitle: Ask about repair availability\\nOwner: Sam Demo\\nStart: unknown\\nEnd: unknown\\nRecipient: maintenance@example.invalid\\nDetails: Please confirm whether Tuesday afternoon is available for a routine visit.\\n\\nAction A3\\nKind: calendar\\nTitle: Review storage boxes\\nOwner: unknown\\nStart: unknown\\nEnd: unknown\\nRecipient: unknown\\nDetails: Review the boxes in storage. The owner and date were not decided.\"\n  },\n  \"extraction\": {\n    \"mode\": \"provided-structured-synthetic-fixture\",\n    \"aiExecuted\": false,\n    \"description\": \"Hand-authored candidate actions, not the result of an LLM. Evidence spans are generated against the explicit note fields.\"\n  },\n  \"proposals\": [\n    {\n      \"actionRef\": \"A1\",\n      \"confidence\": 0.98,\n      \"kind\": \"calendar\",\n      \"title\": \"Filter reminder\",\n      \"owner\": \"Alex Demo\",\n      \"start\": \"2030-05-06T09:00:00-07:00\",\n      \"end\": \"2030-05-06T09:30:00-07:00\",\n      \"recipient\": null,\n      \"details\": \"Replace the hallway air filter.\",\n      \"evidence\": {\n        \"kind\": {\n          \"start\": 78,\n          \"end\": 92,\n          \"quote\": \"Kind: calendar\"\n        },\n        \"title\": {\n          \"start\": 93,\n          \"end\": 115,\n          \"quote\": \"Title: Filter reminder\"\n        },\n        \"owner\": {\n          \"start\": 116,\n          \"end\": 132,\n          \"quote\": \"Owner: Alex Demo\"\n        },\n        \"start\": {\n          \"start\": 133,\n          \"end\": 165,\n          \"quote\": \"Start: 2030-05-06T09:00:00-07:00\"\n        },\n        \"end\": {\n          \"start\": 166,\n          \"end\": 196,\n          \"quote\": \"End: 2030-05-06T09:30:00-07:00\"\n        },\n        \"recipient\": {\n          \"start\": 197,\n          \"end\": 215,\n          \"quote\": \"Recipient: unknown\"\n        },\n        \"details\": {\n          \"start\": 216,\n          \"end\": 256,\n          \"quote\": \"Details: Replace the hallway air filter.\"\n        }\n      }\n    },\n    {\n      \"actionRef\": \"A2\",\n      \"confidence\": 0.97,\n      \"kind\": \"email_draft\",\n      \"title\": \"Ask about repair availability\",\n      \"owner\": \"Sam Demo\",\n      \"start\": null,\n      \"end\": null,\n      \"recipient\": \"maintenance@example.invalid\",\n      \"details\": \"Please confirm whether Tuesday afternoon is available for a routine visit.\",\n      \"evidence\": {\n        \"kind\": {\n          \"start\": 268,\n          \"end\": 285,\n          \"quote\": \"Kind: email_draft\"\n        },\n        \"title\": {\n          \"start\": 286,\n          \"end\": 322,\n          \"quote\": \"Title: Ask about repair availability\"\n        },\n        \"owner\": {\n          \"start\": 323,\n          \"end\": 338,\n          \"quote\": \"Owner: Sam Demo\"\n        },\n        \"start\": {\n          \"start\": 339,\n          \"end\": 353,\n          \"quote\": \"Start: unknown\"\n        },\n        \"end\": {\n          \"start\": 354,\n          \"end\": 366,\n          \"quote\": \"End: unknown\"\n        },\n        \"recipient\": {\n          \"start\": 367,\n          \"end\": 405,\n          \"quote\": \"Recipient: maintenance@example.invalid\"\n        },\n        \"details\": {\n          \"start\": 406,\n          \"end\": 489,\n          \"quote\": \"Details: Please confirm whether Tuesday afternoon is available for a routine visit.\"\n        }\n      }\n    },\n    {\n      \"actionRef\": \"A3\",\n      \"confidence\": 0.58,\n      \"kind\": \"calendar\",\n      \"title\": \"Review storage boxes\",\n      \"owner\": null,\n      \"start\": null,\n      \"end\": null,\n      \"recipient\": null,\n      \"details\": \"Review the boxes in storage. The owner and date were not decided.\",\n      \"evidence\": {\n        \"kind\": {\n          \"start\": 501,\n          \"end\": 515,\n          \"quote\": \"Kind: calendar\"\n        },\n        \"title\": {\n          \"start\": 516,\n          \"end\": 543,\n          \"quote\": \"Title: Review storage boxes\"\n        },\n        \"owner\": {\n          \"start\": 544,\n          \"end\": 558,\n          \"quote\": \"Owner: unknown\"\n        },\n        \"start\": {\n          \"start\": 559,\n          \"end\": 573,\n          \"quote\": \"Start: unknown\"\n        },\n        \"end\": {\n          \"start\": 574,\n          \"end\": 586,\n          \"quote\": \"End: unknown\"\n        },\n        \"recipient\": {\n          \"start\": 587,\n          \"end\": 605,\n          \"quote\": \"Recipient: unknown\"\n        },\n        \"details\": {\n          \"start\": 606,\n          \"end\": 680,\n          \"quote\": \"Details: Review the boxes in storage. The owner and date were not decided.\"\n        }\n      }\n    },\n    {\n      \"actionRef\": \"A1\",\n      \"confidence\": 0.98,\n      \"kind\": \"calendar\",\n      \"title\": \"Filter reminder\",\n      \"owner\": \"Alex Demo\",\n      \"start\": \"2030-05-06T09:00:00-07:00\",\n      \"end\": \"2030-05-06T09:30:00-07:00\",\n      \"recipient\": null,\n      \"details\": \"Replace the hallway air filter.\",\n      \"evidence\": {\n        \"kind\": {\n          \"start\": 78,\n          \"end\": 92,\n          \"quote\": \"Kind: calendar\"\n        },\n        \"title\": {\n          \"start\": 93,\n          \"end\": 115,\n          \"quote\": \"Title: Filter reminder\"\n        },\n        \"owner\": {\n          \"start\": 116,\n          \"end\": 132,\n          \"quote\": \"Owner: Alex Demo\"\n        },\n        \"start\": {\n          \"start\": 133,\n          \"end\": 165,\n          \"quote\": \"Start: 2030-05-06T09:00:00-07:00\"\n        },\n        \"end\": {\n          \"start\": 166,\n          \"end\": 196,\n          \"quote\": \"End: 2030-05-06T09:30:00-07:00\"\n        },\n        \"recipient\": {\n          \"start\": 197,\n          \"end\": 215,\n          \"quote\": \"Recipient: unknown\"\n        },\n        \"details\": {\n          \"start\": 216,\n          \"end\": 256,\n          \"quote\": \"Details: Replace the hallway air filter.\"\n        }\n      }\n    }\n  ]\n}}];"
      }
    },
    {
      "id": "evidence-validation",
      "name": "Validate exact evidence + retain unknowns",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        550,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "/** Pure, dependency-free validator. Also embedded verbatim in the n8n Code node. */\nconst FIELD_LABELS = {\n  kind: 'Kind', title: 'Title', owner: 'Owner', start: 'Start', end: 'End', recipient: 'Recipient', details: 'Details',\n};\n\nfunction readSourceBlocks(note) {\n  if (!note || note.synthetic !== true || !/^[a-z0-9-]{1,80}$/.test(note.id) || !Number.isSafeInteger(note.revision) || note.revision < 1) throw new Error('Invalid synthetic note identity');\n  if (!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$/.test(note.capturedAt) || !isExplicitTimestamp(note.capturedAt)) throw new Error('Invalid capturedAt');\n  if (typeof note.text !== 'string' || note.text.length > 20000 || note.text.includes('\\r')) throw new Error('Invalid source text');\n  const matches = [...note.text.matchAll(/^Action (A[1-9]\\d*)\\n/gm)];\n  if (!matches.length || matches.length > 50) throw new Error('Expected 1-50 explicit source action blocks');\n  const blocks = {};\n  for (let index = 0; index < matches.length; index++) {\n    const match = matches[index];\n    const ref = match[1];\n    if (blocks[ref]) throw new Error('Duplicate source action reference');\n    const start = match.index + match[0].length;\n    const end = matches[index + 1]?.index ?? note.text.length;\n    const block = note.text.slice(start, end).trimEnd();\n    const lines = block.split('\\n');\n    const expected = Object.entries(FIELD_LABELS);\n    if (lines.length !== expected.length) throw new Error('Each block must contain exactly the supported explicit fields');\n    let cursor = start;\n    const fields = {};\n    for (let fieldIndex = 0; fieldIndex < expected.length; fieldIndex++) {\n      const [field, label] = expected[fieldIndex];\n      const line = lines[fieldIndex];\n      const prefix = label + ': ';\n      if (!line.startsWith(prefix) || line.length <= prefix.length || /[\\u0000-\\u0008\\u000b-\\u001f\\u007f]/.test(line)) throw new Error('Invalid source field: ' + field);\n      const value = line.slice(prefix.length);\n      fields[field] = {value: value === 'unknown' ? null : value, evidence: {start: cursor, end: cursor + line.length, quote: line}};\n      cursor += line.length + 1;\n    }\n    blocks[ref] = fields;\n  }\n  return blocks;\n}\n\nfunction isExplicitTimestamp(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})$/.test(value)) return false;\n  const date = value.slice(0, 10);\n  const day = new Date(date + 'T00:00:00Z');\n  if (!Number.isFinite(+day) || day.toISOString().slice(0, 10) !== date) return false;\n  const [, hour, minute, second] = value.match(/T(\\d{2}):(\\d{2}):(\\d{2})/);\n  const offset = value.match(/([+-])(\\d{2}):(\\d{2})$/);\n  if (+hour > 23 || +minute > 59 || +second > 59 || (offset && (+offset[2] > 14 || +offset[3] > 59 || (+offset[2] === 14 && +offset[3] !== 0)))) return false;\n  return Number.isFinite(Date.parse(value));\n}\n\n// Fingerprint raw claims before rejecting invalid variants. Sorting keys makes\n// equivalent JSON objects identical without discarding unsupported fields.\nfunction canonicalClaim(value) {\n  if (Array.isArray(value)) return '[' + value.map(canonicalClaim).join(',') + ']';\n  if (value !== null && typeof value === 'object') return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonicalClaim(value[key])).join(',') + '}';\n  return JSON.stringify(value);\n}\n\nfunction validateEnvelope(envelope) {\n  if (!envelope || envelope.extraction?.mode !== 'provided-structured-synthetic-fixture' || envelope.extraction.aiExecuted !== false) throw new Error('This demo requires a disclosed synthetic fixture; no AI extraction was executed');\n  const blocks = readSourceBlocks(envelope.note);\n  const proposals = envelope.proposals;\n  if (!Array.isArray(proposals) || proposals.length > 100) throw new Error('Expected at most 100 proposals');\n  const claimsByIdentity = new Map();\n  for (const proposal of proposals) {\n    const reference = proposal?.actionRef;\n    if (typeof reference !== 'string' || !Object.hasOwn(blocks, reference)) continue;\n    const fingerprint = canonicalClaim(proposal);\n    if (claimsByIdentity.has(reference) && claimsByIdentity.get(reference) !== fingerprint) {\n      throw new Error('Conflicting duplicate proposal: ' + ['preview-v1', envelope.note.id, envelope.note.revision, reference].join(':'));\n    }\n    claimsByIdentity.set(reference, fingerprint);\n  }\n  const actions = [];\n  const rejected = [];\n  const seen = new Map();\n  let duplicatesCollapsed = 0;\n  for (const proposal of proposals) {\n    const errors = [];\n    const fields = blocks[proposal?.actionRef];\n    const allowed = ['actionRef', 'confidence', 'evidence', ...Object.keys(FIELD_LABELS)].sort();\n    if (!proposal || JSON.stringify(Object.keys(proposal).sort()) !== JSON.stringify(allowed)) errors.push('unsupported_or_missing_fields');\n    if (!fields) errors.push('unknown_action_reference');\n    if (typeof proposal?.confidence !== 'number' || !Number.isFinite(proposal.confidence) || proposal.confidence < 0 || proposal.confidence > 1) errors.push('invalid_confidence');\n    if (fields) {\n      for (const field of Object.keys(FIELD_LABELS)) {\n        const expected = fields[field];\n        const evidence = proposal.evidence?.[field];\n        if (proposal[field] !== expected.value) errors.push('unsupported_' + field);\n        if (!evidence || Object.keys(evidence).sort().join(',') !== 'end,quote,start' || evidence.start !== expected.evidence.start || evidence.end !== expected.evidence.end || evidence.quote !== expected.evidence.quote || envelope.note.text.slice(evidence.start, evidence.end) !== evidence.quote) errors.push('invalid_evidence_' + field);\n      }\n      if (!proposal.evidence || Object.keys(proposal.evidence).sort().join(',') !== Object.keys(FIELD_LABELS).sort().join(',')) errors.push('unsupported_evidence_fields');\n    }\n    if (!['calendar', 'email_draft'].includes(proposal?.kind)) errors.push('unsupported_kind');\n    for (const field of ['title', 'details']) if (typeof proposal?.[field] !== 'string' || !proposal[field].trim() || proposal[field].length > 500) errors.push('invalid_' + field);\n    if (proposal?.owner !== null && (typeof proposal?.owner !== 'string' || !proposal.owner.trim() || proposal.owner.length > 100)) errors.push('invalid_owner');\n    if (proposal?.recipient !== null && (typeof proposal?.recipient !== 'string' || !/^[a-zA-Z0-9._+-]+@example\\.invalid$/.test(proposal.recipient))) errors.push('recipient_must_use_reserved_demo_domain');\n    for (const field of ['start', 'end']) if (proposal?.[field] !== null && !isExplicitTimestamp(proposal[field])) errors.push('invalid_' + field);\n    if (proposal?.start && proposal?.end && Date.parse(proposal.end) <= Date.parse(proposal.start)) errors.push('end_must_follow_start');\n    if (errors.length) { rejected.push({actionRef: proposal?.actionRef ?? null, errors: [...new Set(errors)].sort()}); continue; }\n    const id = ['preview-v1', envelope.note.id, String(envelope.note.revision), proposal.actionRef].join(':');\n    const normalized = {id, actionRef: proposal.actionRef, ...Object.fromEntries(Object.keys(FIELD_LABELS).map(field => [field, proposal[field]])), confidence: proposal.confidence, evidence: Object.fromEntries(Object.keys(FIELD_LABELS).map(field => [field, fields[field].evidence]))};\n    const serialized = JSON.stringify(normalized);\n    if (seen.has(id)) {\n      if (seen.get(id) === serialized) duplicatesCollapsed++;\n      else throw new Error('Conflicting duplicate proposal: ' + id);\n      continue;\n    }\n    seen.set(id, serialized);\n    const clarification = [];\n    if (normalized.confidence < .9) clarification.push('confidence_below_0.90');\n    if (normalized.owner === null) clarification.push('owner_unknown');\n    if (normalized.kind === 'calendar') {\n      if (normalized.start === null) clarification.push('start_unknown');\n      if (normalized.end === null) clarification.push('end_unknown');\n    }\n    if (normalized.kind === 'email_draft' && normalized.recipient === null) clarification.push('recipient_unknown');\n    actions.push({...normalized, status: clarification.length ? 'needs_clarification' : 'ready_for_human_review', clarification});\n  }\n  actions.sort((a, b) => a.id.localeCompare(b.id));\n  return {demo: true, aiExecuted: false, extractionMode: envelope.extraction.mode, note: envelope.note, humanApproval: {required: true, approved: false, deliveryEnabled: false}, actions, rejected, duplicatesCollapsed};\n}\n\nreturn [{json: validateEnvelope($input.first().json)}];"
      }
    },
    {
      "id": "prepare-previews",
      "name": "Prepare unsent EML + ICS + action preview",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        850,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "/** Pure preview formatter. No delivery, external calls, or provider draft creation. */\nfunction escapeIcs(value) {\n  return value.replace(/\\\\/g, '\\\\\\\\').replace(/\\n/g, '\\\\n').replace(/;/g, '\\\\;').replace(/,/g, '\\\\,');\n}\n\nfunction foldIcs(line) {\n  const chunks = [];\n  let current = '';\n  let length = 0;\n  // Limit to 73 UTF-8 bytes, allowing one continuation space.\n  for (const character of line) {\n    const code = character.codePointAt(0);\n    const bytes = code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;\n    if (length + bytes > 73) { chunks.push(current); current = ' '; length = 1; }\n    current += character; length += bytes;\n  }\n  chunks.push(current);\n  return chunks.join('\\r\\n');\n}\n\nfunction prepareValidated(validated) {\n  if (validated?.humanApproval?.required !== true || validated.humanApproval.approved !== false || validated.humanApproval.deliveryEnabled !== false || validated.aiExecuted !== false) throw new Error('Human review boundary was changed');\n  const formatDate = value => new Date(value).toISOString().replace(/[-:]/g, '').replace('.000', '');\n  const artifacts = [];\n  for (const action of validated.actions) {\n    if (action.status !== 'ready_for_human_review') continue;\n    if (action.kind === 'calendar') {\n      const lines = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Civico Due//Synthetic review demo//EN', 'CALSCALE:GREGORIAN', 'BEGIN:VEVENT', 'UID:' + action.id + '@example.invalid', 'DTSTAMP:' + formatDate(validated.note.capturedAt), 'DTSTART:' + formatDate(action.start), 'DTEND:' + formatDate(action.end), 'SUMMARY:' + escapeIcs('[REVIEW] ' + action.title), 'DESCRIPTION:' + escapeIcs(action.details + '\\nOwner: ' + action.owner + '\\nSynthetic preview. Human approval required.'), 'STATUS:TENTATIVE', 'TRANSP:TRANSPARENT', 'END:VEVENT', 'END:VCALENDAR'];\n      artifacts.push({proposalId: action.id, filename: action.actionRef + '-calendar-preview.ics', mediaType: 'text/calendar', content: lines.map(foldIcs).join('\\r\\n') + '\\r\\n'});\n    } else {\n      const lines = ['To: ' + action.recipient, 'Subject: [DRAFT - HUMAN REVIEW] ' + action.title, 'MIME-Version: 1.0', 'Content-Type: text/plain; charset=UTF-8', 'Content-Transfer-Encoding: 8bit', 'X-Unsent: 1', 'X-Demo-Only: true', 'X-Human-Approval-Required: true', '', 'Hello,', '', action.details, '', 'Draft owner: ' + action.owner, '', 'Synthetic preview only. Review the content and recipient before any use.'];\n      artifacts.push({proposalId: action.id, filename: action.actionRef + '-email-preview.eml', mediaType: 'message/rfc822', content: lines.join('\\r\\n') + '\\r\\n'});\n    }\n  }\n  return {...validated, summary: {uniqueActions: validated.actions.length, readyForReview: validated.actions.filter(a => a.status === 'ready_for_human_review').length, needsClarification: validated.actions.filter(a => a.status === 'needs_clarification').length, rejected: validated.rejected.length, duplicatesCollapsed: validated.duplicatesCollapsed, artifacts: artifacts.length}, artifacts};\n}\n\nreturn [{json: prepareValidated($input.first().json)}];"
      }
    },
    {
      "id": "human-boundary",
      "name": "STOP - Human approval required (no delivery)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1160,
        0
      ],
      "notesInFlow": true,
      "notes": "Terminal preview only. No email, calendar, Notion, HTTP or provider draft writes exist in this workflow. approved=false and deliveryEnabled=false. Inspect the final JSON artifacts before any manual use.",
      "parameters": {}
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Synthetic note + provided extraction (NOT AI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthetic note + provided extraction (NOT AI)": {
      "main": [
        [
          {
            "node": "Validate exact evidence + retain unknowns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate exact evidence + retain unknowns": {
      "main": [
        [
          {
            "node": "Prepare unsent EML + ICS + action preview",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare unsent EML + ICS + action preview": {
      "main": [
        [
          {
            "node": "STOP - Human approval required (no delivery)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "description": "Evidence-bound synthetic proof. Prepared artifacts are strings in the output; no LLM or external integration is executed."
  }
}
