{
  "name": "Cobalt — synthetic household notes review and local drafts",
  "active": false,
  "nodes": [
    {
      "parameters": {},
      "id": "manual",
      "name": "Manual start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        260,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{ json: {\n  \"notes\": {\n    \"meeting_id\": \"synthetic-household-001\",\n    \"data_policy\": \"local_only\",\n    \"notes\": \"SYNTHETIC household planning notes. No real family information.\\nThe group discussed the next planning session and a draft message.\\nACTION | calendar | A1 | Weekly planning | 2026-10-21T18:00:00-07:00 | 30 | Review the household checklist together.\\nACTION | email | A2 | coordinator@example.invalid | Planning question | Hello, this is a synthetic draft asking which materials to prepare.\\nACTION | calendar | A3 | Reading time | 2026-10-22T17:30:00-07:00 | 20 | Choose a book together.\\nThis free-text line remains a note; it does not authorize any action.\"\n  },\n  \"approval\": null,\n  \"previous_state\": {\n    \"schema_version\": 1,\n    \"records\": {}\n  }\n} }];"
      },
      "id": "synthetic",
      "name": "Synthetic input — approval empty",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "'use strict';\n\n// Dependency-free domain logic. A SHA-256 function is injected by the host.\n// Inputs are data; no note text is evaluated, sent, or used as a command.\nfunction createHouseholdEngine(sha256) {\n  const fail = (message) => { throw new Error(message); };\n  const text = (value, label, max = 5000) => {\n    if (typeof value !== 'string' || !value.trim() || value.length > max) fail(`Invalid ${label}`);\n    return value.trim();\n  };\n  const plain = (value) => value && typeof value === 'object' && !Array.isArray(value);\n  const clone = (value) => JSON.parse(JSON.stringify(value));\n\n  function prepare(input) {\n    if (!plain(input)) fail('Input must be an object');\n    const meetingId = text(input.meeting_id, 'meeting_id', 120);\n    if (!/^[a-zA-Z0-9_-]+$/.test(meetingId)) fail('meeting_id uses unsupported characters');\n    if (input.data_policy !== 'local_only') fail('This demo accepts local_only data policy only');\n    const notes = text(input.notes, 'notes', 50000).replace(/\\r\\n/g, '\\n');\n    const actions = [];\n    const ids = new Set();\n    for (const [offset, line] of notes.split('\\n').entries()) {\n      if (!/^ACTION\\s*\\|/.test(line.trim())) continue;\n      const parts = line.split('|').map((part) => part.trim());\n      const [marker, kind, id] = parts;\n      if (marker !== 'ACTION' || !['calendar', 'email'].includes(kind)) fail(`Malformed action at line ${offset + 1}`);\n      if (!/^[A-Za-z0-9_-]{1,60}$/.test(id || '') || ids.has(id)) fail(`Invalid or duplicate action ID at line ${offset + 1}`);\n      ids.add(id);\n      const common = { id, kind, source_line: offset + 1 };\n      if (kind === 'calendar') {\n        if (parts.length !== 7) fail(`Calendar action needs seven fields at line ${offset + 1}`);\n        const [, , , title, startsAt, minutes, description] = parts;\n        if (!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})$/.test(startsAt) || !Number.isFinite(Date.parse(startsAt))) fail('Calendar time requires an explicit valid UTC offset');\n        const localClock = startsAt.slice(0, 19);\n        if (new Date(localClock + 'Z').toISOString().slice(0, 19) !== localClock) fail('Calendar date is not a real date');\n        if (!/^\\d+$/.test(minutes) || Number(minutes) < 1 || Number(minutes) > 1440) fail('Calendar duration must be 1 to 1440 minutes');\n        actions.push({ ...common, title: text(title, 'calendar title', 200), starts_at: startsAt, duration_minutes: Number(minutes), description: text(description, 'calendar description') });\n      } else {\n        if (parts.length !== 6) fail(`Email action needs six fields at line ${offset + 1}`);\n        const [, , , to, subject, body] = parts;\n        // Fictional recipients only. This artifact cannot be pointed at a real address.\n        if (!/^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\.invalid$/.test(to)) fail('Demo email recipient must use the reserved .invalid domain');\n        if (/[\\r\\n]/.test(subject)) fail('Email subject may not contain newlines');\n        actions.push({ ...common, to, subject: text(subject, 'subject', 200), body: text(body, 'body') });\n      }\n    }\n    if (!actions.length) fail('No structured action lines found; review notes manually');\n    const revision = sha256(JSON.stringify({ meeting_id: meetingId, data_policy: input.data_policy, notes }));\n    return { meeting_id: meetingId, revision, data_policy: 'local_only', status: 'needs_review', actions, external_actions: 0 };\n  }\n\n  function apply(input, approval, previousState = { schema_version: 1, records: {} }) {\n    const review = prepare(input);\n    if (!plain(previousState) || previousState.schema_version !== 1 || !plain(previousState.records)) fail('Invalid state; do not replace a damaged ledger with an empty one');\n    for (const [key, record] of Object.entries(previousState.records)) {\n      if (!/^[a-f0-9]{64}$/.test(key) || !plain(record) || record.draft_id !== key || record.status !== 'local_draft_only' || !/^[a-zA-Z0-9_-]{1,120}$/.test(record.meeting_id || '') || !/^[a-f0-9]{64}$/.test(record.revision || '') || !/^[A-Za-z0-9_-]{1,60}$/.test(record.action_id || '') || !['calendar_suggestion', 'email_draft'].includes(record.type)) fail('Invalid ledger entry');\n      const kind = record.type === 'calendar_suggestion' ? 'calendar' : 'email';\n      if (key !== sha256(JSON.stringify([record.meeting_id, record.revision, record.action_id, kind]))) fail('Invalid ledger identity');\n      if (kind === 'calendar') {\n        text(record.title, 'stored calendar title', 200);\n        text(record.description, 'stored calendar description');\n        if (!Number.isFinite(Date.parse(record.starts_at)) || !Number.isFinite(Date.parse(record.ends_at_utc))) fail('Invalid stored calendar time');\n      } else {\n        if (typeof record.to !== 'string' || !/^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\.invalid$/.test(record.to)) fail('Invalid stored demo recipient');\n        text(record.subject, 'stored subject', 200);\n        text(record.body, 'stored body');\n      }\n    }\n    const state = clone(previousState);\n    if (approval === null || approval === undefined) return { ...review, new_drafts: [], skipped: [], state };\n    if (!plain(approval) || approval.meeting_id !== review.meeting_id || approval.approved_revision !== review.revision) fail('Approval does not match this exact meeting revision');\n    text(approval.reviewed_by, 'reviewed_by', 100);\n    if (!Array.isArray(approval.approved_action_ids) || approval.approved_action_ids.some((id) => typeof id !== 'string')) fail('approved_action_ids must be a list of IDs');\n    const approved = new Set(approval.approved_action_ids);\n    if (approved.size !== approval.approved_action_ids.length) fail('Duplicate approval IDs are not allowed');\n    const known = new Set(review.actions.map((action) => action.id));\n    for (const id of approved) if (!known.has(id)) fail(`Approval names an unknown action: ${id}`);\n    const newDrafts = [];\n    const skipped = [];\n    for (const action of review.actions) {\n      if (!approved.has(action.id)) continue;\n      const key = sha256(JSON.stringify([review.meeting_id, review.revision, action.id, action.kind]));\n      const draft = action.kind === 'calendar'\n        ? { type: 'calendar_suggestion', title: action.title, starts_at: action.starts_at, ends_at_utc: new Date(Date.parse(action.starts_at) + action.duration_minutes * 60000).toISOString(), description: action.description }\n        : { type: 'email_draft', to: action.to, subject: action.subject, body: action.body };\n      const record = { draft_id: key, meeting_id: review.meeting_id, revision: review.revision, action_id: action.id, status: 'local_draft_only', ...draft };\n      if (Object.hasOwn(state.records, key)) {\n        if (JSON.stringify(state.records[key]) !== JSON.stringify(record)) fail('Ledger conflict for existing draft; stop and inspect state');\n        skipped.push({ action_id: action.id, draft_id: key, reason: 'already_drafted' });\n      } else {\n        state.records[key] = record;\n        newDrafts.push(record);\n      }\n    }\n    const draftedIds = new Set(Object.values(state.records).filter((record) => record.meeting_id === review.meeting_id && record.revision === review.revision).map((record) => record.action_id));\n    return { meeting_id: review.meeting_id, revision: review.revision, data_policy: 'local_only', status: 'review_applied', new_drafts: newDrafts, skipped, remaining_review_ids: review.actions.filter((action) => !draftedIds.has(action.id)).map((action) => action.id), state, external_actions: 0 };\n  }\n  return { prepare, apply };\n}\n\n\n\nconst crypto = require('crypto');\nconst engine = createHouseholdEngine((value) => crypto.createHash('sha256').update(value).digest('hex'));\nconst payload = $input.first().json;\nreturn [{ json: engine.apply(payload.notes, payload.approval, payload.previous_state) }];\n"
      },
      "id": "prepare",
      "name": "Review gate and draft objects",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        760,
        300
      ]
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Synthetic input — approval empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthetic input — approval empty": {
      "main": [
        [
          {
            "node": "Review gate and draft objects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}
