{
  "error": {
    "message": "{\"error\":{\"message\":\"{\\n  \\\"error\\\": {\\n    \\\"code\\\": 403,\\n    \\\"message\\\": \\\"Your API key was reported as leaked. Please use another API key.\\\",\\n    \\\"status\\\": \\\"PERMISSION_DENIED\\\"\\n  }\\n}\\n\",\"code\":403,\"status\":\"Forbidden\"}}",
    "stack": "ApiError: {\"error\":{\"message\":\"{\\n  \\\"error\\\": {\\n    \\\"code\\\": 403,\\n    \\\"message\\\": \\\"Your API key was reported as leaked. Please use another API key.\\\",\\n    \\\"status\\\": \\\"PERMISSION_DENIED\\\"\\n  }\\n}\\n\",\"code\":403,\"status\":\"Forbidden\"}}\n    at throwErrorIfNotOK (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:36178:24)\n    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:35941:7\n    at async Models.generateContentStream (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-67TFD6HC.js:37037:16)\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:278827:19\n    at async file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:255732:23\n    at async retryWithBackoff (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:275704:23)\n    at async GeminiChat.makeApiCallAndProcessStream (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311287:28)\n    at async GeminiChat.streamWithRetries (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311125:29)\n    at async Turn.run (file:///home/claude/.npm-global/lib/node_modules/@google/gemini-cli/bundle/chunk-UQGLVPZQ.js:311618:24)"
  },
  "context": [
    {
      "role": "user",
      "parts": [
        {
          "text": "<session_context>\nThis is the Gemini CLI. We are setting up the context for our chat.\nToday's date is Wednesday, June 24, 2026 (formatted according to the user's locale).\nMy operating system is: linux\nThe project's temporary directory is: /home/claude/.gemini/tmp/pr-review-j7umes\n- **Workspace Directories:**\n  - /tmp/pr-review-J7uMES\n- **Directory Structure:**\n\nShowing up to 200 items (files + folders).\n\n/tmp/pr-review-J7uMES/\n\n\n\n</session_context>"
        }
      ]
    },
    {
      "role": "user",
      "parts": [
        {
          "text": "You are a dedicated code reviewer for this repository. You are deeply familiar with its conventions and codebase.\n\n## Review Task\n\nPR #74: \"fix: guard approve-shifts write plan apply\"\n\nDescription:\n## Summary\n- Rebuild the expected approve-shifts write plan from `amd-counts.json` and `rippling-inspection.json` during `apply`.\n- Refuse stale or inconsistent `write-plan.json` before live Rippling write confirmation can proceed.\n- Add tests for stale-plan detection and the CLI apply refusal path.\n\n## Validation\n- `/Users/Work/.bun/bin/bun test tools/rippling-mcp/shift-approval.test.ts`\n- `/Users/Work/.bun/bin/bun test` from `tools/rippling-mcp`\n\n## Notes\n- Reviewed PR refs #71/#73; both are Rippling token guard/removal work and this change stays separate.\n\nReview the diff below. Provide your response as a JSON object with these keys:\n- \"summary\": a concise markdown summary. Tag issues with severity: [blocking] for must-fix, [suggestion] for improvements, [nitpick] for style.\n- \"comments\": array of { \"path\", \"line\", \"body\" } — inline review comments with severity tags\n- \"verdict\": \"approve\" (no blocking issues) or \"request_changes\" (blocking issues found)\n- \"workflow_suggestions\": array of strings — suggestions for improving the development workflow (e.g., \"Consider splitting this PR\", \"Add a pre-commit hook for X\")\n\nFocus on meaningful issues: bugs, security, performance, correctness. Skip trivial style nits.\nReturn ONLY valid JSON, no markdown fences.\n\nDiff:\n```diff\nFile: tools/rippling-mcp/approve-shifts.ts (modified, +9/-0)\n@@ -9,6 +9,7 @@ import {\n   readCheckpointFile,\n   readJsonFile,\n   renderRunReport,\n+  validateWritePlanAgainstCheckpoints,\n   verifyWritePlan,\n   writeRunArtifacts,\n   type AmdCountsCheckpoint,\n@@ -120,7 +121,15 @@ function apply(args: Args): void {\n   requireCheckpoint(runDir, \"amd-counts.json\");\n   requireCheckpoint(runDir, \"rippling-inspection.json\");\n   requireCheckpoint(runDir, \"write-plan.json\");\n+  const counts = readCheckpointFile<AmdCountsCheckpoint>(join(runDir, \"amd-counts.json\"));\n+  const inspection = readCheckpointFile<RipplingInspection>(join(runDir, \"rippling-inspection.json\"));\n   const plan = readCheckpointFile<WritePlan>(join(runDir, \"write-plan.json\"));\n+  const checkpointIssues = validateWritePlanAgainstCheckpoints(plan, counts, inspection);\n+  if (checkpointIssues.length > 0) {\n+    throw new Error(\n+      `refusing to apply inconsistent write plan: ${checkpointIssues.map((issue) => issue.code).join(\", \")}`\n+    );\n+  }\n   if (plan.blockingIssues.length > 0 || plan.summary.needsHumanReview > 0) {\n     throw new Error(\"refusing to apply a plan with blocking issues or human-review rows\");\n   }\n\nFile: tools/rippling-mcp/shift-approval.test.ts (modified, +71/-0)\n@@ -1,9 +1,14 @@\n import { describe, expect, test } from \"bun:test\";\n+import { mkdtempSync, rmSync } from \"fs\";\n+import { tmpdir } from \"os\";\n+import { join } from \"path\";\n import {\n   buildAmdCounts,\n   buildWritePlan,\n   renderRunReport,\n+  validateWritePlanAgainstCheckpoints,\n   verifyWritePlan,\n+  writeRunArtifacts,\n   type AmdVisitInput,\n   type RipplingInspection,\n   type VerificationCheckpoint,\n@@ -198,6 +203,72 @@ describe(\"shift approval checkpoints\", () => {\n \n     expect(verifyWritePlan(plan, verification).mismatches).toHaveLength(2);\n   });\n+\n+  test(\"detects stale write plans before apply\", () => {\n+    const counts = buildAmdCounts([visit(\"1\", \"2026-05-24\", \"SERAH MAYODI\", 6611, 3)], period, cfg, \"fixture\");\n+    const ripplingInspection = inspection([\n+      {\n+        rowId: \"se-2026-05-24\",\n+        providerEmail: counts.counts[0].providerEmail,\n+        date: \"2026-05-24\",\n+        approved: false,\n+        paid: false,\n+        shiftInputs: {},\n+      },\n+    ]);\n+    const stalePlan = buildWritePlan(counts, ripplingInspection);\n+    expect(stalePlan.actions).toHaveLength(1);\n+    stalePlan.actions[0].expectedInputs.existingPatientAppointments = 2;\n+\n+    expect(validateWritePlanAgainstCheckpoints(stalePlan, counts, ripplingInspection)).toEqual([\n+      {\n+        code: \"write_plan_checkpoint_mismatch\",\n+        message: \"write-plan.json is stale or inconsistent with the current AMD counts and Rippling inspection.\",\n+      },\n+    ]);\n+  });\n+\n+  test(\"apply refuses an inconsistent write-plan checkpoint before the live driver\", () => {\n+    const runDir = mkdtempSync(join(tmpdir(), \"approve-shifts-stale-plan-\"));\n+    try {\n+      const counts = buildAmdCounts([visit(\"1\", \"2026-05-24\", \"SERAH MAYODI\", 6611, 3)], period, cfg, \"fixture\");\n+      const ripplingInspection = inspection([\n+        {\n+          rowId: \"se-2026-05-24\",\n+          providerEmail: counts.counts[0].providerEmail,\n+          date: \"2026-05-24\",\n+          approved: false,\n+          paid: false,\n+          shiftInputs: {},\n+        },\n+      ]);\n+      const stalePlan = buildWritePlan(counts, ripplingInspection);\n+      expect(stalePlan.actions).toHaveLength(1);\n+      stalePlan.actions[0].expectedInputs.existingPatientAppointments = 2;\n+      writeRunArtifacts(runDir, {\n+        amdCounts: counts,\n+        ripplingInspection,\n+        writePlan: stalePlan,\n+      });\n+\n+      const result = Bun.spawnSync([\n+        process.execPath,\n+        new URL(\"./approve-shifts.ts\", import.meta.url).pathname,\n+        \"apply\",\n+        \"--run-dir\",\n+        runDir,\n+        \"--confirm-live\",\n+      ]);\n+      const stderr = new TextDecoder().decode(result.stderr);\n+\n+      expect(result.exitCode).toBe(1);\n+      expect(stderr).toContain(\"refusing to apply inconsistent write plan\");\n+      expect(stderr).toContain(\"write_plan_checkpoint_mismatch\");\n+      expect(stderr).not.toContain(\"Rippling browser write driver is not connected yet\");\n+    } finally {\n+      rmSync(runDir, { recursive: true, force: true });\n+    }\n+  });\n });\n \n function visit(\n\nFile: tools/rippling-mcp/shift-approval.ts (modified, +70/-8)\n@@ -165,6 +165,11 @@ export interface VerificationMismatch {\n   reason: string;\n }\n \n+export interface WritePlanCheckpointIssue {\n+  code: string;\n+  message: string;\n+}\n+\n const ZERO_INPUTS: EncounterInputs = {\n   newPatientAppointments: 0,\n   existingPatientAppointments: 0,\n@@ -302,14 +307,16 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n   }\n \n   const actions: WritePlanAction[] = counts.counts.map((count) => {\n+    // Break aliasing so plan mutations cannot corrupt source counts.\n+    const expectedInputs = { ...count.inputs };\n     const row = rowsByProviderDate.get(`${count.providerEmail.toLowerCase()}|${count.date}`);\n     if (!row) {\n       return {\n         action: \"needs_human_review\",\n         providerEmail: count.providerEmail,\n         providerName: count.providerName,\n         date: count.date,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         reason: \"No matching Rippling shift row exists; automation must not create a new time entry.\",\n       };\n     }\n@@ -325,7 +332,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n             providerName: count.providerName,\n             date: count.date,\n             rowId: row.rowId,\n-            expectedInputs: count.inputs,\n+            expectedInputs,\n             currentInputs,\n             approved: row.approved,\n             paid: row.paid,\n@@ -338,7 +345,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n           providerName: count.providerName,\n           date: count.date,\n           rowId: row.rowId,\n-          expectedInputs: count.inputs,\n+          expectedInputs,\n           currentInputs,\n           approved: row.approved,\n           paid: row.paid,\n@@ -351,7 +358,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -370,7 +377,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -388,7 +395,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -406,7 +413,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -420,7 +427,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n       providerName: count.providerName,\n       date: count.date,\n       rowId: row.rowId,\n-      expectedInputs: count.inputs,\n+      expectedInputs,\n       currentInputs,\n       approved: row.approved,\n       paid: row.paid,\n@@ -501,6 +508,36 @@ export function verifyWritePlan(plan: WritePlan, verification: VerificationCheck\n   };\n }\n \n+export function validateWritePlanAgainstCheckpoints(\n+  plan: WritePlan,\n+  counts: AmdCountsCheckpoint,\n+  inspection: RipplingInspection\n+): WritePlanCheckpointIssue[] {\n+  const issues: WritePlanCheckpointIssue[] = [];\n+  if (plan.period.start !== counts.period.start || plan.period.end !== counts.period.end) {\n+    issues.push({\n+      code: \"write_plan_amd_period_mismatch\",\n+      message: \"write-plan.json period does not match amd-counts.json.\",\n+    });\n+  }\n+  if (plan.period.start !== inspection.period.start || plan.period.end !== inspection.period.end) {\n+    issues.push({\n+      code: \"write_plan_rippling_period_mismatch\",\n+      message: \"write-plan.json period does not match rippling-inspection.json.\",\n+    });\n+  }\n+\n+  const expectedPlan = buildWritePlan(counts, inspection);\n+  if (!deepEqual(comparableWritePlan(plan), comparableWritePlan(expectedPlan))) {\n+    issues.push({\n+      code: \"write_plan_checkpoint_mismatch\",\n+      message: \"write-plan.json is stale or inconsistent with the current AMD counts and Rippling inspection.\",\n+    });\n+  }\n+\n+  return issues;\n+}\n+\n export function renderRunReport(args: {\n   counts: AmdCountsCheckpoint;\n   plan?: WritePlan;\n@@ -700,6 +737,31 @@ function summarizeActions(actions: WritePlanAction[]): WritePlan[\"summary\"] {\n   };\n }\n \n+function comparableWritePlan(plan: WritePlan): Omit<WritePlan, \"generatedAt\"> {\n+  const { generatedAt: _generatedAt, ...comparable } = plan;\n+  return comparable;\n+}\n+\n+function deepEqual(left: unknown, right: unknown): boolean {\n+  if (Object.is(left, right)) return true;\n+  if (typeof left !== typeof right) return false;\n+  if (left === null || right === null) return false;\n+  if (typeof left !== \"object\" || typeof right !== \"object\") return false;\n+\n+  if (Array.isArray(left) || Array.isArray(right)) {\n+    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;\n+    return left.every((item, index) => deepEqual(item, right[index]));\n+  }\n+\n+  const leftRecord = left as Record<string, unknown>;\n+  const rightRecord = right as Record<string, unknown>;\n+  const leftKeys = Object.keys(leftRecord);\n+  const rightKeys = Object.keys(rightRecord);\n+  if (leftKeys.length !== rightKeys.length) return false;\n+  return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key)\n+    && deepEqual(leftRecord[key], rightRecord[key]));\n+}\n+\n function compareProviderDate(a: DailyEncounterCount, b: DailyEncounterCount): number {\n   return a.providerName.localeCompare(b.providerName) || a.date.localeCompare(b.date);\n }\n```\n\n-"
        }
      ]
    },
    {
      "role": "user",
      "parts": [
        {
          "text": "You are a dedicated code reviewer for this repository. You are deeply familiar with its conventions and codebase.\n\n## Review Task\n\nPR #74: \"fix: guard approve-shifts write plan apply\"\n\nDescription:\n## Summary\n- Rebuild the expected approve-shifts write plan from `amd-counts.json` and `rippling-inspection.json` during `apply`.\n- Refuse stale or inconsistent `write-plan.json` before live Rippling write confirmation can proceed.\n- Add tests for stale-plan detection and the CLI apply refusal path.\n\n## Validation\n- `/Users/Work/.bun/bin/bun test tools/rippling-mcp/shift-approval.test.ts`\n- `/Users/Work/.bun/bin/bun test` from `tools/rippling-mcp`\n\n## Notes\n- Reviewed PR refs #71/#73; both are Rippling token guard/removal work and this change stays separate.\n\nReview the diff below. Provide your response as a JSON object with these keys:\n- \"summary\": a concise markdown summary. Tag issues with severity: [blocking] for must-fix, [suggestion] for improvements, [nitpick] for style.\n- \"comments\": array of { \"path\", \"line\", \"body\" } — inline review comments with severity tags\n- \"verdict\": \"approve\" (no blocking issues) or \"request_changes\" (blocking issues found)\n- \"workflow_suggestions\": array of strings — suggestions for improving the development workflow (e.g., \"Consider splitting this PR\", \"Add a pre-commit hook for X\")\n\nFocus on meaningful issues: bugs, security, performance, correctness. Skip trivial style nits.\nReturn ONLY valid JSON, no markdown fences.\n\nDiff:\n```diff\nFile: tools/rippling-mcp/approve-shifts.ts (modified, +9/-0)\n@@ -9,6 +9,7 @@ import {\n   readCheckpointFile,\n   readJsonFile,\n   renderRunReport,\n+  validateWritePlanAgainstCheckpoints,\n   verifyWritePlan,\n   writeRunArtifacts,\n   type AmdCountsCheckpoint,\n@@ -120,7 +121,15 @@ function apply(args: Args): void {\n   requireCheckpoint(runDir, \"amd-counts.json\");\n   requireCheckpoint(runDir, \"rippling-inspection.json\");\n   requireCheckpoint(runDir, \"write-plan.json\");\n+  const counts = readCheckpointFile<AmdCountsCheckpoint>(join(runDir, \"amd-counts.json\"));\n+  const inspection = readCheckpointFile<RipplingInspection>(join(runDir, \"rippling-inspection.json\"));\n   const plan = readCheckpointFile<WritePlan>(join(runDir, \"write-plan.json\"));\n+  const checkpointIssues = validateWritePlanAgainstCheckpoints(plan, counts, inspection);\n+  if (checkpointIssues.length > 0) {\n+    throw new Error(\n+      `refusing to apply inconsistent write plan: ${checkpointIssues.map((issue) => issue.code).join(\", \")}`\n+    );\n+  }\n   if (plan.blockingIssues.length > 0 || plan.summary.needsHumanReview > 0) {\n     throw new Error(\"refusing to apply a plan with blocking issues or human-review rows\");\n   }\n\nFile: tools/rippling-mcp/shift-approval.test.ts (modified, +71/-0)\n@@ -1,9 +1,14 @@\n import { describe, expect, test } from \"bun:test\";\n+import { mkdtempSync, rmSync } from \"fs\";\n+import { tmpdir } from \"os\";\n+import { join } from \"path\";\n import {\n   buildAmdCounts,\n   buildWritePlan,\n   renderRunReport,\n+  validateWritePlanAgainstCheckpoints,\n   verifyWritePlan,\n+  writeRunArtifacts,\n   type AmdVisitInput,\n   type RipplingInspection,\n   type VerificationCheckpoint,\n@@ -198,6 +203,72 @@ describe(\"shift approval checkpoints\", () => {\n \n     expect(verifyWritePlan(plan, verification).mismatches).toHaveLength(2);\n   });\n+\n+  test(\"detects stale write plans before apply\", () => {\n+    const counts = buildAmdCounts([visit(\"1\", \"2026-05-24\", \"SERAH MAYODI\", 6611, 3)], period, cfg, \"fixture\");\n+    const ripplingInspection = inspection([\n+      {\n+        rowId: \"se-2026-05-24\",\n+        providerEmail: counts.counts[0].providerEmail,\n+        date: \"2026-05-24\",\n+        approved: false,\n+        paid: false,\n+        shiftInputs: {},\n+      },\n+    ]);\n+    const stalePlan = buildWritePlan(counts, ripplingInspection);\n+    expect(stalePlan.actions).toHaveLength(1);\n+    stalePlan.actions[0].expectedInputs.existingPatientAppointments = 2;\n+\n+    expect(validateWritePlanAgainstCheckpoints(stalePlan, counts, ripplingInspection)).toEqual([\n+      {\n+        code: \"write_plan_checkpoint_mismatch\",\n+        message: \"write-plan.json is stale or inconsistent with the current AMD counts and Rippling inspection.\",\n+      },\n+    ]);\n+  });\n+\n+  test(\"apply refuses an inconsistent write-plan checkpoint before the live driver\", () => {\n+    const runDir = mkdtempSync(join(tmpdir(), \"approve-shifts-stale-plan-\"));\n+    try {\n+      const counts = buildAmdCounts([visit(\"1\", \"2026-05-24\", \"SERAH MAYODI\", 6611, 3)], period, cfg, \"fixture\");\n+      const ripplingInspection = inspection([\n+        {\n+          rowId: \"se-2026-05-24\",\n+          providerEmail: counts.counts[0].providerEmail,\n+          date: \"2026-05-24\",\n+          approved: false,\n+          paid: false,\n+          shiftInputs: {},\n+        },\n+      ]);\n+      const stalePlan = buildWritePlan(counts, ripplingInspection);\n+      expect(stalePlan.actions).toHaveLength(1);\n+      stalePlan.actions[0].expectedInputs.existingPatientAppointments = 2;\n+      writeRunArtifacts(runDir, {\n+        amdCounts: counts,\n+        ripplingInspection,\n+        writePlan: stalePlan,\n+      });\n+\n+      const result = Bun.spawnSync([\n+        process.execPath,\n+        new URL(\"./approve-shifts.ts\", import.meta.url).pathname,\n+        \"apply\",\n+        \"--run-dir\",\n+        runDir,\n+        \"--confirm-live\",\n+      ]);\n+      const stderr = new TextDecoder().decode(result.stderr);\n+\n+      expect(result.exitCode).toBe(1);\n+      expect(stderr).toContain(\"refusing to apply inconsistent write plan\");\n+      expect(stderr).toContain(\"write_plan_checkpoint_mismatch\");\n+      expect(stderr).not.toContain(\"Rippling browser write driver is not connected yet\");\n+    } finally {\n+      rmSync(runDir, { recursive: true, force: true });\n+    }\n+  });\n });\n \n function visit(\n\nFile: tools/rippling-mcp/shift-approval.ts (modified, +70/-8)\n@@ -165,6 +165,11 @@ export interface VerificationMismatch {\n   reason: string;\n }\n \n+export interface WritePlanCheckpointIssue {\n+  code: string;\n+  message: string;\n+}\n+\n const ZERO_INPUTS: EncounterInputs = {\n   newPatientAppointments: 0,\n   existingPatientAppointments: 0,\n@@ -302,14 +307,16 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n   }\n \n   const actions: WritePlanAction[] = counts.counts.map((count) => {\n+    // Break aliasing so plan mutations cannot corrupt source counts.\n+    const expectedInputs = { ...count.inputs };\n     const row = rowsByProviderDate.get(`${count.providerEmail.toLowerCase()}|${count.date}`);\n     if (!row) {\n       return {\n         action: \"needs_human_review\",\n         providerEmail: count.providerEmail,\n         providerName: count.providerName,\n         date: count.date,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         reason: \"No matching Rippling shift row exists; automation must not create a new time entry.\",\n       };\n     }\n@@ -325,7 +332,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n             providerName: count.providerName,\n             date: count.date,\n             rowId: row.rowId,\n-            expectedInputs: count.inputs,\n+            expectedInputs,\n             currentInputs,\n             approved: row.approved,\n             paid: row.paid,\n@@ -338,7 +345,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n           providerName: count.providerName,\n           date: count.date,\n           rowId: row.rowId,\n-          expectedInputs: count.inputs,\n+          expectedInputs,\n           currentInputs,\n           approved: row.approved,\n           paid: row.paid,\n@@ -351,7 +358,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -370,7 +377,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -388,7 +395,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -406,7 +413,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n         providerName: count.providerName,\n         date: count.date,\n         rowId: row.rowId,\n-        expectedInputs: count.inputs,\n+        expectedInputs,\n         currentInputs,\n         approved: row.approved,\n         paid: row.paid,\n@@ -420,7 +427,7 @@ export function buildWritePlan(counts: AmdCountsCheckpoint, inspection: Rippling\n       providerName: count.providerName,\n       date: count.date,\n       rowId: row.rowId,\n-      expectedInputs: count.inputs,\n+      expectedInputs,\n       currentInputs,\n       approved: row.approved,\n       paid: row.paid,\n@@ -501,6 +508,36 @@ export function verifyWritePlan(plan: WritePlan, verification: VerificationCheck\n   };\n }\n \n+export function validateWritePlanAgainstCheckpoints(\n+  plan: WritePlan,\n+  counts: AmdCountsCheckpoint,\n+  inspection: RipplingInspection\n+): WritePlanCheckpointIssue[] {\n+  const issues: WritePlanCheckpointIssue[] = [];\n+  if (plan.period.start !== counts.period.start || plan.period.end !== counts.period.end) {\n+    issues.push({\n+      code: \"write_plan_amd_period_mismatch\",\n+      message: \"write-plan.json period does not match amd-counts.json.\",\n+    });\n+  }\n+  if (plan.period.start !== inspection.period.start || plan.period.end !== inspection.period.end) {\n+    issues.push({\n+      code: \"write_plan_rippling_period_mismatch\",\n+      message: \"write-plan.json period does not match rippling-inspection.json.\",\n+    });\n+  }\n+\n+  const expectedPlan = buildWritePlan(counts, inspection);\n+  if (!deepEqual(comparableWritePlan(plan), comparableWritePlan(expectedPlan))) {\n+    issues.push({\n+      code: \"write_plan_checkpoint_mismatch\",\n+      message: \"write-plan.json is stale or inconsistent with the current AMD counts and Rippling inspection.\",\n+    });\n+  }\n+\n+  return issues;\n+}\n+\n export function renderRunReport(args: {\n   counts: AmdCountsCheckpoint;\n   plan?: WritePlan;\n@@ -700,6 +737,31 @@ function summarizeActions(actions: WritePlanAction[]): WritePlan[\"summary\"] {\n   };\n }\n \n+function comparableWritePlan(plan: WritePlan): Omit<WritePlan, \"generatedAt\"> {\n+  const { generatedAt: _generatedAt, ...comparable } = plan;\n+  return comparable;\n+}\n+\n+function deepEqual(left: unknown, right: unknown): boolean {\n+  if (Object.is(left, right)) return true;\n+  if (typeof left !== typeof right) return false;\n+  if (left === null || right === null) return false;\n+  if (typeof left !== \"object\" || typeof right !== \"object\") return false;\n+\n+  if (Array.isArray(left) || Array.isArray(right)) {\n+    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;\n+    return left.every((item, index) => deepEqual(item, right[index]));\n+  }\n+\n+  const leftRecord = left as Record<string, unknown>;\n+  const rightRecord = right as Record<string, unknown>;\n+  const leftKeys = Object.keys(leftRecord);\n+  const rightKeys = Object.keys(rightRecord);\n+  if (leftKeys.length !== rightKeys.length) return false;\n+  return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key)\n+    && deepEqual(leftRecord[key], rightRecord[key]));\n+}\n+\n function compareProviderDate(a: DailyEncounterCount, b: DailyEncounterCount): number {\n   return a.providerName.localeCompare(b.providerName) || a.date.localeCompare(b.date);\n }\n```\n\n-"
        }
      ]
    }
  ]
}