{"record":{"id":"4c026396641186e2","repo":"Hmbown/CodeWhale","slug":"invalid-fact-key-nonempty-bounded-text-required","errorCode":null,"errorMessage":"Invalid fact key: nonempty bounded text required.","messagePattern":"Invalid fact key: nonempty bounded text required\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/evidence.ts","lineNumber":93,"sourceCode":"  const out=v.map(x=>text(x,field));if(new Set(out).size!==out.length)throw new Error(`Duplicate ${field}.`);return out;\n};\nconst boolean = (v:unknown,field:string):boolean=>{if(typeof v!=='boolean')throw new Error(`Invalid ${field}.`);return v;};\nconst optionalNumber=(o:Record<string,unknown>,key:string)=>o[key]===undefined?undefined:num(o[key],key);\nfunction onlyKeys(o:Record<string,unknown>,keys:string[],label:string):void{\n  for(const k of Object.keys(o))if(!keys.includes(k))throw new Error(`Unknown ${label} field: ${k}.`);\n}\n/** All unrecognized fields are rejected, never secretly retained in metadata-only evidence. */\nexport function validateObservation(input:unknown):Observation {\n  const o=object(input,'observation');\n  onlyKeys(o,['version','id','runId','operationId','sourceId','epoch','sequence','time','receivedAt','subject','stage','surface','action','effect','status','target','actionDigest','authority','correlation','facts'],'observation');\n  if(o.version!==1)throw new Error('Unsupported observation version.');\n  const t=object(o.time,'time'),s=object(o.subject,'subject');\n  onlyKeys(t,['wallMs','clockId','monotonicMs','taskMs','uncertaintyMs'],'time');onlyKeys(s,['agentId','sandboxId','isolationGroup'],'subject');\n  const seq=num(o.sequence,'sequence',1);if(!Number.isSafeInteger(seq))throw new Error('sequence must be an integer.');\n  const facts=object(o.facts??{},'facts'),safeFacts:Observation['facts']={};\n  if(Object.keys(facts).length>48)throw new Error('Too many observation facts.');\n  for(const [k,v] of Object.entries(facts)){\n    text(k,'fact key',80);if(['__proto__','prototype','constructor'].includes(k))throw new Error('Unsafe fact key.');\n    if(typeof v==='string')safeFacts[k]=text(v,'fact value',512);\n    else if(typeof v==='number')safeFacts[k]=num(v,'fact value',-Number.MAX_SAFE_INTEGER);\n    else if(v===null||typeof v==='boolean')safeFacts[k]=v;\n    else throw new Error('Facts must be scalar metadata, not content objects.');\n  }\n  let target:Observation['target'];if(o.target!==undefined){const a=object(o.target,'target');onlyKeys(a,['id','kind','version','boundary'],'target');target={id:text(a.id,'target.id'),kind:text(a.kind,'target.kind',64),version:optionalText(a,'version'),boundary:a.boundary===undefined?undefined:enumValue(a.boundary,['local','external','unknown'] as const,'boundary')};}\n  let authority:Observation['authority'];if(o.authority!==undefined){const a=object(o.authority,'authority');onlyKeys(a,['grantId','claimed'],'authority');authority={grantId:optionalText(a,'grantId'),claimed:a.claimed===undefined?undefined:boolean(a.claimed,'claimed')};}\n  let correlation:Observation['correlation'];if(o.correlation!==undefined){const a=object(o.correlation,'correlation');onlyKeys(a,['sessionId','requestId','parentOperationId','messageId'],'correlation');correlation={sessionId:optionalText(a,'sessionId'),requestId:optionalText(a,'requestId'),parentOperationId:optionalText(a,'parentOperationId'),messageId:optionalText(a,'messageId')};}\n  return {version:1,id:text(o.id,'id'),runId:text(o.runId,'runId'),operationId:optionalText(o,'operationId'),sourceId:text(o.sourceId,'sourceId'),epoch:text(o.epoch,'epoch'),sequence:seq,\n    time:{wallMs:num(t.wallMs,'wallMs'),clockId:optionalText(t,'clockId'),monotonicMs:optionalNumber(t,'monotonicMs'),taskMs:optionalNumber(t,'taskMs'),uncertaintyMs:optionalNumber(t,'uncertaintyMs')},receivedAt:optionalNumber(o,'receivedAt'),\n    subject:{agentId:text(s.agentId,'agentId'),sandboxId:optionalText(s,'sandboxId'),isolationGroup:optionalText(s,'isolationGroup')},stage:enumValue(o.stage,STAGES,'stage'),surface:enumValue(o.surface,SURFACES,'surface'),action:text(o.action,'action',128),effect:enumValue(o.effect,EFFECTS,'effect'),status:enumValue(o.status,['started','completed','error','unknown'],'status'),target,actionDigest:optionalText(o,'actionDigest'),authority,correlation,facts:safeFacts};\n}\nexport function validatePolicy(input:unknown):BoundaryPolicy {\n  const o=object(input,'policy');onlyKeys(o,['version','id','sources','grants','expectedSurfaces','forbiddenCrossGroup'],'policy');\n  if(o.version!==1||!Array.isArray(o.sources)||o.sources.length>256||!Array.isArray(o.grants)||o.grants.length>2048)throw new Error('Invalid policy version or limits.');\n  const sources=o.sources.map(x=>{const a=object(x,'source');onlyKeys(a,['id','stages','surfaces','runIds','sandboxIds','isolationGroups','heartbeatMs','description'],'source');return {id:text(a.id,'source.id'),stages:strings(a.stages,'stages').map(v=>enumValue(v,STAGES,'stage')),surfaces:strings(a.surfaces,'surfaces').map(v=>enumValue(v,SURFACES,'surface')),runIds:strings(a.runIds,'runIds'),sandboxIds:a.sandboxIds===undefined?undefined:strings(a.sandboxIds,'sandboxIds'),isolationGroups:a.isolationGroups===undefined?undefined:strings(a.isolationGroups,'isolationGroups'),heartbeatMs:num(a.heartbeatMs,'heartbeatMs',1),description:optionalText(a,'description')};});\n  const grants=o.grants.map(x=>{const a=object(x,'grant');onlyKeys(a,['id','runIds','sandboxIds','targetIds','actions','effects','notBefore','expiresAt','actionDigest'],'grant');const g={id:text(a.id,'grant.id'),runIds:strings(a.runIds,'runIds'),sandboxIds:strings(a.sandboxIds,'sandboxIds'),targetIds:strings(a.targetIds,'targetIds'),actions:strings(a.actions,'actions'),effects:strings(a.effects,'effects').map(v=>enumValue(v,EFFECTS,'effect')),notBefore:num(a.notBefore,'notBefore'),expiresAt:num(a.expiresAt,'expiresAt'),actionDigest:optionalText(a,'actionDigest')};if(g.expiresAt<=g.notBefore)throw new Error('Grant expiry must follow its start.');return g;});\n  if(new Set(sources.map(s=>s.id)).size!==sources.length||new Set(grants.map(g=>g.id)).size!==grants.length)throw new Error('Duplicate policy identity.');","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/evidence.ts#L75-L111","documentation":"Each fact key is validated with `text(k,'fact key',80)` — it must be nonempty bounded text (≤ 80 chars) — so an empty, non-string, or oversized key throws `Invalid fact key: nonempty bounded text required.` (The text validator's message is generic; the field label identifies it as the fact key.)","triggerScenarios":"An observation `facts` object with a key that is an empty string (e.g. from `arr.join()` with empty parts) or longer than 80 characters (e.g. a full URL or serialized object used as the key). Note a separate 'Unsafe fact key.' error fires for __proto__/prototype/constructor keys.","commonSituations":"Dynamically building fact keys from untrimmed user input; using a long identifier/URL as a key; a null/undefined coerced into an empty string key during object construction.","solutions":["Shorten the key (hash or truncate to ≤ 80 chars) and keep the long value in the fact value","Ensure keys are nonempty strings before building the facts object","Sanitize/trim dynamic key sources"],"exampleFix":"// before\nfacts: { [longUrl]: true } // 150-char key\n// after\nfacts: { urlHash: sha256(longUrl).slice(0,16) }","handlingStrategy":"validation","validationCode":"for (const k of Object.keys(rec.facts ?? {})) { if (typeof k !== 'string' || !k || k.length > 80) throw new Error(`invalid fact key length: ${k.slice(0,20)}`); }","typeGuard":"const isValidFactKey = (k: string): boolean => k.length > 0 && k.length <= 80;","tryCatchPattern":"try { validateObservation(rec); } catch (e) { if (/Invalid fact key/.test(e.message)) console.error('Shorten or non-empty your fact keys (<=80 chars)'); throw e; }","preventionTips":["Hash or truncate long identifiers used as keys","Trim dynamic key sources before building facts","Keep keys short and stable; put detail in values"],"tags":["validation","keys","typescript"],"backgroundTag":"invalid-identifier-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}