feat: define dynamic birth time choice protocol

This commit is contained in:
Jesse_Chen
2026-07-19 00:08:45 +08:00
parent 91e348f89b
commit cc74caf9da
7 changed files with 605 additions and 0 deletions
@@ -0,0 +1,135 @@
import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import { publicDynamicChoiceQuestionSchema } from "../src/lib/birth-time-dynamic-choice.ts";
import { persistedDynamicChoiceQuestionSchema } from "../src/lib/birth-time-dynamic-choice-internal.ts";
const internalQuestion = {
questionId: "11111111-1111-4111-8111-111111111111",
opportunityId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
dimensionCode: "career_change",
estimatedInformationGain: 0.7,
scoringVersion: "birth-time-choice-scoring-v2",
source: "fallback",
questionFingerprint: "question-fingerprint",
candidatePartitionFingerprint: "partition-fingerprint",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{
optionId: "22222222-2222-4222-8222-222222222222",
label: "2018—2020 年",
kind: "primary",
partitionId: "career-2018-2020",
candidateScores: { "09:00": 0.8 },
},
{
optionId: "33333333-3333-4333-8333-333333333333",
label: "2021—2023 年",
kind: "primary",
partitionId: "career-2021-2023",
candidateScores: { "09:30": 0.6 },
},
{
optionId: "44444444-4444-4444-8444-444444444444",
label: "不确定 / 不记得",
kind: "unknown",
partitionId: null,
candidateScores: null,
},
{
optionId: "55555555-5555-4555-8555-555555555555",
label: "都不符合",
kind: "unmatched",
partitionId: null,
candidateScores: null,
},
],
} as const;
function sourceFiles(directory: string): readonly string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory() ? sourceFiles(path) : [path];
});
}
test("public questions never expose partition ids", () => {
const parsed = publicDynamicChoiceQuestionSchema.parse({
questionId: "11111111-1111-4111-8111-111111111111",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "22222222-2222-4222-8222-222222222222", label: "2018—2020 年", kind: "primary" },
{ optionId: "33333333-3333-4333-8333-333333333333", label: "2021—2023 年", kind: "primary" },
{ optionId: "44444444-4444-4444-8444-444444444444", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "55555555-5555-4555-8555-555555555555", label: "都不符合", kind: "unmatched" },
],
});
assert.equal("partitionId" in parsed.options[0], false);
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
...parsed,
options: [{ ...parsed.options[0], partitionId: "private" }, ...parsed.options.slice(1)],
}).success, false);
});
test("internal primary choices require a server partition", () => {
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse(internalQuestion).success, true);
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse({
...internalQuestion,
options: internalQuestion.options.map((option) => option.kind === "primary"
? { optionId: option.optionId, label: option.label, kind: option.kind, partitionId: null }
: option),
}).success, false);
});
test("choice questions require a bounded complete option set", () => {
assert.equal(publicDynamicChoiceQuestionSchema.safeParse({
questionId: "11111111-1111-4111-8111-111111111111",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "22222222-2222-4222-8222-222222222222", label: "2018—2020 年", kind: "primary" },
{ optionId: "44444444-4444-4444-8444-444444444444", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "55555555-5555-4555-8555-555555555555", label: "都不符合", kind: "unmatched" },
],
}).success, false);
});
test("dynamic schemas accept opaque server-issued identifiers", () => {
const publicQuestion = {
questionId: "question-career-window",
prompt: "哪一个时间段更接近这次工作变化?",
options: [
{ optionId: "window-a", label: "2018—2020 年", kind: "primary" },
{ optionId: "window-b", label: "2021—2023 年", kind: "primary" },
{ optionId: "unknown", label: "不确定 / 不记得", kind: "unknown" },
{ optionId: "unmatched", label: "都不符合", kind: "unmatched" },
],
};
assert.equal(publicDynamicChoiceQuestionSchema.safeParse(publicQuestion).success, true);
assert.equal(persistedDynamicChoiceQuestionSchema.safeParse({
...internalQuestion,
...publicQuestion,
opportunityId: "career-window",
options: [
{ ...publicQuestion.options[0], partitionId: "window-a", candidateScores: { "09:00": 0.8 } },
{ ...publicQuestion.options[1], partitionId: "window-b", candidateScores: { "09:30": 0.6 } },
{ ...publicQuestion.options[2], partitionId: null, candidateScores: null },
{ ...publicQuestion.options[3], partitionId: null, candidateScores: null },
],
}).success, true);
});
test("public code never imports the internal dynamic choice contract", () => {
const publicSourceFiles = [
...sourceFiles(new URL("../src/components", import.meta.url).pathname),
...sourceFiles(new URL("../src/hooks", import.meta.url).pathname),
...sourceFiles(new URL("../src/lib", import.meta.url).pathname).filter((path) =>
path.includes("client") || path.endsWith("response-schema.ts")),
];
for (const path of publicSourceFiles) {
assert.equal(readFileSync(path, "utf8").includes("birth-time-dynamic-choice-internal"), false, path);
}
});
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import test from "node:test";
import { decideDynamicStop } from "../src/lib/birth-time-dynamic-stop-policy.ts";
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
const lowCandidate: CandidateResult = {
resultId: "11111111-1111-4111-8111-111111111111",
confidence: "low",
canApply: false,
winningSegment: null,
eventCount: 1,
domainCount: 1,
topScore: 10,
secondScore: 9,
marginPercent: 10,
reasons: ["Candidate scores remain close."],
evidence: [],
algorithmVersion: "birth-time-choice-scoring-v2",
};
const mediumCandidate: CandidateResult = {
...lowCandidate,
resultId: "22222222-2222-4222-8222-222222222222",
confidence: "medium",
marginPercent: 15,
};
function decisionFor(overrides: Partial<Parameters<typeof decideDynamicStop>[0]>) {
return decideDynamicStop({
result: lowCandidate,
effectiveAnswer: true,
previousResult: null,
priorPlateauCount: 0,
usefulOpportunityCount: 1,
repeatedOnly: false,
effectiveAnswerCount: 1,
...overrides,
});
}
function finishReason(overrides: Partial<Parameters<typeof decideDynamicStop>[0]>) {
const decision = decisionFor(overrides);
if (decision.kind !== "finish") assert.fail("expected a terminal decision");
return decision.reason;
}
test("two effective unchanged scores stop without starting another question", () => {
const decision = decideDynamicStop({
result: mediumCandidate,
effectiveAnswer: true,
previousResult: mediumCandidate,
priorPlateauCount: 1,
usefulOpportunityCount: 3,
repeatedOnly: false,
effectiveAnswerCount: 6,
});
assert.deepEqual(decision, { kind: "finish", reason: "plateau", plateauCount: 2 });
});
test("unknown answers do not advance plateau or the effective safety count", () => {
const decision = decideDynamicStop({
result: lowCandidate,
effectiveAnswer: false,
previousResult: lowCandidate,
priorPlateauCount: 1,
usefulOpportunityCount: 2,
repeatedOnly: false,
effectiveAnswerCount: 4,
});
assert.deepEqual(decision, { kind: "continue", plateauCount: 1 });
});
test("terminal conditions are deterministic", () => {
assert.equal(finishReason({ result: { ...lowCandidate, confidence: "high", canApply: true, winningSegment: {
startTime: "09:00", endTime: "09:05", representativeTime: "09:03", widthMinutes: 5,
}, eventCount: 4, domainCount: 3, marginPercent: 20 } }), "high_confidence");
assert.equal(finishReason({ usefulOpportunityCount: 0 }), "no_information_gain");
assert.equal(finishReason({ repeatedOnly: true }), "repeated_partition");
assert.equal(finishReason({ effectiveAnswerCount: 10 }), "safety_cap");
});
test("a two point margin change resets the plateau", () => {
const decision = decisionFor({
result: { ...mediumCandidate, marginPercent: 17 },
previousResult: mediumCandidate,
priorPlateauCount: 1,
});
assert.deepEqual(decision, { kind: "continue", plateauCount: 0 });
});