feat(consult): condensed career / marriage / wealth checklist for the web consultation

TASK-consult-evidence-card-v2-20260927 T4 (方案 B). Career, marriage and
wealth hand the answer model a 5-7 line condensed checklist
(frontend/src/lib/consultation-condensed-checklist.ts) in place of the full
strict route plus event-judgment file: must-see items (all on the card),
the three layers (接触 / 结构性机会 / 公开落地; 心动接触 / 关系成对 /
社会法律落地; 挣钱机会 / 真实增长 / 到账变现), the forbidden statements
(5L 大运 ≠ 法律婚, 金星过本命月 ≠ 心动月, Punarphoo 不得写成结婚,
土星回到本命月是观察项), the gender-unknown rule, and the lookups (KP
blocked). The shared baseline still travels; timing / health keep their
full router sections; the router, the event-judgment files, the bound
system method and the skill are unchanged (no skill version change).
Only the consultation tool builds the methodology, so reports and other
surfaces are unaffected (source test).

Single-domain model-visible size, 3 public AA charts x (10 questions +
annual + timing): career 9.8K, marriage 10.5K, wealth 9.9K, annual 11.5K,
timing 11.3K; all <= 12,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-27 16:51:29 +08:00
co-authored by Claude Opus 5.5
parent db17074430
commit 7ad4004369
5 changed files with 261 additions and 5 deletions
@@ -0,0 +1,53 @@
import type { ConsultationDomain } from "./consultation-domain-registry.ts";
/**
* Condensed consultation checklists (TASK-consult-evidence-card-v2-20260927
* T4, decision "方案 B"): what the web consultation hands the answer model for
* career, marriage and wealth instead of the full strict route and the full
* event-judgment file.
*
* Condensed from the live skill's `references/strict-workflow-router.md`
* (§2 shared baseline, §3 career-timing-strict, §4 relationship-timing-strict,
* §5 finance-timing-strict) and `references/event_judgment_career.md`,
* `event_judgment_marriage.md`, `event_judgment_wealth.md`. Each list is 5–8
* lines: the must-see items (all of them on the evidence card), the three
* layers to keep apart, the forbidden statements, and what stays a lookup.
*
* Consultation only. The router and the event-judgment files are unchanged and
* still serve reports and every other surface; this file does not edit the
* skill, so there is no skill version change.
*/
export const CONDENSED_CHECKLIST_SOURCE =
"consultation condensed checklist (from references/strict-workflow-router.md §2–§5 and references/event_judgment_*.md)";
export const CONSULTATION_CONDENSED_CHECKLISTS: Readonly<Partial<Record<ConsultationDomain, readonly string[]>>> = {
career: [
"必看:D1 10 宫与 10 宫主、AmK;卡上 d9 段确认 10 宫主与 AmK 的旺弱、Vargottama、D1/D9 反转;D10 上升与 10 宫;AL 与 A10;10 宫 SAV 与木星、土星所在宫的 SAV。",
"时间:Vimshottari(MD/AD/PD)与 Narayana 双轨同向才谈应期,看大运主与 10 宫、10 宫主、D10 的关系;每颗星先按功能吉凶定性。",
"分三层说,不得合成一句「事业机会」:①接触(消息、面试、初步接洽)②结构性机会(合同、正式职位、长期项目)③公开落地(发布、到账、公开的职位与名声)。",
"禁写:本命承诺弱时,不得因一段大运或一次行运断言「事业必成」;不得把接触窗写成落地窗。",
"卡外按需补取一次:Karakamsha、宫主链、D1→D9→D10 联动、Argala、Chara 大运;KP 精确宫头仍 blocked,只作参考、不作依据。",
],
marriage: [
"第一句先冻结判定对象:心动接触 / 关系成对(契约)/ 社会法律落地;对象不清就先说清楚再往下。",
"必看:D1 7 宫与 7 宫主、5 宫与 5 宫主、金星、木星、DK、UL;卡上 d9 段(D9 上升、金木与 DK 的旺弱);昼夜盘;Vivah Saham 的度数与落宫。",
"分三层说:①心动接触:5 宫、5 宫主、Punarphoo 观察 ②关系成对:7 宫主、DK、UL ③社会法律落地:D9 + UL + Vivah Saham + 双大运同向。",
"时间:Vimshottari 与 Narayana 双轨;双重过运(木星、土星同时激活 7 宫 / 7 宫主 / DK / UL)只是激活窗,不是事件日。",
"性别未知:金星与木星都要看,不按性别指定「夫星」「妻星」下定论。",
"禁写:5 宫主大运 ≠ 法律婚;金星过本命月亮 ≠ 心动月;Punarphoo 不得写成结婚;土星回到本命月亮是观察项,不是结婚。",
"卡外按需补取一次:Chara 大运、KP 7 宫副主星(KP 精确宫头仍 blocked,只作参考、不作依据)。",
],
wealth: [
"必看:D1 2、11、5、9 宫及宫主,8 宫(共享资源、突然得失)与 12 宫(支出、外流);木星、金星、水星;D2、D11;2、11 宫 SAV 与最强、最弱星座;卡上 d9 段确认 2 宫主、11 宫主的旺弱。",
"时间:2、11、5、9 宫主(收入来自工作时加 10 宫主)大运是否激活,Vimshottari 与 Narayana 双轨同向;每颗星先按功能吉凶定性。",
"分三层说:①挣钱机会 ②收入或资产真实增长 ③到账变现;不得合成一句「财运好」。",
"禁写:本命承诺弱时,不得因一次行运或一段大运断言「发财」;不给投资建议、不保证收益。",
"卡外按需补取一次:收入来自工作时补取 D10;Argala、KP 到账类(KP 精确宫头仍 blocked,只作参考、不作依据)。",
],
};
/** The condensed list for one domain, or null when the domain keeps its full route. */
export function condensedConsultationChecklist(domain: ConsultationDomain): string | null {
const lines = CONSULTATION_CONDENSED_CHECKLISTS[domain];
return lines ? lines.map((line, index) => `${index + 1}. ${line}`).join("\n") : null;
}
@@ -7,6 +7,7 @@ import {
type ConsultationDomain,
} from "./consultation-domain-registry.ts";
import { resolveLiveJyotishSkill } from "./skill-package-registry.ts";
import { CONDENSED_CHECKLIST_SOURCE, condensedConsultationChecklist } from "./consultation-condensed-checklist.ts";
/**
* The strict checklist each consultation domain must be read against, named in
@@ -23,6 +24,12 @@ import { resolveLiveJyotishSkill } from "./skill-package-registry.ts";
* checklist each route requires, so the selection needs no model turn: read the
* mandated sections here and deliver them with the evidence. A checklist the
* router does not declare is reported as absent instead of substituted.
*
* Career, marriage and wealth deliver the condensed consultation checklist
* (consultation-condensed-checklist.ts, TASK-consult-evidence-card-v2-20260927
* T4) in place of the full strict route plus event-judgment file; the table
* below still names the route those lists were condensed from. This function
* serves the web consultation only; the router file itself is unchanged.
*/
const domainMethodology: Readonly<Record<ConsultationDomain, { strictRoute: string | null; eventJudgment: string | null }>> = {
career: { strictRoute: "career-timing-strict", eventJudgment: "event_judgment_career.md" },
@@ -165,6 +172,11 @@ export function consultationMethodologyForDomains(
const withoutChecklist: ConsultationDomain[] = [];
for (const domain of unique) {
const { strictRoute, eventJudgment } = domainMethodology[domain];
const condensed = condensedConsultationChecklist(domain);
if (condensed) {
push(`${consultationDomainDefinition(domain).label} · ${strictRoute} · 普通对话精简清单`, CONDENSED_CHECKLIST_SOURCE, condensed);
continue;
}
if (!strictRoute) {
withoutChecklist.push(domain);
} else {
+1 -1
View File
@@ -23,7 +23,7 @@ ${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, 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. 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'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. 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.
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.
@@ -0,0 +1,180 @@
// TASK-consult-evidence-card-v2-20260927 T4: the web consultation hands the
// answer model a condensed checklist for career, marriage and wealth (must-see
// items, the three layers, forbidden statements, the gender-unknown rule), and
// nothing else changes: the router file, the event-judgment files, the bound
// system method and every other route are as before.
//
// Real `getJyotishAgent` + prompt-recording fake model; the calculation is the
// golden engine capture of public AA charts (AGENTS §7.4).
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import {
CONDENSED_CHECKLIST_SOURCE,
CONSULTATION_CONDENSED_CHECKLISTS,
} from "../src/lib/consultation-condensed-checklist.ts";
import { consultationMethodologyForDomains, sharedConsultationMethodMarkdown } from "../src/lib/consultation-methodology.ts";
import { consultationDomainIds, type ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
import { REPORT_HEADING } from "../src/lib/consultation-thinking-plan.ts";
import { createConsultationRuntimeState, createConsultationTools } from "../src/mastra/consultation-tools.ts";
import { firstPromptWithToolResult, pieces, runNatalAgent, type Turn } from "./consult-natal-agent-test-support.ts";
type Json = Record<string, unknown>;
type GoldenChart = { id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json };
const golden = JSON.parse(readFileSync(
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
"utf8",
)) as { charts: GoldenChart[] };
const QUESTIONS: Record<"career" | "marriage" | "wealth", string> = {
career: "未来一年,事业该关注什么?",
marriage: "今年婚恋怎么样?",
wealth: "我的财富增长方式是什么?",
};
const ANSWER = [
"表面是一个人在扛,底下是一群人在推,这叫「借力成事」。",
"",
`## ${REPORT_HEADING.question}`,
"能成,但要分步。",
"",
`## ${REPORT_HEADING.support}`,
"十宫主在九分盘里站得住。",
"",
`## ${REPORT_HEADING.timing}`,
"先看接下来这段子运。",
"",
`## ${REPORT_HEADING.action}`,
"- **这周**:把要谈的事写下来。",
"",
].join("\n");
function toolResultText(prompt: unknown[] | undefined) {
return JSON.stringify((prompt ?? []).filter((message) => {
const value = message as { role?: string; content?: unknown };
return value.role === "tool" && JSON.stringify(value.content).includes("run-jyotish-consultation");
}));
}
async function writerPrompt(domain: "career" | "marriage" | "wealth") {
const calc: Turn = {
parts: [{ tool: { name: "run-jyotish-consultation", input: { question: QUESTIONS[domain], domains: [domain] } } }],
finish: "tool-calls",
};
const result = await runNatalAgent([calc, { parts: pieces(ANSWER), finish: "stop" }], {
workflow: golden.charts[0]!.workflow,
theme: domain,
question: QUESTIONS[domain],
});
assert.deepEqual(result.terminal.map((event) => event.type), ["run.completed"]);
const writer = firstPromptWithToolResult(result.prompts, "run-jyotish-consultation");
assert.ok(writer > 0);
return toolResultText(result.prompts[writer]);
}
test("each condensed list is 5–8 lines", () => {
for (const [domain, lines] of Object.entries(CONSULTATION_CONDENSED_CHECKLISTS)) {
assert.ok(lines!.length >= 5 && lines!.length <= 8, `${domain}: ${lines!.length} lines`);
}
assert.deepEqual(Object.keys(CONSULTATION_CONDENSED_CHECKLISTS).sort(), ["career", "marriage", "wealth"]);
});
test("the writer's prompt carries the condensed list with the three layers and the forbidden statements, not the long checklist", async () => {
const required: Record<"career" | "marriage" | "wealth", string[]> = {
career: ["接触", "结构性机会", "公开落地", "事业必成", "d9 段", "KP 精确宫头仍 blocked"],
marriage: [
"心动接触", "关系成对", "社会法律落地",
"5 宫主大运 ≠ 法律婚", "金星过本命月亮 ≠ 心动月", "Punarphoo 不得写成结婚", "土星回到本命月亮是观察项,不是结婚",
"性别未知:金星与木星都要看", "夫星", "Vivah Saham",
],
wealth: ["8 宫", "12 宫", "挣钱机会", "到账变现", "发财", "D10"],
};
const longChecklist = [
"Opportunity contact", "Mandatory modules", "Career timing output", "Relationship timing output",
"Marriage Event Adjudicator", "Wealth Event Adjudicator", "Career Event Adjudicator", "Route Freeze",
"Evidence Ledger Roles",
];
for (const domain of ["career", "marriage", "wealth"] as const) {
const prompt = await writerPrompt(domain);
assert.ok(prompt.includes(CONDENSED_CHECKLIST_SOURCE), `${domain}: condensed source`);
for (const line of CONSULTATION_CONDENSED_CHECKLISTS[domain]!) {
assert.ok(prompt.includes(line.slice(0, 24)), `${domain}: ${line.slice(0, 24)}`);
}
for (const phrase of required[domain]) assert.ok(prompt.includes(phrase), `${domain}: ${phrase}`);
for (const phrase of longChecklist) assert.equal(prompt.includes(phrase), false, `${domain}: long checklist text "${phrase}"`);
// The shared baseline still travels with the result.
assert.ok(prompt.includes("Shared mandatory baseline"), `${domain}: shared baseline`);
}
});
test("other routes still quote the live skill, and the bound system method is unchanged", () => {
const timing = consultationMethodologyForDomains(["timing"])!;
const eventTiming = timing.sections.find((section) => section.title.includes("event-timing-strict"));
assert.ok(eventTiming);
assert.match(eventTiming.source, /references\/strict-workflow-router\.md$/);
const health = consultationMethodologyForDomains(["health"])!;
assert.match(health.sections.find((section) => section.title.includes("health-timing-strict"))!.source, /strict-workflow-router\.md$/);
for (const domain of consultationDomainIds.filter((id) => !(id in CONSULTATION_CONDENSED_CHECKLISTS))) {
const methodology = consultationMethodologyForDomains([domain])!;
assert.equal(methodology.sections.some((section) => section.source === CONDENSED_CHECKLIST_SOURCE), false, domain);
}
const shared = sharedConsultationMethodMarkdown();
assert.match(shared, /Full-Spectrum Invocation Contract/);
assert.match(shared, /Event Judgment Skeleton|event judgment/i);
});
test("only the consultation tool builds the methodology: reports and other surfaces never read it", () => {
const root = new URL("../src/", import.meta.url).pathname;
const users: string[] = [];
const walk = (dir: string) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) walk(path);
else if (/\.(ts|tsx)$/.test(entry.name) && readFileSync(path, "utf8").includes("consultationMethodologyForDomains")) {
users.push(path.slice(root.length));
}
}
};
walk(root);
assert.deepEqual(users.sort(), ["lib/consultation-methodology.ts", "mastra/consultation-tools.ts"]);
const condensedUsers: string[] = [];
const walkCondensed = (dir: string) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) walkCondensed(path);
else if (/\.(ts|tsx)$/.test(entry.name) && readFileSync(path, "utf8").includes("consultation-condensed-checklist")) {
condensedUsers.push(path.slice(root.length));
}
}
};
walkCondensed(root);
assert.deepEqual(condensedUsers, ["lib/consultation-methodology.ts"]);
});
test("size: every single-domain turn stays within 12,000 characters, contract and checklist included", 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" },
truth: { birthTimeSource: "reported" },
};
for (const chart of golden.charts) {
for (const [workflow, domain] of [
...consultationDomainIds.filter((id) => id !== "annual" && id !== "timing").map((id) => [chart.workflow, id] as const),
[chart.annual_workflow, "annual"] as const,
[chart.timing_workflow, "timing"] as const,
] as Array<readonly [Json, ConsultationDomain]>) {
const state = createConsultationRuntimeState();
const tools = createConsultationTools({
userId: "u", sessionId: "s", requestId: `size-${chart.id}-${domain}`, consultationMode: "verified_chart",
serverChart: serverChart as never, state,
runWorkflow: async () => structuredClone(workflow) as never,
});
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}`);
}
}
});
@@ -6,6 +6,7 @@ import {
METHODOLOGY_SECTION_MAX_CHARS,
METHODOLOGY_TOTAL_MAX_CHARS,
} from "../src/lib/consultation-methodology.ts";
import { CONDENSED_CHECKLIST_SOURCE } from "../src/lib/consultation-condensed-checklist.ts";
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
import { resolveLiveJyotishSkill } from "../src/lib/skill-package-registry.ts";
@@ -14,10 +15,15 @@ test("the route's own strict checklist reaches the answer, not just the package
assert.ok(methodology, "career must resolve a methodology");
const career = methodology.sections.find((section) => section.title.includes("career-timing-strict"));
assert.ok(career, "the career strict route must be delivered as its own section");
assert.match(career.source, /strict-workflow-router\.md$/);
// Former: source was references/strict-workflow-router.md and the text the full
// route ("Opportunity contact" …), plus event_judgment_career.md as a second section.
// New: the condensed consultation checklist, condensed from both, as one section.
// Reason: TASK-consult-evidence-card-v2-20260927 T4 (方案 B, consultation only).
assert.equal(career.source, CONDENSED_CHECKLIST_SOURCE);
assert.match(career.source, /strict-workflow-router\.md/);
// The checklist is only useful if the specific instructions arrive with it.
assert.match(career.text, /D10/);
assert.match(career.text, /Opportunity contact/);
assert.match(career.text, /接触/);
assert.ok(
methodology.sections.some((section) => section.title.includes("Shared mandatory baseline")),
"every route is read against the shared baseline",
@@ -32,7 +38,8 @@ test("the route's own strict checklist reaches the answer, not just the package
0,
);
const domainSections = methodology.sections.filter((section) => !section.title.includes("Shared mandatory baseline"));
assert.equal(domainSections.length, 2);
// Former: 2 (strict route + event judgment). New: 1 (condensed list). Reason: as above.
assert.equal(domainSections.length, 1);
assert.deepEqual(methodology.domains_without_strict_checklist, []);
});
@@ -72,7 +79,11 @@ test("wealth uses finance-timing-strict as the live checklist, with the wealth a
assert.deepEqual(methodology.domains_without_strict_checklist, []);
const wealth = methodology.sections.find((section) => section.title.includes("finance-timing-strict"));
assert.ok(wealth, "wealth must deliver finance-timing-strict, not the retired primary name");
assert.match(wealth.text, /wealth-timing-strict/);
// Former: the full router section, whose heading names the legacy alias (wealth-timing-strict).
// New: the condensed consultation checklist condensed from that section, titled by the live name.
// Reason: TASK-consult-evidence-card-v2-20260927 T4 (方案 B, consultation only).
assert.equal(wealth.source, CONDENSED_CHECKLIST_SOURCE);
assert.match(wealth.text, /D2/);
assert.ok(
!methodology.sections.some((section) => (
section.title.includes("wealth-timing-strict") && !section.title.includes("finance-timing-strict")