fix(web): stop rectification interview when remaining minutes no longer split
Coverage-complete ties stayed in discrimination because whole-window D9/D24 follow-ups were treated as probes, and restated dates inserted duplicate evidence. Skip encoded remaining layers, ask leftover D4 or offer a provisional range, and dedupe dated rows by kind and date. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
parseAgentChoiceCopy,
|
||||
parseChoiceKeyFromUserMessage,
|
||||
parseRectificationChoiceCard,
|
||||
serverOwnedChoiceCopy,
|
||||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import { buildMethodFollowupPlan, projectRectificationChoiceCard } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||||
@@ -74,6 +75,22 @@ test("choice frames ask one biographical event from a server probe, not competin
|
||||
assert.equal(mergeChoiceCard(frame, null), null);
|
||||
});
|
||||
|
||||
test("server-owned card names the event family, not a generic 有没有这件事", () => {
|
||||
const frame = buildChoiceFrame(
|
||||
{
|
||||
method_id: "d4_home",
|
||||
ask_theme: "home_change",
|
||||
domain: "relocation",
|
||||
user_prompt_hint: "unused",
|
||||
},
|
||||
{ probes: [MOVE_PROBE] },
|
||||
);
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
assert.ok(copy);
|
||||
assert.match(copy.prompt, /搬家、离乡或长期异地/);
|
||||
assert.doesNotMatch(copy.prompt, /有没有这件事/);
|
||||
});
|
||||
|
||||
test("the visible card uses Agent copy for A/B/C/D, not the server hint", () => {
|
||||
const frame = buildChoiceFrame({
|
||||
method_id: "d4_home",
|
||||
@@ -580,6 +597,8 @@ test("GET choice_card stays after coverage when remaining minutes still split on
|
||||
propose_allowed: true,
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d9_lagna_count: 1,
|
||||
d10_lagna_count: 3,
|
||||
d10_candidates_differ: true,
|
||||
d10_sign_names: ["巨蟹座", "狮子座", "处女座"],
|
||||
d24_candidates_differ: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
buildCandidateContrastPacket,
|
||||
selectDiscriminatorProbe,
|
||||
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
@@ -111,7 +112,7 @@ test("missing method coverage stays in fact collection even if scores look separ
|
||||
assert.equal(next.type, "ask_fact_collection");
|
||||
});
|
||||
|
||||
test("D9/D10 sign differences synthesize a contrast probe when engine probes are empty", () => {
|
||||
test("whole-window D9/D10 signs do not synthesize a discriminator without remaining-minute splits", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "set-a",
|
||||
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
||||
@@ -120,11 +121,56 @@ test("D9/D10 sign differences synthesize a contrast probe when engine probes are
|
||||
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
|
||||
],
|
||||
});
|
||||
assert.equal(selectDiscriminatorProbe(packet), null);
|
||||
});
|
||||
|
||||
test("exam quality and occupation notes skip remaining D24/D10 and keep D4", () => {
|
||||
const evidence = [
|
||||
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
|
||||
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员 / 前端", status: "confirmed" },
|
||||
];
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
||||
candidateTimes: ["05:00", "05:03", "05:04"],
|
||||
transitions: [
|
||||
{ layer: "d4", at: "05:00" },
|
||||
{ layer: "d4", at: "05:03" },
|
||||
{ layer: "d10", at: "05:00" },
|
||||
{ layer: "d10", at: "05:03" },
|
||||
{ layer: "d24", at: "05:00" },
|
||||
{ layer: "d24", at: "05:03" },
|
||||
{ layer: "d9", at: "04:52" },
|
||||
{ layer: "d9", at: "05:08" },
|
||||
{ layer: "d5", at: "05:15" },
|
||||
],
|
||||
askedKeys: askedKeysFromLedgerEvidence(evidence),
|
||||
});
|
||||
const probe = selectDiscriminatorProbe(packet);
|
||||
assert.ok(probe);
|
||||
assert.ok(probe.expectedOutcomes.length >= 2);
|
||||
assert.equal(probe.sourceFeatures[0]?.calculationResultId, "22222222-2222-4222-8222-222222222222");
|
||||
assert.match(probe.question, /职业前事|事业盘/);
|
||||
assert.match(probe.semanticKey, /varga\.d4/);
|
||||
assert.equal(probe.domain, "relocation");
|
||||
assert.match(probe.question, /搬家|离乡/);
|
||||
});
|
||||
|
||||
test("encoded D24/D10/D4 remaining splits leave no discriminator", () => {
|
||||
const evidence = [
|
||||
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
|
||||
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员", status: "confirmed" },
|
||||
{ domain: "relocation", eventKind: "home_change", summary: "搬家离乡", status: "confirmed" },
|
||||
];
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
candidateTimes: ["05:00", "05:03", "05:04"],
|
||||
transitions: [
|
||||
{ layer: "d4", at: "05:00" },
|
||||
{ layer: "d4", at: "05:03" },
|
||||
{ layer: "d10", at: "05:00" },
|
||||
{ layer: "d24", at: "05:00" },
|
||||
],
|
||||
askedKeys: askedKeysFromLedgerEvidence(evidence),
|
||||
});
|
||||
assert.equal(selectDiscriminatorProbe(packet), null);
|
||||
});
|
||||
|
||||
test("remaining-candidate D24 split beats window D10 signs", () => {
|
||||
|
||||
@@ -3,6 +3,10 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, latestUserStoppedCollecting } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
buildCandidateContrastPacket,
|
||||
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
import {
|
||||
internalObservationsFromWindowScan,
|
||||
parseWindowScan,
|
||||
@@ -814,7 +818,7 @@ test("precision stage lagna_frame waits for uncovered career before asking anoth
|
||||
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
|
||||
});
|
||||
|
||||
test("lagna_frame after classic coverage still discriminates when candidates are tied", () => {
|
||||
test("lagna_frame after classic coverage does not keep a tie in discrimination without a remaining split", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: CLASSIC_COVERAGE,
|
||||
precisionStage: "lagna_frame",
|
||||
@@ -829,12 +833,13 @@ test("lagna_frame after classic coverage still discriminates when candidates are
|
||||
confirmationAllowed: false,
|
||||
nextFollowup: plan.next_followup,
|
||||
methods: plan.methods,
|
||||
discriminatorProbe: null,
|
||||
candidateScores: [
|
||||
{ time: "05:00", score: 34 },
|
||||
{ time: "05:01", score: 33 },
|
||||
{ time: "05:02", score: 33 },
|
||||
],
|
||||
}), "discriminate_candidates");
|
||||
}), "provisional_range");
|
||||
});
|
||||
|
||||
test("precision stage d4 asks home change not family, and d5 asks education", () => {
|
||||
@@ -1188,6 +1193,181 @@ test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/);
|
||||
});
|
||||
|
||||
const DUMP_COVERAGE = [
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "education",
|
||||
datePrecision: "month" as const,
|
||||
occurredFrom: "2016-09-01",
|
||||
occurredTo: null,
|
||||
eventKind: "education_start",
|
||||
summary: "上大学",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "education",
|
||||
datePrecision: "month" as const,
|
||||
occurredFrom: "2015-06-01",
|
||||
occurredTo: null,
|
||||
eventKind: "education_interruption",
|
||||
summary: "高考失利复读",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "relationship",
|
||||
datePrecision: "day" as const,
|
||||
occurredFrom: "2024-05-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_start",
|
||||
summary: "开始一段感情",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "relationship",
|
||||
datePrecision: "day" as const,
|
||||
occurredFrom: "2024-08-08",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_end",
|
||||
summary: "感情结束",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "career",
|
||||
datePrecision: "day" as const,
|
||||
occurredFrom: "2024-04-07",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
summary: "入职",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "family",
|
||||
datePrecision: "month" as const,
|
||||
occurredFrom: "2016-05-01",
|
||||
occurredTo: null,
|
||||
eventKind: "family_event",
|
||||
summary: "家人变化",
|
||||
},
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown" as const,
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
summary: "互联网程序员 / 前端 / Agent 开发",
|
||||
},
|
||||
];
|
||||
|
||||
const DUMP_TRANSITIONS = [
|
||||
{ layer: "d4", at: "05:00" },
|
||||
{ layer: "d4", at: "05:03" },
|
||||
{ layer: "d10", at: "05:00" },
|
||||
{ layer: "d10", at: "05:03" },
|
||||
{ layer: "d24", at: "05:00" },
|
||||
{ layer: "d24", at: "05:03" },
|
||||
{ layer: "d9", at: "04:52" },
|
||||
{ layer: "d9", at: "05:08" },
|
||||
{ layer: "d5", at: "05:15" },
|
||||
];
|
||||
|
||||
const DUMP_SCORES = [
|
||||
{ time: "05:00", score: 34 },
|
||||
{ time: "05:03", score: 33 },
|
||||
{ time: "05:04", score: 33 },
|
||||
];
|
||||
|
||||
test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollment or breakup", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
candidateTimes: DUMP_SCORES.map((item) => item.time),
|
||||
transitions: DUMP_TRANSITIONS,
|
||||
askedKeys: askedKeysFromLedgerEvidence(DUMP_COVERAGE),
|
||||
});
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: DUMP_COVERAGE,
|
||||
contrastPacket: packet,
|
||||
askedProbeKeys: askedKeysFromLedgerEvidence(DUMP_COVERAGE),
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "relocation");
|
||||
assert.equal(plan.next_followup?.kind_hint, "home_change");
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /varga\.d4/);
|
||||
assert.doesNotMatch(plan.next_followup?.kind_hint ?? "", /education_start|relationship_end/);
|
||||
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /大学哪年入学|高考是 \d{4}|哪年毕业/);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
nextFollowup: plan.next_followup,
|
||||
methods: plan.methods,
|
||||
discriminatorProbe: packet.probes[0] ?? null,
|
||||
candidateScores: DUMP_SCORES,
|
||||
}), "discriminate_candidates");
|
||||
});
|
||||
|
||||
test("coverage-complete tie with no remaining split offers a provisional range", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:04",
|
||||
candidateTimes: DUMP_SCORES.map((item) => item.time),
|
||||
transitions: DUMP_TRANSITIONS,
|
||||
askedKeys: askedKeysFromLedgerEvidence([
|
||||
...DUMP_COVERAGE,
|
||||
{
|
||||
status: "confirmed",
|
||||
domain: "relocation",
|
||||
eventKind: "home_change",
|
||||
summary: "搬家离乡",
|
||||
},
|
||||
]),
|
||||
});
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
...DUMP_COVERAGE,
|
||||
{
|
||||
status: "confirmed" as const,
|
||||
domain: "relocation",
|
||||
datePrecision: "year" as const,
|
||||
occurredFrom: "2016-09-01",
|
||||
occurredTo: null,
|
||||
eventKind: "home_change",
|
||||
summary: "搬家离乡",
|
||||
},
|
||||
],
|
||||
contrastPacket: packet,
|
||||
sessionOutcome: "provisional_range",
|
||||
});
|
||||
assert.equal(plan.next_followup, null);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
nextFollowup: {
|
||||
method_id: "d9_relationship",
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: "relationship_style",
|
||||
domain: "relationship",
|
||||
kind_hint: "relationship_change",
|
||||
user_prompt_hint: "当前候选在关系主题上仍分不开。",
|
||||
must_not_label: false,
|
||||
choice_frame: null,
|
||||
source: "varga_observation",
|
||||
},
|
||||
methods: plan.methods,
|
||||
discriminatorProbe: null,
|
||||
candidateScores: DUMP_SCORES,
|
||||
}), "provisional_range");
|
||||
const action = buildNextUserAction({
|
||||
scorableCount: 6,
|
||||
evidenceCount: 8,
|
||||
hasLatestResult: true,
|
||||
selectionAllowed: true,
|
||||
sessionOutcome: "provisional_range",
|
||||
nextFollowup: null,
|
||||
workingTime: "05:00",
|
||||
});
|
||||
assert.equal(action.id, "offer_provisional_range");
|
||||
});
|
||||
|
||||
test("没有了 is a user stop", () => {
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有了" }]), true);
|
||||
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没了" }]), true);
|
||||
@@ -1239,6 +1419,49 @@ test("same domain different year still asks a conflict probe", () => {
|
||||
assert.equal(plan.next_followup?.domain, "career");
|
||||
});
|
||||
|
||||
test("adjacent education year does not re-ask enrollment after a recorded start", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
eventKind: "education_start",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2016-09-01",
|
||||
occurredTo: null,
|
||||
summary: "2016年9月上大学",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
eventKind: "relationship_end",
|
||||
datePrecision: "day",
|
||||
occurredFrom: "2024-08-08",
|
||||
occurredTo: "2024-08-08",
|
||||
summary: "一段感情结束",
|
||||
}],
|
||||
eventProbes: [{
|
||||
year: 2015,
|
||||
year_label: "2015 年前后",
|
||||
domain: "education",
|
||||
event_family: "升学、高考、转学或学习环境变化",
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: "年份锁定 2015 年前后。事件家族:升学、高考、转学或学习环境变化。请写成一句自然语言是/否题。不得改年份。",
|
||||
role: "reverse_verify",
|
||||
information_gain: 0.21,
|
||||
semantic_key: "education.2015.dasha_activation",
|
||||
}],
|
||||
});
|
||||
assert.notEqual(plan.next_followup?.domain, "education");
|
||||
assert.notEqual(plan.next_followup?.source, "event_probe");
|
||||
assert.equal(plan.next_followup?.method_id, "d10_career");
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2016 年入学/);
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2024 年感情结束/);
|
||||
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不要再问这些事发生在哪一年/);
|
||||
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /高考是 2015/);
|
||||
});
|
||||
|
||||
test("high information_gain leftover probe still blocks offering after coverage", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
|
||||
@@ -1466,7 +1689,7 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("offer-candidates refuses a 34/33/33 tie after method coverage", async () => {
|
||||
test("offer-candidates allows a 34/33/33 tie after method coverage when remaining minutes do not split", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
@@ -1559,11 +1782,16 @@ test("offer-candidates refuses a 34/33/33 tie after method coverage", async () =
|
||||
turnId: TURN_ID,
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
await assert.rejects(
|
||||
() => (tools["rectification-offer-candidates"] as unknown as {
|
||||
execute(input: unknown): Promise<unknown>;
|
||||
}).execute({ caseId: CASE_ID }),
|
||||
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
|
||||
const projection = await (tools["rectification-offer-candidates"] as unknown as {
|
||||
execute(input: unknown): Promise<{ session_outcome: { kind: string } }>;
|
||||
}).execute({ caseId: CASE_ID });
|
||||
assert.equal(projection.session_outcome.kind, "provisional_range");
|
||||
assert.equal(
|
||||
accounting.calls.some((call) =>
|
||||
call.fn === "transition_agentic_rectification_case_status"
|
||||
&& call.args.p_status === "candidate_ready"
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -70,6 +70,29 @@ function candidates(scores: Readonly<Record<string, number>>): InferenceCandidat
|
||||
}));
|
||||
}
|
||||
|
||||
test("discriminating result does not stay in event_collection", () => {
|
||||
const state = buildInferenceState({
|
||||
range_start: "04:50",
|
||||
range_end: "05:10",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", relative_support: 34 },
|
||||
{ id: "05:03", time: "05:03", relative_support: 33 },
|
||||
{ id: "05:04", time: "05:04", relative_support: 33 },
|
||||
],
|
||||
events: [
|
||||
{ id: "e1", domain: "education", year: 2016, precision: "month" },
|
||||
{ id: "e2", domain: "career", year: 2018, precision: "year" },
|
||||
{ id: "e3", domain: "relationship", year: 2021, precision: "year" },
|
||||
{ id: "e4", domain: "family", year: 2023, precision: "year" },
|
||||
{ id: "e5", domain: "relocation", year: 2017, precision: "year" },
|
||||
],
|
||||
probes: [probe({ id: "open", gain: 0.3, yesSupports: ["05:00"], yesConflicts: ["05:03"] })],
|
||||
phase: "event_collection",
|
||||
});
|
||||
assert.equal(state.result_status, "discriminating");
|
||||
assert.equal(state.phase, "discrimination");
|
||||
});
|
||||
|
||||
test("an informative answer lowers entropy and cannot revive an eliminated candidate", () => {
|
||||
const conflict = probe({
|
||||
id: "p1",
|
||||
|
||||
@@ -73,8 +73,8 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
assert.match(prompt, /对用户说的话必须自己写在正文里,不要只写规划等服务器代写/);
|
||||
assert.match(prompt, /skill_verification_report/);
|
||||
assert.match(prompt, /ask_candidate_discriminator/);
|
||||
assert.match(prompt, /方法覆盖已齐只进入候选区分/);
|
||||
assert.doesNotMatch(prompt, /方法覆盖已齐且 propose_allowed 时本轮 adopt/);
|
||||
assert.match(prompt, /offer_provisional_range/);
|
||||
assert.match(prompt, /不要再问整窗 D9\/D24/);
|
||||
assert.match(prompt, /不得询问外貌、体质、胎记或疤痕/);
|
||||
assert.match(prompt, /不要调用 rectification-set-focus/);
|
||||
assert.match(prompt, /open_question\.prompt/);
|
||||
@@ -83,6 +83,8 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
assert.match(prompt, /verify_adopted_time/);
|
||||
assert.match(prompt, /event_probe/);
|
||||
assert.match(prompt, /不得发明年份/);
|
||||
assert.match(prompt, /不得根据出生年推算高考或入学年份/);
|
||||
assert.match(prompt, /不要再问那一件发生在哪一年/);
|
||||
assert.doesNotMatch(prompt, /两套盘各自的前事/);
|
||||
assert.doesNotMatch(prompt, /外貌、体质、胎记或疤痕可以问/);
|
||||
assert.doesNotMatch(prompt, /分盘句和宫位表由界面展示/);
|
||||
|
||||
@@ -1254,6 +1254,31 @@ test("PR-4 exposes only the explicit V2 service-role RPC signatures", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("kind-date evidence dedup stays out of the identity foundation", () => {
|
||||
const migration = readFileSync(
|
||||
new URL("../supabase/migrations/20260825010000_rectification_evidence_kind_date_dedup.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const dbCopy = fileURLToPath(
|
||||
new URL("../db/migrations/20260825010000_rectification_evidence_kind_date_dedup.sql", import.meta.url),
|
||||
);
|
||||
assert.equal(
|
||||
existsSync(dbCopy),
|
||||
false,
|
||||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||||
);
|
||||
assert.match(migration, /^begin;[\s\S]*^commit;$/m);
|
||||
assert.match(
|
||||
migration,
|
||||
/Same dated event restated with a new quote must not insert a second row/,
|
||||
);
|
||||
assert.match(
|
||||
migration,
|
||||
/e\.event_kind = v_kind[\s\S]*e\.occurred_from is not distinct from v_from/,
|
||||
);
|
||||
assert.match(migration, /grant execute on function public\.record_agentic_rectification_evidence_batch/);
|
||||
});
|
||||
|
||||
test("turn origin migration stays out of the identity foundation", () => {
|
||||
const turnOriginMigration = readFileSync(
|
||||
new URL("../supabase/migrations/20260824030000_rectification_turn_origin.sql", import.meta.url),
|
||||
|
||||
Reference in New Issue
Block a user