fix(rectification): make delivery report use inference-window facts (BUG-593)
The verification template was still quoting the engine's pre-inference span and eliminated dasha tops, and D9/D10 signs were left for the model to guess from transition clocks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,13 @@ export function timeToMinutes(value: string): number | null {
|
||||
* Width of the indistinguishable top cluster, in minutes.
|
||||
* Uses the engine's tied_minute_count and the inclusive span of public times.
|
||||
*/
|
||||
export function inclusiveClockWidthMinutes(start: string, end: string): number | null {
|
||||
const from = timeToMinutes(start);
|
||||
const to = timeToMinutes(end);
|
||||
if (from === null || to === null) return null;
|
||||
return Math.max(to - from + 1, 1);
|
||||
}
|
||||
|
||||
export function indistinguishableWidthMinutes(
|
||||
candidates: readonly Readonly<{
|
||||
time: string;
|
||||
|
||||
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
|
||||
export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.15";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.16";
|
||||
|
||||
@@ -122,7 +122,7 @@ function signFromProbe(
|
||||
return null;
|
||||
}
|
||||
|
||||
function signFromTransitions(
|
||||
export function signFromTransitions(
|
||||
transitions: readonly WindowScanTransition[],
|
||||
layer: "d9" | "d10",
|
||||
time: string,
|
||||
|
||||
@@ -384,6 +384,63 @@ export function parseDashaAgreement(value: unknown): DashaAgreement | null {
|
||||
};
|
||||
}
|
||||
|
||||
function dashaAgreementCopy(
|
||||
status: DashaAgreement["status"],
|
||||
vim: string | null,
|
||||
nar: string | null,
|
||||
): DashaAgreement {
|
||||
if (status === "conflict" && vim && nar) {
|
||||
return {
|
||||
status,
|
||||
vimshottari_top: vim,
|
||||
narayana_top: nar,
|
||||
user_meaning: `主限更偏向 ${vim},分盘大运更偏向 ${nar}。冲突时不能按更高把握收口。`,
|
||||
};
|
||||
}
|
||||
if (status === "agree") {
|
||||
return {
|
||||
status,
|
||||
vimshottari_top: vim,
|
||||
narayana_top: nar,
|
||||
user_meaning: "主限和分盘大运都更支持同一段代表性时间。这仍不是唯一分钟确认。",
|
||||
};
|
||||
}
|
||||
if (status === "partial") {
|
||||
return {
|
||||
status,
|
||||
vimshottari_top: vim,
|
||||
narayana_top: nar,
|
||||
user_meaning: "主限和分盘大运还不能做成完整对照,只作观察。",
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "unavailable",
|
||||
vimshottari_top: null,
|
||||
narayana_top: null,
|
||||
user_meaning: "还没有足够的大运对照。",
|
||||
};
|
||||
}
|
||||
|
||||
/** Recompute Vimshottari / Narayana tops inside the still-active inference minutes. */
|
||||
export function dashaAgreementAmongActive(
|
||||
engine: DashaAgreement | null,
|
||||
activeTimes: readonly string[],
|
||||
): DashaAgreement | null {
|
||||
if (!engine) return null;
|
||||
const active = new Set(activeTimes);
|
||||
if (active.size === 0) return dashaAgreementCopy("unavailable", null, null);
|
||||
const vim = engine.vimshottari_top && active.has(engine.vimshottari_top)
|
||||
? engine.vimshottari_top
|
||||
: null;
|
||||
const nar = engine.narayana_top && active.has(engine.narayana_top)
|
||||
? engine.narayana_top
|
||||
: null;
|
||||
if (vim && nar && vim === nar) return dashaAgreementCopy("agree", vim, nar);
|
||||
if (vim && nar) return dashaAgreementCopy("conflict", vim, nar);
|
||||
if (!vim && !nar) return dashaAgreementCopy("agree", null, null);
|
||||
return dashaAgreementCopy("partial", vim, nar);
|
||||
}
|
||||
|
||||
function parseLords(value: unknown): LagnaContrastInterval["lords"] | null {
|
||||
const row = asRecord(value);
|
||||
if (!row) return null;
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* fate promises.
|
||||
*/
|
||||
|
||||
import type { EventDashaLedgerRow } from "./refinement-packet";
|
||||
import type { DashaAgreement, EventDashaLedgerRow, WindowScanTransition } from "./refinement-packet";
|
||||
import type { WindowScan } from "./varga-observations";
|
||||
import type { CandidateSeparation } from "../core/candidate-separation";
|
||||
import { signFromTransitions } from "./divergence-panel";
|
||||
import { D9_TYPE_TABLE, D10_TYPE_TABLE, signKey } from "./varga-type-tables";
|
||||
|
||||
function typeRow(sign: string, table: "d9" | "d10"): string {
|
||||
@@ -23,6 +24,18 @@ function typeRow(sign: string, table: "d9" | "d10"): string {
|
||||
return `| ${key} | ${row.trait} | ${row.occupation} | ${row.style} |`;
|
||||
}
|
||||
|
||||
export type CandidateVargaSigns = Readonly<{
|
||||
d9: string | null;
|
||||
d10: string | null;
|
||||
}>;
|
||||
|
||||
export type SkillVerificationReport = Readonly<{
|
||||
width_minutes: number | null;
|
||||
dasha_agreement: DashaAgreement | null;
|
||||
sign_by_candidate: Readonly<Record<string, CandidateVargaSigns>>;
|
||||
markdown: string;
|
||||
}>;
|
||||
|
||||
export type SkillVerificationReportInput = Readonly<{
|
||||
representativeTime: string | null;
|
||||
widthMinutes: number | null;
|
||||
@@ -38,7 +51,8 @@ export type SkillVerificationReportInput = Readonly<{
|
||||
user_meaning: string;
|
||||
unique_minute_claim: false;
|
||||
}> | null;
|
||||
dashaAgreement?: Readonly<{ status: string; user_meaning: string }> | null;
|
||||
dashaAgreement?: DashaAgreement | null;
|
||||
signByCandidate?: Readonly<Record<string, CandidateVargaSigns>>;
|
||||
techniqueAuditTable?: readonly Readonly<{
|
||||
technique?: string;
|
||||
status?: string;
|
||||
@@ -49,26 +63,71 @@ export type SkillVerificationReportInput = Readonly<{
|
||||
engineResultId?: string | null;
|
||||
}>;
|
||||
|
||||
export function signByCandidateFromTransitions(
|
||||
times: readonly string[],
|
||||
transitions: readonly WindowScanTransition[],
|
||||
): Readonly<Record<string, CandidateVargaSigns>> {
|
||||
const out: Record<string, CandidateVargaSigns> = {};
|
||||
for (const time of times) {
|
||||
out[time] = {
|
||||
d9: signFromTransitions(transitions, "d9", time),
|
||||
d10: signFromTransitions(transitions, "d10", time),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function uniqueSigns(signs: readonly (string | null | undefined)[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const sign of signs) {
|
||||
const keyed = sign ? signKey(sign) : "";
|
||||
if (!keyed || seen.has(keyed)) continue;
|
||||
seen.add(keyed);
|
||||
out.push(keyed);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildSkillVerificationReport(input: SkillVerificationReportInput): string {
|
||||
return buildSkillVerificationPacket(input).markdown;
|
||||
}
|
||||
|
||||
export function buildSkillVerificationPacket(input: SkillVerificationReportInput): SkillVerificationReport {
|
||||
const time = input.representativeTime ?? "尚未选定";
|
||||
const width = input.widthMinutes == null ? "未知" : `${input.widthMinutes} 分钟`;
|
||||
const lagna = input.houseLagna?.trim() || "未交付";
|
||||
const fit = input.eventFitRate;
|
||||
const ledger = input.eventDashaLedger ?? [];
|
||||
const d9Signs = input.windowScan?.d9_sign_names ?? [];
|
||||
const d10Signs = input.windowScan?.d10_sign_names ?? [];
|
||||
const signByCandidate = input.signByCandidate
|
||||
?? signByCandidateFromTransitions(
|
||||
input.candidates.map((row) => row.time),
|
||||
input.windowScan?.transitions ?? [],
|
||||
);
|
||||
const d9Signs = uniqueSigns(Object.values(signByCandidate).map((row) => row.d9));
|
||||
const d10Signs = uniqueSigns(Object.values(signByCandidate).map((row) => row.d10));
|
||||
const d9TableSigns = d9Signs.length > 0 ? d9Signs : uniqueSigns(input.windowScan?.d9_sign_names ?? []);
|
||||
const d10TableSigns = d10Signs.length > 0 ? d10Signs : uniqueSigns(input.windowScan?.d10_sign_names ?? []);
|
||||
const d9Differs = d9Signs.length >= 2
|
||||
|| (d9Signs.length === 0 && input.windowScan?.d9_candidates_differ === true);
|
||||
const d10Differs = d10Signs.length >= 2
|
||||
|| (d10Signs.length === 0 && input.windowScan?.d10_candidates_differ === true);
|
||||
const tied = input.separation?.status === "not_separated";
|
||||
const candidateLines = input.candidates.slice(0, 5).map((row) => (
|
||||
`| ${row.rank} | ${row.time} | ${row.relativeSupport} |`
|
||||
));
|
||||
const signRows = input.candidates.map((row) => {
|
||||
const signs = signByCandidate[row.time];
|
||||
return `| ${row.time} | ${signs?.d9 ?? "—"} | ${signs?.d10 ?? "—"} |`;
|
||||
});
|
||||
const dashaRows = ledger.length > 0
|
||||
? ledger.map((row) => `| ${row.summary} | ${row.match_label} | ${row.user_meaning} |`)
|
||||
: ["| (本轮还没有已确认事件行) | — | 先收带日期经历再填这张表 |"];
|
||||
const d9Table = d9Signs.length > 0
|
||||
? ["| D9上升 | 感情特质 | 配偶类型 | 婚姻特点 |", "|---|---|---|---|", ...d9Signs.map((sign) => typeRow(sign, "d9"))]
|
||||
const d9Table = d9TableSigns.length > 0
|
||||
? ["| D9上升 | 感情特质 | 配偶类型 | 婚姻特点 |", "|---|---|---|---|", ...d9TableSigns.map((sign) => typeRow(sign, "d9"))]
|
||||
: ["当前窗还没有可对照的 D9 上升名,类型表待候选换升后填写。"];
|
||||
const d10Table = d10Signs.length > 0
|
||||
? ["| D10上升 | 事业特质 | 职业类型 | 工作风格 |", "|---|---|---|---|", ...d10Signs.map((sign) => typeRow(sign, "d10"))]
|
||||
const d10Table = d10TableSigns.length > 0
|
||||
? ["| D10上升 | 事业特质 | 职业类型 | 工作风格 |", "|---|---|---|---|", ...d10TableSigns.map((sign) => typeRow(sign, "d10"))]
|
||||
: ["当前窗还没有可对照的 D10 上升名,类型表待候选换升后填写。"];
|
||||
const audit = (input.techniqueAuditTable ?? []).slice(0, 16);
|
||||
const auditRows = audit.length > 0
|
||||
@@ -89,9 +148,9 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
"| 占问 | observation_only | AI 暂不支持独立占问;有问起时间则观察 |",
|
||||
];
|
||||
|
||||
return [
|
||||
const markdown = [
|
||||
"## 生时纠正验证报告(skill 八法)",
|
||||
"这是当前窗的相对拟合,标签是 `candidate_range_not_birth_time_truth`。采用只会把代表性时间写入当前排盘,不等于确认唯一出生分钟。",
|
||||
"这是当前窗的相对拟合,标签是 `candidate_range_not_birth_time_truth`。采用只会把代表性时间写入当前排盘,不是已确认的唯一出生分钟。",
|
||||
"",
|
||||
"### 筛选",
|
||||
tied
|
||||
@@ -99,10 +158,10 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
: `- 当前代表分钟:${time}`,
|
||||
`- 不可分宽度:${width}`,
|
||||
`- 本命上升(该分钟):${lagna}`,
|
||||
input.windowScan?.d9_candidates_differ
|
||||
d9Differs
|
||||
? "- D9 仍会换升。这是候选结构差异,应生成区分探针,不能直接宣布不可分。"
|
||||
: "- D9 上升在当前窗较稳。",
|
||||
input.windowScan?.d10_candidates_differ
|
||||
d10Differs
|
||||
? "- D10 仍会换升。这是候选结构差异,应生成区分探针,不能直接宣布不可分。"
|
||||
: "- D10 上升在当前窗较稳。",
|
||||
"",
|
||||
@@ -111,6 +170,11 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
"|---|---|---|",
|
||||
...(candidateLines.length > 0 ? candidateLines : ["| — | — | 还没有可出示的候选 |"]),
|
||||
"",
|
||||
"### 各候选分盘上升(只抄本表)",
|
||||
"| 时间 | D9 | D10 |",
|
||||
"|---|---|---|",
|
||||
...(signRows.length > 0 ? signRows : ["| — | — | 还没有可出示的候选 |"]),
|
||||
"",
|
||||
"### 方法1:Dasha + Gochara",
|
||||
fit
|
||||
? `${fit.user_meaning} ${fit.label}。这是事件拟合程度,不是候选区分程度。`
|
||||
@@ -122,7 +186,7 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
input.dashaAgreement ? `双轨:${input.dashaAgreement.user_meaning}` : "Narayana 对照未交付时只保留 Vimshottari。",
|
||||
"",
|
||||
"### 方法2–3:D9 / D10 类型表",
|
||||
"写的是「该分钟下分盘升 X,与用户所述特质的对应/冲突」。这是校时方法,不是咨询命运承诺。",
|
||||
"写的是「该分钟下分盘升 X,与用户所述特质的对应/冲突」。这是校时方法,不是咨询命运承诺。分盘上升只抄本报告 `sign_by_candidate`,不得自行按换升时刻推算。",
|
||||
...d9Table,
|
||||
"",
|
||||
...d10Table,
|
||||
@@ -147,4 +211,11 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
"",
|
||||
"不得把 VedAstro/holdout 或高吻合写成已校到唯一分钟。",
|
||||
].join("\n");
|
||||
|
||||
return {
|
||||
width_minutes: input.widthMinutes,
|
||||
dasha_agreement: input.dashaAgreement ?? null,
|
||||
sign_by_candidate: signByCandidate,
|
||||
markdown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@ import {
|
||||
isHoldoutVerificationQuote,
|
||||
isPersistedFocusId,
|
||||
} from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import { indistinguishableWidthMinutes } from "@/lib/rectification-agentic/v9/candidate-plateau";
|
||||
import { inclusiveClockWidthMinutes, indistinguishableWidthMinutes } from "@/lib/rectification-agentic/v9/candidate-plateau";
|
||||
import { rankActive } from "@/lib/rectification-agentic/core/convergence-evaluator";
|
||||
import { unionStillValidRange } from "@/lib/rectification-agentic/core/credible-range";
|
||||
import { followupCaseArgs } from "@/lib/rectification-agentic/v9/block-scan";
|
||||
import { scoreAndPersistCurrentEvidence as persistCurrentEvidenceScore } from "@/lib/rectification-agentic/v9/score-persist";
|
||||
import {
|
||||
@@ -73,7 +75,7 @@ import {
|
||||
spokenCollectFallbackFollowup,
|
||||
collectQuestionDomain,
|
||||
} from "@/lib/rectification-agentic/v9/method-followup";
|
||||
import { refinementFromDecisionReceipt } from "@/lib/rectification-agentic/v9/refinement-packet";
|
||||
import { dashaAgreementAmongActive, refinementFromDecisionReceipt } from "@/lib/rectification-agentic/v9/refinement-packet";
|
||||
import { rangeDeliveryForSnapshot } from "@/lib/rectification-agentic/v9/divergence-panel";
|
||||
import { rectificationLabel } from "@/lib/rectification-agentic/v9/rectification-label";
|
||||
import {
|
||||
@@ -128,7 +130,10 @@ import {
|
||||
storedSnapshotIsCurrent,
|
||||
SNAPSHOT_STALE_COPY,
|
||||
} from "@/lib/rectification-agentic/core/snapshot-source";
|
||||
import { buildSkillVerificationReport } from "@/lib/rectification-agentic/v9/skill-verification-report";
|
||||
import {
|
||||
buildSkillVerificationPacket,
|
||||
signByCandidateFromTransitions,
|
||||
} from "@/lib/rectification-agentic/v9/skill-verification-report";
|
||||
import {
|
||||
internalObservationsFromWindowScan,
|
||||
windowScanFromDecisionReceipt,
|
||||
@@ -458,15 +463,35 @@ export function latestResultToolProjection(
|
||||
latest as unknown as Record<string, unknown>,
|
||||
latest.decisionReceipt ?? null,
|
||||
);
|
||||
const reportCandidates = candidateProjection.fromInference
|
||||
? candidateProjection.candidates
|
||||
: latest.candidates;
|
||||
const activeTimes = inference
|
||||
? rankActive(inference.candidates).map((item) => item.time)
|
||||
: reportCandidates.map((item) => item.time);
|
||||
const reportRange = inference
|
||||
? inference.credible_range ?? unionStillValidRange(inference.candidates)
|
||||
: null;
|
||||
const reportWidth = reportRange
|
||||
? inclusiveClockWidthMinutes(reportRange[0], reportRange[1]) ?? width
|
||||
: width;
|
||||
const engineDasha = refinement.dasha_agreement;
|
||||
const reportDasha = inference
|
||||
? dashaAgreementAmongActive(engineDasha, activeTimes)
|
||||
: engineDasha;
|
||||
const signByCandidate = signByCandidateFromTransitions(
|
||||
activeTimes.length > 0 ? activeTimes : reportCandidates.map((item) => item.time),
|
||||
windowScan?.transitions ?? [],
|
||||
);
|
||||
const base = {
|
||||
result_id: latest.resultId,
|
||||
candidates: candidateProjection.fromInference ? candidateProjection.candidates : latest.candidates,
|
||||
candidates: reportCandidates,
|
||||
confirmation_allowed: confirmationGate.confirmation_allowed,
|
||||
representative_time: representativeTime,
|
||||
selected_time: latest.selectedTime,
|
||||
selection_kind: latest.selectionKind,
|
||||
algorithm_version: latest.algorithmVersion,
|
||||
indistinguishable_width_minutes: width,
|
||||
engine_indistinguishable_width_minutes: width,
|
||||
window_scan: windowScan,
|
||||
confirmation_gate: confirmationGate,
|
||||
candidate_separation: decision.separation,
|
||||
@@ -477,7 +502,8 @@ export function latestResultToolProjection(
|
||||
candidate_contrast_packet: contrastPacket,
|
||||
event_dasha_ledger: refinement.event_dasha_ledger,
|
||||
event_fit_rate: refinement.event_fit_rate,
|
||||
dasha_agreement: refinement.dasha_agreement,
|
||||
dasha_agreement: reportDasha,
|
||||
...(inference ? { dasha_agreement_pre_inference: engineDasha } : {}),
|
||||
lagna_contrast: refinement.lagna_contrast,
|
||||
nakshatra_boundary: refinement.nakshatra_boundary,
|
||||
precision_stage: refinement.precision_stage,
|
||||
@@ -488,15 +514,16 @@ export function latestResultToolProjection(
|
||||
candidate_contrast_opportunities: refinement.candidate_contrast_opportunities,
|
||||
unique_minute_claim: false,
|
||||
candidate_range_not_birth_time_truth: true,
|
||||
skill_verification_report: buildSkillVerificationReport({
|
||||
skill_verification_report: buildSkillVerificationPacket({
|
||||
representativeTime,
|
||||
widthMinutes: width,
|
||||
candidates: candidateProjection.fromInference ? candidateProjection.candidates : latest.candidates,
|
||||
widthMinutes: reportWidth,
|
||||
candidates: reportCandidates,
|
||||
houseLagna: chart.houseTable?.lagna ?? null,
|
||||
windowScan,
|
||||
eventDashaLedger: refinement.event_dasha_ledger,
|
||||
eventFitRate: refinement.event_fit_rate,
|
||||
dashaAgreement: refinement.dasha_agreement,
|
||||
dashaAgreement: reportDasha,
|
||||
signByCandidate,
|
||||
techniqueAuditTable: Array.isArray(latest.decisionReceipt?.technique_audit_table)
|
||||
? latest.decisionReceipt.technique_audit_table as Array<{
|
||||
technique?: string;
|
||||
@@ -551,6 +578,7 @@ function agentVisibleLatestProjection(
|
||||
evidence_collection_probes: _collect,
|
||||
candidate_contrast_opportunities: _opportunities,
|
||||
inference_state: inference,
|
||||
dasha_agreement_pre_inference: _preDasha,
|
||||
...rest
|
||||
} = projection;
|
||||
const currentQuestion = extras.openQuestion
|
||||
|
||||
@@ -239,3 +239,11 @@ test("E-2 display copy keeps engine meaning out of instruction tone", () => {
|
||||
assert.match(board, /engineMeaningToDisplayCopy/);
|
||||
assert.match(gate, /engineMeaningToDisplayCopy/);
|
||||
});
|
||||
|
||||
test("skill forbids computing D9/D10 signs from transition clocks", () => {
|
||||
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
|
||||
const comparison = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/references/candidate-comparison.md", import.meta.url), "utf8");
|
||||
const sentence = "分盘上升只抄 `skill_verification_report.sign_by_candidate`,不得自行按换升时刻推算";
|
||||
assert.match(skill, new RegExp(sentence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(comparison, new RegExp(sentence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
});
|
||||
|
||||
@@ -462,8 +462,8 @@ function rpcDossier(decision: DecisionDossier, activeFocus?: Record<string, unkn
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version stays 10.0.15 after the range-delivery bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
test("skill version stays 10.0.16 after the delivery-report-facts bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("revision 5 with uncovered relatives asks the dated family collect, not a yearless D12 card", () => {
|
||||
|
||||
@@ -269,7 +269,7 @@ test("a 25-minute plateau stays confirmation-blocked as an indistinguishable ran
|
||||
algorithmVersion: "rectification-v5",
|
||||
}, collectDecision(PLATEAU));
|
||||
const gate = projection.confirmation_gate as ReturnType<typeof buildConfirmationGate>;
|
||||
assert.ok(Number(projection.indistinguishable_width_minutes) >= 25);
|
||||
assert.ok(Number(projection.engine_indistinguishable_width_minutes) >= 25);
|
||||
assert.equal(projection.confirmation_allowed, false);
|
||||
assert.equal(gate.confirmation_allowed, false);
|
||||
assert.equal(blocker(gate, "adjacent_minutes_indistinguishable").status, "blocked");
|
||||
@@ -359,7 +359,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
|
||||
assert.match(agentSource, /不得宣称唯一出生分钟/);
|
||||
assert.doesNotMatch(agentSource, /±2 分钟/);
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import { signFromTransitions } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import {
|
||||
OPEN_ENGINE_CAPABILITY_CEILING,
|
||||
CANDIDATE_ID,
|
||||
RESULT_ID,
|
||||
SECOND_CANDIDATE_ID,
|
||||
} from "./rectification-v9-test-support.ts";
|
||||
|
||||
const ELIMINATED_059_ID = "88888888-8888-4888-8888-888888888884";
|
||||
const ELIMINATED_506_ID = "88888888-8888-4888-8888-888888888885";
|
||||
const SKILL_SIGN_SENTENCE = "分盘上升只抄 `skill_verification_report.sign_by_candidate`,不得自行按换升时刻推算";
|
||||
|
||||
const ENGINE_CANDIDATES = [
|
||||
{ candidateId: CANDIDATE_ID, time: "04:51", rank: 1, relativeSupport: 38, tiedMinuteCount: 29 },
|
||||
{ candidateId: SECOND_CANDIDATE_ID, time: "04:53", rank: 2, relativeSupport: 36, tiedMinuteCount: 29 },
|
||||
{ candidateId: ELIMINATED_059_ID, time: "04:59", rank: 3, relativeSupport: 14, tiedMinuteCount: 29 },
|
||||
{ candidateId: ELIMINATED_506_ID, time: "05:06", rank: 4, relativeSupport: 12, tiedMinuteCount: 29 },
|
||||
] as const;
|
||||
|
||||
function accidentInferenceState() {
|
||||
const times = ENGINE_CANDIDATES.map((item) => item.time);
|
||||
return {
|
||||
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
||||
candidate_set_id: candidateSetId("04:45", "05:13", times),
|
||||
revision: 1,
|
||||
phase: "discrimination" as const,
|
||||
result_status: "credible_range" as const,
|
||||
range_start: "04:45",
|
||||
range_end: "05:13",
|
||||
candidates: [
|
||||
{
|
||||
id: CANDIDATE_ID,
|
||||
time: "04:51",
|
||||
cluster_range: ["04:51", "04:53"] as const,
|
||||
prior_score: 38,
|
||||
posterior_score: 48,
|
||||
probability: 0.4,
|
||||
status: "active" as const,
|
||||
rank: 2,
|
||||
strong_conflict_count: 0,
|
||||
},
|
||||
{
|
||||
id: SECOND_CANDIDATE_ID,
|
||||
time: "04:53",
|
||||
cluster_range: ["04:51", "04:53"] as const,
|
||||
prior_score: 36,
|
||||
posterior_score: 52,
|
||||
probability: 0.52,
|
||||
status: "active" as const,
|
||||
rank: 1,
|
||||
strong_conflict_count: 0,
|
||||
},
|
||||
{
|
||||
id: ELIMINATED_059_ID,
|
||||
time: "04:59",
|
||||
cluster_range: ["04:59", "04:59"] as const,
|
||||
prior_score: 14,
|
||||
posterior_score: 8,
|
||||
probability: 0.04,
|
||||
status: "eliminated" as const,
|
||||
rank: 3,
|
||||
strong_conflict_count: 3,
|
||||
},
|
||||
{
|
||||
id: ELIMINATED_506_ID,
|
||||
time: "05:06",
|
||||
cluster_range: ["05:06", "05:06"] as const,
|
||||
prior_score: 12,
|
||||
posterior_score: 6,
|
||||
probability: 0.04,
|
||||
status: "eliminated" as const,
|
||||
rank: 4,
|
||||
strong_conflict_count: 3,
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 1.1,
|
||||
representative_time: "04:53",
|
||||
credible_range: ["04:51", "04:53"] as const,
|
||||
};
|
||||
}
|
||||
|
||||
function accidentProjection() {
|
||||
return latestResultToolProjection({
|
||||
resultId: RESULT_ID,
|
||||
candidates: ENGINE_CANDIDATES,
|
||||
selectionAllowed: true,
|
||||
confirmationAllowed: false,
|
||||
representativeTime: "05:06",
|
||||
selectedTime: null,
|
||||
selectionKind: null,
|
||||
algorithmVersion: "rectification-v5",
|
||||
decisionReceipt: {
|
||||
acceptance_allowed: true,
|
||||
selection_allowed: true,
|
||||
propose_allowed: true,
|
||||
confirmation_allowed: false,
|
||||
inference_state: accidentInferenceState(),
|
||||
dasha_agreement: {
|
||||
status: "conflict",
|
||||
vimshottari_top: "05:06",
|
||||
narayana_top: "04:59",
|
||||
user_meaning: "主限更偏向 05:06,分盘大运更偏向 04:59。冲突时不能按更高把握收口。",
|
||||
},
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d9_lagna_count: 1,
|
||||
d10_lagna_count: 2,
|
||||
d9_candidates_differ: false,
|
||||
d10_candidates_differ: true,
|
||||
d9_sign_names: ["天秤座"],
|
||||
d10_sign_names: ["巨蟹座", "狮子座"],
|
||||
transitions: [
|
||||
{
|
||||
layer: "d10",
|
||||
at: "05:00",
|
||||
from_sign: "巨蟹座",
|
||||
to_sign: "狮子座",
|
||||
user_meaning: "D10 在 05:00 发生变化",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, decideRectification({
|
||||
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
||||
methodCoverageAll: true,
|
||||
trainingGateOpen: true,
|
||||
candidateScores: ENGINE_CANDIDATES.map((item) => ({ time: item.time, score: item.relativeSupport })),
|
||||
}));
|
||||
}
|
||||
|
||||
function reportPacket(value: unknown): {
|
||||
width_minutes: number | null;
|
||||
dasha_agreement: { status: string; vimshottari_top: string | null; narayana_top: string | null; user_meaning: string } | null;
|
||||
sign_by_candidate: Record<string, { d9: string | null; d10: string | null }>;
|
||||
markdown: string;
|
||||
} {
|
||||
assert.ok(value && typeof value === "object" && !Array.isArray(value));
|
||||
const row = value as Record<string, unknown>;
|
||||
assert.equal(typeof row.markdown, "string");
|
||||
assert.ok(row.sign_by_candidate && typeof row.sign_by_candidate === "object");
|
||||
return row as ReturnType<typeof reportPacket>;
|
||||
}
|
||||
|
||||
test("D10 still Cancer at 04:53 when the scan only flips to Leo at 05:00", () => {
|
||||
const transitions = [
|
||||
{ layer: "d10" as const, at: "05:00", from_sign: "巨蟹座", to_sign: "狮子座", user_meaning: "D10 在 05:00 发生变化" },
|
||||
];
|
||||
assert.equal(signFromTransitions(transitions, "d10", "04:51"), "巨蟹座");
|
||||
assert.equal(signFromTransitions(transitions, "d10", "04:53"), "巨蟹座");
|
||||
assert.equal(signFromTransitions(transitions, "d10", "05:00"), "狮子座");
|
||||
});
|
||||
|
||||
test("delivery report uses the inference window and active dasha tops, not the pre-inference engine span", () => {
|
||||
const projection = accidentProjection();
|
||||
const report = reportPacket(projection.skill_verification_report);
|
||||
assert.equal(report.width_minutes, 3);
|
||||
assert.equal(projection.engine_indistinguishable_width_minutes, 29);
|
||||
assert.equal(projection.indistinguishable_width_minutes, undefined);
|
||||
const dasha = report.dasha_agreement;
|
||||
assert.ok(dasha);
|
||||
const tops = [dasha.vimshottari_top, dasha.narayana_top].filter(Boolean);
|
||||
for (const time of tops) {
|
||||
assert.ok(time === "04:51" || time === "04:53", time ?? "");
|
||||
}
|
||||
assert.notEqual(dasha.status, "conflict");
|
||||
assert.doesNotMatch(report.markdown, /29 分钟/);
|
||||
assert.doesNotMatch(report.markdown, /05:06/);
|
||||
assert.doesNotMatch(report.markdown, /04:59/);
|
||||
assert.match(report.markdown, /3 分钟/);
|
||||
assert.match(report.markdown, /不是已确认的唯一出生分钟/);
|
||||
const pre = projection.dasha_agreement_pre_inference as { vimshottari_top?: string; narayana_top?: string } | null;
|
||||
assert.equal(pre?.vimshottari_top, "05:06");
|
||||
assert.equal(pre?.narayana_top, "04:59");
|
||||
const live = projection.dasha_agreement as { vimshottari_top?: string | null; narayana_top?: string | null };
|
||||
assert.notEqual(live.vimshottari_top, "05:06");
|
||||
assert.notEqual(live.narayana_top, "04:59");
|
||||
});
|
||||
|
||||
test("delivery report gives 04:53 D10 as Cancer instead of letting the model infer Leo", () => {
|
||||
const projection = accidentProjection();
|
||||
const report = reportPacket(projection.skill_verification_report);
|
||||
assert.equal(report.sign_by_candidate["04:53"]?.d10, "巨蟹座");
|
||||
assert.equal(report.sign_by_candidate["04:51"]?.d10, "巨蟹座");
|
||||
assert.doesNotMatch(report.markdown, /04:53[^\n]*狮子座/);
|
||||
assert.match(report.markdown, /04:53 \| .*巨蟹座/);
|
||||
});
|
||||
|
||||
test("skill 10.0.16 forbids computing varga signs from transition times", () => {
|
||||
const skillDir = fileURLToPath(new URL("../../skills/jyotish-birth-time-rectification", import.meta.url));
|
||||
const skill = readFileSync(`${skillDir}/SKILL.md`, "utf8");
|
||||
const comparison = readFileSync(`${skillDir}/references/candidate-comparison.md`, "utf8");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
assert.match(skill, /^version: 10\.0\.16$/m);
|
||||
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
});
|
||||
@@ -993,7 +993,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
|
||||
internal_observations: Array<{ layer: string; ask_theme: string | null }>;
|
||||
latest_result: {
|
||||
confirmation_allowed: boolean;
|
||||
indistinguishable_width_minutes: number;
|
||||
engine_indistinguishable_width_minutes: number;
|
||||
window_scan: { d9_candidates_differ: boolean } | null;
|
||||
session_outcome: string;
|
||||
};
|
||||
@@ -1018,7 +1018,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
|
||||
assert.equal(projection.latest_result.session_outcome, "collect_evidence");
|
||||
assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style");
|
||||
assert.equal(projection.latest_result.confirmation_allowed, false);
|
||||
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
|
||||
assert.ok(projection.latest_result.engine_indistinguishable_width_minutes >= 25);
|
||||
assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true);
|
||||
assert.doesNotMatch(JSON.stringify(projection), UNIQUE_MINUTE_COPY);
|
||||
assert.doesNotMatch(JSON.stringify(projection), /A\/B\/C\/D/);
|
||||
@@ -1454,9 +1454,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.15", () => {
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.16", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -1495,9 +1495,9 @@ test("public tool surface stays at 14 and new cases bind 10.0.15", () => {
|
||||
}, collectDecision(plateauCandidates));
|
||||
assert.equal(plateau.confirmation_allowed, false);
|
||||
assert.equal(plateau.unique_minute_claim, false);
|
||||
assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/);
|
||||
assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
|
||||
assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY);
|
||||
assert.match(String((plateau.skill_verification_report as { markdown?: string }).markdown ?? plateau.skill_verification_report), /Dasha \+ Gochara/);
|
||||
assert.match(String((plateau.skill_verification_report as { markdown?: string }).markdown ?? plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
|
||||
assert.doesNotMatch(String((plateau.skill_verification_report as { markdown?: string }).markdown ?? plateau.skill_verification_report), UNIQUE_MINUTE_COPY);
|
||||
assert.equal(plateau.session_outcome, "collect_evidence");
|
||||
// Previously this asserted multi_adapter_consensus from execution names alone.
|
||||
// The execution ledger does not prove same-range support or raw-result agreement.
|
||||
|
||||
@@ -576,8 +576,8 @@ function warnLines(run: () => Promise<unknown> | unknown) {
|
||||
}).then((result) => ({ result, lines }));
|
||||
}
|
||||
|
||||
test("skill version stays 10.0.15 after the range-delivery bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
test("skill version stays 10.0.16 after the delivery-report-facts bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("USER_COLLECT_QUESTION no longer has an other fallback", () => {
|
||||
|
||||
@@ -134,7 +134,7 @@ test("a 25-minute tied plateau projects width and forbids unique-minute confirma
|
||||
trainingGateOpen: false,
|
||||
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
|
||||
}));
|
||||
assert.equal(projection.indistinguishable_width_minutes, width);
|
||||
assert.equal(projection.engine_indistinguishable_width_minutes, width);
|
||||
assert.equal(projection.confirmation_allowed, false);
|
||||
assert.equal(
|
||||
(projection.confirmation_gate as { confirmation_allowed: boolean }).confirmation_allowed,
|
||||
@@ -194,13 +194,13 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
const projection = await (tools["rectification-read-case"] as unknown as {
|
||||
execute(input: unknown): Promise<{
|
||||
evidence_context: Array<{ date_precision: string; display_date_label: string }>;
|
||||
latest_result: { confirmation_allowed: boolean; indistinguishable_width_minutes: number };
|
||||
latest_result: { confirmation_allowed: boolean; engine_indistinguishable_width_minutes: number };
|
||||
}>;
|
||||
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
|
||||
assert.equal(projection.evidence_context[0]?.date_precision, "day");
|
||||
assert.equal(projection.evidence_context[0]?.display_date_label, "2024-08-08");
|
||||
assert.doesNotMatch(projection.evidence_context[0]?.display_date_label ?? "", /年份已确定为/);
|
||||
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
|
||||
assert.ok(projection.latest_result.engine_indistinguishable_width_minutes >= 25);
|
||||
assert.equal(projection.latest_result.confirmation_allowed, false);
|
||||
|
||||
await (tools["rectification-confirm-evidence"] as unknown as {
|
||||
@@ -213,9 +213,9 @@ test("read-case evidence context keeps day labels and confirm does not rewrite d
|
||||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||||
});
|
||||
|
||||
test("new-case skill identity is 10.0.15 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
assert.match(skill, /^version: 10\.0\.15$/m);
|
||||
test("new-case skill identity is 10.0.16 and the prompt prefers batch ingest", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
assert.match(skill, /^version: 10\.0\.16$/m);
|
||||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||||
assert.match(agentSource, /新事件走 rectification-record-evidence-batch/);
|
||||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||||
|
||||
@@ -184,8 +184,8 @@ const CANDIDATE_IDS = [
|
||||
"88888888-8888-4888-8888-888888888882",
|
||||
] as const;
|
||||
|
||||
test("skill version stays 10.0.15 after the range-delivery bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
test("skill version stays 10.0.16 after the delivery-report-facts bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
|
||||
|
||||
@@ -426,8 +426,8 @@ function rpcDossier(decision: DecisionDossier) {
|
||||
});
|
||||
}
|
||||
|
||||
test("skill version stays 10.0.15 after the range-delivery bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
test("skill version stays 10.0.16 after the delivery-report-facts bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("pre-fix dual-exit constant is gone; range narration carries numbers and the disclaimer", () => {
|
||||
|
||||
@@ -85,8 +85,8 @@ function collectPersistResult(overrides: {
|
||||
};
|
||||
}
|
||||
|
||||
test("skill version stays 10.0.15 after the range-delivery bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
test("skill version stays 10.0.16 after the delivery-report-facts bump", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("cases current_question remains the submit contract, not a visual slot", () => {
|
||||
|
||||
@@ -88,11 +88,11 @@ test("system prompt carries only high-priority boundaries, never the method copy
|
||||
test("agent pins the dedicated rectification skill and its fixed version", () => {
|
||||
assert.equal(RECTIFICATION_V9_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(basename(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_SKILL_NAME);
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.15"));
|
||||
assert.ok(RECTIFICATION_V9_PACKAGE_PATH.endsWith("skills/jyotish-birth-time-rectification/versions/10.0.16"));
|
||||
assert.notEqual(RECTIFICATION_V9_SKILL_PATH, RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(realpathSync(RECTIFICATION_V9_SKILL_PATH), RECTIFICATION_V9_PACKAGE_PATH);
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
});
|
||||
|
||||
test("step budgets are bounded per action with a hard ceiling", () => {
|
||||
|
||||
@@ -95,9 +95,9 @@ test("terminal transitions are one-way and evidence writes stop at terminal", ()
|
||||
|
||||
test("the active rectification skill pins the v10 identity and lives in the right directory", () => {
|
||||
assert.equal(RECTIFICATION_SKILL_NAME, "jyotish-birth-time-rectification");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.15");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.16");
|
||||
assert.match(skill, /^---\nname: jyotish-birth-time-rectification/m);
|
||||
assert.match(skill, /^version: 10\.0\.15$/m);
|
||||
assert.match(skill, /^version: 10\.0\.16$/m);
|
||||
assert.match(skill, /至多一个主问题且唯一来源:[\s\S]*不得自行提出、复述、改写或预告问题/);
|
||||
for (const reference of references) {
|
||||
const content = readFileSync(`${skillDirectory}/references/${reference}`, "utf8");
|
||||
|
||||
@@ -209,7 +209,7 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
session_id: SESSION_ID,
|
||||
status: "draft",
|
||||
should_start_opening: true,
|
||||
skill_version: "10.0.15",
|
||||
skill_version: "10.0.16",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -247,11 +247,11 @@ test("open RPC passes the pinned skill and server-derived baseline only", async
|
||||
});
|
||||
assert.equal(response.disposition, "created");
|
||||
assert.equal(response.shouldStartOpening, true);
|
||||
assert.equal(response.skillVersion, "10.0.15");
|
||||
assert.equal(response.skillVersion, "10.0.16");
|
||||
const openCall = accounting.calls.find((call) => call.fn === "open_agentic_rectification_case_v2");
|
||||
assert.ok(openCall);
|
||||
assert.equal(openCall.args.p_skill_name, "jyotish-birth-time-rectification");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.15");
|
||||
assert.equal(openCall.args.p_skill_version, "10.0.16");
|
||||
assert.equal(openCall.args.p_user_id, "user-1");
|
||||
// The server derives the baseline; the request never carries it from the browser.
|
||||
assert.equal("birth_date" in openCall.args, false);
|
||||
|
||||
@@ -85,8 +85,8 @@ test("checked-in registry verifies hashed product packages and leaves consult on
|
||||
[
|
||||
{
|
||||
name: "jyotish-birth-time-rectification",
|
||||
version: "10.0.15",
|
||||
sha256: "4cadf231520907b6faea9d06f698141e592e525c6bb91df43bc555be9dc5cf8c",
|
||||
version: "10.0.16",
|
||||
sha256: "b25dca7fe3630aceaf152ec99d37a2d946e4d71f1f191fc9071e11bc297e763f",
|
||||
},
|
||||
{
|
||||
name: "jyotish-personal-report",
|
||||
|
||||
Reference in New Issue
Block a user