d35828e76d
Manual SKILL.md updates were blocked by registry sha256 and a byte-equal versions/ gate. Consult now loads the operator-maintained tree; rectification and personal-report stay hashed. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
7.8 KiB
TypeScript
192 lines
7.8 KiB
TypeScript
import { readFileSync } 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";
|
|
|
|
/**
|
|
* 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. A checklist the
|
|
* router does not declare is reported as absent instead of substituted.
|
|
*/
|
|
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: "wealth-timing-strict", eventJudgment: "event_judgment_wealth.md" },
|
|
timing: { strictRoute: "event-timing-strict", eventJudgment: null },
|
|
health: { strictRoute: "health-timing-strict", eventJudgment: null },
|
|
annual: { strictRoute: null, eventJudgment: null },
|
|
general: { strictRoute: null, eventJudgment: null },
|
|
education: { strictRoute: null, eventJudgment: null },
|
|
migration: { strictRoute: null, eventJudgment: null },
|
|
family: { strictRoute: null, eventJudgment: null },
|
|
};
|
|
|
|
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;
|
|
/** How many of the references SKILL.md names are offered as a navigable index. */
|
|
export const METHODOLOGY_INDEX_MAX_ENTRIES = 24;
|
|
|
|
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>();
|
|
|
|
function packageFile(relativePath: string): string | null {
|
|
const hit = fileCache.get(relativePath);
|
|
if (hit !== undefined) return hit;
|
|
let content: string | null = null;
|
|
try {
|
|
content = readFileSync(resolve(skill.resolvedPath, relativePath), "utf8");
|
|
} catch {
|
|
content = null;
|
|
}
|
|
fileCache.set(relativePath, content);
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* The reference paths SKILL.md names, in the order it names them.
|
|
*
|
|
* Derived rather than listed so that the index follows the skill: when SKILL.md
|
|
* starts pointing somewhere else, the model is offered the new pointer without
|
|
* this file being edited. Paths that are not in the package are dropped, so a
|
|
* stale pointer cannot be advertised as readable.
|
|
*/
|
|
function namedReferences(instructions: string): string[] {
|
|
const found = new Set<string>();
|
|
for (const match of instructions.matchAll(/references\/[\w./-]*[\w/-]/g)) {
|
|
const path = match[0].replace(/[.]+$/, "");
|
|
if (path.includes("..") || !/\.\w+$/.test(path)) continue;
|
|
if (packageFile(path) === null) continue;
|
|
found.add(path);
|
|
if (found.size >= METHODOLOGY_INDEX_MAX_ENTRIES) break;
|
|
}
|
|
return [...found];
|
|
}
|
|
|
|
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[],
|
|
): ConsultationMethodology | null {
|
|
const unique = [...new Set(domains)];
|
|
const key = unique.join(",");
|
|
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 });
|
|
};
|
|
|
|
push("Shared mandatory baseline", ROUTER_FILE, markdownSection(router, SHARED_BASELINE_HEADING));
|
|
push("Full-spectrum invocation", ROUTER_FILE, markdownSection(router, "Full-Spectrum Invocation Contract"));
|
|
push("Event judgment skeleton", SKELETON_FILE, packageFile(SKELETON_FILE));
|
|
|
|
const withoutChecklist: ConsultationDomain[] = [];
|
|
for (const domain of unique) {
|
|
const { strictRoute, eventJudgment } = domainMethodology[domain];
|
|
if (!strictRoute) {
|
|
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: namedReferences(instructions),
|
|
truncated,
|
|
});
|
|
planCache.set(key, methodology);
|
|
return methodology;
|
|
}
|