{"record":{"id":"8ab1dc9ba0395aa0","repo":"Hmbown/CodeWhale","slug":"duplicate-field","errorCode":null,"errorMessage":"Duplicate ${field}.","messagePattern":"Duplicate (.+?)\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/evidence.ts","lineNumber":75,"sourceCode":"  unknownEffects: number; authority: { permitted: number; denied: number; unknown: number };\n  provenance: string;\n}\nconst enumValue = <T extends string>(v:unknown, values: readonly T[], field:string): T => {\n  if(typeof v!=='string'||!values.includes(v as T))throw new Error(`Invalid ${field}.`);return v as T;\n};\nconst object = (v:unknown, field:string): Record<string,unknown> => {\n  if(!v||typeof v!=='object'||Array.isArray(v))throw new Error(`Invalid ${field}: object required.`);return v as Record<string,unknown>;\n};\nconst text = (v:unknown, field:string, max=256):string => {\n  if(typeof v!=='string'||!v.length||v.length>max||/[\\u0000-\\u001f\\u007f]/.test(v))throw new Error(`Invalid ${field}: nonempty bounded text required.`);return v;\n};\nconst num = (v:unknown,field:string,min=0):number => {\n  if(typeof v!=='number'||!Number.isFinite(v)||v<min||Math.abs(v)>Number.MAX_SAFE_INTEGER)throw new Error(`Invalid ${field}.`);return v;\n};\nconst optionalText = (o:Record<string,unknown>,key:string):string|undefined => o[key]===undefined?undefined:text(o[key],key);\nconst strings = (v:unknown,field:string,allowEmpty=false):string[]=>{\n  if(!Array.isArray(v)||(!allowEmpty&&!v.length)||v.length>256)throw new Error(`Invalid ${field}.`);\n  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.');","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/evidence.ts#L57-L93","documentation":"The `strings` validator in evidence.ts parses an unknown value into a bounded, non-empty string array and rejects arrays containing repeated elements. This library treats duplicate entries in evidence fields (stages, surfaces, runIds, grant lists, etc.) as malformed policy/evidence input rather than silently de-duplicating, because duplicates in authorization data can change semantics. The thrown message includes the field name, e.g. `Duplicate runIds.`","triggerScenarios":"Calling validatePolicy or validateBundle with a policy whose source/grant arrays (stages, surfaces, runIds, sandboxIds, isolationGroups, targetIds, actions, effects) contain the same string twice; also validateObservation via sources/g when building arrays with accidental repeats.","commonSituations":"Hand-authored policy JSON where a run ID is copy-pasted twice; generated policies that append to an existing list without dedup; merging two configs where both contributed the same sandboxId.","solutions":["De-duplicate the array before validation, e.g. `[...new Set(arr)]`","Check the field name in the message and remove the repeated entry from the policy/bundle JSON","If the duplicates come from merging configs, merge with a Set/union instead of concatenation"],"exampleFix":"// before\n{\"stages\":[\"plan\",\"plan\",\"act\"]}\n// after\n{\"stages\":[\"plan\",\"act\"]}","handlingStrategy":"validation","validationCode":"function assertNoDuplicates(arr, name) { if (new Set(arr).size !== arr.length) throw new Error(`Duplicate ${name}.`); }","typeGuard":"const hasNoDuplicates = (a: unknown[]): a is string[] => new Set(a).size === a.length;","tryCatchPattern":"try { validatePolicy(input); } catch (e) { if (/^Duplicate /.test(e.message)) console.error('De-duplicate field named in message'); throw e; }","preventionTips":["Build lists with Set semantics when merging configs","Run validatePolicy in CI on every policy file","Store policies as canonical de-duplicated JSON"],"tags":["validation","schema","policy","typescript"],"backgroundTag":"schema-validation-failed","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"}