feat(consult): annual and timing turns get the shared reading and annual list (product budget decision 2026-10-02)

Annual / timing single-domain turns take the career / marriage / wealth budget:
contract + card <= 12,000, total with the checklist <= 18,000.
CONSULT_READING_BUDGET_BLOCKED removed; size assertions and the pinned
blocker test changed (原值/新值/原因 in the tests and brief 2's progress record);
BUG-1160/1161 resolved, BLOCKED.md entry struck through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-02 00:46:59 +08:00
co-authored by Claude Opus 5.5
parent 071a196d11
commit ec06d422d3
7 changed files with 67 additions and 41 deletions
+6 -20
View File
@@ -65,20 +65,10 @@ const domainMethodology: Readonly<Record<ConsultationDomain, { strictRoute: stri
children: { strictRoute: "consult-children", eventJudgment: null },
};
/**
* BLOCKED on a product decision (TASK-consult-affliction-reading-20261001,
* BUG-1160/1161): a single-domain annual or timing turn is already within a few
* hundred characters of the 12,000-character model-visible ceiling asserted by
* consult-condensed-checklist-20260927 and consult-evidence-card-20260927
* (annual up to 11,918 before this brief). The shared reading (733 characters
* as a section) and the annual list (334) would take annual to 12,979 and timing
* to 12,257, and those assertions are not this brief's to change. Until the
* product re-decides the ceiling, a turn whose plan holds only these domains
* keeps the pre-brief method: no shared reading, and annual's list is not
* delivered (annual stays in domains_without_strict_checklist). Remove a domain
* from this set once its budget is decided.
*/
export const CONSULT_READING_BUDGET_BLOCKED: ReadonlySet<ConsultationDomain> = new Set(["annual", "timing"]);
// Annual and timing single-domain turns used to skip the shared reading and the
// annual list on a 12,000-character ceiling (BUG-1160/1161). Product decision
// 2026-10-02: they take the career / marriage / wealth budget instead (contract
// + card <= 12,000, total with the checklist <= 18,000), so every turn gets both.
/** Domains whose list was written by the affliction-reading brief rather than condensed from the router. */
const BRIEF_WRITTEN_DOMAINS: ReadonlySet<ConsultationDomain> = new Set([
@@ -209,17 +199,13 @@ export function consultationMethodologyForDomains(
};
// Once per turn, before anything else (T1), however many domains it plans.
if (unique.some((domain) => !CONSULT_READING_BUDGET_BLOCKED.has(domain))) {
push(CONDENSED_SHARED_READING_TITLE, CONDENSED_SHARED_READING_SOURCE, sharedConsultationReading());
}
push(CONDENSED_SHARED_READING_TITLE, CONDENSED_SHARED_READING_SOURCE, sharedConsultationReading());
push("Shared mandatory baseline", ROUTER_FILE, markdownSection(router, SHARED_BASELINE_HEADING));
const withoutChecklist: ConsultationDomain[] = [];
for (const domain of unique) {
const { strictRoute, eventJudgment } = domainMethodology[domain];
const condensed = BRIEF_WRITTEN_DOMAINS.has(domain) && CONSULT_READING_BUDGET_BLOCKED.has(domain)
? null
: condensedConsultationChecklist(domain, { gender });
const condensed = condensedConsultationChecklist(domain, { gender });
if (condensed) {
push(
`${consultationDomainDefinition(domain).label} · ${strictRoute} · 普通对话精简清单`,
@@ -16,10 +16,7 @@ import {
CONSULT_BRIEF_CHECKLIST_SOURCE,
CONSULTATION_CONDENSED_CHECKLISTS,
} from "../src/lib/consultation-condensed-checklist.ts";
import {
CONSULT_READING_BUDGET_BLOCKED,
consultationMethodologyForDomains,
} from "../src/lib/consultation-methodology.ts";
import { consultationMethodologyForDomains } from "../src/lib/consultation-methodology.ts";
import {
consultationDomainDefinition,
consultationDomainIds,
@@ -43,7 +40,10 @@ const golden = JSON.parse(readFileSync(
const titles = (domains: ConsultationDomain[]) => consultationMethodologyForDomains(domains)!.sections.map((section) => section.title);
test("T1: the shared reading is the first section, once per turn, whatever the plan", () => {
for (const domain of consultationDomainIds.filter((id) => !CONSULT_READING_BUDGET_BLOCKED.has(id))) {
// 原值: consultationDomainIds.filter((id) => !CONSULT_READING_BUDGET_BLOCKED.has(id))(年运、时运除外)
// 新值: 全部 12 个领域
// 原因: 产品 2026-10-02 决定年运 / 时运按事业 / 婚姻 / 财富同口径计字数(BUG-1160/1161 解除)
for (const domain of consultationDomainIds) {
const list = titles([domain]);
assert.equal(list[0], CONDENSED_SHARED_READING_TITLE, domain);
assert.equal(list.filter((title) => title === CONDENSED_SHARED_READING_TITLE).length, 1, domain);
@@ -62,16 +62,23 @@ test("T1: the shared reading is the first section, once per turn, whatever the p
assert.equal(CONDENSED_SHARED_READING_LINES.length, 7);
});
test("T1/T2 budget blocker is pinned: a single annual or timing turn keeps its pre-brief method", () => {
// BLOCKED on the 12,000-character ceiling (see CONSULT_READING_BUDGET_BLOCKED); this
// test documents the gap so lifting it is a deliberate change.
assert.deepEqual([...CONSULT_READING_BUDGET_BLOCKED].sort(), ["annual", "timing"]);
assert.deepEqual(titles(["annual"]), ["Shared mandatory baseline"]);
assert.deepEqual(consultationMethodologyForDomains(["annual"])!.domains_without_strict_checklist, ["annual"]);
assert.deepEqual(titles(["timing"]), ["Shared mandatory baseline", "时运 · event-timing-strict"]);
// 原值: test "T1/T2 budget blocker is pinned: a single annual or timing turn keeps its pre-brief method"
// (年运单领域只有 Shared mandatory baseline、年运列在 domains_without_strict_checklist;时运无通用读法)
// 新值: 年运、时运单领域轮第一段是通用读法;年运带本产品中文清单,时运带英文路由
// 原因: 产品 2026-10-02 决定年运 / 时运字数口径同事业 / 婚姻 / 财富(契约 + 卡 ≤ 12,000,含清单 ≤ 18,000),阻塞解除
test("annual and timing single-domain turns get the shared reading, and annual its list (budget decided 2026-10-02)", () => {
assert.deepEqual(titles(["annual"]), [CONDENSED_SHARED_READING_TITLE, "Shared mandatory baseline", "年运 · consult-annual · 普通对话精简清单"]);
const annual = consultationMethodologyForDomains(["annual"])!;
assert.deepEqual(annual.domains_without_strict_checklist, []);
assert.equal(annual.sections[2]!.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
for (const line of CONSULTATION_CONDENSED_CHECKLISTS.annual!) assert.ok(annual.sections[2]!.text.includes(line), line.slice(0, 20));
assert.deepEqual(titles(["timing"]), [CONDENSED_SHARED_READING_TITLE, "Shared mandatory baseline", "时运 · event-timing-strict"]);
});
test("T2: every domain section, in order, and no domain without a list (annual pending the budget)", () => {
// 原值: 标题「…(annual pending the budget)」,annual 期望 "",年运 / 时运跳过
// 新值: annual / timing 一并核对;annual 期望本产品清单标题
// 原因: 产品 2026-10-02 字数口径决定(BUG-1160/1161 解除)
test("T2: every domain section, in order, and no domain without a list", () => {
const expected: Record<ConsultationDomain, string> = {
career: "事业 · career-timing-strict · 普通对话精简清单",
marriage: "关系 · relationship-timing-strict · 普通对话精简清单",
@@ -82,13 +89,12 @@ test("T2: every domain section, in order, and no domain without a list (annual p
family: "家庭 · consult-family · 普通对话精简清单",
parents: "父母 · consult-parents · 普通对话精简清单",
children: "子女 · consult-children · 普通对话精简清单",
annual: "",
annual: "年运 · consult-annual · 普通对话精简清单",
timing: "时运 · event-timing-strict",
general: "综合 · consult-general · 普通对话精简清单",
};
for (const domain of consultationDomainIds) {
const methodology = consultationMethodologyForDomains([domain])!;
if (CONSULT_READING_BUDGET_BLOCKED.has(domain)) continue;
assert.deepEqual(methodology.sections.map((section) => section.title), [
CONDENSED_SHARED_READING_TITLE, "Shared mandatory baseline", expected[domain],
], domain);
@@ -167,7 +167,10 @@ test("only the consultation tool builds the methodology: reports and other surfa
assert.deepEqual(condensedUsers, ["lib/consultation-methodology.ts"]);
});
test("size: every single-domain turn stays within 12,000 characters, contract and checklist included", async () => {
// 原值: 标题「every single-domain turn stays within 12,000 characters, contract and checklist included」;全部 12 个领域 total ≤ 12,000
// 新值: 年运、时运按事业 / 婚姻 / 财富同口径:契约 + 卡 ≤ 12,000、含清单 total ≤ 18,000;其余领域 total 仍 ≤ 12,000
// 原因: 产品 2026-10-02 决定(BUG-1160/1161):年运 / 时运单领域轮加通用读法与年运清单后约 12,980,原口径无余量
test("size: single-domain turns stay within 12,000 characters with the checklist; annual and timing within 12,000 without it and 18,000 with it", async () => {
const serverChart = {
name: "public",
toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" },
@@ -188,7 +191,13 @@ test("size: every single-domain turn stays within 12,000 characters, contract an
const view = await tools["run-jyotish-consultation"].execute!({ question: "q", domains: [domain] } as never, { writer: { custom: async () => {} } } as never);
const total = JSON.stringify(view).length;
assert.equal(state.modelVisibleChars, total);
assert.ok(total <= 12_000, `${chart.id}/${domain}: ${total}`);
if (domain === "annual" || domain === "timing") {
const contractAndCard = JSON.stringify({ ...(view as Json), methodology: undefined }).length;
assert.ok(contractAndCard <= 12_000, `${chart.id}/${domain}: contract + card ${contractAndCard}`);
assert.ok(total <= 18_000, `${chart.id}/${domain}: ${total}`);
} else {
assert.ok(total <= 12_000, `${chart.id}/${domain}: ${total}`);
}
}
}
});
@@ -274,7 +274,10 @@ test("the 「本轮技法」 rows are exactly the rows the full result produced
});
test("model-visible size falls to card scale; only the method checklist can push it past 12,000", async () => {
const methodHeavy = new Set(["career", "marriage", "wealth"]);
// 原值: new Set(["career", "marriage", "wealth"])
// 新值: 加 annual、timing(契约 + 卡 ≤ 12,000,含清单 ≤ 18,000)
// 原因: 产品 2026-10-02 决定年运 / 时运与事业 / 婚姻 / 财富同口径,通用读法与年运清单下发(BUG-1160/1161)
const methodHeavy = new Set(["career", "marriage", "wealth", "annual", "timing"]);
for (const chart of golden.charts) {
for (const domain of consultationDomainIds) {
const { view, state } = await runTool(chart.workflow, [domain]);