From 33739e3789f3ff7c4ece07db354f5dcd8b2bb261 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 27 Sep 2026 16:31:20 +0800 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- frontend/src/mastra/consultation-workflow.ts | 69 ++++++++++++ ...consult-projection-native-20260927.test.ts | 103 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 frontend/tests/consult-projection-native-20260927.test.ts diff --git a/frontend/src/mastra/consultation-workflow.ts b/frontend/src/mastra/consultation-workflow.ts index 8f9d0a4f..1dd57d9a 100644 --- a/frontend/src/mastra/consultation-workflow.ts +++ b/frontend/src/mastra/consultation-workflow.ts @@ -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, name: string) { + return record(record(context.local_layers.native_layers)[name]); +} + +function projectNativeLayers( + context: ReturnType, + layers: ReadonlyArray]>, +) { + 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) { const foundation = projectAllowlistedTree({ ascendant: context.chart.ascendant, @@ -519,6 +574,12 @@ function projectNatalFoundation(context: ReturnType; +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); +});