Dialogue cards now take numbers from the same packet as the full-data report. Each of the twelve domains gets one table the card builder assembles. Lookups allow three calls a turn. further_reading is a per-domain technique index. BUG-1293, BUG-1294, BUG-1295.
393 lines
17 KiB
TypeScript
393 lines
17 KiB
TypeScript
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { z } from "zod";
|
|
import {
|
|
consultationDomainDefinition,
|
|
consultationDomainIds,
|
|
type ConsultationDomain,
|
|
} from "./consultation-domain-registry.ts";
|
|
import { resolveLiveJyotishSkill } from "./skill-package-registry.ts";
|
|
import {
|
|
CONDENSED_CHECKLIST_SOURCE,
|
|
CONDENSED_SHARED_READING_SOURCE,
|
|
CONDENSED_SHARED_READING_TITLE,
|
|
CONSULT_BRIEF_CHECKLIST_SOURCE,
|
|
condensedConsultationChecklist,
|
|
sharedConsultationReading,
|
|
} from "./consultation-condensed-checklist.ts";
|
|
import type { ProfileGender } from "./profile-gender.ts";
|
|
|
|
/**
|
|
* The strict checklist each consultation domain must be read against, named in
|
|
* the vocabulary of the skill's own router rather than this codebase's.
|
|
*
|
|
* Activating the skill hands the model its instructions plus a flat list of
|
|
* every file in the package: on the Jyotish package that is over 1500 paths and
|
|
* about 99KB of undifferentiated filenames, against 30KB of actual method. The
|
|
* one line telling it to open `references/strict-workflow-router.md` is inside
|
|
* that method, so in practice no reference was ever opened and every answer was
|
|
* composed from the model's own background knowledge over server evidence.
|
|
*
|
|
* The route is already decided server-side, and the skill already states which
|
|
* checklist each route requires, so the selection needs no model turn: read the
|
|
* mandated sections here and deliver them with the evidence.
|
|
*
|
|
* 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.
|
|
*
|
|
* TASK-consult-affliction-reading-20261001 decision D6 overturns the older rule
|
|
* "a checklist the router does not declare is reported as absent instead of
|
|
* substituted" for the web consultation: every domain now has a list. The
|
|
* `consult-<domain>` names below are local names for the lists that brief
|
|
* wrote (T2); they are not router routes. Health uses its own Chinese list in
|
|
* place of the router's health-timing-strict; timing keeps the router route.
|
|
* Every turn opens with the shared reading (T1) once, before anything else.
|
|
*/
|
|
const domainMethodology: Readonly<Record<ConsultationDomain, { strictRoute: string | null; eventJudgment: string | null }>> = {
|
|
career: { strictRoute: "career-timing-strict", eventJudgment: "event_judgment_career.md" },
|
|
marriage: { strictRoute: "relationship-timing-strict", eventJudgment: "event_judgment_marriage.md" },
|
|
wealth: { strictRoute: "finance-timing-strict", eventJudgment: "event_judgment_wealth.md" },
|
|
timing: { strictRoute: "event-timing-strict", eventJudgment: null },
|
|
// Local list names (TASK-consult-affliction-reading-20261001 T2). The router
|
|
// declares no named strict checklist for these domains (its shared baseline
|
|
// only names D12 for family/ancestral themes and D7 for children), and its
|
|
// health-timing-strict route keeps serving reports.
|
|
health: { strictRoute: "consult-health", eventJudgment: null },
|
|
annual: { strictRoute: "consult-annual", eventJudgment: null },
|
|
general: { strictRoute: "consult-general", eventJudgment: null },
|
|
education: { strictRoute: "consult-education", eventJudgment: null },
|
|
migration: { strictRoute: "consult-migration", eventJudgment: null },
|
|
family: { strictRoute: "consult-family", eventJudgment: null },
|
|
parents: { strictRoute: "consult-parents", eventJudgment: null },
|
|
children: { strictRoute: "consult-children", eventJudgment: null },
|
|
};
|
|
|
|
// Annual and timing single-domain turns used to skip the shared reading and the
|
|
// annual list on a 12,000-character ceiling (BUG-1160/1161). Product decision
|
|
// 2026-10-02: they take the career / marriage / wealth budget instead (contract
|
|
// + card <= 12,000, total with the checklist <= 18,000), so every turn gets both.
|
|
// 2026-10-03 (BUG-1221): every card carries the Rath running MD; the budget is
|
|
// now 12,500 / 18,500.
|
|
|
|
/** Domains whose list was written by the affliction-reading brief rather than condensed from the router. */
|
|
const BRIEF_WRITTEN_DOMAINS: ReadonlySet<ConsultationDomain> = new Set([
|
|
"health", "annual", "general", "education", "migration", "family", "parents", "children",
|
|
]);
|
|
|
|
const ROUTER_FILE = "references/strict-workflow-router.md";
|
|
const SHARED_BASELINE_HEADING = "Shared mandatory baseline";
|
|
const SKELETON_FILE = "references/event_judgment_skeleton.md";
|
|
|
|
/** Per-section and whole-block ceilings, so one long section cannot crowd out the evidence. */
|
|
export const METHODOLOGY_SECTION_MAX_CHARS = 6_000;
|
|
export const METHODOLOGY_TOTAL_MAX_CHARS = 24_000;
|
|
/** Per-domain technique index: up to 8 files times the six-domain turn cap. */
|
|
export const METHODOLOGY_INDEX_MAX_ENTRIES = 48;
|
|
|
|
const DOMAIN_TECHNIQUE_PATHS: Readonly<Record<ConsultationDomain, readonly string[]>> = {
|
|
career: [
|
|
"references/event_judgment_career.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/argala-complete-guide.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
],
|
|
marriage: [
|
|
"references/event_judgment_marriage.md",
|
|
"references/navamsa-marriage-deep-analysis.md",
|
|
"references/darakaraka-complete-guide.md",
|
|
"references/navamsa-d9-interpretation-template.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
],
|
|
wealth: [
|
|
"references/event_judgment_wealth.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/deep-analysis-complete-workflow.md",
|
|
"references/promise-assessment-templates.md",
|
|
],
|
|
health: [
|
|
"references/strict-workflow-router.md",
|
|
"references/shodasavarga-complete-guide.md",
|
|
"references/badhaka-obstacle-planet-guide.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/deep-varga-avastha-execution-guide.md",
|
|
],
|
|
education: [
|
|
"references/strict-workflow-router.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/shodasavarga-complete-guide.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/deep-analysis-complete-workflow.md",
|
|
"references/event_judgment_skeleton.md",
|
|
],
|
|
migration: [
|
|
"references/birth-time-rectification-decision-tree.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/event_judgment_skeleton.md",
|
|
],
|
|
family: [
|
|
"references/strict-workflow-router.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/deep-analysis-complete-workflow.md",
|
|
"references/argala-complete-guide.md",
|
|
],
|
|
parents: [
|
|
"references/strict-workflow-router.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/shodasavarga-complete-guide.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/deep-varga-avastha-execution-guide.md",
|
|
],
|
|
children: [
|
|
"references/strict-workflow-router.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/deep-analysis-complete-workflow.md",
|
|
],
|
|
annual: [
|
|
"references/varshaphala-annual-chart-guide.md",
|
|
"references/tajika-yoga-complete-guide.md",
|
|
"references/vimshottari_dasha_guide.md",
|
|
"references/bphs-ch48-narayana-dasha.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/multi-dasha-convergence-protocol.md",
|
|
"references/alternative-dasha-systems.md",
|
|
"references/event_judgment_skeleton.md",
|
|
],
|
|
timing: [
|
|
"references/bphs-ch48-narayana-dasha.md",
|
|
"references/vimshottari_dasha_guide.md",
|
|
"references/multi-dasha-convergence-protocol.md",
|
|
"references/alternative-dasha-systems.md",
|
|
"references/varshaphala-annual-chart-guide.md",
|
|
"references/strict-workflow-router.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/tajika-yoga-complete-guide.md",
|
|
],
|
|
general: [
|
|
"references/strict-workflow-router.md",
|
|
"references/house-domain-planet-mapping.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
"references/jaimini-complete-system.md",
|
|
"references/pdf-chart-reading-guide.md",
|
|
"references/varga-divisional-charts-quick-reference.md",
|
|
],
|
|
};
|
|
|
|
export const consultationMethodologySectionSchema = z.object({
|
|
title: z.string().max(200),
|
|
source: z.string().max(300),
|
|
text: z.string().max(METHODOLOGY_SECTION_MAX_CHARS),
|
|
}).strict();
|
|
|
|
export const consultationMethodologySchema = z.object({
|
|
skill: z.string().max(120),
|
|
version: z.string().max(60),
|
|
/** Domains whose strict checklist the router does not declare; nothing was substituted for them. */
|
|
domains_without_strict_checklist: z.array(z.enum(consultationDomainIds)).max(6),
|
|
sections: z.array(consultationMethodologySectionSchema).max(12),
|
|
/** The references SKILL.md itself names, for reading on purpose with skill_read. */
|
|
further_reading: z.array(z.string().max(300)).max(METHODOLOGY_INDEX_MAX_ENTRIES),
|
|
truncated: z.boolean(),
|
|
}).strict();
|
|
|
|
export type ConsultationMethodology = z.infer<typeof consultationMethodologySchema>;
|
|
|
|
const skill = resolveLiveJyotishSkill();
|
|
|
|
/** Live skill files are reread only after process restart; consult requests share one cache. */
|
|
const fileCache = new Map<string, string | null>();
|
|
|
|
/** A Windows checkout may store a skill symlink as a one-line relative path. */
|
|
function resolvePackageFile(relativePath: string): string {
|
|
const parts = relativePath.split(/[\\/]/).filter(Boolean);
|
|
let current = skill.resolvedPath;
|
|
for (let index = 0; index < parts.length; index += 1) {
|
|
const next = resolve(current, parts[index]!);
|
|
if (index < parts.length - 1 && existsSync(next)) {
|
|
const stats = statSync(next);
|
|
if (stats.isFile() && stats.size <= 512) {
|
|
const text = readFileSync(next, "utf8").replace(/^\uFEFF/, "").trim();
|
|
if (text.startsWith(".") && !/[\r\n\0]/.test(text)) {
|
|
current = resolve(current, text);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
current = next;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function packageFile(relativePath: string): string | null {
|
|
const hit = fileCache.get(relativePath);
|
|
if (hit !== undefined) return hit;
|
|
let content: string | null = null;
|
|
try {
|
|
content = readFileSync(resolvePackageFile(relativePath), "utf8");
|
|
} catch {
|
|
content = null;
|
|
}
|
|
fileCache.set(relativePath, content);
|
|
return content;
|
|
}
|
|
|
|
function domainTechniqueIndex(domains: readonly ConsultationDomain[]): string[] {
|
|
const rows: string[] = [];
|
|
for (const domain of domains) {
|
|
let kept = 0;
|
|
for (const path of DOMAIN_TECHNIQUE_PATHS[domain]) {
|
|
if (kept >= 8) break;
|
|
if (path.includes("oracle/") || path.toLowerCase().includes("governance")) continue;
|
|
if (packageFile(path) === null) continue;
|
|
rows.push(`${domain}: ${path}`);
|
|
kept += 1;
|
|
if (rows.length >= METHODOLOGY_INDEX_MAX_ENTRIES) return rows;
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* The body of one `## ...` section, found by a heading substring so the section
|
|
* numbering in the router can be renumbered without breaking the lookup.
|
|
*/
|
|
export function markdownSection(source: string, headingContains: string): string | null {
|
|
const lines = source.split("\n");
|
|
const start = lines.findIndex((line) => line.startsWith("## ") && line.includes(headingContains));
|
|
if (start === -1) return null;
|
|
const rest = lines.slice(start + 1);
|
|
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
const body = (end === -1 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
return body.length > 0 ? `${lines[start]}\n\n${body}` : null;
|
|
}
|
|
|
|
function clamp(text: string) {
|
|
return text.length > METHODOLOGY_SECTION_MAX_CHARS
|
|
? `${text.slice(0, METHODOLOGY_SECTION_MAX_CHARS - 3)}...`
|
|
: text;
|
|
}
|
|
|
|
const planCache = new Map<string, ConsultationMethodology>();
|
|
|
|
/**
|
|
* The strict method for one domain plan, read from the live skill tree.
|
|
*/
|
|
export function consultationMethodologyForDomains(
|
|
domains: readonly ConsultationDomain[],
|
|
options: Readonly<{ gender?: ProfileGender | null }> = {},
|
|
): ConsultationMethodology | null {
|
|
const unique = [...new Set(domains)];
|
|
// The marriage checklist depends on the current subject's gender, so the
|
|
// cache is per gender too: one person's value never reaches another's turn.
|
|
const gender = options.gender ?? null;
|
|
const key = `${unique.join(",")}|${gender ?? "unknown"}`;
|
|
const hit = planCache.get(key);
|
|
if (hit) return hit;
|
|
|
|
const router = packageFile(ROUTER_FILE);
|
|
const instructions = packageFile("SKILL.md");
|
|
if (!router || !instructions) return null;
|
|
|
|
const sections: z.infer<typeof consultationMethodologySectionSchema>[] = [];
|
|
let budget = METHODOLOGY_TOTAL_MAX_CHARS;
|
|
let truncated = false;
|
|
|
|
const push = (title: string, source: string, text: string | null) => {
|
|
if (!text) return;
|
|
const bounded = clamp(text);
|
|
if (bounded.length > budget) {
|
|
truncated = true;
|
|
return;
|
|
}
|
|
budget -= bounded.length;
|
|
sections.push({ title, source, text: bounded });
|
|
};
|
|
|
|
// Once per turn, before anything else (T1), however many domains it plans.
|
|
push(CONDENSED_SHARED_READING_TITLE, CONDENSED_SHARED_READING_SOURCE, sharedConsultationReading());
|
|
push("Shared mandatory baseline", ROUTER_FILE, markdownSection(router, SHARED_BASELINE_HEADING));
|
|
|
|
const withoutChecklist: ConsultationDomain[] = [];
|
|
for (const domain of unique) {
|
|
const { strictRoute, eventJudgment } = domainMethodology[domain];
|
|
const condensed = condensedConsultationChecklist(domain, { gender });
|
|
if (condensed) {
|
|
push(
|
|
`${consultationDomainDefinition(domain).label} · ${strictRoute} · 普通对话精简清单`,
|
|
BRIEF_WRITTEN_DOMAINS.has(domain) ? CONSULT_BRIEF_CHECKLIST_SOURCE : CONDENSED_CHECKLIST_SOURCE,
|
|
condensed,
|
|
);
|
|
continue;
|
|
}
|
|
if (!strictRoute || BRIEF_WRITTEN_DOMAINS.has(domain)) {
|
|
withoutChecklist.push(domain);
|
|
} else {
|
|
const section = markdownSection(router, strictRoute);
|
|
if (section) {
|
|
push(`${consultationDomainDefinition(domain).label} · ${strictRoute}`, ROUTER_FILE, section);
|
|
} else {
|
|
withoutChecklist.push(domain);
|
|
}
|
|
}
|
|
if (eventJudgment) {
|
|
push(`${consultationDomainDefinition(domain).label} · event judgment`, `references/${eventJudgment}`, packageFile(`references/${eventJudgment}`));
|
|
}
|
|
}
|
|
|
|
const methodology = consultationMethodologySchema.parse({
|
|
skill: skill.name,
|
|
version: skill.version,
|
|
domains_without_strict_checklist: withoutChecklist,
|
|
sections,
|
|
further_reading: domainTechniqueIndex(unique),
|
|
truncated,
|
|
});
|
|
planCache.set(key, methodology);
|
|
return methodology;
|
|
}
|
|
|
|
/** Cross-domain method that belongs in the cached system block, not the tool result. */
|
|
export function sharedConsultationMethodMarkdown(): string {
|
|
const router = packageFile(ROUTER_FILE);
|
|
const fullSpectrum = router ? markdownSection(router, "Full-Spectrum Invocation Contract") : null;
|
|
const skeleton = packageFile(SKELETON_FILE);
|
|
return [fullSpectrum, skeleton].filter((part): part is string => Boolean(part)).join("\n\n");
|
|
}
|