feat(consult): plain answer shape — conclusion first, one section per person, no word caps (BUG-1132~1134)
Retire the 09-17 opener shape (coined pattern name, push/tidy periods, play-a-role) that produced riddles; one shape definition in consultation-thinking-plan; parents card names mother/father roles with MK; empty-house rule; thinking bar closes the opener at any H2. 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:
co-authored by
Claude Opus 5.5
parent
c304cf3a02
commit
2ec2762dcf
@@ -49,6 +49,7 @@ import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-
|
||||
import {
|
||||
consultationContinueMessages,
|
||||
followUpAnswerShapeInstruction,
|
||||
natalAnswerShapeBody,
|
||||
hasPriorAssistantAnswer,
|
||||
natalUserTurnShape,
|
||||
natalConsultationThinkingPlan,
|
||||
@@ -1586,7 +1587,7 @@ export async function POST(request: Request) {
|
||||
// / follow-up split as the agentic path (BUG-1071), one statement of it.
|
||||
const legacyNatalInstruction = hasPriorAssistantAnswer(history, { summaryText: historyWindow.summaryText })
|
||||
? followUpAnswerShapeInstruction()
|
||||
: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」;开场不写行动。然后按三个二级标题写:## 盘里支持这个判断的地方(原始结构、六步宫位、Yoga 表作为依据,只写开场没说过的,表格与正文二选一)、## 时间怎么看、## 这周可以做的一件事(最多三条行动,只在这里出现一次)。全文不超过 900 字。骨架不可省略;不写「先回答你的问题」这个标题,文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。";
|
||||
: `${natalAnswerShapeBody()}骨架不可省略;文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。`;
|
||||
|
||||
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
|
||||
const legacyHistory = history.map((message) => message.role === "user"
|
||||
|
||||
@@ -43,6 +43,9 @@ export const EVIDENCE_CARD_LABELS = {
|
||||
genderMale: "男",
|
||||
/** The annual pack could not be produced reliably; natal data is never put in its place. */
|
||||
annualNotAttached: "年盘未接入",
|
||||
/** Who a parents-section entry is about (TASK-consult-plain-answer-20261001 D6). */
|
||||
roleMother: "母亲",
|
||||
roleFather: "父亲",
|
||||
} as const;
|
||||
|
||||
/** KP stays off every card: exact cusps are still blocked (decision table). */
|
||||
@@ -86,6 +89,24 @@ export type EvidenceCardDomainSpec = Readonly<{
|
||||
annualHouses: boolean;
|
||||
/** Vargas follow the other domain of the turn; `vargas` is the default when alone. */
|
||||
followVargas: boolean;
|
||||
/**
|
||||
* Who each piece of the section is about (parents: mother / father), so the
|
||||
* answer can keep the people apart (TASK-consult-plain-answer-20261001 D6).
|
||||
*/
|
||||
roles: readonly EvidenceCardRole[];
|
||||
}>;
|
||||
|
||||
export type EvidenceCardRole = Readonly<{
|
||||
key: string;
|
||||
label: string;
|
||||
house: number;
|
||||
planets: readonly string[];
|
||||
/** Chara karakas that must be on the card; a missing one is a gap. */
|
||||
karakas: readonly string[];
|
||||
/** Chara karakas taken only when the engine's scheme has them (PiK is 8-karaka only). */
|
||||
optionalKarakas: readonly string[];
|
||||
/** The varga whose same-numbered house is named for this role. */
|
||||
varga?: string;
|
||||
}>;
|
||||
|
||||
const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSpec, "vargas">): EvidenceCardDomainSpec => ({
|
||||
@@ -107,6 +128,7 @@ const spec = (value: Partial<EvidenceCardDomainSpec> & Pick<EvidenceCardDomainSp
|
||||
annualChart: false,
|
||||
annualHouses: false,
|
||||
followVargas: false,
|
||||
roles: [],
|
||||
...value,
|
||||
});
|
||||
|
||||
@@ -141,7 +163,13 @@ export const EVIDENCE_CARD_SPECS: Readonly<Record<ConsultationDomain, EvidenceCa
|
||||
education: spec({ vargas: ["D5", "D24"], houses: [5, 9], planets: ["Mercury", "Jupiter"] }),
|
||||
migration: spec({ vargas: ["D4", "D12"], houses: [4, 12] }),
|
||||
family: spec({ vargas: ["D7", "D12"], houses: [4, 5, 9] }),
|
||||
parents: spec({ vargas: ["D12"], houses: [4, 9], planets: ["Sun", "Moon"] }),
|
||||
parents: spec({
|
||||
vargas: ["D12"], houses: [4, 9], planets: ["Sun", "Moon"], karakas: ["MK"],
|
||||
roles: [
|
||||
{ key: "mother", label: EVIDENCE_CARD_LABELS.roleMother, house: 4, planets: ["Moon"], karakas: ["MK"], optionalKarakas: [], varga: "D12" },
|
||||
{ key: "father", label: EVIDENCE_CARD_LABELS.roleFather, house: 9, planets: ["Sun"], karakas: [], optionalKarakas: ["PiK"], varga: "D12" },
|
||||
],
|
||||
}),
|
||||
children: spec({ vargas: ["D7"], houses: [5], karakas: ["PK"], planets: ["Jupiter"] }),
|
||||
annual: spec({ vargas: ["D1"], houses: [1], annualChart: true, annualHouses: true, slowTransits: "key_dates" }),
|
||||
timing: spec({
|
||||
@@ -237,6 +265,8 @@ export type EvidenceCardDomainSection = {
|
||||
house_basis?: JsonRecord;
|
||||
/** The turn's other domain whose section already carries the vargas this section follows. */
|
||||
vargas_follow?: ConsultationDomain;
|
||||
/** Per-person index into this section (parents: mother / father). */
|
||||
roles?: JsonRecord;
|
||||
/**
|
||||
* Present only when this domain's own base differs from the card's base
|
||||
* (the first domain's). The same chart gives the same base; a difference is
|
||||
@@ -343,6 +373,23 @@ const FUNCTIONAL_KEYS = [
|
||||
const PERIOD_KEYS = ["lord", "start", "end"] as const;
|
||||
const NARAYANA_PERIOD_KEYS = ["sign", "lord", "years", "start_age", "end_age"] as const;
|
||||
const KARAKA_KEYS = ["planet", "degree_in_sign", "rank"] as const;
|
||||
|
||||
const ZODIAC_SIGNS = [
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
] as const;
|
||||
|
||||
/** The sign and occupants of house `number` counted from a varga's own lagna. */
|
||||
function vargaHouse(row: unknown, number: number): JsonRecord | undefined {
|
||||
const lagna = record(row).lagna;
|
||||
const start = typeof lagna === "string" ? ZODIAC_SIGNS.indexOf(lagna as (typeof ZODIAC_SIGNS)[number]) : -1;
|
||||
if (start < 0) return undefined;
|
||||
const sign = ZODIAC_SIGNS[(start + number - 1) % 12]!;
|
||||
const occupants = Object.entries(record(record(row).planets))
|
||||
.filter(([, value]) => value === sign)
|
||||
.map(([name]) => name);
|
||||
return { house: number, sign, occupants };
|
||||
}
|
||||
const ARUDHA_KEYS = ["sign", "lord"] as const;
|
||||
/** The engine writes the Arudha Lagna as pada A1 (its own name: "Arudha Lagna (AL)"). */
|
||||
const ARUDHA_ENGINE_KEY: Readonly<Record<string, string>> = { AL: "A1" };
|
||||
@@ -538,6 +585,37 @@ function domainSection(
|
||||
}
|
||||
section.karakas = karakas;
|
||||
}
|
||||
if (rule.roles.length) {
|
||||
const table = record(record(localLayers.jaimini).chara_karakas);
|
||||
const roles: JsonRecord = {};
|
||||
for (const role of rule.roles) {
|
||||
const houseRow = record(section.houses[String(role.house)]);
|
||||
// The role names its karakas; their values sit once in section.karakas.
|
||||
// A required one missing there is already a gap; an optional one (PiK)
|
||||
// joins section.karakas only when the engine's scheme has it.
|
||||
const karakas = role.karakas.filter((code) => section.karakas?.[code] !== undefined);
|
||||
for (const code of role.optionalKarakas) {
|
||||
const picked = pickKeys(table[code], KARAKA_KEYS);
|
||||
if (!nonEmpty(picked)) continue;
|
||||
section.karakas = { ...(section.karakas ?? {}), [code]: picked };
|
||||
karakas.push(code);
|
||||
}
|
||||
const entry: JsonRecord = {
|
||||
label: role.label,
|
||||
house: role.house,
|
||||
house_lords: Array.isArray(houseRow.lords) ? houseRow.lords : [],
|
||||
significators: role.planets.filter((name) => caseInsensitive(base.natal.planets, name)),
|
||||
};
|
||||
if (karakas.length) entry.karakas = karakas;
|
||||
if (role.varga) {
|
||||
const varga = vargaHouse(section.vargas[role.varga], role.house);
|
||||
if (varga) entry[`${role.varga.toLowerCase()}_house`] = varga;
|
||||
else gaps.push(`${domain}.role.${role.key}.${role.varga}`);
|
||||
}
|
||||
roles[role.key] = entry;
|
||||
}
|
||||
section.roles = roles;
|
||||
}
|
||||
if (rule.yogas) {
|
||||
const yogas = record(natal.yogas);
|
||||
const rows = Array.isArray(yogas.yogas) ? yogas.yogas.map(record) : [];
|
||||
|
||||
@@ -12,15 +12,24 @@ export const VISIBLE_THINKING_STEP_LIMIT = 4;
|
||||
/**
|
||||
* The natal body headings (TASK-consult-answer-the-question-20260927 D8 /
|
||||
* BUG-1073). The heading-free spoken opener is the answer, so the body no
|
||||
* longer restarts with 「先回答你的问题」: it holds only what the opener did not
|
||||
* say, the timing, and the actions, once.
|
||||
* longer restarts with 「先回答你的问题」. Since TASK-consult-plain-answer-20261001
|
||||
* (BUG-1132) the per-person sections the question names come first, under the
|
||||
* user's own words; these fixed headings follow them, and the first two are
|
||||
* omitted when they have nothing to add.
|
||||
*/
|
||||
export const REPORT_HEADING = {
|
||||
support: "盘里支持这个判断的地方",
|
||||
support: "盘上依据",
|
||||
timing: "时间怎么看",
|
||||
action: "这周可以做的一件事",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The one statement of the first-turn natal answer shape (TASK-consult-plain-
|
||||
* answer-20261001 D2/D7/D8): every English prompt that names the shape quotes
|
||||
* this, and the Chinese user-turn instruction below says the same thing.
|
||||
*/
|
||||
export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session: a heading-free opener of one to three plain sentences whose first sentence is the conclusion, with no action list in the opener; then one H2 section per person or sub-question the user named, titled with the user's own words (## 你和妈妈, ## 你和爸爸, ## 他们怎么对你), each in plain words with its chart evidence in parentheses; then ## ${REPORT_HEADING.support} (only what the sections did not cite; omit when nothing is new), ## ${REPORT_HEADING.timing} (omit when no period or transit bears on the question), and ## ${REPORT_HEADING.action} (one to three actions, once). A question about one person or one thing is not split. There is no length cap: write until it is clear and say each thing once. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone.`;
|
||||
|
||||
/** The declared-window reply still opens its stable-layer section with this heading. */
|
||||
export const WINDOW_OPEN_HEADING = "先回答你的问题";
|
||||
|
||||
@@ -124,7 +133,8 @@ export function natalConsultationThinkingPlan(input: {
|
||||
|
||||
return [
|
||||
// The opener has no heading of its own; it is finished when the first body
|
||||
// H2 appears, so this section shares the support heading (BUG-1072/1073).
|
||||
// H2 appears — a per-person section or a fixed heading (BUG-1132), see
|
||||
// applyThinkingSectionProgress. It keeps the support heading as its key.
|
||||
publicThinkingSectionSchema.parse({
|
||||
id: "question",
|
||||
title: OPENER_THINKING_TITLE,
|
||||
@@ -132,7 +142,7 @@ export function natalConsultationThinkingPlan(input: {
|
||||
steps: [
|
||||
{
|
||||
id: "question-read",
|
||||
label: input.followUp ? "直接回答这句话" : "用盘上的反差回答这个问题",
|
||||
label: input.followUp ? "直接回答这句话" : "先用一句话回答",
|
||||
status: "pending",
|
||||
},
|
||||
],
|
||||
@@ -155,7 +165,7 @@ export function natalConsultationThinkingPlan(input: {
|
||||
}),
|
||||
publicThinkingSectionSchema.parse({
|
||||
id: "action",
|
||||
title: "这周可以扮演哪个象",
|
||||
title: "这周可以做什么",
|
||||
heading: REPORT_HEADING.action,
|
||||
steps: [
|
||||
{ id: "action-write", label: "给出可立即执行的一两件事", status: "pending" },
|
||||
@@ -236,9 +246,14 @@ export function applyThinkingSectionProgress(
|
||||
const present = new Set(
|
||||
[...answerText.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => match[1]?.trim() ?? ""),
|
||||
);
|
||||
// Sections are written in plan order, and a section the answer leaves out
|
||||
// (an optional heading) is behind it once a later heading shows; the natal
|
||||
// opener ends at the first H2 of any name, since the per-person sections the
|
||||
// question names come before the fixed headings (BUG-1132).
|
||||
const lastPresent = sections.reduce((last, section, index) => (present.has(section.heading) ? index : last), -1);
|
||||
let activeAssigned = false;
|
||||
return sections.map((section) => {
|
||||
const headingPresent = present.has(section.heading);
|
||||
return sections.map((section, index) => {
|
||||
const headingPresent = index <= lastPresent || (section.id === "question" && present.size > 0);
|
||||
let status: ThinkingStepStatus = "pending";
|
||||
if (headingPresent) status = "done";
|
||||
else if (!activeAssigned) {
|
||||
@@ -294,7 +309,7 @@ export function consultationSpokenHeadingRule(kind: "natal" | "general" | "windo
|
||||
const activity = "Do not invent a thinking-process checklist. Activity, progress, and receipts are server-owned.";
|
||||
if (kind === "natal") {
|
||||
return [
|
||||
`On the first natal answer of a session: after a 3-6 sentence spoken reply with no heading that answers the user's question (反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)→ 谁在推、谁在修 → 别去应 X 的象、去扮演 Y 的象; no action list in the opener; total ≤ 400 characters), write the rest with these exact Markdown H2 headings in order: ## ${REPORT_HEADING.support}, then ## ${REPORT_HEADING.timing}, then ## ${REPORT_HEADING.action}. The body never restarts the answer under a 先回答 heading; the support section holds only what the opener did not say; the actions appear once, in the last section. Do not write 统一参数与原始结构 or 技法审计表 into the body; those stay on the evidence panel. A follow-up turn follows the shape instruction in the user turn instead.`,
|
||||
`${NATAL_ANSWER_SHAPE_SUMMARY} The body never restarts the answer under a 先回答 heading. Do not write 统一参数与原始结构 or 技法审计表 into the body; those stay on the evidence panel. A follow-up turn follows the shape instruction in the user turn instead.`,
|
||||
"Parallel points such as today's transits, 适合推进, 需要避开, and candidate windows must be Markdown bullet lists. Bold a short label, then one clause; do not stack those as plain paragraphs.",
|
||||
activity,
|
||||
secrets,
|
||||
@@ -342,7 +357,7 @@ export function consultationContinuePrompt(output: string): string {
|
||||
return [
|
||||
"上一轮用户可见正文因长度在标题处停下。从最后一个完整二级标题之后继续写完,不要重复已写出的段落,不要写思考过程清单。",
|
||||
last ? `最后一个完整标题是:${last}` : "上一轮还没有写出完整的二级标题。",
|
||||
`必须继续使用这些二级标题(尚未写到的才写):## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action}。不要写「先回答你的问题」这个标题,不要写统一参数或技法审计表。`,
|
||||
`问题里点到的人或子问题还没写到的,先按对象分段写完;然后继续使用这些二级标题(尚未写到的才写):## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action}。不要写「先回答你的问题」这个标题,不要写统一参数或技法审计表。`,
|
||||
"已写出的末尾摘录:",
|
||||
output.slice(-800),
|
||||
].join("\n");
|
||||
@@ -356,16 +371,28 @@ export function consultationContinuePrompt(output: string): string {
|
||||
*/
|
||||
const ANSWER_FROM_EVIDENCE = "拿到本轮计算结果后直接写给用户的回答,只用结果里的盘面事实下判断;调用工具前后都不要写过程说明。";
|
||||
|
||||
/** Natal first-turn shape (VOICE §7, D8): heading-free opener, then three headings, each thing said once. */
|
||||
export function natalAnswerShapeInstruction(): string {
|
||||
/**
|
||||
* Natal first-turn shape in the user turn (VOICE §7; TASK-consult-plain-
|
||||
* answer-20261001 D2–D4/D8): plain opener, one section per person or
|
||||
* sub-question, then the fixed headings; no length cap. The legacy grounded
|
||||
* route sends the same body with its own evidence line.
|
||||
*/
|
||||
export function natalAnswerShapeBody(): string {
|
||||
return [
|
||||
ANSWER_FROM_EVIDENCE,
|
||||
"开场不要标题,先用不超过 400 字口语回答问题:反差只写盘上的结构(表面 A、底下 B 都是星、宫、大运或分盘),说清谁在推谁在修,然后「别去应 X 的象,去扮演 Y 的象」;开场不写行动清单。",
|
||||
`然后一次写完这三个二级标题:## ${REPORT_HEADING.support}(只写开场没说过的依据,表格与正文二选一,不把开场的判断再列一遍)、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action}(最多三条行动,只在这里出现一次)。全文不超过 900 字。`,
|
||||
"开场不要标题,先用一到三句人话回答问题,第一句就是结论;不给这件事起名字,开场不写行动清单。",
|
||||
"问题里点到几个人或几个子问题(比如妈妈、爸爸、他们怎么对我),开场之后每个各写一段,二级标题用问题里的词(例:## 你和妈妈、## 你和爸爸、## 他们怎么对你);问父母时妈妈和爸爸分开写。只问一个人或一件事时不拆段。",
|
||||
"每段先用人话讲清楚这个人是什么样、你们怎么相处、事情会怎么走,再在括号里给撑住这段判断的盘上依据。",
|
||||
`然后依次写:## ${REPORT_HEADING.support}(只写上面没引过、判断又需要的依据,表格与正文二选一;没有新依据就省掉这一节)、## ${REPORT_HEADING.timing}(卡里没有相关大运或行运就省掉)、## ${REPORT_HEADING.action}(一到三条行动,只在这里出现一次)。`,
|
||||
"不限字数,讲清楚为准;同一件事只说一遍。",
|
||||
"把每句话括号里的内容删掉,不懂占星的人也要能看懂:不起「外X内Y」这类格局名,不把大运写成推着或收尾的角色,不写「去当某颗星的样子」,不把星名当形容词的主人;空宫不单独下结论,要说它的宫主落在哪。",
|
||||
"不要写「先回答你的问题」这个标题,不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。",
|
||||
].join("");
|
||||
}
|
||||
|
||||
export function natalAnswerShapeInstruction(): string {
|
||||
return `${ANSWER_FROM_EVIDENCE}${natalAnswerShapeBody()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow-up turn shape (TASK-consult-answer-the-question-20260927 D3 /
|
||||
* BUG-1071): the session already holds a natal answer, so this turn answers
|
||||
@@ -377,8 +404,8 @@ export function natalAnswerShapeInstruction(): string {
|
||||
export function followUpAnswerShapeInstruction(): string {
|
||||
return [
|
||||
ANSWER_FROM_EVIDENCE,
|
||||
"这是追问轮:本会话前面已经写过完整解读。不超过 200 字,第一句就回答用户这句话(是非题先答「是」「不是」或「看情况:……」,再给盘上的依据);只引与这句话直接相关的盘面事实,最多三处;不写二级标题,不写行动清单(用户问了才给);不重述上一轮已经写过的结构;不解释用户为什么问,不替用户说他想要什么。",
|
||||
`只有用户明确换了领域或要求完整看一下,才回到口语开场加三个二级标题(## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action})的完整形状。`,
|
||||
"这是追问轮:本会话前面已经写过完整解读。第一句就回答用户这句话(是非题先答「是」「不是」或「看情况:……」,再给盘上的依据),讲清楚为准,不限字数;只引与这句话直接相关的盘面事实,够说明就停;用人话说,盘上依据放括号里;不写二级标题,不写行动清单(用户问了才给);不重述上一轮已经写过的结构;不解释用户为什么问,不替用户说他想要什么。",
|
||||
`只有用户明确换了领域或要求完整看一下,才回到完整形状:口语开场、按对象分段,再接二级标题(## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action})。`,
|
||||
"不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。",
|
||||
].join("");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toAgentConsultationContext } from "./consultation-workflow.ts";
|
||||
import { evidenceDraftModelOutputSchema } from "../lib/birth-time-guide-agent.ts";
|
||||
import type { ResolvedLanguageModel } from "./model";
|
||||
import { natalSpokenReportContract, productConversationVoice } from "./product-voice";
|
||||
import { consultationSpokenHeadingRule } from "../lib/consultation-thinking-plan.ts";
|
||||
import { consultationSpokenHeadingRule, NATAL_ANSWER_SHAPE_SUMMARY } from "../lib/consultation-thinking-plan.ts";
|
||||
import {
|
||||
jyotishSkillBinding,
|
||||
jyotishSkillMethodBlock,
|
||||
@@ -18,14 +18,14 @@ export type { ConsultationInput } from "./consultation-workflow.ts";
|
||||
const jyotishInstructions = `You are the guide for a conversational Vedic astrology product.
|
||||
${productConversationVoice}
|
||||
${natalSpokenReportContract}
|
||||
Write in Simplified Chinese. On the first natal answer of a session: a heading-free spoken opener first (反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)→ 谁在推、谁在修 → 别去应 X 的象、去扮演 Y 的象; no action list in the opener; ≤ 400 characters), then the skill Level 2 report skeleton under the three body headings (## 盘里支持这个判断的地方, ## 时间怎么看, ## 这周可以做的一件事), each thing said once, first-turn total ≤ 900 characters. A follow-up turn follows the shape instruction in the user turn. Use a Markdown table for raw structure only when the prose does not already carry it and, when the card carries yogas, for the Yoga table; the Technique Audit Table is folded by the product UI and never written in the body.
|
||||
Write in Simplified Chinese. ${NATAL_ANSWER_SHAPE_SUMMARY} The skill Level 2 report skeleton supplies the evidence under those body headings. A follow-up turn follows the shape instruction in the user turn. Use a Markdown table for raw structure only when the prose does not already carry it and, when the card carries yogas, for the Yoga table; the Technique Audit Table is folded by the product UI and never written in the body.
|
||||
${jyotishSkillMethodBlock}
|
||||
The bound skill method is this product's answering contract, including its report order. Use run-jyotish-consultation for actual chart calculations instead of inventing results. 骨架不可省略,但必须以直接回应开场. Do not replace the skeleton with spoken-only chat.
|
||||
Call run-jyotish-consultation before answering every turn, including short follow-ups; the calculation is request-scoped and is never carried over from an earlier turn.
|
||||
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. List every domain the question actually needs, in priority order, up to six. Do not drop a relevant domain to keep the plan short—the natal compute already ran the full technique spectrum. A question about one's parents uses parents and one about one's children uses children; family is only for the household as a whole. The server canonicalizes aliases, rejects unsupported/product domains, executes as many accepted domains as the wall clock can pay for (about ${MAX_CONSULTATION_DOMAINS}), and returns the rest in omitted_domains. The actual executed domains are in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
|
||||
The tool result's methodology field is the domain checklist for the routes that actually ran. For career, marriage and wealth it is the condensed consultation checklist (must-see items, the three layers to keep apart, forbidden statements, and for marriage the gender-unknown rule), condensed from the skill's strict route and event-judgment file; every other route's section is quoted from the live skill. The shared Full-spectrum invocation and Event judgment skeleton are bound in the system prompt; methodology.sections carries only the domain-specific checklists with the tool result. Treat those domain sections as the method for this answer, not as background: work through their mandatory modules against the evidence you were given, and obey their output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. The checklist is a list of things to check, not a list of paragraphs to write: put into the body only the structures that bear on this question, and leave the checked-but-irrelevant ones out. Those domain sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no named checklist for it: still follow the bound Full-spectrum invocation, Event judgment skeleton, and shared baseline, and do not imply a named strict route was followed. When methodology is absent, follow the bound skill method above.
|
||||
The tool result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—even when several domains ran. For a multi-domain plan that top level is the most restrictive merge of the executed domains, so obey it exactly as written; consultations carries each domain's own contract. Never treat an absent top-level field as permission to answer without a contract.
|
||||
The evidence card is the chart evidence for this answer. claim_cards holds it: natal_foundation (ascendant, the twelve house signs, planet placements with degrees, functional benefics/malefics with the houses each planet rules, and d9: the D9 lagna, each planet's D9 sign and dignity, Vargottama and major D1/D9 dignity reversals — use it to confirm or weaken a D1 promise), timing (the running Vimshottari mahadasha, antardasha and pratyantardasha with dates, the antardashas inside the mahadasha, the running Narayana period) and domain (one section per executed domain: its vargas, houses with occupants and lords, focus planets and the layers that domain uses). The values are copied from this request's calculation; quote them as given. A value tagged parameter_sensitive (the annual Tajika chart) or observation_only (Punarphoo) is structure to weigh, not a verdict; an annual_chart whose status is 年盘未接入 means there is no annual chart this turn, so say so and never stand natal data in for it. Chara Dasha and KP are never on the card; KP exact cusps are still blocked, so a KP lookup is reference only and never the basis of a conclusion. The server calculated the full spectrum and keeps the rest; evidence_card.supplementable_sections names what else this calculation holds. When the question genuinely needs one of those sections (for example a varga the card does not carry, a Western layer, the yoga details, Chara Dasha, Karakamsha, Argala, the dispositor chain, the D1/D9/D10 linkage, or the Moon transit for a month- or day-level question), call read-consultation-evidence once, before writing any answer text; it returns that section of this calculation or says it is unavailable, and it never calculates. Answer from the card, plus that one lookup when you used it.
|
||||
The evidence card is the chart evidence for this answer. claim_cards holds it: natal_foundation (ascendant, the twelve house signs, planet placements with degrees, functional benefics/malefics with the houses each planet rules, and d9: the D9 lagna, each planet's D9 sign and dignity, Vargottama and major D1/D9 dignity reversals — use it to confirm or weaken a D1 promise), timing (the running Vimshottari mahadasha, antardasha and pratyantardasha with dates, the antardashas inside the mahadasha, the running Narayana period) and domain (one section per executed domain: its vargas, houses with occupants and lords, focus planets and the layers that domain uses; the parents section also carries roles, which says which house, lords, significator, karaka and D12 house belong to the mother and which to the father — write about each person from that person's entries). The values are copied from this request's calculation; quote them as given. A value tagged parameter_sensitive (the annual Tajika chart) or observation_only (Punarphoo) is structure to weigh, not a verdict; an annual_chart whose status is 年盘未接入 means there is no annual chart this turn, so say so and never stand natal data in for it. Chara Dasha and KP are never on the card; KP exact cusps are still blocked, so a KP lookup is reference only and never the basis of a conclusion. The server calculated the full spectrum and keeps the rest; evidence_card.supplementable_sections names what else this calculation holds. When the question genuinely needs one of those sections (for example a varga the card does not carry, a Western layer, the yoga details, Chara Dasha, Karakamsha, Argala, the dispositor chain, the D1/D9/D10 linkage, or the Moon transit for a month- or day-level question), call read-consultation-evidence once, before writing any answer text; it returns that section of this calculation or says it is unavailable, and it never calculates. Answer from the card, plus that one lookup when you used it.
|
||||
evidence_card.backstage says global web evidence and real-case calibration were not done for this answer, so confidence is capped: keep conclusions conditional, and mention it only when the user asks about method or certainty.
|
||||
When omitted_domains is non-empty, do not answer those domains and never present the reply as covering the whole plan. Stay with what was calculated. Do not announce a skipped-domain inventory or say this round was incomplete unless the user asked about coverage.
|
||||
Activity, progress, tool status, and execution receipts are server-owned. Never imitate data-jyotish-activity, activity events, tool-started/tool-completed messages, or receipts in the answer text.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING } from "../lib/consultation-thinking-plan.ts";
|
||||
|
||||
export const productConversationVoice = `VISIBLE VOICE
|
||||
PERSONA: 把对面的人当一个人认真对待——不是工单,不是要被安抚的情绪,不是一张等着填完的表。你不是只会点头的工具人,更像他身边一个行动力很强、嘴有点毒但靠谱的同事:给你一个问题,你自己找路、自己补信息、把脏活干完,把结论丢回他面前。
|
||||
直接,有立场,带一点锋利。毒只对处境,不对人:每一句锋利都要有盘上的证据撑着(哪颗星、哪个宫、哪段大运),没有证据就不许刻薄。
|
||||
@@ -11,30 +13,55 @@ PERSONA: 把对面的人当一个人认真对待——不是工单,不是要
|
||||
|
||||
Five principles: answer the person before unfolding method; say progress when the numbers are already on the table; explain why a next step matters in one clause; restate the same constraint in different words; spoken Simplified Chinese to 你, warm but not cute. No 亲爱的, no 感谢你的信任, no emoji, no greeting spam.
|
||||
|
||||
HOPE DISCIPLINE: 盘是客观的,活出它哪一个象是他的选择。不顺就说不顺,但每次都要指出这段时间可以扮演哪个象。
|
||||
HOPE DISCIPLINE: 盘是客观的,这段时间怎么过是他的选择。不顺就说不顺,但每次都要说清这段时间能做什么。
|
||||
- 盘上确有转机、且 answer_policy.can_answer_precise_timing 允许精确应期时,把转机说到月份(哪颗星换宫、哪段大运交接)。
|
||||
- 不允许精确应期、或这一段确实没有转机时,说清这段时间是拿来干什么的(攒、守、清账、练手),以及可以扮演哪个象。不编月份。
|
||||
- 不是每个人都有转机,但每个人都有能扮演的象。不撒谎,也不用普遍性安慰顶替。
|
||||
- 不允许精确应期、或这一段确实没有转机时,说清这段时间是拿来干什么的(攒、守、清账、练手),不编月份。
|
||||
- 不是每个人都有转机,但每个人这段时间都有能做的事。不撒谎,也不用普遍性安慰顶替。
|
||||
Never write 一切都会好, 相信自己, 加油, 你值得更好的, or 宇宙自有安排.
|
||||
|
||||
纯寒暄豁免(natal / general / declared-window 三种模式共用):只是打招呼、道谢、告别时,回一句话,不要套开场形状。Short chit-chat without a domain claim stays conversational. 最多20字,不以句号结尾,不讲星盘、运势或应期,不写客服套话;混合咨询、解释前文、纠错和抱怨不是纯寒暄。
|
||||
|
||||
OPENER SHAPE (natal / general / declared-window 三种模式共用)
|
||||
开场不是一句判词,是一个形状;全程无标题,总长 ≤ 400 Chinese characters:
|
||||
1. 反差:先说「表面 A,底下 B」,并把这个反差命名成一个格局(例:外松内紧)。A 和 B 都必须是盘上的结构——哪颗星、哪个宫、哪段大运、哪张分盘;问的是别人(父母、伴侣、子女、同事)时,反差写那个人在你盘上对应的宫位与代表星,不写那个人的心思。不要用一句判词开场。
|
||||
2. 谁在推、谁在修:动力层与形式层分开讲——哪一层在推这段时间(大运主星),哪一层只负责把结果修得体面(行运)。
|
||||
3. 别去应 X 的象,去扮演 Y 的象:盘是客观的,活出它哪一个象是他的选择。哪怕是重的一年,也总有一个能扮演的象。这是希望的唯一来源,不许用一句空话替代。
|
||||
4. 最多三条短行动:破折号短句,每条 ≤ 20 Chinese characters,做完就能看到改变。本命解读里这三条不写在开场,只写在正文最后一节「这周可以做的一件事」,出现一次;无出生分钟与申报时段没有正文骨架,行动就写在开场结尾。
|
||||
术语当场用引号里的白话套住:土星的「磨」、木星的「享受」、土星的「守」。第一次出现就套,不另起名词解释段。
|
||||
降级路线(形状照给,只换填法):
|
||||
- 申报时段(declared birth window):第 2 步只讲窗口内稳定的那一层——行星落座、公开日历;上升、宫位、大运、分盘不进这一步,也不写月份。第 3 步照给。
|
||||
- 无出生分钟(no birth minute):第 2 步只讲公开日历这一天在推什么,不点个人大运,不写月份。第 3 步照给,能扮演的象取自公开日历,不取自个人盘。
|
||||
ANSWER SHAPE (natal / general / declared-window 三种模式共用)
|
||||
不设字数上限,讲清楚为准。同一件事只说一遍;不为显得周全写问题用不到的盘面事实。
|
||||
1. 先答:开场一到三句人话,第一句就是结论,回答用户问的那句话。开场不写标题、不写行动,不给这件事起名字。
|
||||
2. 按对象分段:问题里点到几个对象(妈妈、爸爸、伴侣、老板、孩子……)或几个子问题(关系如何、他们怎么对我、什么时候),开场之后每个各写一段,二级标题用问题里的词(例:## 你和妈妈、## 你和爸爸、## 他们怎么对你)。问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段。
|
||||
3. 每段先用人话讲清楚:这个人是什么样、你们怎么相处、事情会怎么走,写到讲清楚为止;然后在括号里给出撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),够用就停。问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思。
|
||||
4. 行动:一到三条破折号短句,做完就能看到改变。本命解读里只写在最后一节「这周可以做的一件事」,出现一次;无出生分钟与申报时段没有正文骨架,行动就写在回答结尾。
|
||||
人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么;做不到就重写这句。禁止:
|
||||
- 自造格局名、四字概括(「外X内Y」这一类);
|
||||
- 把大运、行运写成拟人的角色(「推这一段的是某某大运」「修形式的是某某子运」「交给某某去收尾」),以及「去当某颗星的某种样子」「别去应某颗星的某个象」这类角色修辞;
|
||||
- 把星名当形容词的主人(「土星的冷」「月亮的照看」);
|
||||
- 括号外每段最多一个占星术语,第一次出现时用白话套住(例:「现在这十年(月亮大运)」「4 宫(母亲宫)」)。
|
||||
推断边界:空宫不单独下结论——要说这个宫,就说它的宫主落在哪、什么状态。只用卡上和补取到的事实。
|
||||
降级路线(形状照给,只换依据):
|
||||
- 申报时段(declared birth window):依据只取窗口内稳定的那一层——行星落座、公开日历;上升、宫位、大运、分盘不作依据,也不写月份。
|
||||
- 无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份;能做的事取自公开日历,不取自个人盘。
|
||||
|
||||
Good (口气与形状的定稿样例,盘上数据是示意;本命解读里最后三行行动移到「这周可以做的一件事」那一节,开场到「别人松你不松」为止):
|
||||
今年你的事业是「外松内紧」:表面上木星走到你的事业宫,机会、人缘都在,看着轻松;底下土星压着你的工作宫,做事的规矩和身体的负担在收紧。推动这一年的是土星,不是木星,木星只负责把结果修得体面。所以别去应木星的「享受」,也别去应土星的「磨」,去扮演土星的「守」:把规矩立起来,别人松你不松。
|
||||
- 上半年别接口头承诺,只接写下来的。
|
||||
- 6 月前身体的小毛病别拖。
|
||||
- 9 月木星换宫之后再谈升职,之前谈是白谈。
|
||||
Good (父母题首轮,按对象分段;盘上数据是示意):
|
||||
你和父母不疏远,但亲近主要落在「为你的前途操心」上,聊心事的时候少。妈妈管得细、操心多;爸爸话少,关心靠做事来表达。
|
||||
## 你和妈妈
|
||||
她对你的事很上心,但常把关心说成提醒和安排,所以你们聊工作、聊打算的时候多,聊感受的时候少。(4 宫母亲宫的宫主水星落在 10 宫事业宫;代表母亲的月亮也在 10 宫)
|
||||
## 你和爸爸
|
||||
他在你心里分量重,是你做事的标杆;他不太会说软话,你们之间更像互相看着对方做事。(代表父亲的太阳在 10 宫;9 宫父亲宫的宫主木星落 12 宫,他的心力有一部分在远处或在自己的事上)
|
||||
## 他们怎么对你
|
||||
两个人对你都是期望多于宠,要求不少,但出发点是想让你立得住。
|
||||
## 时间怎么看
|
||||
现在这十年(月亮大运,到 2034 年 2 月)和妈妈的来往会比以前多,家里的事更容易落到你身上。
|
||||
## 这周可以做的一件事
|
||||
- 给妈妈打电话时先问她最近怎样,再说你自己的事。
|
||||
- 有决定要做时,先听听爸爸的意见,哪怕最后不照做。
|
||||
|
||||
Good (单对象事业题首轮,不拆段;盘上数据是示意):
|
||||
今年工作上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
|
||||
## 盘上依据
|
||||
木星今年走过你的事业宫,机会和帮你的人都在(木星行运 10 宫);同时土星压在你的工作宫,事情多、要求严,身体也容易被拖累(土星行运 6 宫)。
|
||||
## 时间怎么看
|
||||
9 月木星换宫之前,机会多但不稳;之后再谈升职更有把握。
|
||||
## 这周可以做的一件事
|
||||
- 口头答应的事,补一份书面确认。
|
||||
- 身体的小毛病这周就去看。
|
||||
|
||||
Bad (多对象题,改写示意): 你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(自造格局名、大运写成角色、星名当形容词,爸妈揉成一段;括号外没有一句不懂占星的人能看懂。)
|
||||
|
||||
Good (追问轮,用户说「你在说什么鬼」): 说直接点:这份工作还值得留,但你得换个位置,不是换公司。哪句没看懂,指给我。
|
||||
|
||||
@@ -44,21 +71,22 @@ Bad (同一问题): 你其实要的是被听见,她给的是安排;你是否
|
||||
Good: 你这段婚姻更像先把已经在一起的关系过稳,而不是立刻再开一段。眼下更值得把相处里反复出现的摩擦说清楚。
|
||||
Bad: opening a natal answer with infrastructure, 根据当前计算结果, or a degrees table before answering what the person asked.
|
||||
|
||||
Bad: 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。(像顾问报告:没有反差,没说谁在推谁在修,没有可扮演的象。)
|
||||
Bad: 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。(像顾问报告:只有结论,没有盘上依据,读者不知道凭什么这样说。)
|
||||
Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与行运触发未在本轮完整计算。
|
||||
|
||||
Never open with 根据系统计算, 根据当前计算结果, 综合来看, 需要指出的是, 当前状态为, 本轮未完整计算, or 从星盘来看. Never quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains). Never dump YAML or raw JSON.`;
|
||||
|
||||
export const natalSpokenReportContract = `NATAL REPORT CONTRACT (spoken reply first, then skill Level 2 skeleton)
|
||||
The skill governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES, and the Level 2 skeleton — not the opening paragraphs.
|
||||
For career / wealth / marriage / family (and any natal domain reading), on the first natal answer of a session: open with the OPENER SHAPE above, heading-free, before the first H2 — 先给反差(表面 A,底下 B,都是盘上的结构,并命名成一个格局),再说清谁在推、谁在修,然后「别去应 X 的象,去扮演 Y 的象」. The opener carries no action list. Spoken-layer total ≤ 400 Chinese characters. Then write the skill Level 2 template under the three body headings below. 骨架不可省略,但必须以直接回应开场. The product UI renders the whole answer inline; there is no collapsed 完整分析 control any more. Write the full skeleton in the body and assume every line of it is read. Every H2 must start its own line — an H2 marker that continues the previous sentence renders as literal text, not a heading.
|
||||
export const natalSpokenReportContract = `NATAL REPORT CONTRACT (plain answer first, then one section per person or sub-question, then the skill Level 2 evidence)
|
||||
The skill governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES, and the Level 2 evidence — not the answer's shape.
|
||||
For career / wealth / marriage / family / parents / children (and any natal domain reading), on the first natal answer of a session: follow the ANSWER SHAPE above. ${NATAL_ANSWER_SHAPE_SUMMARY} 骨架不可省略,但必须以直接回应开场. The product UI renders the whole answer inline; there is no collapsed 完整分析 control any more. Write the full answer in the body and assume every line of it is read. Every H2 must start its own line — an H2 marker that continues the previous sentence renders as literal text, not a heading.
|
||||
|
||||
Say each thing once (D8): the opener is the answer, so the body never restarts it under a 先回答 heading, never re-lists the opener's judgment in a table, and gives the actions once. First-turn total ≤ 900 Chinese characters.
|
||||
Required body order after that opener, using these exact H2 headings: ## 盘里支持这个判断的地方, then ## 时间怎么看, then ## 这周可以做的一件事. Do not write a thinking-process checklist; Activity is server-owned. Do not write 统一参数与原始结构 or 技法审计表 into the body — the evidence panel already renders those rows.
|
||||
1. The opener answers the question: the contrast (on the chart), who is pushing / who is dressing the result, which role to play. Not the person's motives.
|
||||
2. Ground the judgment in the evidence card's chart fields (houses, lords, yogas, strength) — cite only the fields that support this judgment, and only what the opener did not already say. A table or prose, not both. Raman six-step house judgment and the Yoga table stay in this section as evidence when the card carries them, not as a parameter dump. Cite, do not dump degrees tables.
|
||||
3. Timing: current Dasha and transits when delivered. Phrase as 能量活跃度/概率, never an absolute date guarantee.
|
||||
4. One to three actions for this week, here and nowhere else. Direct, not footnote-soft.
|
||||
Say each thing once (D8): the opener is the answer, so the body never restarts it under a 先回答 heading, never re-lists the opener's judgment in a table, and gives the actions once. There is no length cap: write until it is clear, and stop there.
|
||||
Do not write a thinking-process checklist; Activity is server-owned. Do not write 统一参数与原始结构 or 技法审计表 into the body — the evidence panel already renders those rows.
|
||||
1. The opener answers the question in plain words: its first sentence is the conclusion. Not the person's motives.
|
||||
2. One section per person or sub-question the user named (妈妈 and 爸爸 are two sections, never one): plain words first, then the chart evidence for that section in parentheses.
|
||||
3. ## ${REPORT_HEADING.support}: ground the judgment in the evidence card's chart fields (houses, lords, yogas, strength) — cite only the fields that support this judgment, and only what the opener and the sections did not already cite. A table or prose, not both. Raman six-step house judgment and the Yoga table stay in this section as evidence when the card carries them, not as a parameter dump. Cite, do not dump degrees tables. Omit the section when there is nothing new to cite.
|
||||
4. ## ${REPORT_HEADING.timing}: current Dasha and transits when delivered. Phrase as 能量活跃度/概率, never an absolute date guarantee. Omit the section when the card carries no period or transit that bears on the question.
|
||||
5. ## ${REPORT_HEADING.action}: One to three actions for this week, here and nowhere else. Direct, not footnote-soft.
|
||||
|
||||
Address the reader as 你. Gloss a technical term once in plain Chinese (婚姻分盘(D9), 当前大运) before reuse.
|
||||
Raman yogas and house steps remain governed_support_only_candidate_ready: write strength and promotion/hindrance in skill language, but do not upgrade them into verified fate truth.
|
||||
@@ -67,5 +95,7 @@ Never:
|
||||
- Skip a card fact the question needs in order to keep the answer short — and never write a card fact the question does not need in order to look thorough.
|
||||
- Put the Technique Audit Table or 统一参数 heading back into the spoken body.
|
||||
- State what the user or the person asked about wants, needs, or feels. Say where the chart puts them instead.
|
||||
- Draw a conclusion from an empty house alone. Say where its lord sits and in what state instead.
|
||||
- Coin a name for the pattern, write a period as a person who pushes or tidies up, or make a planet's name the owner of an adjective.
|
||||
|
||||
The first natal career/wealth/marriage/family answer of a session uses the opener-plus-skeleton above; a follow-up turn follows the shape instruction carried in the user turn (a direct answer of about 200 characters, no headings) unless the user changes domain or asks for the full reading.`;
|
||||
The first natal career/wealth/marriage/family answer of a session uses the opener-plus-skeleton above (opener, per-person sections, then the body headings); a follow-up turn follows the shape instruction carried in the user turn (answer the sentence directly, no headings, no restart of the earlier reading) unless the user changes domain or asks for the full reading.`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolveLiveJyotishSkillRuntimePath,
|
||||
} from "../lib/skill-package-registry.ts";
|
||||
import { sharedConsultationMethodMarkdown } from "../lib/consultation-methodology.ts";
|
||||
import { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING } from "../lib/consultation-thinking-plan.ts";
|
||||
|
||||
const skill = resolveLiveJyotishSkill();
|
||||
|
||||
@@ -111,7 +112,7 @@ function boundMethod(): string {
|
||||
|
||||
export const BOUND_METHOD_MARKER = `<jyotish-skill name="${skill.name}">`;
|
||||
|
||||
const NATAL_REPORT_SKELETON = "For career, wealth, marriage, and family answers, on the first natal answer of a session, present its Level 2 report template in the chat body after a 3-6 sentence spoken reply with no heading (反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)→ 谁在推、谁在修 → 别去应 X 的象、去扮演 Y 的象; no action list in the opener; spoken layer ≤ 400 characters) under the product's three body headings — 盘里支持这个判断的地方 (raw structure, six-step houses and the Yoga table as evidence, only what the opener did not say), 时间怎么看, 这周可以做的一件事 (the actions, once); the Technique Audit Table stays on the evidence panel, and a follow-up turn follows the shape instruction in the user turn instead. ";
|
||||
const NATAL_REPORT_SKELETON = `For career, wealth, marriage, family, parents and children answers, present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself. ${NATAL_ANSWER_SHAPE_SUMMARY} Raw structure, six-step houses and the Yoga table go under ## ${REPORT_HEADING.support}; the actions appear once; the Technique Audit Table stays on the evidence panel, and a follow-up turn follows the shape instruction in the user turn instead. `;
|
||||
|
||||
function methodBlock(reportSkeleton: string) {
|
||||
return `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. ${reportSkeleton}Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block.
|
||||
|
||||
@@ -62,7 +62,11 @@ test("a follow-up turn still calculates once, and its short heading-free answer
|
||||
// ② The writer's prompt carries the follow-up shape, not the skeleton.
|
||||
const writerPrompt = JSON.stringify(run.prompts[1]);
|
||||
assert.match(writerPrompt, /这是追问轮/);
|
||||
assert.match(writerPrompt, /不超过 200 字/);
|
||||
// 原值: assert.match(writerPrompt, /不超过 200 字/)
|
||||
// 新值: 写作提示带「讲清楚为准,不限字数」,且没有任何「不超过 N 字」
|
||||
// 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(追问轮仍不重开骨架)
|
||||
assert.match(writerPrompt, /讲清楚为准,不限字数/);
|
||||
assert.doesNotMatch(writerPrompt, /不超过 \d+ 字/);
|
||||
assert.doesNotMatch(writerPrompt, /这三个二级标题:/);
|
||||
assert.doesNotMatch(writerPrompt, /开场不写行动清单/);
|
||||
// The answer has no heading and never crossed ANSWER_RELEASE_CHARS, yet it
|
||||
@@ -111,5 +115,7 @@ test("the first turn of a session keeps the opener-plus-skeleton shape (control)
|
||||
assert.ok(run.answer.startsWith(OPENER), "the opener has no heading");
|
||||
const sections = run.events.filter((event) => event.type === "thinking.section") as unknown as Array<{ title: string; steps: Array<{ label: string }> }>;
|
||||
const opener = sections.find((section) => section.title === OPENER_THINKING_TITLE);
|
||||
assert.equal(opener?.steps[0]?.label, "用盘上的反差回答这个问题");
|
||||
// 原值: "用盘上的反差回答这个问题";新值: "先用一句话回答"
|
||||
// 原因: TASK-consult-plain-answer-20261001 D1/T2(BUG-1132):反差开场形状下线
|
||||
assert.equal(opener?.steps[0]?.label, "先用一句话回答");
|
||||
});
|
||||
|
||||
@@ -150,6 +150,9 @@ test("every card value is a value the projection or the engine already holds (no
|
||||
// 新值: the same, except the two labels the v2 card writes itself
|
||||
// (EVIDENCE_CARD_LABELS: 性别未知, 年盘未接入).
|
||||
// 原因: TASK-consult-evidence-card-v2-20260927 decision table mandates both labels.
|
||||
// 原值: 白名单是 EVIDENCE_CARD_LABELS 的两条(性别未知、年盘未接入)
|
||||
// 新值: 同一来源多了 roleMother / roleFather(母亲、父亲),断言本身不变
|
||||
// 原因: TASK-consult-plain-answer-20261001 D6(BUG-1133):父母卡标出每条依据属于谁,标签由卡自己写
|
||||
const cardLabels = new Set(Object.values(EVIDENCE_CARD_LABELS).map((label) => JSON.stringify(label)));
|
||||
for (const chart of golden.charts) {
|
||||
for (const domain of consultationDomainIds) {
|
||||
@@ -307,3 +310,51 @@ test("citation counting keeps field ids only and needs a date, degree or planet-
|
||||
assert.deepEqual(citedEvidenceCardFields(card, `月亮 ${moonSign}`), []);
|
||||
assert.deepEqual(citedEvidenceCardFields(card, ""), []);
|
||||
});
|
||||
|
||||
test("the parents section says which evidence is the mother's and which the father's (BUG-1133)", () => {
|
||||
for (const chart of golden.charts) {
|
||||
const card = cardFor(chart.workflow, "parents");
|
||||
const section = card.domain_sections.parents!;
|
||||
const roles = section.roles as Record<string, Json>;
|
||||
const where = chart.id;
|
||||
assert.deepEqual(Object.keys(roles), ["mother", "father"], where);
|
||||
const mother = roles.mother!;
|
||||
const father = roles.father!;
|
||||
assert.equal(mother.label, EVIDENCE_CARD_LABELS.roleMother);
|
||||
assert.equal(father.label, EVIDENCE_CARD_LABELS.roleFather);
|
||||
assert.equal(mother.house, 4);
|
||||
assert.equal(father.house, 9);
|
||||
// House lords are the section's own 4H / 9H lords, copied, not re-derived.
|
||||
assert.deepEqual(mother.house_lords, dig(section.houses, "4", "lords"), where);
|
||||
assert.deepEqual(father.house_lords, dig(section.houses, "9", "lords"), where);
|
||||
assert.deepEqual(mother.significators, ["Moon"]);
|
||||
assert.deepEqual(father.significators, ["Sun"]);
|
||||
// MK is on the card (7-karaka scheme); its value sits once in section.karakas.
|
||||
assert.deepEqual(mother.karakas, ["MK"], where);
|
||||
assert.equal(typeof dig(section.karakas, "MK", "planet"), "string", where);
|
||||
// The engine's scheme has no PiK, so the father carries none and no gap is recorded.
|
||||
assert.equal(father.karakas, undefined, where);
|
||||
assert.equal(dig(section.karakas, "PiK"), undefined, where);
|
||||
// D12 4th / 9th counted from the D12 lagna; occupants are D12 planets in that sign.
|
||||
const d12 = section.vargas.D12 as { lagna: string; planets: Record<string, string> };
|
||||
for (const [role, number] of [[mother, 4], [father, 9]] as const) {
|
||||
const house = role.d12_house as { house: number; sign: string; occupants: string[] };
|
||||
assert.equal(house.house, number);
|
||||
const signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
|
||||
assert.equal(house.sign, signs[(signs.indexOf(d12.lagna) + number - 1) % 12], where);
|
||||
assert.deepEqual(house.occupants, Object.entries(d12.planets).filter(([, sign]) => sign === house.sign).map(([name]) => name), where);
|
||||
}
|
||||
assert.deepEqual(card.meta.gaps, [], where);
|
||||
}
|
||||
});
|
||||
|
||||
test("a parents card without MK records the gap and the mother names no karaka (BUG-1133)", () => {
|
||||
const workflow = structuredClone(golden.charts[0]!.workflow) as Json;
|
||||
const input = sources(workflow, "parents");
|
||||
const table = dig(input.context, "local_layers", "jaimini", "chara_karakas") as Json;
|
||||
delete table.MK;
|
||||
delete table.Matrikaraka;
|
||||
const card = buildEvidenceCard([input]);
|
||||
assert.ok(card.meta.gaps.includes("parents.karaka.MK"));
|
||||
assert.equal((card.domain_sections.parents!.roles as Record<string, Json>).mother!.karakas, undefined);
|
||||
});
|
||||
|
||||
@@ -366,7 +366,10 @@ test("consult route expands an optional entrypoint for both Agent and tool input
|
||||
// 原值: /先用 3–6 句口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
|
||||
// 新值: /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
|
||||
// 原因: 用户回合文案与新开场形状对齐(反差 / 谁推谁修 / 扮演哪个象 / 最多三条行动)。
|
||||
assert.match(source, /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
|
||||
// 原值(2): /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
|
||||
// 新值(2): /natalAnswerShapeBody\(\)[\s\S]*?resolvedQuestion\.modelQuestion/ —— 用户回合引用唯一的形状定义
|
||||
// 原因(2): TASK-consult-plain-answer-20261001 D7/D8:形状只在 consultation-thinking-plan 定义一处,且不再有字数上限
|
||||
assert.match(source, /natalAnswerShapeBody\(\)[\s\S]*?resolvedQuestion\.modelQuestion/);
|
||||
assert.doesNotMatch(source, /JSON\.stringify\(toolInput\)/);
|
||||
assert.match(source, /shouldLoadGeneralDailyPanchanga\(\{/);
|
||||
assert.doesNotMatch(source, /用户明确选择的无出生分钟一般咨询/);
|
||||
|
||||
@@ -51,7 +51,10 @@ test("multi-domain thinking plan uses Chinese product labels and hides tool ids"
|
||||
assert.equal(sections[0]?.title, OPENER_THINKING_TITLE);
|
||||
assert.equal(sections[0]?.title, "先回答你问的这件事");
|
||||
assert.equal(JSON.stringify(sections).includes("先抓住你真正在问的事"), false);
|
||||
assert.equal(sections[0]?.steps[0]?.label, "用盘上的反差回答这个问题");
|
||||
// 原值: "用盘上的反差回答这个问题"
|
||||
// 新值: "先用一句话回答"
|
||||
// 原因: TASK-consult-plain-answer-20261001 D1/T2(BUG-1132):反差开场形状下线,开场改为第一句给结论
|
||||
assert.equal(sections[0]?.steps[0]?.label, "先用一句话回答");
|
||||
assert.ok(sections.some((section) => section.id === "support"));
|
||||
assert.ok(JSON.stringify(sections).includes("事业"));
|
||||
assert.equal(encoded.includes("run-jyotish"), false);
|
||||
@@ -124,6 +127,17 @@ test("progress marks the current heading active and completed ones done", () =>
|
||||
assert.equal(progressed[2]?.steps[0]?.status, "active");
|
||||
});
|
||||
|
||||
test("the opener section closes at the first per-person heading and skipped optional headings close behind later ones (BUG-1132)", () => {
|
||||
const sections = natalConsultationThinkingPlan({ domains: ["parents"] });
|
||||
const opener = "你和父母不疏远,但亲近主要落在为你的前途操心上。\n";
|
||||
const onPerson = applyThinkingSectionProgress(sections, `${opener}## 你和妈妈\n她对你的事很上心。\n`);
|
||||
assert.equal(onPerson[0]?.steps[0]?.status, "done", "the opener ends at the first H2, whatever its name");
|
||||
assert.equal(onPerson[1]?.steps[0]?.status, "active");
|
||||
// 盘上依据 omitted: once 时间怎么看 shows, the support section is behind it.
|
||||
const onTiming = applyThinkingSectionProgress(sections, `${opener}## 你和妈妈\n上心。\n## ${REPORT_HEADING.timing}\n这十年。\n`);
|
||||
assert.deepEqual(onTiming.map((section) => section.steps[0]?.status), ["done", "done", "done", "active"]);
|
||||
});
|
||||
|
||||
test("continue prompt asks to resume after the last complete heading", () => {
|
||||
const prompt = consultationContinuePrompt("## 事业\n方向稳定。\n");
|
||||
assert.match(prompt, /最后一个完整标题是:## 事业/);
|
||||
@@ -160,12 +174,27 @@ test("section prompt asks for one heading and forbids another calculation", () =
|
||||
// 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073
|
||||
for (const heading of Object.values(REPORT_HEADING)) assert.ok(natal.includes(`## ${heading}`));
|
||||
assert.equal(Object.values(REPORT_HEADING).length, 3);
|
||||
assert.match(natal, /这三个二级标题/);
|
||||
// 原值: assert.match(natal, /这三个二级标题/)
|
||||
// 新值: 先按对象分段,再「依次写」三个固定标题(前两个可省)
|
||||
// 原因: TASK-consult-plain-answer-20261001 D2(BUG-1132):问到几个人就分几段,固定标题跟在后面
|
||||
assert.match(natal, /问题里点到几个人或几个子问题/);
|
||||
assert.match(natal, /## 你和妈妈、## 你和爸爸、## 他们怎么对你/);
|
||||
assert.match(natal, /问父母时妈妈和爸爸分开写/);
|
||||
assert.match(natal, /然后依次写:/);
|
||||
assert.match(natal, /开场不写行动清单/);
|
||||
assert.match(natal, /只在这里出现一次/);
|
||||
assert.match(natal, /全文不超过 900 字/);
|
||||
// 原值: assert.match(natal, /全文不超过 900 字/)
|
||||
// 新值: 不限字数、讲清楚为准,且不得出现任何「不超过 N 字」
|
||||
// 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1073 的 900 字)
|
||||
assert.match(natal, /不限字数,讲清楚为准;同一件事只说一遍/);
|
||||
assert.doesNotMatch(natal, /不超过 \d+ 字/);
|
||||
assert.match(natal, /不要写「先回答你的问题」这个标题/);
|
||||
assert.match(natal, /反差只写盘上的结构/);
|
||||
// 原值: assert.match(natal, /反差只写盘上的结构/)
|
||||
// 新值: 人话自检与禁止清单、空宫规则
|
||||
// 原因: TASK-consult-plain-answer-20261001 D1/D3/D4(BUG-1132/1134):反差形状下线;它防的「读心」仍由共享口吻的不揣测段负责
|
||||
assert.match(natal, /把每句话括号里的内容删掉,不懂占星的人也要能看懂/);
|
||||
assert.match(natal, /空宫不单独下结论,要说它的宫主落在哪/);
|
||||
assert.doesNotMatch(natal, /反差|扮演|谁在推/);
|
||||
assert.match(natal, /不要写「统一参数与原始结构」/);
|
||||
assert.match(natal, /不要写技法审计表/);
|
||||
assert.doesNotMatch(natal, /不要再调用排盘工具/, "the loop still has to call the tool before writing");
|
||||
@@ -216,7 +245,11 @@ test("empty-retry section prompt asks to write the heading directly", () => {
|
||||
|
||||
test("a follow-up turn answers directly and the thinking bar says so (TASK-consult-answer-the-question-20260927 T2/T4)", () => {
|
||||
const followUp = followUpAnswerShapeInstruction();
|
||||
assert.match(followUp, /不超过 200 字/);
|
||||
// 原值: assert.match(followUp, /不超过 200 字/)
|
||||
// 新值: 不限字数、讲清楚为准,且不得出现任何「不超过 N 字」;仍是第一句就答、无标题、不重述
|
||||
// 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1072 的 200 字,追问判定逻辑不变)
|
||||
assert.match(followUp, /讲清楚为准,不限字数/);
|
||||
assert.doesNotMatch(followUp, /不超过 \d+ 字/);
|
||||
assert.match(followUp, /第一句就回答用户这句话/);
|
||||
assert.match(followUp, /是非题先答「是」「不是」或「看情况:……」/);
|
||||
assert.match(followUp, /不写二级标题/);
|
||||
|
||||
@@ -27,12 +27,18 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton"
|
||||
// 原值: assert.match(voice, /3–6 heading-free sentences/)
|
||||
// 新值: assert.match(voice, /open with the OPENER SHAPE above, heading-free/)
|
||||
// 原因: 开场从「3–6 句」改成固定形状,句数不再是约束。
|
||||
assert.match(voice, /open with the OPENER SHAPE above, heading-free/);
|
||||
// 原值(2): assert.match(voice, /open with the OPENER SHAPE above, heading-free/)
|
||||
// 新值(2): 合同改为跟随 ANSWER SHAPE,并引用唯一的形状摘要 NATAL_ANSWER_SHAPE_SUMMARY
|
||||
// 原因(2): TASK-consult-plain-answer-20261001 D1/D7(BUG-1132):四步开场形状下线,形状只定义一处
|
||||
assert.match(voice, /follow the ANSWER SHAPE above\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/);
|
||||
// 原值: assert.match(voice, /one conclusion sentence, then 2–3 short point-sentences/)
|
||||
// 新值: assert.match(voice, /别去应 X 的象,去扮演 Y 的象/)
|
||||
// 原因: 旧形状(结论+要点+下一步)被反差→谁推谁修→扮演象→短行动取代。
|
||||
assert.match(voice, /别去应 X 的象,去扮演 Y 的象/);
|
||||
assert.match(voice, /Spoken-layer total ≤ 400 Chinese characters/);
|
||||
// 原值(2): assert.match(voice, /别去应 X 的象,去扮演 Y 的象/); assert.match(voice, /Spoken-layer total ≤ 400 Chinese characters/)
|
||||
// 新值(2): 两句都不得再出现;改锁「不设字数上限,讲清楚为准」
|
||||
// 原因(2): TASK-consult-plain-answer-20261001 D1/D8:扮演象修辞下线,产品取消字数上限
|
||||
assert.doesNotMatch(voice, /扮演|≤ 400|Spoken-layer total/);
|
||||
assert.match(voice, /不设字数上限,讲清楚为准/);
|
||||
assert.match(voice, /骨架不可省略,但必须以直接回应开场/);
|
||||
|
||||
assert.match(mastra, /productConversationVoice/);
|
||||
@@ -40,7 +46,10 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton"
|
||||
assert.match(mastra, /skill Level 2 report skeleton/);
|
||||
assert.match(mastra, /Do not paste the Technique Audit Table into the answer body/);
|
||||
assert.match(binding, /Follow this method and its truth boundaries/);
|
||||
assert.match(binding, /present its Level 2 report template in the chat body after a 3-6 sentence spoken reply/);
|
||||
// 原值: assert.match(binding, /present its Level 2 report template in the chat body after a 3-6 sentence spoken reply/)
|
||||
// 新值: Level 2 模板是回答形状的依据层,形状引用 NATAL_ANSWER_SHAPE_SUMMARY
|
||||
// 原因: TASK-consult-plain-answer-20261001 D2/D7:先答、按对象分段,Level 2 内容落在「盘上依据」
|
||||
assert.match(binding, /present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/);
|
||||
assert.match(mastra, /The bound skill method is this product's answering contract/);
|
||||
assert.match(mastra, /Do not use a restricted technique/);
|
||||
// 原值: assert.match(mastra, /must_use_layers is the executed shortlist/)
|
||||
@@ -55,18 +64,42 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton"
|
||||
// 新值: 正文只有三个标题,「先回答你的问题」不再出现在合同里;开场不带行动、行动只在最后一节、首轮 ≤ 900 字
|
||||
// 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073:开场就是回答,同一判断不再说三遍
|
||||
assert.doesNotMatch(voice, /## 先回答你的问题/);
|
||||
assert.match(voice, /## 盘里支持这个判断的地方, then ## 时间怎么看, then ## 这周可以做的一件事/);
|
||||
assert.match(voice, /The opener carries no action list/);
|
||||
// 原值: assert.match(voice, /## 盘里支持这个判断的地方, then ## 时间怎么看, then ## 这周可以做的一件事/)
|
||||
// 新值: 按对象分段之后才是三个固定标题,标题取自 REPORT_HEADING(「盘里支持这个判断的地方」改名「盘上依据」),前两个可省
|
||||
// 原因: TASK-consult-plain-answer-20261001 D2/D7(BUG-1132)
|
||||
assert.match(voice, /One section per person or sub-question the user named \(妈妈 and 爸爸 are two sections, never one\)/);
|
||||
assert.match(voice, /## \$\{REPORT_HEADING\.support\}[\s\S]*Omit the section when there is nothing new to cite[\s\S]*## \$\{REPORT_HEADING\.timing\}[\s\S]*## \$\{REPORT_HEADING\.action\}/);
|
||||
// 原值: assert.match(voice, /The opener carries no action list/)
|
||||
// 新值: 「开场不写行动」写在共享 ANSWER SHAPE 第 1 步与形状摘要里
|
||||
// 原因: TASK-consult-plain-answer-20261001 D2:合同不再复述开场细节,引用唯一定义
|
||||
assert.match(voice, /开场不写标题、不写行动/);
|
||||
assert.match(voice, /Say each thing once \(D8\)/);
|
||||
assert.match(voice, /First-turn total ≤ 900 Chinese characters/);
|
||||
assert.match(voice, /cite only the fields that support this judgment, and only what the opener did not already say/);
|
||||
// 原值: assert.match(voice, /First-turn total ≤ 900 Chinese characters/)
|
||||
// 新值: 没有字数上限,写到讲清楚为止
|
||||
// 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1073 的 900 字,保留「只说一遍」)
|
||||
assert.doesNotMatch(voice, /900/);
|
||||
assert.match(voice, /There is no length cap: write until it is clear, and stop there/);
|
||||
// 原值: /cite only the fields that support this judgment, and only what the opener did not already say/
|
||||
// 新值: 「只写开场与对象段都没引过的」
|
||||
// 原因: TASK-consult-plain-answer-20261001 D2:对象段已经在括号里给过依据
|
||||
assert.match(voice, /cite only the fields that support this judgment, and only what the opener and the sections did not already cite/);
|
||||
assert.match(voice, /One to three actions for this week, here and nowhere else/);
|
||||
assert.doesNotMatch(voice, /One natal career\/wealth\/marriage\/family question still uses the opener-plus-skeleton above/);
|
||||
assert.match(voice, /a follow-up turn follows the shape instruction carried in the user turn/);
|
||||
assert.match(mastra, /each thing said once, first-turn total ≤ 900 characters/);
|
||||
// 原值: assert.match(mastra, /each thing said once, first-turn total ≤ 900 characters/)
|
||||
// 新值: 指令引用 NATAL_ANSWER_SHAPE_SUMMARY(摘要里写「无字数上限、只说一遍」),不再含 900
|
||||
// 原因: TASK-consult-plain-answer-20261001 D7/D8
|
||||
assert.match(mastra, /Write in Simplified Chinese\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/);
|
||||
assert.doesNotMatch(mastra, /≤ 900|≤ 400|反差|扮演/);
|
||||
assert.match(mastra, /The checklist is a list of things to check, not a list of paragraphs to write/);
|
||||
assert.match(binding, /no action list in the opener/);
|
||||
assert.match(binding, /the actions, once/);
|
||||
// 原值: assert.match(binding, /no action list in the opener/)
|
||||
// 新值: 同一句改由引用的 NATAL_ANSWER_SHAPE_SUMMARY 携带(见下方 shape-summary 测试)
|
||||
// 原因: TASK-consult-plain-answer-20261001 D7:形状只定义一处
|
||||
assert.doesNotMatch(binding, /反差|扮演|≤ 400/);
|
||||
// 原值: assert.match(binding, /the actions, once/)
|
||||
// 新值: assert.match(binding, /the actions appear once/)
|
||||
// 原因: TASK-consult-plain-answer-20261001 D7:骨架句改写,语义不变
|
||||
assert.match(binding, /the actions appear once/);
|
||||
assert.match(mastra, /consultationSpokenHeadingRule\("natal"\)/);
|
||||
assert.match(workflow, /template: "skill_level_2"/);
|
||||
assert.match(workflow, /raman_six_step/);
|
||||
@@ -106,25 +139,56 @@ test("consult user turn no longer dumps tool JSON that the model would parrot",
|
||||
// 原值: /先用 3–6 句口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
|
||||
// 新值: /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/
|
||||
// 原因: 用户回合文案与新开场形状对齐,约束从句数改成字数加形状。
|
||||
assert.match(chartBranch, /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
|
||||
assert.match(chartBranch, /别去应 X 的象,去扮演 Y 的象/);
|
||||
// 原值(3): /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/ 与 /别去应 X 的象,去扮演 Y 的象/
|
||||
// 新值(3): 用户回合引用 natalAnswerShapeBody(),路由源码里不再有字数上限和扮演象
|
||||
// 原因(3): TASK-consult-plain-answer-20261001 D1/D7/D8
|
||||
assert.match(chartBranch, /natalAnswerShapeBody\(\)[\s\S]*?resolvedQuestion\.modelQuestion/);
|
||||
assert.doesNotMatch(route, /扮演|不超过 400 字|不超过 900 字/);
|
||||
assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/);
|
||||
assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/);
|
||||
assert.match(route, /文末技法审计表/);
|
||||
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/);
|
||||
});
|
||||
|
||||
test("the opener is a shape — 反差, 谁推谁修, 扮演哪个象, then at most three actions", () => {
|
||||
assert.match(voice, /OPENER SHAPE \(natal \/ general \/ declared-window 三种模式共用\)/);
|
||||
assert.match(voice, /反差/);
|
||||
assert.match(voice, /表面 A,底下 B/);
|
||||
assert.match(voice, /谁在推、谁在修/);
|
||||
assert.match(voice, /扮演/);
|
||||
assert.match(voice, /最多三条短行动/);
|
||||
assert.match(voice, /≤ 20 Chinese characters/);
|
||||
// 降级路线:申报时段与无出生分钟都不编月份,但「扮演哪个象」照给。
|
||||
assert.match(voice, /申报时段(declared birth window)[\s\S]*?也不写月份。第 3 步照给/);
|
||||
assert.match(voice, /无出生分钟(no birth minute)[\s\S]*?不写月份。第 3 步照给/);
|
||||
// 原值: test "the opener is a shape — 反差, 谁推谁修, 扮演哪个象, then at most three actions"(锁 OPENER SHAPE 四步、≤ 20 字行动、降级路线「第 3 步照给」)
|
||||
// 新值: 同位置改锁 ANSWER SHAPE:先答、按对象分段、人话自检与禁止清单、空宫规则、无字数上限、降级路线两条
|
||||
// 原因: TASK-consult-plain-answer-20261001 D1–D4/D8(BUG-1132/1134):产品推翻 09-17 四步开场形状
|
||||
test("the answer is a shape — conclusion first, one section per person, plain words checked, no length cap (BUG-1132)", () => {
|
||||
assert.match(voice, /ANSWER SHAPE \(natal \/ general \/ declared-window 三种模式共用\)/);
|
||||
assert.doesNotMatch(voice, /OPENER SHAPE|表面 A,底下 B|谁在推|命名成一个格局|外松内紧/);
|
||||
assert.match(voice, /不设字数上限,讲清楚为准。同一件事只说一遍/);
|
||||
assert.match(voice, /开场一到三句人话,第一句就是结论/);
|
||||
assert.match(voice, /按对象分段:问题里点到几个对象/);
|
||||
assert.match(voice, /问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段/);
|
||||
assert.match(voice, /然后在括号里给出撑住这段判断的盘上依据/);
|
||||
assert.match(voice, /人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么/);
|
||||
for (const banned of ["自造格局名、四字概括", "把大运、行运写成拟人的角色", "把星名当形容词的主人", "括号外每段最多一个占星术语"]) {
|
||||
assert.ok(voice.includes(banned), banned);
|
||||
}
|
||||
assert.match(voice, /推断边界:空宫不单独下结论——要说这个宫,就说它的宫主落在哪、什么状态/);
|
||||
assert.match(voice, /一到三条破折号短句/);
|
||||
// 降级路线:申报时段与无出生分钟都不编月份;形状照给。
|
||||
assert.match(voice, /申报时段(declared birth window):依据只取窗口内稳定的那一层[\s\S]*?也不写月份。/);
|
||||
assert.match(voice, /无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份/);
|
||||
assert.doesNotMatch(voice, /≤ 20 Chinese characters|≤ 400|≤ 900/);
|
||||
});
|
||||
|
||||
test("one shape definition: contract, thinking plan, user turn and agent prompts share the headings and summary (D7)", async () => {
|
||||
const { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING, consultationSpokenHeadingRule, natalAnswerShapeBody } = await import("../src/lib/consultation-thinking-plan.ts");
|
||||
const { natalSpokenReportContract } = await import("../src/mastra/product-voice.ts");
|
||||
assert.equal(REPORT_HEADING.support, "盘上依据");
|
||||
for (const heading of Object.values(REPORT_HEADING)) {
|
||||
assert.ok(NATAL_ANSWER_SHAPE_SUMMARY.includes(`## ${heading}`), heading);
|
||||
assert.ok(natalSpokenReportContract.includes(`## ${heading}`), heading);
|
||||
assert.ok(natalAnswerShapeBody().includes(`## ${heading}`), heading);
|
||||
}
|
||||
assert.ok(natalSpokenReportContract.includes(NATAL_ANSWER_SHAPE_SUMMARY));
|
||||
assert.ok(consultationSpokenHeadingRule("natal").includes(NATAL_ANSWER_SHAPE_SUMMARY));
|
||||
assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /There is no length cap/);
|
||||
assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /no conclusion from an empty house alone/);
|
||||
for (const text of [NATAL_ANSWER_SHAPE_SUMMARY, natalSpokenReportContract, natalAnswerShapeBody(), consultationSpokenHeadingRule("natal")]) {
|
||||
assert.doesNotMatch(text, /扮演|谁在推|命名成一个格局|外松内紧|≤ 400|≤ 900|不超过 \d+ 字/);
|
||||
}
|
||||
});
|
||||
|
||||
test("persona is the sharp-but-reliable colleague, and hope never becomes a platitude", () => {
|
||||
@@ -139,29 +203,45 @@ test("persona is the sharp-but-reliable colleague, and hope never becomes a plat
|
||||
assert.match(voice, /单义的问题直接答,不改写它/);
|
||||
assert.match(voice, /HOPE DISCIPLINE/);
|
||||
assert.match(voice, /answer_policy\.can_answer_precise_timing 允许精确应期时/);
|
||||
assert.match(voice, /不是每个人都有转机,但每个人都有能扮演的象/);
|
||||
// 原值: assert.match(voice, /不是每个人都有转机,但每个人都有能扮演的象/)
|
||||
// 新值: assert.match(voice, /不是每个人都有转机,但每个人这段时间都有能做的事/)
|
||||
// 原因: TASK-consult-plain-answer-20261001 D5:希望纪律保留,删「扮演哪个象」说法
|
||||
assert.match(voice, /不是每个人都有转机,但每个人这段时间都有能做的事/);
|
||||
assert.match(voice, /每次都要说清这段时间能做什么/);
|
||||
// 空话必须留在禁令里,但不得出现在 Good 例里。
|
||||
assert.match(voice, /Never write 一切都会好, 相信自己, 加油, 你值得更好的, or 宇宙自有安排。?/);
|
||||
assert.match(voice, /No 亲爱的, no 感谢你的信任, no emoji/);
|
||||
});
|
||||
|
||||
test("the pinned Good example carries the new shape; the old consultant-report line is now Bad", () => {
|
||||
const goodStart = voice.indexOf("Good (口气与形状的定稿样例");
|
||||
const goodEnd = voice.indexOf("Good (追问轮");
|
||||
assert.ok(goodStart > -1 && goodEnd > goodStart);
|
||||
const good = voice.slice(goodStart, goodEnd);
|
||||
assert.match(good, /今年你的事业是「外松内紧」/);
|
||||
assert.match(good, /推动这一年的是土星,不是木星/);
|
||||
assert.match(good, /去扮演土星的「守」/);
|
||||
assert.match(good, /- 9 月木星换宫之后再谈升职,之前谈是白谈。/);
|
||||
// 拍板样例里不得出现空话安慰;禁令本身仍写在 HOPE DISCIPLINE 段。
|
||||
assert.doesNotMatch(good, /一切都会好|相信自己|加油|宇宙自有安排/);
|
||||
// 原值: test "the pinned Good example carries the new shape; the old consultant-report line is now Bad"(锁「外松内紧」定稿样例、扮演土星的守、顾问报告 Bad 的理由「没有反差…没有可扮演的象」)
|
||||
// 新值: 锁父母题按对象分段的 Good、单对象不拆段的 Good、格局名/大运拟人的 Bad;顾问报告 Bad 保留、理由改成「没有盘上依据」
|
||||
// 原因: TASK-consult-plain-answer-20261001 D1/D2/T1(BUG-1132):09-17 定稿样例随形状一起下线
|
||||
test("the pinned Good examples split by person and stay plain; the coined-pattern opener is Bad", () => {
|
||||
const parentsStart = voice.indexOf("Good (父母题首轮,按对象分段;盘上数据是示意)");
|
||||
const careerStart = voice.indexOf("Good (单对象事业题首轮,不拆段;盘上数据是示意)");
|
||||
const badStart = voice.indexOf("Bad (多对象题,改写示意)");
|
||||
assert.ok(parentsStart > -1 && careerStart > parentsStart && badStart > careerStart);
|
||||
const parents = voice.slice(parentsStart, careerStart);
|
||||
for (const heading of ["## 你和妈妈", "## 你和爸爸", "## 他们怎么对你", "## 时间怎么看", "## 这周可以做的一件事"]) {
|
||||
assert.ok(parents.includes(heading), heading);
|
||||
}
|
||||
assert.match(parents, /(4 宫母亲宫的宫主水星落在 10 宫事业宫;代表母亲的月亮也在 10 宫)/);
|
||||
const career = voice.slice(careerStart, badStart);
|
||||
assert.doesNotMatch(career, /## 你和/);
|
||||
assert.match(career, /## 盘上依据/);
|
||||
for (const good of [parents, career]) {
|
||||
assert.doesNotMatch(good, /一切都会好|相信自己|加油|宇宙自有安排|扮演|格局/);
|
||||
// No coined 外X内Y pattern names and no empty-house verdicts in the pinned examples.
|
||||
assert.doesNotMatch(good, /「外.内.」|空宫/);
|
||||
}
|
||||
const bad = voice.slice(badStart, voice.indexOf("\n", badStart));
|
||||
assert.match(bad, /自造格局名、大运写成角色、星名当形容词,爸妈揉成一段/);
|
||||
|
||||
assert.match(voice, /Good \(追问轮,用户说「你在说什么鬼」\): 说直接点:这份工作还值得留/);
|
||||
|
||||
const badLine = voice.split("\n").find((line) => line.startsWith("Bad: 你更适合把已经积累的专业能力做成长期事业"));
|
||||
assert.ok(badLine, "旧 Good 例必须改挂在 Bad 上");
|
||||
assert.match(badLine!, /像顾问报告:没有反差,没说谁在推谁在修,没有可扮演的象/);
|
||||
assert.match(badLine!, /像顾问报告:只有结论,没有盘上依据/);
|
||||
});
|
||||
|
||||
test("the voice answers the sentence asked: no motive guessing, yes/no first, contrast on the chart (BUG-1070)", () => {
|
||||
@@ -172,12 +252,16 @@ test("the voice answers the sentence asked: no motive guessing, yes/no first, co
|
||||
assert.match(discipline, /任何替用户或当事人陈述动机、需求、感受的句子/);
|
||||
assert.match(discipline, /不得写「她问的是结果,你要的是被听」/);
|
||||
assert.match(discipline, /是非题(是不是 \/ 会不会 \/ 能不能 \/ 有没有 \/ ……吗):第一句就是「是」「不是」或「看情况:……」/);
|
||||
// The contrast is a chart structure, never the person's mind (D2).
|
||||
assert.match(voice, /A 和 B 都必须是盘上的结构——哪颗星、哪个宫、哪段大运、哪张分盘/);
|
||||
assert.match(voice, /反差写那个人在你盘上对应的宫位与代表星,不写那个人的心思/);
|
||||
// 原值(2): /A 和 B 都必须是盘上的结构——…/ 与 /反差写那个人在你盘上对应的宫位与代表星,不写那个人的心思/
|
||||
// 新值(2): 反差形状下线;「问别人时写宫位与代表星,不写心思」移到对象段规则里
|
||||
// 原因(2): TASK-consult-plain-answer-20261001 D1(BUG-1070 的目的保留)
|
||||
assert.match(voice, /问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思/);
|
||||
// Actions stay in the opener only where there is no body skeleton.
|
||||
assert.match(voice, /本命解读里这三条不写在开场,只写在正文最后一节「这周可以做的一件事」/);
|
||||
assert.match(voice, /无出生分钟与申报时段没有正文骨架,行动就写在开场结尾/);
|
||||
// 原值(2): /本命解读里这三条不写在开场,只写在正文最后一节「这周可以做的一件事」/、/…行动就写在开场结尾/
|
||||
// 新值(2): /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/、/…行动就写在回答结尾/
|
||||
// 原因(2): TASK-consult-plain-answer-20261001 D2:开场之后还有对象段,「开场结尾」改「回答结尾」
|
||||
assert.match(voice, /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/);
|
||||
assert.match(voice, /无出生分钟与申报时段没有正文骨架,行动就写在回答结尾/);
|
||||
// Yes/no Good and Bad pair, fictional chart.
|
||||
assert.match(voice, /Good \(是非题,用户问「我妈是不是不太在意我」/);
|
||||
assert.match(voice, /不是。4 宫主水星逆行落 10 宫/);
|
||||
@@ -188,4 +272,6 @@ test("the voice answers the sentence asked: no motive guessing, yes/no first, co
|
||||
// The three-mode shared voice still carries all of it (general and window expand the same literal).
|
||||
const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract"));
|
||||
assert.ok(sharedVoice.includes("不揣测(回答对象是用户问的那句话)"));
|
||||
// BUG-1134: the empty-house rule is also in the natal Never list.
|
||||
assert.match(voice, /Draw a conclusion from an empty house alone\. Say where its lord sits and in what state instead/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user