{"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../../src/core/web-research/temporal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,wBAAwB,EACxB,mBAAmB,EAEnB,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAoCpB,qBAAa,gBAAgB;IAC5B;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,wBAAwB,CAsJ/D;CACD;AAUD,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,OAAO,CAAC,GACtF;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAMjE;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD","sourcesContent":["import type {\n\tTemporalResolution,\n\tTemporalResolutionResult,\n\tTemporalSourceFacts,\n\tTemporalValueClass,\n\tWebEvidenceRecord,\n} from \"./types.js\";\n\n/**\n * Deterministic temporal source-resolution engine.\n *\n * Distinguishes `historical`, `current`, `superseded`, `contradiction` and\n * `uncertain_current` from dated, authoritative evidence. It never blindly\n * prefers the newest page: authority and explicit effective dates matter.\n *\n * Inputs used:\n * - publishedAt (official patch/rebalance/publication date)\n * - effectiveAt (date stated in the content)\n * - source authority rank\n * - explicit \"changed from A to B\" language (handled by the caller-provided facts)\n * - maintained/current source\n * - later corroboration count\n */\n\nconst AUTHORITY_LABEL = [\"community\", \"community\", \"official\"];\n\nfunction normalizeDate(source: unknown): number | undefined {\n\tif (typeof source !== \"string\") return undefined;\n\tconst match = source.match(/(20\\d{2})[-/]?(\\d{2})?[-/]?(\\d{2})?/);\n\tif (!match) return undefined;\n\tconst year = Number(match[1]);\n\tconst month = match[2] ? Number(match[2]) : 1;\n\tconst day = match[3] ? Number(match[3]) : 1;\n\tconst value = Date.UTC(year, month - 1, day);\n\treturn Number.isNaN(value) ? undefined : value;\n}\n\ninterface ClassifiedObservation {\n\tfacts: TemporalSourceFacts;\n\tdate?: number;\n}\n\nexport class TemporalResolver {\n\t/**\n\t * Resolve a set of observations for one quantity into a temporal verdict.\n\t */\n\tresolve(inputs: TemporalSourceFacts[]): TemporalResolutionResult {\n\t\tconst observations: ClassifiedObservation[] = inputs.map((facts) => {\n\t\t\tconst date =\n\t\t\t\tnormalizeDate(facts.effectiveAt) ??\n\t\t\t\tnormalizeDate(facts.publishedAt) ??\n\t\t\t\t(facts.isMaintained ? Number.MAX_SAFE_INTEGER : undefined);\n\t\t\treturn { facts, date };\n\t\t});\n\n\t\tconst reasoning: string[] = [];\n\t\tconst resolutions: TemporalResolution[] = [];\n\t\tif (observations.length === 0) {\n\t\t\treasoning.push(\"No observations provided; current value unknown.\");\n\t\t\treturn { resolutions, unresolved: true, reasoning };\n\t\t}\n\n\t\t// A maintained current source is the most direct evidence of the\n\t\t// *current* state, independent of simple recency.\n\t\tconst maintained = observations.filter((o) => o.facts.isMaintained);\n\t\t// Dated observations, newest first.\n\t\tconst dated = observations\n\t\t\t.filter((o) => o.date !== undefined)\n\t\t\t.sort((a, b) => (b.date as number) - (a.date as number));\n\n\t\tconst bestCandidate =\n\t\t\tmaintained.length > 0\n\t\t\t\t? maintained.reduce((best, cur) => (cur.facts.authority > best.facts.authority ? cur : best), maintained[0])\n\t\t\t\t: dated[0];\n\n\t\tif (!bestCandidate) {\n\t\t\t// No dating and no maintained source: cannot resolve temporally.\n\t\t\tconst valueSet = new Set(observations.map((o) => String(o.facts.value)));\n\t\t\tif (valueSet.size <= 1) {\n\t\t\t\t// Single consistent value, but undated.\n\t\t\t\tconst classed: TemporalValueClass = \"uncertain_current\";\n\t\t\t\treasoning.push(\n\t\t\t\t\t\"Only undated observations. Value is not temporally resolvable; classified as uncertain_current, not authoritative.\",\n\t\t\t\t);\n\t\t\t\tfor (const o of observations) {\n\t\t\t\t\tresolutions.push({\n\t\t\t\t\t\tsourceUrl: o.facts.sourceUrl ?? \"\",\n\t\t\t\t\t\tevidenceId: o.facts.evidenceId,\n\t\t\t\t\t\tclass: classed,\n\t\t\t\t\t\tvalue: o.facts.value,\n\t\t\t\t\t\treasoning: [\"undated; authority and effective date unknown\"],\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn { resolutions, unresolved: true, reasoning };\n\t\t\t}\n\t\t\t// Undated sources disagree: unresolved contradiction.\n\t\t\treasoning.push(\n\t\t\t\t\"Two or more undated community sources disagree; unresolved contradiction, current value unknown.\",\n\t\t\t);\n\t\t\tfor (const o of observations) {\n\t\t\t\tresolutions.push({\n\t\t\t\t\tsourceUrl: o.facts.sourceUrl ?? \"\",\n\t\t\t\t\tevidenceId: o.facts.evidenceId,\n\t\t\t\t\tclass: \"contradiction\",\n\t\t\t\t\tvalue: o.facts.value,\n\t\t\t\t\tconflictingEvidenceIds: observations\n\t\t\t\t\t\t.filter((other) => String(other.facts.value) !== String(o.facts.value))\n\t\t\t\t\t\t.map((other) => other.facts.evidenceId),\n\t\t\t\t\treasoning: [\"undated conflicting community sources cannot be resolved temporally\"],\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { resolutions, unresolved: true, reasoning };\n\t\t}\n\n\t\t// Group values and pick the set matching the best candidate's value.\n\t\tconst bestValue = String(bestCandidate.facts.value);\n\t\tconst allValues = new Set(observations.map((o) => String(o.facts.value)));\n\n\t\tif (allValues.size === 1) {\n\t\t\t// Only one observed value across all sources.\n\t\t\treasoning.push(`${bestValue} is the only observed value across ${observations.length} source(s).`);\n\t\t\tconst theValue = observations[0].facts.value;\n\t\t\tfor (const o of observations) {\n\t\t\t\tresolutions.push({\n\t\t\t\t\tsourceUrl: o.facts.sourceUrl ?? \"\",\n\t\t\t\t\tevidenceId: o.facts.evidenceId,\n\t\t\t\t\tclass: o.facts.isMaintained || o.facts.authority >= 2 ? \"current\" : \"uncertain_current\",\n\t\t\t\t\tvalue: o.facts.value,\n\t\t\t\t\treasoning: [\"single consistent value; no contradictory evidence\"],\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tresolutions,\n\t\t\t\tcurrentValue: theValue,\n\t\t\t\tunresolved: false,\n\t\t\t\treasoning,\n\t\t\t};\n\t\t}\n\n\t\t// Multiple distinct values: classify each against the best candidate.\n\t\tconst current = bestValue;\n\t\treasoning.push(\n\t\t\t`Newest maintained/dated authoritative source reports ${current}; earlier values are classified as historical/superseded.`,\n\t\t);\n\t\tfor (const o of observations) {\n\t\t\tconst isCurrent = String(o.facts.value) === current;\n\t\t\tconst superseded =\n\t\t\t\to.date !== undefined &&\n\t\t\t\tbestCandidate.date !== undefined &&\n\t\t\t\t(o.date as number) < (bestCandidate.date as number) &&\n\t\t\t\t// A value is only \"superseded\" when it is an earlier authoritative\n\t\t\t\t// observation replaced by a later one. A newer but lower-authority\n\t\t\t\t// claim that disagrees with the maintained current value is a\n\t\t\t\t// contradiction, not a superseded value.\n\t\t\t\to.facts.authority >= bestCandidate.facts.authority;\n\t\t\tlet classed: TemporalValueClass;\n\t\t\tlet supersededBy: string | undefined;\n\t\t\tconst localReasoning: string[] = [];\n\t\t\tif (isCurrent) {\n\t\t\t\tclassed = \"current\";\n\t\t\t\tlocalReasoning.push(\"matches the current authoritative/maintained value\");\n\t\t\t} else if (superseded) {\n\t\t\t\tclassed = \"superseded\";\n\t\t\t\tsupersededBy = current;\n\t\t\t\tlocalReasoning.push(\"older dated authoritative value superseded by later rebalance\");\n\t\t\t} else {\n\t\t\t\t// Same/newer date but different value, newer lower-authority claim,\n\t\t\t\t// or undated lower authority: treat as unresolved contradiction.\n\t\t\t\tclassed = \"contradiction\";\n\t\t\t\tlocalReasoning.push(\"conflicting or lower-authority value; not temporally resolvable\");\n\t\t\t}\n\t\t\tresolutions.push({\n\t\t\t\tsourceUrl: o.facts.sourceUrl ?? \"\",\n\t\t\t\tevidenceId: o.facts.evidenceId,\n\t\t\t\tclass: classed,\n\t\t\t\tvalue: o.facts.value,\n\t\t\t\tsupersededBy,\n\t\t\t\teffectiveAt: o.facts.effectiveAt,\n\t\t\t\treasoning: localReasoning,\n\t\t\t});\n\t\t}\n\n\t\tconst hasCurrent = resolutions.some((r) => r.class === \"current\");\n\t\tconst hasUncertainCurrent = resolutions.some((r) => r.class === \"uncertain_current\");\n\t\t// Unresolved only when no authoritative current value is established, or\n\t\t// the only current-class signal is uncertain. A low-authority outlier\n\t\t// contradiction alongside an authoritative current value is noted but\n\t\t// does not make the current value unknown.\n\t\tconst unresolved = !hasCurrent || hasUncertainCurrent;\n\t\tconst currentValue = currentResolvedValue(resolutions, bestCandidate.facts.value);\n\t\treturn {\n\t\t\tresolutions,\n\t\t\tcurrentValue: unresolved ? undefined : currentValue,\n\t\t\tunresolved,\n\t\t\treasoning,\n\t\t};\n\t}\n}\n\nfunction currentResolvedValue(\n\tresolutions: TemporalResolution[],\n\tfallback: string | number,\n): string | number | undefined {\n\tconst current = resolutions.find((r) => r.class === \"current\");\n\treturn current ? current.value : fallback;\n}\n\nexport function temporalFactsFromEvidence(\n\trecord: Pick<WebEvidenceRecord, \"evidenceId\" | \"canonicalUrl\" | \"publishedAt\" | \"title\">,\n): { evidenceId: string; sourceUrl: string; publishedAt?: string } {\n\treturn {\n\t\tevidenceId: record.evidenceId,\n\t\tsourceUrl: record.canonicalUrl,\n\t\tpublishedAt: record.publishedAt,\n\t};\n}\n\nexport function authorityLabel(authority: number): string {\n\treturn AUTHORITY_LABEL[authority] ?? \"community\";\n}\n"]}