Carry explicit local date intervals instead of inferring the day from clock order. Cluster width, delivery, adoption, and reports keep the actual civil date; adopted date is stored separately from the reported birth_date. Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation thresholds, and Skill version are unchanged. Isolated Linux final-3 gates passed; four pre-existing Python failures remain. This is not a production release.
141 lines
10 KiB
TypeScript
141 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
|
|
import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
|
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
|
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
|
import { engineRequestBody, runV9CandidateScore } from "../src/lib/rectification-agentic/v9/engine-client.ts";
|
|
import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts";
|
|
import { indistinguishableWidthMinutes } from "../src/lib/rectification-agentic/v9/candidate-plateau.ts";
|
|
import type { InferenceCandidate } from "../src/lib/rectification-agentic/core/types.ts";
|
|
|
|
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import { nakshatraBoundaryProbe } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
|
import { requestCandidateIntervals, parseCandidateIntervals } from "../src/lib/rectification-agentic/core/candidate-window.ts";
|
|
|
|
import { midnightSideChoice } from "../src/lib/rectification-agentic/v9/block-scan.ts";
|
|
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
|
import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
|
import { expectedAnswerSchemaFor } from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
|
import { CHOICE_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts";
|
|
import { CASE_ID, FOCUS_ID, SESSION_ID, TURN_ID, USER_ID, activeFocusFixture, dossierFixture, fakeAccounting, receiptHandlers } from "./rectification-v9-test-support.ts";
|
|
|
|
const intervals = [{ start_index: 0, end_index: 2, start_offset_minutes: 0, end_offset_minutes: 2,
|
|
start_at: "2000-06-14T23:58", end_at: "2000-06-15T00:00", segment_index: 0 }];
|
|
|
|
test("983 request explicitly anchors midnight center on declared date", () => {
|
|
const body = engineRequestBody({ baselineBirthSnapshot: {
|
|
birth_date: "2000-06-15", reported_birth_time: "00:10", birth_time_source: "approximate",
|
|
latitude: 0, longitude: 0, timezone_offset: 0,
|
|
}, candidateRange: { start_time: "23:55", end_time: "00:25" }, events: [{
|
|
id: "00000000-0000-4000-8000-000000000001", domain: "career", event_kind: "career_entry",
|
|
date_start: "2020-01-01", date_end: "2020-01-01", precision: "day", summary: "fictional",
|
|
}] });
|
|
assert.deepEqual(body.candidate_intervals, [{ start_at: "2000-06-14T23:55", end_at: "2000-06-15T00:25" }]);
|
|
});
|
|
|
|
test("982 credible union uses ordinal endpoints, not clock min/max", () => {
|
|
const row: InferenceCandidate = { id: "a", time: "23:59", cluster_range: ["23:58", "00:00"],
|
|
cluster_intervals: intervals, window_index: 1, window_offset_minutes: 1,
|
|
candidate_date: "2000-06-14", prior_score: 10, posterior_score: 10,
|
|
probability: 1, status: "active", rank: 1, strong_conflict_count: 0 };
|
|
assert.deepEqual(unionStillValidRange([row as InferenceCandidate]), ["23:58", "00:00"]);
|
|
});
|
|
|
|
test("nakshatra earlier/later groups follow civil ordinals across midnight", () => {
|
|
const state = buildInferenceState({ range_start: "23:58", range_end: "00:00", events: [], probes: [],
|
|
candidates: ["23:58", "23:59", "00:00"].map((time, index) => ({ id: time, time, relative_support: 50,
|
|
candidate_date: index === 2 ? "2000-06-15" : "2000-06-14", window_index: index,
|
|
window_offset_minutes: index, segment_index: 0, cluster_intervals: intervals })) });
|
|
const probe = nakshatraBoundaryProbe(state, { near_boundary: true, user_meaning: null, options: [
|
|
{ key: "A", time_bias: "earlier", traits: ["直接"] }, { key: "B", time_bias: "later", traits: ["谨慎"] },
|
|
] });
|
|
assert.deepEqual(probe?.expected_outcomes[0]?.supports, ["23:58", "23:59"]);
|
|
assert.deepEqual(probe?.expected_outcomes[1]?.supports, ["00:00"]);
|
|
});
|
|
|
|
test("narrowed previous-day window never reanchors to declaration day", () => {
|
|
assert.deepEqual(requestCandidateIntervals({ birth_date: "2000-03-01", reported_birth_time: "00:10" },
|
|
{ start_time: "23:55", end_time: "23:59" }), [{ start_at: "2000-02-29T23:55", end_at: "2000-02-29T23:59" }]);
|
|
assert.equal(parseCandidateIntervals([{ start_at: "2000-02-30T00:00", end_at: "2000-03-01T00:00" }]), null);
|
|
});
|
|
|
|
for (const option of ["A", "B", "C", "skip_probe"] as const) test(`D1 initial no-result action ${option} reaches dated RPC once`, async () => {
|
|
const range = { start_time: "23:00", end_time: "03:59", midnight_side_pending: true, candidate_intervals: [
|
|
{ start_at: "2000-06-15T00:00", end_at: "2000-06-15T03:59" },
|
|
{ start_at: "2000-06-15T23:00", end_at: "2000-06-15T23:59" },
|
|
] };
|
|
const midnight = midnightSideChoice(range)!;
|
|
const plan = buildMethodFollowupPlan({ evidence: [], candidateRange: range, caseStage: "block_scan" });
|
|
assert.equal(plan.next_followup?.midnight_side, true);
|
|
const schema = expectedAnswerSchemaFor(midnight.frame, midnight.frame.question_id, null, plan.next_followup!);
|
|
assert.ok(schema, "initial followup must produce a real renderable focus schema");
|
|
let current = dossierFixture({ stage: "block_scan", candidateRange: range, latestResult: null, evidence: [],
|
|
conversationSummary: { active_focus: activeFocusFixture({ intent: "choose_birth_block", questionId: midnight.frame.question_id,
|
|
expectedAnswerSchema: schema }), declined_skipped_topics: [] } });
|
|
const accounting = fakeAccounting({ ...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => current,
|
|
advance_agentic_rectification_dated_window: (_fn, args) => {
|
|
const narrowed = { start_time: String(args.p_start_time), end_time: String(args.p_end_time),
|
|
candidate_intervals: args.p_candidate_intervals, midnight_side_pending: false };
|
|
current = { ...current, case: { ...current.case, candidate_range: narrowed } };
|
|
return { candidate_range: narrowed };
|
|
},
|
|
apply_agentic_rectification_choice_action: (_fn, args) => ({ status: "applied", idempotent: false, question_id: args.p_question_id,
|
|
option_id: args.p_option_id, revision: 1, narration: args.p_narration, focus_status: args.p_focus_status }),
|
|
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
|
|
set_agentic_rectification_conversation_focus: () => ({ focus: null, idempotent: false }),
|
|
});
|
|
await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID,
|
|
actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: option, expectedRevision: 0 });
|
|
const calls = accounting.calls.filter((item) => item.fn === "advance_agentic_rectification_dated_window");
|
|
assert.equal(calls.length, 1);
|
|
assert.equal((calls[0].args.p_candidate_intervals as unknown[]).length, option === "A" || option === "B" ? 1 : 2);
|
|
assert.equal(midnightSideChoice(current.case.candidate_range), null);
|
|
});
|
|
|
|
test("native golden crosses engine parsing, inference and dated delivery without reanchoring", async () => {
|
|
const golden = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url), "utf8"));
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async () => new Response(JSON.stringify(golden.response), { status: 200 })) as typeof fetch;
|
|
try {
|
|
const request = golden.request;
|
|
const range = { start_time: request.start_time, end_time: request.end_time, candidate_intervals: request.candidate_intervals };
|
|
const scored = await runV9CandidateScore({ baselineBirthSnapshot: { birth_date: request.birth_date,
|
|
latitude: request.lat, longitude: request.lon, timezone_offset: request.tz }, candidateRange: range, events: request.events });
|
|
const inference = buildCaseInferenceState({ range, candidates: scored.candidates, evidence: [], probes: [],
|
|
transitions: scored.windowScan?.transitions });
|
|
const delivery = buildRangeDelivery({ inference, publicCandidates: scored.candidates, windowScan: scored.windowScan });
|
|
const result = parseRectificationCandidateResult({ ...scored, resultId: scored.engineResultId,
|
|
decisionReceipt: { ...scored.decisionReceipt, inference_state: inference }, rangeDelivery: delivery });
|
|
assert.ok(result);
|
|
assert.equal(result.declaredBirthDate, request.birth_date);
|
|
assert.deepEqual(result.candidates.map((row) => row.candidate_date), scored.candidates.map((row) => row.candidate_date));
|
|
assert.equal(result.credibleIntervals?.[0]?.start_at, "2000-02-29T23:58");
|
|
assert.ok(delivery.columns.length > 0, "cross-midnight range must not filter out every candidate");
|
|
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
|
result, acceptingCandidateId: null, readonly: true, onAccept: () => undefined,
|
|
}));
|
|
assert.match(html, /前一天/);
|
|
assert.match(html, /2000-02-29/);
|
|
assert.match(html, /原填报日期保留/);
|
|
assert.doesNotMatch(html, /2000-03-02/);
|
|
} finally { globalThis.fetch = originalFetch; }
|
|
});
|
|
|
|
for (const invalid of [null, [], {}, "", [{ start_at: "2000-03-01T00:00" }]]) test(`explicit invalid intervals never fall back: ${JSON.stringify(invalid)}`, () => {
|
|
assert.throws(() => engineRequestBody({ baselineBirthSnapshot: { birth_date: "2000-03-01", latitude: 0, longitude: 0, timezone_offset: 0 },
|
|
candidateRange: { start_time: "23:58", end_time: "00:02", candidate_intervals: invalid } as never, events: [{
|
|
id: "00000000-0000-4000-8000-000000000001", domain: "career", event_kind: "career_entry",
|
|
date_start: "2020-01-01", date_end: "2020-01-01", precision: "day", summary: "fictional",
|
|
}] }), /invalid_candidate_intervals/);
|
|
});
|
|
|
|
test("982 plateau width retains actual minutes across midnight", () => {
|
|
assert.equal(indistinguishableWidthMinutes([{ time: "23:59", rank: 1, tiedMinuteCount: 1,
|
|
clusterStart: "23:58", clusterEnd: "00:00", clusterIntervals: intervals }]), 3);
|
|
});
|