fix(web): offer representative time after occupation notes cover

Dateless occupation_note stayed draft, so classic coverage never finished
and offer-candidates stayed blocked. Confirm those notes, stop crowding
dasha probes with encoded exam quality, and adopt once blocking methods
are covered.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-23 17:21:49 +08:00
co-authored by Cursor
parent 6b225a288c
commit 75dbab08b9
11 changed files with 774 additions and 28 deletions
@@ -80,6 +80,7 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
assert.match(migration.stdout, /applied 20260818010000_admin_product_catalog_mutations\.sql/);
assert.match(migration.stdout, /applied 20260819010000_rectification_ingest_precision_plateau\.sql/);
assert.match(migration.stdout, /applied 20260822010000_declared_birth_window_clocks\.sql/);
assert.match(migration.stdout, /applied 20260823010000_rectification_occupation_dateless\.sql/);
assert.equal(
fixture.psql(`
@@ -372,3 +372,86 @@ test("distinguish follow-up copy forbids competing-chart ranking", () => {
assert.match(plan.next_followup?.user_prompt_hint ?? "", /自己写题干/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不得发明年份/);
});
test("GET A/B card stays hidden once representative time can be offered", () => {
const card = projectRectificationChoiceCard({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "relationship",
datePrecision: "year",
occurredFrom: "2018-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
}, {
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2020-01-01",
occurredTo: null,
}, {
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
}],
eventProbes: [MOVE_PROBE],
selectionAllowed: true,
proposeAllowed: true,
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "relocation",
targetKind: "home_change",
expectedAnswerSchema: { choice: SAMPLE_COPY },
},
});
assert.equal(card, null);
});
test("GET reverse-verify card still appears after a time is accepted", () => {
const card = projectRectificationChoiceCard({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "day",
occurredFrom: "2024-04-07",
occurredTo: null,
}],
accepted: true,
sessionOutcome: "adopt_representative",
selectionAllowed: true,
proposeAllowed: true,
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify",
}],
activeFocus: {
intent: "reverse_verify",
targetDomain: "career",
targetKind: "career_change",
expectedAnswerSchema: { choice: SAMPLE_COPY },
},
});
assert.equal(card?.prompt, SAMPLE_COPY.prompt);
assert.ok(parseRectificationChoiceCard(card));
});
@@ -291,6 +291,20 @@ test("adopt_representative defers method follow-up instead of asking this turn",
assert.equal(plan.session_outcome, "adopt_representative");
});
test("adopt_representative ignores leftover distinguish focus", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
sessionOutcome: "adopt_representative",
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "relocation",
targetKind: "home_change",
},
});
assert.equal(plan.next_followup, null);
assert.equal(plan.session_outcome, "adopt_representative");
});
test("declined relationship skips to career and leaves horary uncovered", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
@@ -993,6 +1007,123 @@ test("career evidence does not cover occupation; occupation still blocks until a
}), "collect_evidence");
});
test("draft occupation_note without a date covers occupation and unblocks offering", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "职业类型轨迹为技术开发",
},
],
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
});
test("stale occupation collect focus does not keep interviewing after occupation is covered", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "前端工程师",
},
],
activeFocus: {
intent: "collect_method_evidence",
targetDomain: "occupation",
targetKind: "occupation_note",
},
});
assert.notEqual(plan.next_followup?.method_id, "active_focus");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
});
test("event_probe does not block offering once blocking methods are covered", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。",
role: "reverse_verify",
}],
});
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
});
test("accepted time reverse-verifies an uncovered year in a covered domain", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "day",
occurredFrom: "2024-04-07",
occurredTo: null,
}],
accepted: true,
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify",
}],
});
assert.equal(plan.next_followup?.source, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后");
});
test("declining occupation covers the method; declining horary is skipped_by_policy", () => {
const plan = buildMethodFollowupPlan({
evidence: [
+32 -1
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
@@ -228,3 +228,34 @@ test("receipt allowlist includes D5, D7 and D3 public methods", () => {
);
assert.match(d3Migration, /'d3-drekkana'/);
});
test("dateless occupation_note confirms on write and backfills existing drafts", () => {
const migration = readFileSync(
new URL("../supabase/migrations/20260823010000_rectification_occupation_dateless.sql", import.meta.url),
"utf8",
);
assert.match(migration, /^begin;[\s\S]*^commit;$/m);
assert.match(
migration,
/create or replace function public\.agentic_rectification_allows_dateless_confirm\(p_kind text\)/,
);
assert.match(migration, /p_kind in \('occupation_note', 'appearance_note', 'birthmark_or_scar'\)/);
assert.match(
migration,
/if \(v_precision = 'unknown' or v_from is null\)\s+and not public\.agentic_rectification_allows_dateless_confirm\(v_kind\) then/,
);
assert.match(
migration,
/elsif public\.agentic_rectification_allows_dateless_confirm\(v_kind\)\s+and \(v_precision = 'unknown' or v_from is null\) then/,
);
assert.match(
migration,
/where event_kind in \('occupation_note', 'appearance_note', 'birthmark_or_scar'\)[\s\S]*status in \('draft', 'pending_confirmation'\)/,
);
assert.match(migration, /grant execute on function public\.record_agentic_rectification_evidence_batch/);
assert.equal(
existsSync(new URL("../db/migrations/20260823010000_rectification_occupation_dateless.sql", import.meta.url)),
false,
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
);
});