fix: harden grounded rectification narratives
This commit is contained in:
@@ -26,6 +26,9 @@ type ParsedDate = {
|
||||
|
||||
const chineseDatePattern = /(?:19|20)\d{2}\s*年(?:\s*\d{1,2}\s*月(?:\s*\d{1,2}\s*(?:日|号))?)?/g;
|
||||
const isoDatePattern = /(?:19|20)\d{2}-(?:0[1-9]|1[0-2])(?:-(?:0[1-9]|[12]\d|3[01]))?/g;
|
||||
const unresolvedRelativeTimePattern = /(?:次年|第二年|后来|此前|同年|当年|那年|随后|先前|然后|之前|之后|今年|去年|前年|明年)/;
|
||||
const leadingRelativeTimePattern = /^\s*(?:(?:次年|第二年|后来(?:又)?|此前|同年|当年|那年|随后|先前|然后|之前|之后|今年|去年|前年|明年)\s*)+/;
|
||||
const missingEventSummary = "事件内容待补充";
|
||||
|
||||
function normalizedDate(value: string): ParsedDate | null {
|
||||
const chinese = value.match(/^((?:19|20)\d{2})\s*年(?:\s*(\d{1,2})\s*月(?:\s*(\d{1,2})\s*(?:日|号))?)?$/);
|
||||
@@ -66,10 +69,13 @@ function eventSummary(fragment: string): string {
|
||||
.replace(chineseDatePattern, "")
|
||||
.replace(isoDatePattern, "")
|
||||
.replace(/^\s*(?:更正|纠正|修正)\s*[::]?\s*/, "")
|
||||
.replace(/^\s*(?:后来|然后|同时|又)\s*/, "")
|
||||
.replace(leadingRelativeTimePattern, "")
|
||||
.replace(/^\s*(?:同时|又)\s*/, "")
|
||||
.trim()
|
||||
.replace(/^[,,、::\s]+|[,,、::\s]+$/g, "");
|
||||
return withoutDates || fragment.trim();
|
||||
return /[A-Za-z0-9\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/.test(withoutDates)
|
||||
? withoutDates
|
||||
: missingEventSummary;
|
||||
}
|
||||
|
||||
function classifyDomain(summary: string): RectificationEvidenceDomain {
|
||||
@@ -120,9 +126,12 @@ export function extractLifeEventEvidence(
|
||||
const sharedDate = sentenceDates.length === 1 ? sentenceDates[0] ?? null : null;
|
||||
for (const fragment of fragments) {
|
||||
const ownDates = datesIn(fragment);
|
||||
const date = ownDates.length === 1 ? ownDates[0] ?? null : sharedDate;
|
||||
const unresolvedRelativeTime = ownDates.length === 0 && unresolvedRelativeTimePattern.test(fragment);
|
||||
const date = ownDates.length === 1
|
||||
? ownDates[0] ?? null
|
||||
: ownDates.length === 0 && !unresolvedRelativeTime ? sharedDate : null;
|
||||
const summary = eventSummary(fragment);
|
||||
const complete = summary.length > 0 && date !== null;
|
||||
const complete = summary !== missingEventSummary && date !== null && !unresolvedRelativeTime;
|
||||
const extractionStatus = !complete
|
||||
? "needs_clarification"
|
||||
: corrections.length > 0 ? "corrected" : "clear";
|
||||
|
||||
@@ -9,8 +9,30 @@ export type RectificationNarrativePhase = "first" | "intermediate" | "final";
|
||||
|
||||
const timeSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
|
||||
const modelIdSchema = z.string().trim().min(1).max(120);
|
||||
const validatorVersion = "rectification-narrative-grounding-v1";
|
||||
const validatorVersion = "rectification-narrative-grounding-v2";
|
||||
const domainSchema = z.enum(["career", "education", "relocation", "relationship", "family", "other"]);
|
||||
const stableSemanticsPattern = /(?:稳定|保持|不变|一致|stable|unchanged)/i;
|
||||
const sensitiveSemanticsPattern = /(?:敏感|变化|差异|切换|不同|sensitive|changes?|differs?)/i;
|
||||
const discriminationSemanticsPattern = /(?:区分|辨别|判别|验证|差异|变化|discriminat|distinguish)/i;
|
||||
const broadYearRangePattern = /(?:19|20)\d{2}\s*年?\s*(?:[-–—~~至到\/]|\.\.)\s*(?:19|20)\d{2}\s*年?/i;
|
||||
const explicitYearPattern = /(?:19|20)\d{2}\s*年?/g;
|
||||
const choiceQuestionPattern = /(?:哪(?:一|个)?(?:年份|年代|时间段|区间|时期)|哪个时间段|选择|选项|更符合|更匹配|A\s*[.、::)]|B\s*[.、::)]|which\s+(?:year|period|range)|options?)/i;
|
||||
const domainSemantics = {
|
||||
career: /(?:事业|工作|职业|career)/i,
|
||||
education: /(?:教育|学业|学校|education)/i,
|
||||
relocation: /(?:搬迁|搬家|迁居|异地|居住|relocation)/i,
|
||||
relationship: /(?:关系|婚恋|伴侣|relationship)/i,
|
||||
family: /(?:家庭|家人|父母|孩子|family)/i,
|
||||
other: /(?:其他|其它|other)/i,
|
||||
} as const satisfies Readonly<Record<RectificationEvidenceDomain, RegExp>>;
|
||||
const domainLabels = {
|
||||
career: "事业",
|
||||
education: "学业",
|
||||
relocation: "迁居",
|
||||
relationship: "关系",
|
||||
family: "家庭",
|
||||
other: "其他",
|
||||
} as const satisfies Readonly<Record<RectificationEvidenceDomain, string>>;
|
||||
const narrativeOutputSchema = z.object({
|
||||
narrative: z.string().trim().min(1).max(12_000),
|
||||
candidateStatus: z.enum(["pending_validation", "ready_for_confirmation"]),
|
||||
@@ -95,6 +117,54 @@ function narrativeReferences(value: string): string[] {
|
||||
return unique([...bracketed, ...plainTechnicalIds]);
|
||||
}
|
||||
|
||||
function isGenericBroadYearChoiceQuestionnaire(value: string): boolean {
|
||||
const distinctYears = unique((value.match(explicitYearPattern) ?? [])
|
||||
.map((year) => year.replace(/\s*年$/, "")));
|
||||
return choiceQuestionPattern.test(value)
|
||||
&& (broadYearRangePattern.test(value) || distinctYears.length >= 2);
|
||||
}
|
||||
|
||||
function proseFields(output: RectificationNarrativeModelOutput): readonly {
|
||||
readonly path: string;
|
||||
readonly value: string;
|
||||
}[] {
|
||||
return [
|
||||
{ path: "narrative", value: output.narrative },
|
||||
...output.domainReasons.map((item, index) => ({
|
||||
path: `domainReasons[${index}].reason`,
|
||||
value: item.reason,
|
||||
})),
|
||||
...(output.evidenceRequest
|
||||
? [{ path: "evidenceRequest.prompt", value: output.evidenceRequest.prompt }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
function pairKey(value: { readonly domain: RectificationEvidenceDomain; readonly layer: string }): string {
|
||||
return `${value.domain}\0${value.layer}`;
|
||||
}
|
||||
|
||||
function narrativeHasDomainDiscrimination(
|
||||
narrative: string,
|
||||
domain: RectificationEvidenceDomain,
|
||||
layer: string,
|
||||
): boolean {
|
||||
return narrative.split(/[。!?!?;;\n]/).some((clause) => domainSemantics[domain].test(clause)
|
||||
&& clause.includes(layer)
|
||||
&& discriminationSemanticsPattern.test(clause));
|
||||
}
|
||||
|
||||
function narrativeHasLayerEvidence(
|
||||
narrative: string,
|
||||
item: RectificationTechnicalPacket["stableLayers"][number],
|
||||
semantics: RegExp,
|
||||
requiredValueCount: number,
|
||||
): boolean {
|
||||
return narrative.split(/[。!?!?;;\n]/).some((clause) => clause.includes(item.layer)
|
||||
&& item.values.filter((value) => clause.includes(value)).length >= requiredValueCount
|
||||
&& semantics.test(clause));
|
||||
}
|
||||
|
||||
export function validateNarrativeAgainstPacket(
|
||||
output: RectificationNarrativeModelOutput,
|
||||
packet: RectificationTechnicalPacket,
|
||||
@@ -132,18 +202,32 @@ export function validateNarrativeAgainstPacket(
|
||||
if (!packet.referenceIds.includes(reference)) issues.push(`reference ${reference} is not packet-grounded`);
|
||||
}
|
||||
const allowedDomains = new Map(packet.suggestedDomains.map((item) => [item.domain, item.layer]));
|
||||
for (const reason of output.domainReasons) {
|
||||
if (allowedDomains.get(reason.domain) !== reason.layer || !reason.reason.includes(reason.layer)) {
|
||||
const packetReasons = new Map(packet.suggestedDomains.map((item) => [pairKey(item), item.reason]));
|
||||
for (const [index, reason] of output.domainReasons.entries()) {
|
||||
const expectedReason = packetReasons.get(pairKey(reason));
|
||||
if (!expectedReason) {
|
||||
issues.push(`domain reason ${reason.domain}/${reason.layer} is not packet-grounded`);
|
||||
} else if (reason.reason !== expectedReason) {
|
||||
issues.push(`domainReasons[${index}].reason must use the packet discrimination explanation`);
|
||||
}
|
||||
}
|
||||
if (phase === "first" && output.domainReasons.length < 2) {
|
||||
issues.push("first turn requires two discriminating domain reasons");
|
||||
if (phase === "first") {
|
||||
const expectedPairs = packet.suggestedDomains.map(pairKey);
|
||||
const actualPairs = output.domainReasons.map(pairKey);
|
||||
if (expectedPairs.length < 2 || !sameMembers(actualPairs, expectedPairs)) {
|
||||
issues.push("first turn must carry at least two unique packet discrimination pairs");
|
||||
}
|
||||
}
|
||||
if (output.evidenceRequest) {
|
||||
for (const domain of output.evidenceRequest.domains) {
|
||||
if (!allowedDomains.has(domain)) issues.push(`evidence domain ${domain} is not packet-grounded`);
|
||||
}
|
||||
if (phase === "first" && !sameMembers(
|
||||
output.evidenceRequest.domains,
|
||||
packet.suggestedDomains.map((item) => item.domain),
|
||||
)) {
|
||||
issues.push("first evidence request must match the packet discrimination domains");
|
||||
}
|
||||
if (!/(?:已经发生|已发生|过去)/.test(output.evidenceRequest.prompt)
|
||||
|| !/年/.test(output.evidenceRequest.prompt)
|
||||
|| !/月/.test(output.evidenceRequest.prompt)) {
|
||||
@@ -154,24 +238,48 @@ export function validateNarrativeAgainstPacket(
|
||||
}
|
||||
|
||||
const allowedTimes = [candidate.representativeTime, candidate.range.startTime, candidate.range.endTime];
|
||||
for (const time of narrativeTimes(output.narrative)) {
|
||||
if (!allowedTimes.includes(time)) issues.push(`narrative time ${time} is not packet-grounded`);
|
||||
}
|
||||
const allowedLayers = [...allowedStable, ...allowedSensitive];
|
||||
for (const layer of narrativeLayers(output.narrative)) {
|
||||
if (!allowedLayers.includes(layer)) issues.push(`narrative layer ${layer} is not packet-grounded`);
|
||||
}
|
||||
for (const reference of narrativeReferences(output.narrative)) {
|
||||
if (!packet.referenceIds.includes(reference)) issues.push(`narrative reference ${reference} is not packet-grounded`);
|
||||
for (const field of proseFields(output)) {
|
||||
const layers = narrativeLayers(field.value);
|
||||
for (const time of narrativeTimes(field.value)) {
|
||||
if (!allowedTimes.includes(time)) issues.push(`${field.path} time ${time} is not packet-grounded`);
|
||||
}
|
||||
for (const layer of layers) {
|
||||
if (!allowedLayers.includes(layer)) issues.push(`${field.path} layer ${layer} is not packet-grounded`);
|
||||
}
|
||||
for (const reference of narrativeReferences(field.value)) {
|
||||
if (!layers.includes(reference) && !packet.referenceIds.includes(reference)) {
|
||||
issues.push(`${field.path} reference ${reference} is not packet-grounded`);
|
||||
}
|
||||
}
|
||||
if (isGenericBroadYearChoiceQuestionnaire(field.value)) {
|
||||
issues.push(`${field.path} is a forbidden generic broad-year choice questionnaire`);
|
||||
}
|
||||
}
|
||||
if (phase === "first") {
|
||||
if (!output.narrative.includes(candidate.representativeTime)
|
||||
|| !/(?:待验证|候选)/.test(output.narrative)) {
|
||||
issues.push("first narrative must state the pending candidate time");
|
||||
}
|
||||
if (!allowedStable.every((layer) => output.narrative.includes(layer))
|
||||
|| !allowedSensitive.every((layer) => output.narrative.includes(layer))) {
|
||||
issues.push("first narrative must explain stable and sensitive layers");
|
||||
for (const item of packet.stableLayers) {
|
||||
if (!narrativeHasLayerEvidence(output.narrative, item, stableSemanticsPattern, 1)) {
|
||||
issues.push(`first narrative lacks stable evidence semantics for ${item.layer}`);
|
||||
}
|
||||
}
|
||||
for (const item of packet.sensitiveLayers) {
|
||||
if (!narrativeHasLayerEvidence(
|
||||
output.narrative,
|
||||
item,
|
||||
sensitiveSemanticsPattern,
|
||||
Math.min(2, item.values.length),
|
||||
)) {
|
||||
issues.push(`first narrative lacks sensitive evidence semantics for ${item.layer}`);
|
||||
}
|
||||
}
|
||||
for (const item of packet.suggestedDomains) {
|
||||
if (!narrativeHasDomainDiscrimination(output.narrative, item.domain, item.layer)) {
|
||||
issues.push(`first narrative must explain how ${item.domain}/${item.layer} discriminates`);
|
||||
}
|
||||
}
|
||||
if (!/(?:已经发生|已发生|过去)/.test(output.narrative)
|
||||
|| !/年/.test(output.narrative)
|
||||
@@ -182,7 +290,8 @@ export function validateNarrativeAgainstPacket(
|
||||
issues.push("first narrative must state the candidate use boundary");
|
||||
}
|
||||
}
|
||||
return { valid: issues.length === 0, issues };
|
||||
const uniqueIssues = unique(issues);
|
||||
return { valid: uniqueIssues.length === 0, issues: uniqueIssues };
|
||||
}
|
||||
|
||||
function grounding(packet: RectificationTechnicalPacket) {
|
||||
@@ -191,6 +300,7 @@ function grounding(packet: RectificationTechnicalPacket) {
|
||||
calculationVersion: packet.calculationVersion,
|
||||
candidate: projected.candidate,
|
||||
useBoundary: packet.useBoundary,
|
||||
sensitivityScope: projected.technicalReceipt.sensitivityScope,
|
||||
stableLayers: packet.stableLayers,
|
||||
sensitiveLayers: packet.sensitiveLayers,
|
||||
scoredHistoricalEvidence: packet.scoredHistoricalEvidence,
|
||||
@@ -218,6 +328,9 @@ function promptFor(
|
||||
outputContract: {
|
||||
candidateFactsMustMatch: true,
|
||||
onlyListedLayersAndReferences: true,
|
||||
everyAuthoredStringMustBeGrounded: true,
|
||||
includeStableAndSensitiveLayerValues: phase === "first",
|
||||
usePacketDomainReasonTextExactly: true,
|
||||
requestRealPastEventsByYearAndMonth: phase !== "final",
|
||||
futureWindowsAreContextOnly: true,
|
||||
genericBroadYearRangeQuestionnaireForbidden: true,
|
||||
@@ -232,10 +345,10 @@ function fallbackNarrative(packet: RectificationTechnicalPacket, phase: Rectific
|
||||
.map((item) => `${item.layer}(${item.values.join(" / ")})保持稳定`)
|
||||
.join(";");
|
||||
const sensitive = packet.sensitiveLayers
|
||||
.map((item) => `${item.layer}(${item.values.join(" / ")})`)
|
||||
.map((item) => `${item.layer}(${item.values.join(" / ")})呈现分钟敏感差异`)
|
||||
.join(";");
|
||||
const reasons = packet.suggestedDomains
|
||||
.map((item) => `${item.domain}事件可区分 ${item.layer}`)
|
||||
.map((item) => `${domainLabels[item.domain]}事件可区分 ${item.layer}`)
|
||||
.join(";");
|
||||
const phaseLine = phase === "final"
|
||||
? "当前证据已形成候选总结,但仍有残余不确定性;只有明确确认后才会替换当前排盘时间。"
|
||||
|
||||
@@ -15,6 +15,10 @@ export type ServerComputedRectificationConsultation = {
|
||||
readonly calculationVersion: string;
|
||||
readonly availableLayers: readonly string[];
|
||||
readonly layerReferences: Readonly<Record<string, readonly string[]>>;
|
||||
readonly timeLinkedScanSamples: readonly {
|
||||
readonly sampleIndex: number;
|
||||
readonly time: string;
|
||||
}[];
|
||||
readonly boundaryDistanceMinutes: number | null;
|
||||
readonly futureWindows: readonly {
|
||||
readonly label: string;
|
||||
@@ -49,6 +53,12 @@ export type RectificationTechnicalPacket = {
|
||||
readonly partitionIds: readonly string[];
|
||||
readonly d1Stability: "stable" | "sensitive" | "unavailable";
|
||||
readonly boundaryDistanceMinutes: number | null;
|
||||
readonly sensitivityScope: {
|
||||
readonly source: "time_linked_candidate_scan_samples";
|
||||
readonly rangeStart: string;
|
||||
readonly rangeEnd: string;
|
||||
readonly sampleTimes: readonly string[];
|
||||
};
|
||||
readonly stableLayers: readonly RectificationLayerEvidence[];
|
||||
readonly sensitiveLayers: readonly RectificationLayerEvidence[];
|
||||
readonly supportedSensitiveLayers: readonly string[];
|
||||
@@ -76,6 +86,11 @@ type PacketInput = {
|
||||
readonly consultation: ServerComputedRectificationConsultation;
|
||||
};
|
||||
|
||||
type TimeLinkedVargaSample = {
|
||||
readonly time: string;
|
||||
readonly sample: RectificationQuestionnaire["samples"][number];
|
||||
};
|
||||
|
||||
const layerFields = [
|
||||
["D1", "ascendantSign"],
|
||||
["D4", "d4Sign"],
|
||||
@@ -92,6 +107,15 @@ const domainByLayer = {
|
||||
D4: "relocation",
|
||||
} as const satisfies Readonly<Record<string, RectificationEvidenceDomain>>;
|
||||
|
||||
const domainLabels = {
|
||||
career: "事业",
|
||||
education: "教育",
|
||||
relocation: "迁居",
|
||||
relationship: "关系",
|
||||
family: "家庭",
|
||||
other: "其他",
|
||||
} as const satisfies Readonly<Record<RectificationEvidenceDomain, string>>;
|
||||
|
||||
function unique(values: readonly string[]): string[] {
|
||||
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
||||
}
|
||||
@@ -106,6 +130,44 @@ function minuteToTime(value: number): string {
|
||||
return `${String(Math.floor(normalized / 60)).padStart(2, "0")}:${String(normalized % 60).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function sampleClockTime(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const match = value.trim().match(
|
||||
/(?:^|[T\s])(([01]\d|2[0-3]):[0-5]\d)(?::[0-5]\d)?(?:Z|[+-]\d{2}:?\d{2})?$/,
|
||||
);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function timeIsInsideRange(time: string, startTime: string, endTime: string): boolean {
|
||||
const minute = timeToMinute(time);
|
||||
const start = timeToMinute(startTime);
|
||||
const end = timeToMinute(endTime);
|
||||
return end >= start
|
||||
? minute >= start && minute <= end
|
||||
: minute >= start || minute <= end;
|
||||
}
|
||||
|
||||
function timeLinkedSamples(
|
||||
scan: RectificationQuestionnaire,
|
||||
links: ServerComputedRectificationConsultation["timeLinkedScanSamples"],
|
||||
): readonly TimeLinkedVargaSample[] {
|
||||
const byTime = new Map<string, TimeLinkedVargaSample>();
|
||||
const linkedIndexes = new Set<number>();
|
||||
for (const link of links) {
|
||||
const sample = scan.samples[link.sampleIndex];
|
||||
const time = sampleClockTime(link.time);
|
||||
if (!Number.isInteger(link.sampleIndex) || link.sampleIndex < 0 || !sample || !time) {
|
||||
throw new TypeError("rectification packet received an invalid time-linked scan sample");
|
||||
}
|
||||
if (linkedIndexes.has(link.sampleIndex) || byTime.has(time)) {
|
||||
throw new TypeError("rectification packet received duplicate time-linked scan samples");
|
||||
}
|
||||
linkedIndexes.add(link.sampleIndex);
|
||||
byTime.set(time, { time, sample });
|
||||
}
|
||||
return [...byTime.values()];
|
||||
}
|
||||
|
||||
function midpoint(startTime: string, endTime: string): string {
|
||||
const start = timeToMinute(startTime);
|
||||
let end = timeToMinute(endTime);
|
||||
@@ -129,11 +191,14 @@ function eventDomain(domain: CandidateResult["evidence"][number]["domain"]): Rec
|
||||
return domain === "finance" || domain === "health_pressure" ? "other" : domain;
|
||||
}
|
||||
|
||||
function layerEvidence(input: PacketInput): RectificationLayerEvidence[] {
|
||||
function layerEvidence(
|
||||
samples: readonly RectificationQuestionnaire["samples"][number][],
|
||||
consultation: ServerComputedRectificationConsultation,
|
||||
): RectificationLayerEvidence[] {
|
||||
return layerFields.map(([layer, field]) => ({
|
||||
layer,
|
||||
values: unique(input.scan.samples.map((sample) => sample[field] ?? "")),
|
||||
referenceIds: unique(input.consultation.layerReferences[layer] ?? []),
|
||||
values: unique(samples.map((sample) => sample[field] ?? "")),
|
||||
referenceIds: unique(consultation.layerReferences[layer] ?? []),
|
||||
})).filter((item) => item.values.length > 0);
|
||||
}
|
||||
|
||||
@@ -144,7 +209,7 @@ function suggestedDomains(layers: readonly RectificationLayerEvidence[]): Sugges
|
||||
return [{
|
||||
domain,
|
||||
layer: item.layer,
|
||||
reason: `${item.layer} 在候选范围内呈现 ${item.values.join(" / ")} 差异,可用已发生的${domain}事件区分。`,
|
||||
reason: `${item.layer} 在候选范围内呈现 ${item.values.join(" / ")} 差异,可用已发生的${domainLabels[domain]}事件区分。`,
|
||||
}];
|
||||
});
|
||||
}
|
||||
@@ -159,7 +224,14 @@ export function buildRectificationTechnicalPacket(input: PacketInput): Rectifica
|
||||
: input.candidateDifferences.packet.currentRange;
|
||||
const representativeTime = eventSegment?.representativeTime
|
||||
?? midpoint(range.startTime, range.endTime);
|
||||
const layers = layerEvidence(input);
|
||||
const selectedSamples = timeLinkedSamples(input.scan, input.consultation.timeLinkedScanSamples)
|
||||
.filter((item) => timeIsInsideRange(item.time, range.startTime, range.endTime));
|
||||
if (selectedSamples.length < 2) {
|
||||
throw new TypeError(
|
||||
"rectification packet requires two time-linked scan samples inside the selected candidate range",
|
||||
);
|
||||
}
|
||||
const layers = layerEvidence(selectedSamples.map((item) => item.sample), input.consultation);
|
||||
const d1 = layers.find((item) => item.layer === "D1");
|
||||
const d1Stability = !d1 ? "unavailable" : d1.values.length === 1 ? "stable" : "sensitive";
|
||||
const available = new Set(input.consultation.availableLayers);
|
||||
@@ -168,7 +240,9 @@ export function buildRectificationTechnicalPacket(input: PacketInput): Rectifica
|
||||
&& available.has(item.layer));
|
||||
const domains = suggestedDomains(sensitiveLayers);
|
||||
if (domains.length < 2) {
|
||||
throw new TypeError("rectification packet requires two server-computed discriminating domains");
|
||||
throw new TypeError(
|
||||
"rectification packet requires two time-linked discriminating domains inside the selected candidate range",
|
||||
);
|
||||
}
|
||||
const scoredHistoricalEvidence = (input.eventScore?.evidence ?? []).map((item) => ({
|
||||
evidenceId: item.eventId,
|
||||
@@ -177,7 +251,12 @@ export function buildRectificationTechnicalPacket(input: PacketInput): Rectifica
|
||||
score: item.points,
|
||||
ruleRefs: [...item.ruleIds],
|
||||
}));
|
||||
const opportunityRefs = input.candidateDifferences.packet.opportunities.map((item) => item.opportunityId);
|
||||
const scanRange = input.candidateDifferences.packet.currentRange;
|
||||
const selectedRangeMatchesScan = range.startTime === scanRange.startTime
|
||||
&& range.endTime === scanRange.endTime;
|
||||
const opportunityRefs = selectedRangeMatchesScan
|
||||
? input.candidateDifferences.packet.opportunities.map((item) => item.opportunityId)
|
||||
: [];
|
||||
const ruleRefs = scoredHistoricalEvidence.flatMap((item) => item.ruleRefs);
|
||||
const layerRefs = layers.flatMap((item) => item.referenceIds);
|
||||
const modelVersion = input.candidateDifferences.candidateModel.version;
|
||||
@@ -205,6 +284,12 @@ export function buildRectificationTechnicalPacket(input: PacketInput): Rectifica
|
||||
partitionIds,
|
||||
d1Stability,
|
||||
boundaryDistanceMinutes: input.consultation.boundaryDistanceMinutes,
|
||||
sensitivityScope: {
|
||||
source: "time_linked_candidate_scan_samples",
|
||||
rangeStart: range.startTime,
|
||||
rangeEnd: range.endTime,
|
||||
sampleTimes: selectedSamples.map((item) => item.time),
|
||||
},
|
||||
stableLayers: d1Stability === "stable" && d1 ? [d1] : [],
|
||||
sensitiveLayers,
|
||||
supportedSensitiveLayers: sensitiveLayers.map((item) => item.layer),
|
||||
@@ -228,6 +313,10 @@ export function projectRectificationTechnicalPacket(packet: RectificationTechnic
|
||||
calculationVersion: packet.calculationVersion,
|
||||
stableLayers: packet.stableLayers.map((item) => item.layer),
|
||||
sensitiveLayers: [...packet.supportedSensitiveLayers],
|
||||
sensitivityScope: {
|
||||
...packet.sensitivityScope,
|
||||
sampleTimes: [...packet.sensitivityScope.sampleTimes],
|
||||
},
|
||||
candidateDifferenceRefs: packet.candidateDifferenceRefs
|
||||
.filter((reference) => reference.trim().length > 0 && reference.length <= 120)
|
||||
.slice(0, 40),
|
||||
|
||||
Reference in New Issue
Block a user