{"record":{"id":"7dc5e0faea5dd6b5","repo":"koala73/worldmonitor","slug":"get-china-decision-signals-returned-an-invalid-can","errorCode":null,"errorMessage":"get-china-decision-signals returned an invalid canonical payload","messagePattern":"get-china-decision-signals returned an invalid canonical payload","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/rpc-tools.ts","lineNumber":619,"sourceCode":"      const url = `${base}/api/intelligence/v1/get-china-decision-signals`;\n      const auth = await buildAuthHeaders(context, 'GET', url, null);\n      const response = await fetch(url, {\n        headers: { ...auth, 'User-Agent': 'worldmonitor-mcp-edge/1.0' },\n        signal: AbortSignal.timeout(12_000),\n      });\n      await assertMcpToolFetchOk(response, {\n        operation: 'get-china-decision-signals',\n        tool: 'get_china_decision_signals',\n        auth: context,\n        execution,\n      });\n      const wire = await response.json() as { payloadJson?: unknown };\n      if (typeof wire.payloadJson !== 'string') {\n        throw new Error('get-china-decision-signals returned no canonical payload');\n      }\n      const payload = JSON.parse(wire.payloadJson) as unknown;\n      if (!isChinaDecisionSignalSnapshot(payload)) {\n        throw new Error('get-china-decision-signals returned an invalid canonical payload');\n      }\n      return payload;\n    },\n    _coverageKeys: [\n      'china:policy-events:v1',\n      'military:cross-strait-activity:v1',\n      'military:cross-strait-activity-bootstrap:v1',\n      'market:china:corporate-disclosures:v1',\n      'intelligence:china-decision-signals:v1',\n    ],\n    _apiPaths: [\n      'GET /api/intelligence/v1/get-china-decision-signals',\n    ],\n  },\n  {\n    name: 'get_procurement_opportunities',\n    _outputBudgetBytes: 65536,\n    description: 'Search open global public-procurement opportunities through the canonical Pro route. Default output is 10 compact records (maximum 25), without descriptions or submission/eligibility payloads. automationFit is keyword relevance evidence only, never bidding eligibility; participationMode \"unknown\" remains unknown.',","sourceCodeStart":601,"sourceCodeEnd":637,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/mcp/registry/rpc-tools.ts#L601-L637","documentation":"Thrown by the get_china_decision_signals MCP tool when the response carried a string payloadJson but after JSON.parse the result failed the isChinaDecisionSignalSnapshot type guard (defined in shared/china-decision-signals.ts). This means the producer wrote a payload that does not conform to the canonical snapshot schema — a field is missing, mistyped, or structurally changed.","triggerScenarios":"The china-decision-signals seeder wrote a snapshot to Redis that violates the isChinaDecisionSignalSnapshot schema — e.g. a required field (like a nested array or timestamp) is absent, a field type changed (string vs number), or the snapshot was partially written. The handler faithfully wrapped it in payloadJson, but the MCP layer rejects it because the shape does not match the canonical contract.","commonSituations":"A seeder version change that altered the snapshot shape without updating the type guard (or vice versa); a partial Redis write where the seeder crashed mid-serialization; a upstream source (cross-strait activity, policy events) schema change that propagated into the snapshot undetected.","solutions":["Inspect the actual snapshot in Redis (the intelligence:china-decision-signals:v1 key) and diff its fields against isChinaDecisionSignalSnapshot in shared/china-decision-signals.ts.","Run scripts/audit-china-decision-parity.mjs which validates the parity between the snapshot shape and the type guard.","If the seeder changed the shape, update both the seeder output and the type guard together so they agree.","Re-seed the snapshot after fixing the producer; the stale/malformed key will be overwritten."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Validate the snapshot shape client-side using the shared guard (if available)\nimport { isChinaDecisionSignalSnapshot } from '../shared/china-decision-signals';\nconst probe = await fetch('/api/intelligence/v1/get-china-decision-signals', { headers: authHeaders });\nconst body = await probe.json();\nconst payload = JSON.parse(body.payloadJson);\nif (!isChinaDecisionSignalSnapshot(payload)) {\n  throw new Error('Snapshot fails schema guard — seeder/handler version mismatch');\n}","typeGuard":"function isInvalidCanonicalPayloadError(e: unknown): boolean {\n  return e instanceof Error && e.message.includes('returned an invalid canonical payload');\n}","tryCatchPattern":"try {\n  const snapshot = await callMcpTool('get_china_decision_signals', {});\n} catch (e) {\n  if (e instanceof Error && e.message.includes('returned an invalid canonical payload')) {\n    // Schema regression in the seeder output — do not retry, report it\n    logSchemaRegression('china-decision-signals', e);\n  } else throw e;\n}","preventionTips":["Keep isChinaDecisionSignalSnapshot in shared/ in sync with the seeder output schema — update both together.","Run the parity audit script after any schema-affecting change.","Monitor for this error specifically — it means the producer wrote a structurally invalid snapshot."],"tags":["mcp","schema-regression","type-guard","china-decision-signals"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}