feat(consult): project the native technique layers with per-layer allowlists

TASK-consult-evidence-card-v2-20260927 T2. toAgentConsultationContext carries
chart.consultation_native_layers as local_layers.native_layers; toModelOutput
projects d9_summary / punarphoo / vivah_saham / day_night into the natal card
and slow_transits / double_transit / annual_tajika into the timing card, each
through its own nested-key allowlist (as BUG-1054 did for Narayana), so the
existing natal and timing key sets are not widened and the depth / list caps
are unchanged. Pada A1 (the engine's Arudha Lagna) is allowlisted for the
career card's AL.

Golden test: every projected layer equals the engine layer value for value on
3 public AA charts (family, timing and annual routes); the Double Transit
per-house summary line is the only field left out.

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:31:20 +08:00
co-authored by Claude Opus 5.5
parent d5616c347e
commit 33739e3789
2 changed files with 172 additions and 0 deletions
@@ -322,6 +322,8 @@ const natalFoundationKeys = new Set([
"functionalbeneficmalefic", "functionalbenefics", "functionalmalefics", "functionalneutrals",
"yogakarakas", "ownedhouses", "ascendantsign", "effectonconfidence",
"yogas", "category", "arudhapadas", "padas", "a10", "a7", "ul", "al", "gulika", "mandi", "kakshya",
// Evidence card v2: the engine writes the Arudha Lagna (AL) as pada `A1`.
"a1",
"kpcusps", "cusps", "sublord", "star", "starlord", "claimboundary", "reason",
]);
@@ -502,6 +504,59 @@ function projectWesternSpectrum(value: unknown): ModelOutputValue | undefined {
return Object.keys(output).length ? output : undefined;
}
/*
* Evidence card v2 (TASK-consult-evidence-card-v2-20260927 T2): the native
* technique layers the engine now attaches (chart.consultation_native_layers,
* scripts/consultation_native_layers.py). Each layer has its own allowlist of
* nested keys, as BUG-1054 did for the Narayana period, so the existing natal
* and timing sets are not widened; depth and list caps are unchanged.
*/
const nativeD9SummaryKeys = new Set([
"status", "source", "lagna", "lagnavargottama", "planets", "d9sign", "d9dignity", "vargottama",
"reversals", "planet", "d1", "d9", "reversalrule",
]);
const nativePunarphooKeys = new Set([
"status", "source", "natalpresent", "orbdeg", "orblimitdeg", "samehouse", "saturnreturntonatalmoon",
"active", "claimboundary",
]);
const nativeVivahSahamKeys = new Set([
"status", "source", "longitude", "sign", "degreeinsign", "house", "natalconjuncts", "planet", "diffdeg", "boundary",
]);
const nativeDayNightKeys = new Set(["status", "source", "isday"]);
const nativeSlowTransitKeys = new Set([
"status", "source", "referencedate", "window", "start", "end", "current", "sign", "degreeinsign",
"retrograde", "natalhouse", "sav", "bav", "keydates", "kind", "date", "body", "fromsign", "tosign",
"tonatalhouse", "motion", "housesinwindow", "boundary",
]);
const nativeDoubleTransitKeys = new Set([
"status", "source", "date", "activehouses", "conclusions", "house", "layer", "target", "strength",
"jupiterpac", "saturnpac", "conclusioncount", "karakatargets", "dk", "ul", "doubletransit", "method", "boundary",
]);
const nativeAnnualKeys = new Set([
"status", "reason", "source", "targetyear", "age", "varshayear", "start", "end", "annuallagna", "varshesha",
"muntha", "sign", "lord", "houseinannualchart", "houseinnatalchart", "muddadasha", "months",
"tajikaaspects", "name", "planets", "aspect", "motion", "tajikaaspectsstatus", "producers",
"solarreturn", "annualchart", "yearlord", "tajikayogas", "boundary",
]);
function nativeLayer(context: ReturnType<typeof toAgentConsultationContext>, name: string) {
return record(record(context.local_layers.native_layers)[name]);
}
function projectNativeLayers(
context: ReturnType<typeof toAgentConsultationContext>,
layers: ReadonlyArray<readonly [string, ReadonlySet<string>]>,
) {
const output: { [key: string]: ModelOutputValue } = {};
for (const [name, keys] of layers) {
const layer = nativeLayer(context, name);
if (!Object.keys(layer).length) continue;
const projected = projectAllowlistedTree(layer, keys, { allowAstrologyEntityKeys: true });
if (projected !== undefined) output[name] = projected;
}
return output;
}
function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationContext>) {
const foundation = projectAllowlistedTree({
ascendant: context.chart.ascendant,
@@ -519,6 +574,12 @@ function projectNatalFoundation(context: ReturnType<typeof toAgentConsultationCo
const output = record(foundation);
const varga = projectVargaSpectrum(context.local_layers.varga_spectrum);
if (varga !== undefined) output.varga_spectrum = varga;
Object.assign(output, projectNativeLayers(context, [
["d9_summary", nativeD9SummaryKeys],
["punarphoo", nativePunarphooKeys],
["vivah_saham", nativeVivahSahamKeys],
["day_night", nativeDayNightKeys],
]));
return Object.keys(output).length ? output : undefined;
}
@@ -569,6 +630,11 @@ function projectTimingEvidence(context: ReturnType<typeof toAgentConsultationCon
const output = record(timing);
const western = projectWesternSpectrum(context.evidence_contract.western_spectrum);
if (western !== undefined) output.western_spectrum = western;
Object.assign(output, projectNativeLayers(context, [
["slow_transits", nativeSlowTransitKeys],
["double_transit", nativeDoubleTransitKeys],
["annual_tajika", nativeAnnualKeys],
]));
return Object.keys(output).length ? output : undefined;
}
@@ -782,6 +848,9 @@ export function toAgentConsultationContext(data: JsonRecord) {
transits: modules.transits,
functional_benefic_malefic: modules.functional_benefic_malefic
|| record(data.machine_evidence_packet).functional_benefic_malefic,
// Evidence card v2: native technique layers (D9 summary, Punarphoo,
// Double Transit, slow transits, annual Tajika pack, lookup-only layers).
native_layers: chart.consultation_native_layers,
},
rectification: {
boundary: rectificationBoundaryFromGate(rectification),
@@ -0,0 +1,103 @@
// TASK-consult-evidence-card-v2-20260927 T2: the native technique layers the
// engine now attaches (chart.consultation_native_layers) reach the model
// projection through their own nested-key allowlists, like BUG-1054.
//
// Fixture: real engine consultation responses for three public AA charts, the
// family, annual and timing routes (scripts/research/capture_consult_evidence_card_golden.py,
// AGENTS §7.4). The projected layer must equal the engine's layer value for
// value, not merely carry the key.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
import type { ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
import {
consultationWorkflowResponseSchema,
toAgentConsultationContext,
toModelOutput,
} from "../src/mastra/consultation-workflow.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[] };
function nativeLayers(workflow: Json): Json {
return ((workflow.chart as Json).consultation_native_layers ?? {}) as Json;
}
function evidence(workflow: Json, domain: ConsultationDomain, category: "natal_foundation" | "timing"): Json {
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: domain }), domain);
const card = packet.claim_cards.find((item) => item.category === category);
return (card?.evidence ?? {}) as Json;
}
test("the natal layers are projected value for value: D9 summary, Punarphoo, Vivah Saham, day / night", () => {
for (const chart of golden.charts) {
const engine = nativeLayers(chart.workflow);
const natal = evidence(chart.workflow, "marriage", "natal_foundation");
for (const name of ["d9_summary", "punarphoo", "vivah_saham", "day_night"]) {
assert.equal((engine[name] as Json).status === "blocked", false, `${chart.id} ${name} executed`);
assert.deepEqual(natal[name], engine[name], `${chart.id} ${name}`);
}
const d9 = natal.d9_summary as Json;
assert.equal(Object.keys(d9.planets as Json).length, 9, `${chart.id} nine D9 rows`);
}
});
test("the timing layers are projected value for value: slow transits, Double Transit conclusions", () => {
for (const chart of golden.charts) {
const engine = nativeLayers(chart.timing_workflow);
const timing = evidence(chart.timing_workflow, "timing", "timing");
assert.deepEqual(timing.slow_transits, engine.slow_transits, `${chart.id} slow_transits`);
// The per-house summary line of the producer is left out; the conclusions are the result.
const { house_7_summary: summary, ...double } = engine.double_transit as Json;
assert.equal(typeof summary, "string");
assert.deepEqual(timing.double_transit, double, `${chart.id} double_transit`);
const current = (timing.slow_transits as Json).current as Json;
assert.deepEqual(Object.keys(current).sort(), ["Jupiter", "Ketu", "Rahu", "Saturn"]);
for (const body of ["Jupiter", "Saturn"]) {
assert.equal(typeof (current[body] as Json).sav, "number", `${chart.id} ${body} SAV`);
assert.equal(typeof (current[body] as Json).bav, "number", `${chart.id} ${body} BAV`);
}
assert.equal((timing.annual_tajika as Json).status, "not_applicable", `${chart.id} no annual pack on the timing route`);
}
});
test("the annual pack is projected value for value on the annual route", () => {
for (const chart of golden.charts) {
const engine = nativeLayers(chart.annual_workflow).annual_tajika as Json;
const timing = evidence(chart.annual_workflow, "annual", "timing");
assert.equal(engine.status, "parameter_sensitive");
assert.deepEqual(timing.annual_tajika, engine, `${chart.id} annual_tajika`);
const mudda = (timing.annual_tajika as Json).mudda_dasha as Json[];
assert.ok(mudda.length >= 8, `${chart.id} Mudda segments`);
for (const row of mudda) {
assert.match(String(row.start), /^\d{4}-\d{2}-\d{2}$/);
assert.match(String(row.end), /^\d{4}-\d{2}-\d{2}$/);
}
}
});
test("the Arudha Lagna (pada A1) now reaches the projection", () => {
for (const chart of golden.charts) {
const padas = ((((chart.workflow.chart as Json).modules as Json).arudha_padas as Json).padas as Json);
const natal = evidence(chart.workflow, "career", "natal_foundation");
const projected = ((natal.arudha_padas as Json).padas as Json).A1 as Json;
assert.equal(projected.sign, (padas.A1 as Json).sign, `${chart.id} AL sign`);
assert.equal(projected.lord, (padas.A1 as Json).lord, `${chart.id} AL lord`);
}
});
test("a response without the native layers projects exactly as before", () => {
const workflow = structuredClone(golden.charts[0]!.workflow);
delete (workflow.chart as Json).consultation_native_layers;
const natal = evidence(workflow, "marriage", "natal_foundation");
const timing = evidence(workflow, "timing", "timing");
for (const name of ["d9_summary", "punarphoo", "vivah_saham", "day_night"]) assert.equal(name in natal, false, name);
for (const name of ["slow_transits", "double_transit", "annual_tajika"]) assert.equal(name in timing, false, name);
});