diff --git a/docs/RULESETS.md b/docs/RULESETS.md index 7e69f39..6042d71 100644 --- a/docs/RULESETS.md +++ b/docs/RULESETS.md @@ -41,7 +41,11 @@ version, brightness table version, whether true solar time applies, and limit co ## Vedic (Jyotish) — `vedic-parashara-lahiri@0.1.0` (PLANNED, not implemented) Roadmap only — **no Vedic calculation exists in the engine today** and no user-facing surface may -claim otherwise until the ADR 0013 P5 slice ships. The frozen engineering boundaries (Lahiri = +claim otherwise until the ADR 0013 P5 slice ships. P1 status: the contracts reserve the `vedic` +system id (`VedicSettings`/`VedicChartResult`, opt-in only — the default `systems` array stays +three-system) and `@ming/vedic` / `@ming/vedic-rules` exist as skeletons whose provider always +returns `SYSTEM_NOT_YET_IMPLEMENTED`; nothing is computed and no defaults are wired for the +unresolved knobs (`nodes`, `dashaYear`). The frozen engineering boundaries (Lahiri = IAE-1985 standard / Swiss `SE_SIDM_LAHIRI`, Ketu = Rahu+180°, whole-sign bhava, 27-nakshatra scheme, instantaneous panchanga, D1/D9) plus the proposed-but-unconfirmed defaults (mean Rahu; upper-limb sunrise target; Vimshottari year model **candidate `julian-365.25` — P3 diff --git a/docs/STATUS.md b/docs/STATUS.md index c34045d..6d459f7 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -179,7 +179,7 @@ never by hand. | Command | Result | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pnpm run typecheck` | clean (tsc strict over packages, tools, tests) | -| `pnpm run test` | 692 tests / 35 files ?all passing (all systems + JPL Horizons 独立 golden + interpret + 吉凶 + 合婚 + reading-lint/空话/重复/越界 + validate-answer v2 结构与措辞门禁(约束引用事实豁免+全可见文本安全扫?资源上限+有界解析入口,非语义正确性证明) + western-rules/ziwei-rules 语义规则 + 版本迁移/回滚/目标白名?+ PII 隐私护栏 green) | +| `pnpm run test` | 704 tests / 38 files ?all passing (all systems + JPL Horizons 独立 golden + interpret + 吉凶 + 合婚 + reading-lint/空话/重复/越界 + validate-answer v2 结构与措辞门禁(约束引用事实豁免+全可见文本安全扫?资源上限+有界解析入口,非语义正确性证明) + western-rules/ziwei-rules 语义规则 + 版本迁移/回滚/目标白名?+ PII 隐私护栏 green) | | `pnpm run build` | `engine.mjs` ?2.8 MB + `sbom.cdx.json` + `sbom.spdx.json` (6 runtime deps) | | `pnpm run validate:skill` | 40 / 40 (incl. scripts/ no-stray-files guard + CycloneDX/SPDX SBOM checks + validate-answer/lint-reading gate-workflow doc checks) | | `pnpm run validate:reading` | 53 / 53 (topic example libraries + output-spec structure + 无术语区 firewall; offline, no LLM) | diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 6943557..3f77c2a 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -59,7 +59,7 @@ the identical table in [STATUS.md](./STATUS.md) ("Commands & results"). Do not h resolve a disagreement; re-run the suite and copy the actual count. `pnpm run check:doc-counts` re-runs the suite and fails if either doc's `N tests / M files` count drifts from the real run. -- Typecheck: clean. Tests: **692 tests / 35 files ?all passing**. The Western provider +- Typecheck: clean. Tests: **704 tests / 38 files ?all passing**. The Western provider (astronomy-engine, VSOP87 + NOVAS) passes the ADR-0003 ??gate two ways: wrapper-consistency (vs astronomy-engine's own output) plus an **independent JPL Horizons golden** (10 bodies × 3 technical epochs fetched from the NASA/JPL Horizons service, query recorded in diff --git a/packages/contracts/src/birth-input.ts b/packages/contracts/src/birth-input.ts index 3d5c2ec..80812a5 100644 --- a/packages/contracts/src/birth-input.ts +++ b/packages/contracts/src/birth-input.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { SCHEMA_VERSION } from './version.ts'; +import { VedicSettings } from './vedic.ts'; /** YYYY-MM-DD (calendar-agnostic wall date). */ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; @@ -12,7 +13,12 @@ export type CalendarSystem = z.infer; export const TimeAccuracy = z.enum(['exact', 'approximate', 'unknown']); export type TimeAccuracy = z.infer; -export const ChartSystem = z.enum(['western', 'bazi', 'ziwei']); +/** + * Chart systems. 'vedic' is contract-reserved as of P1 (ADR 0013): explicitly + * requesting it is valid input, but the provider returns SYSTEM_NOT_YET_IMPLEMENTED + * until the P2/P3 slices land. It is NOT part of the default systems array. + */ +export const ChartSystem = z.enum(['western', 'bazi', 'ziwei', 'vedic']); export type ChartSystem = z.infer; /** DST fall-back disambiguation: which of two identical wall clocks to keep. */ @@ -63,10 +69,12 @@ export const ZiweiSettings = z.strictObject({ export type ZiweiSettings = z.infer; export const CalculationSettings = z.strictObject({ + /** Default stays the three implemented systems; 'vedic' is opt-in only (ADR 0013 P1). */ systems: z.array(ChartSystem).min(1).default(['western', 'bazi', 'ziwei']), western: WesternSettings.prefault({}), bazi: BaziSettings.prefault({}), ziwei: ZiweiSettings.prefault({}), + vedic: VedicSettings.prefault({}), }); export type CalculationSettings = z.infer; diff --git a/packages/contracts/src/chart-bundle.ts b/packages/contracts/src/chart-bundle.ts index cff9f73..18538e1 100644 --- a/packages/contracts/src/chart-bundle.ts +++ b/packages/contracts/src/chart-bundle.ts @@ -5,6 +5,7 @@ import { Provenance } from './provenance.ts'; import { BaziChartResult } from './bazi.ts'; import { ZiweiChartResult } from './ziwei.ts'; import { WesternChartResult } from './western.ts'; +import { VedicChartResult } from './vedic.ts'; /** * Per-system result envelopes. Each system keeps its own schema rather than being @@ -41,6 +42,8 @@ export const ChartBundle = z.object({ western: WesternChartResult.optional(), bazi: BaziChartResult.optional(), ziwei: ZiweiChartResult.optional(), + /** Reserved slot (ADR 0013 P1): never populated until the Vedic provider computes for real. */ + vedic: VedicChartResult.optional(), warnings: z.array(EngineWarning), provenance: Provenance, }); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index a7cfaf7..18e3840 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -15,6 +15,7 @@ export * from './western-interpretation.ts'; export * from './ziwei-interpretation.ts'; export * from './ziwei.ts'; export * from './western.ts'; +export * from './vedic.ts'; export * from './interpretation.ts'; export * from './answer-plan.ts'; export * from './validate-answer.ts'; diff --git a/packages/contracts/src/interpretation.ts b/packages/contracts/src/interpretation.ts index 342d657..71bf871 100644 --- a/packages/contracts/src/interpretation.ts +++ b/packages/contracts/src/interpretation.ts @@ -31,6 +31,9 @@ export const EvidenceKind = z.enum([ 'bazi-rule', 'western-rule', 'ziwei-rule', + // Reserved for the Vedic P4 slice (ADR 0013); no producer emits these yet. + 'vedic', + 'vedic-rule', 'time', ]); export type EvidenceKind = z.infer; diff --git a/packages/contracts/src/vedic.ts b/packages/contracts/src/vedic.ts new file mode 100644 index 0000000..1b16ddf --- /dev/null +++ b/packages/contracts/src/vedic.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { ProviderRef } from './provenance.ts'; + +/** + * Vedic (Jyotish) domain contracts — P1 skeleton (ADR 0013, Status: Proposed). + * + * IMPORTANT: no Vedic calculation exists yet. The provider (`@ming/vedic`) returns + * null + SYSTEM_NOT_YET_IMPLEMENTED for every request; graha placements, Lagna, + * nakshatra, panchanga, vargas and dashas land in P2/P3 behind their own goldens. + * Nothing in this file may be read as a shipped capability. + */ + +/** + * Vedic settings. Two conventions are deliberately OPTIONAL WITHOUT DEFAULTS — + * they are unresolved owner decisions (ADR 0013 "Open questions" 1 and 2) and no + * layer (schema, docs, runtime) may quietly harden a value before those land: + * + * - `nodes`: mean vs true Rahu (proposed default 'mean', NOT confirmed). + * - `dashaYear`: the Vimshottari year model. Candidate 'julian-365.25'; the P3 + * dasha implementation is BLOCKED on the owner decision plus a same-model + * dual-implementation cross-check. The enum is reserved here so the contract + * surface is stable, but no value is wired anywhere. + * + * The sunrise model for Vaara (ADR 0013 §9) is likewise undecided and therefore + * has NO field at all until its P2 backend-mapping verification is done. + */ +export const VedicSettings = z.strictObject({ + rulesetId: z.string().default('vedic-parashara-lahiri@0.1.0'), + /** Rahu node model. No default: owner decision pending (ADR 0013 Open question 1). */ + nodes: z.enum(['mean', 'true']).optional(), + /** Vimshottari year model. No default: BLOCKED owner decision (ADR 0013 Open question 2). */ + dashaYear: z.enum(['julian-365.25', 'savana-360', 'sidereal']).optional(), +}); +export type VedicSettings = z.infer; + +/** + * Vedic chart result envelope. P1 ships ZERO instances of this schema — the + * provider always returns null. It exists so ChartBundle has a typed slot and the + * P2/P3 slices can extend it (grahas, Lagna, bhavas, nakshatra, panchanga, D1/D9, + * Vimshottari) without another bundle-shape change. + */ +export const VedicChartResult = z.object({ + rulesetId: z.string(), + provider: ProviderRef, +}); +export type VedicChartResult = z.infer; diff --git a/packages/contracts/src/warnings.ts b/packages/contracts/src/warnings.ts index 1e05c59..cde11d2 100644 --- a/packages/contracts/src/warnings.ts +++ b/packages/contracts/src/warnings.ts @@ -35,7 +35,7 @@ export type WarningCode = (typeof WARNING_CODES)[keyof typeof WARNING_CODES]; export const WarningSeverity = z.enum(['info', 'warning']); export type WarningSeverity = z.infer; -export const EngineSystem = z.enum(['time', 'western', 'bazi', 'ziwei', 'engine']); +export const EngineSystem = z.enum(['time', 'western', 'bazi', 'ziwei', 'vedic', 'engine']); export type EngineSystem = z.infer; export const EngineWarning = z.object({ diff --git a/packages/contracts/test/vedic-contracts.test.ts b/packages/contracts/test/vedic-contracts.test.ts new file mode 100644 index 0000000..1afb005 --- /dev/null +++ b/packages/contracts/test/vedic-contracts.test.ts @@ -0,0 +1,50 @@ +// Synthetic fixture — fictional data only; not a real person. +import { describe, expect, it } from 'vitest'; +import { BirthInput, VedicSettings, parseBirthInput } from '@ming/contracts'; + +/** + * Vedic P1 contract tests (ADR 0013): the 'vedic' system id is reserved in the + * contracts, old inputs stay valid, and the two unresolved owner decisions + * (nodes, dashaYear) have NO hidden defaults anywhere in the schema. + */ +const base = { + calendar: 'gregorian', + localDate: '1990-06-15', + localTime: '14:30', + timeAccuracy: 'exact', + timezone: 'Asia/Shanghai', + location: { latitude: 31.23, longitude: 121.47, source: 'user' }, +}; + +describe('vedic contracts (P1 skeleton)', () => { + it('legacy input without any vedic key still parses; default systems stay three', () => { + const parsed = parseBirthInput(base); + expect(parsed.settings.systems).toEqual(['western', 'bazi', 'ziwei']); + expect(parsed.settings.systems).not.toContain('vedic'); + // The vedic settings block exists via prefault, without breaking old JSON. + expect(parsed.settings.vedic.rulesetId).toBe('vedic-parashara-lahiri@0.1.0'); + }); + + it('explicitly requesting the vedic system is accepted by the contract', () => { + const parsed = parseBirthInput({ ...base, settings: { systems: ['vedic'] } }); + expect(parsed.settings.systems).toEqual(['vedic']); + }); + + it('nodes and dashaYear have NO defaults (unresolved owner decisions)', () => { + const settings = VedicSettings.parse({}); + expect(settings.nodes).toBeUndefined(); + expect(settings.dashaYear).toBeUndefined(); + const parsed = parseBirthInput({ ...base, settings: { systems: ['vedic'] } }); + expect(parsed.settings.vedic.nodes).toBeUndefined(); + expect(parsed.settings.vedic.dashaYear).toBeUndefined(); + }); + + it('rejects values outside the reserved enums', () => { + expect(VedicSettings.safeParse({ dashaYear: 'gregorian-365' }).success).toBe(false); + expect(VedicSettings.safeParse({ nodes: 'osculating' }).success).toBe(false); + expect(VedicSettings.safeParse({ unknownKnob: true }).success).toBe(false); + expect(BirthInput.safeParse({ ...base, settings: { systems: ['nakshatra'] } }).success).toBe( + false, + ); + }); +}); diff --git a/packages/orchestrator/package.json b/packages/orchestrator/package.json index 9ef3c02..613d575 100644 --- a/packages/orchestrator/package.json +++ b/packages/orchestrator/package.json @@ -17,6 +17,7 @@ "@ming/interpret": "workspace:*", "@ming/synastry": "workspace:*", "@ming/time-location": "workspace:*", + "@ming/vedic": "workspace:*", "@ming/western": "workspace:*", "@ming/ziwei": "workspace:*", "zod": "^4.4.3" diff --git a/packages/orchestrator/src/calculate.ts b/packages/orchestrator/src/calculate.ts index 6a563d7..e591509 100644 --- a/packages/orchestrator/src/calculate.ts +++ b/packages/orchestrator/src/calculate.ts @@ -25,6 +25,7 @@ import { import { computeBazi, lunarToGregorian } from '@ming/bazi'; import { computeZiwei, computeZiweiHoroscope } from '@ming/ziwei'; import { computeWestern } from '@ming/western'; +import { computeVedic } from '@ming/vedic'; import { buildProvenance, parseRulesetId } from './provenance-build.ts'; export interface CalculateOptions { @@ -187,6 +188,15 @@ export function calculate(input: BirthInput, options: CalculateOptions = {}): Ch providers.push(result.provider); rulesets.push(parseRulesetId(result.rulesetId)); } + } else if (system === 'vedic') { + // ADR 0013 P1: skeleton only. computeVedic always returns null + a + // SYSTEM_NOT_YET_IMPLEMENTED warning; nothing is fabricated, so no + // bundle slot, provider or ruleset is recorded until P2/P3 compute for real. + const { result, warnings: vedicWarnings } = computeVedic(normalized, resolved.settings.vedic); + warnings.push(...vedicWarnings); + if (result !== null) { + bundle.vedic = result; + } } else { warnings.push(pendingSystemWarning(system)); } diff --git a/packages/orchestrator/test/vedic-dispatch.test.ts b/packages/orchestrator/test/vedic-dispatch.test.ts new file mode 100644 index 0000000..65fae16 --- /dev/null +++ b/packages/orchestrator/test/vedic-dispatch.test.ts @@ -0,0 +1,72 @@ +// Synthetic fixture — fictional data only; not a real person. +import { describe, expect, it } from 'vitest'; +import { ChartBundle, parseBirthInput } from '@ming/contracts'; +import { calculate, runAnswerPlan } from '@ming/orchestrator'; + +/** + * Vedic P1 dispatch tests (ADR 0013): explicit systems:['vedic'] routes through + * computeVedic and yields an honest pending warning — never a fabricated result, + * provider or ruleset. The default three-system behavior and the v1 public + * contracts are byte-for-byte unaffected. + */ +const FIXED = Date.parse('2026-01-01T00:00:00Z'); + +const raw = { + calendar: 'gregorian', + localDate: '1990-03-10', + localTime: '08:15:00', + timeAccuracy: 'exact', + timezone: 'Asia/Shanghai', + location: { latitude: 30.5, longitude: 114.3, source: 'user' }, +}; + +describe('calculate: explicit vedic dispatch (P1 skeleton)', () => { + it('returns no vedic result, one pending warning, and no vedic provenance', () => { + const input = parseBirthInput({ ...raw, settings: { systems: ['vedic'] } }); + const bundle = calculate(input, { now: FIXED }); + expect(bundle.vedic).toBeUndefined(); + const pending = bundle.warnings.filter( + (w) => w.code === 'SYSTEM_NOT_YET_IMPLEMENTED' && w.system === 'vedic', + ); + expect(pending).toHaveLength(1); + expect(bundle.provenance.providers.map((p) => p.id)).toEqual([]); + expect(bundle.provenance.rulesets.map((r) => r.id)).not.toContain('vedic-parashara-lahiri'); + // The bundle (with the reserved slot absent) still validates against the schema. + expect(ChartBundle.safeParse(bundle).success).toBe(true); + }); + + it('mixed request: real systems compute, vedic only warns', () => { + const input = parseBirthInput({ ...raw, settings: { systems: ['western', 'vedic'] } }); + const bundle = calculate(input, { now: FIXED }); + expect(bundle.western).toBeDefined(); + expect(bundle.vedic).toBeUndefined(); + expect( + bundle.warnings.some((w) => w.code === 'SYSTEM_NOT_YET_IMPLEMENTED' && w.system === 'vedic'), + ).toBe(true); + // Only the real Western provider appears in provenance. + expect(bundle.provenance.providers.map((p) => p.id)).toEqual(['astronomy-engine']); + }); + + it('default input behavior is unchanged: no vedic slot, no vedic warnings', () => { + const bundle = calculate(parseBirthInput(raw), { now: FIXED }); + expect(bundle.vedic).toBeUndefined(); + expect(bundle.warnings.some((w) => w.system === 'vedic')).toBe(false); + expect(bundle.western).toBeDefined(); + expect(bundle.bazi).toBeDefined(); + }); +}); + +describe('public v1 contracts stay three-system (P4 owns the v2 break)', () => { + it('PublicResult.systems keeps exactly the three implemented systems', () => { + const input = parseBirthInput({ + ...raw, + ruleGender: 'female', + settings: { systems: ['vedic'] }, + }); + const { publicResult, answerPlan } = runAnswerPlan(input, { now: FIXED, topic: 'career' }); + expect(publicResult.systems).toHaveLength(3); + expect(publicResult.systems.map((s) => s.system)).toEqual(['western', 'bazi', 'ziwei']); + expect(publicResult.contractVersion).toBe('public-result/v1'); + expect(answerPlan.contractVersion).toBe('answer-plan/v1'); + }); +}); diff --git a/packages/vedic-rules/package.json b/packages/vedic-rules/package.json new file mode 100644 index 0000000..1896c25 --- /dev/null +++ b/packages/vedic-rules/package.json @@ -0,0 +1,16 @@ +{ + "name": "@ming/vedic-rules", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Vedic (Jyotish) interpretation-rule skeleton (ADR 0013 P1). Contains NO rules yet; sourced findings (BPHS-cited, caveated) land in the P4 slice.", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@ming/contracts": "workspace:*" + } +} diff --git a/packages/vedic-rules/src/index.ts b/packages/vedic-rules/src/index.ts new file mode 100644 index 0000000..034fbf4 --- /dev/null +++ b/packages/vedic-rules/src/index.ts @@ -0,0 +1,10 @@ +/** + * @ming/vedic-rules — Vedic (Jyotish) interpretation-rule skeleton (ADR 0013 P1). + * + * Deliberately empty: interpretation rules must be sourced (BPHS chapter-cited, + * public-domain originals only), caveated and versioned before any finding is + * emitted — that is the P4 slice, gated behind the P2/P3 calculation goldens. + * Exporting anything rule-like from here before then would violate the engine's + * no-fabricated-verdicts golden rule. + */ +export const VEDIC_RULES_IMPLEMENTED = false as const; diff --git a/packages/vedic/package.json b/packages/vedic/package.json new file mode 100644 index 0000000..30fdc0d --- /dev/null +++ b/packages/vedic/package.json @@ -0,0 +1,19 @@ +{ + "name": "@ming/vedic", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Vedic (Jyotish) provider skeleton (ADR 0013 P1). Computes NOTHING yet: always returns null + SYSTEM_NOT_YET_IMPLEMENTED. Precise Lahiri ayanamsha, grahas, nodes and Lagna land in P2 behind an independent Swiss golden.", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@ming/contracts": "workspace:*" + }, + "devDependencies": { + "@ming/time-location": "workspace:*" + } +} diff --git a/packages/vedic/src/index.ts b/packages/vedic/src/index.ts new file mode 100644 index 0000000..2d766b2 --- /dev/null +++ b/packages/vedic/src/index.ts @@ -0,0 +1,5 @@ +/** + * @ming/vedic — Vedic (Jyotish) provider skeleton (ADR 0013 P1). + * No calculation is implemented; see ./vedic-provider.ts. + */ +export { computeVedic } from './vedic-provider.ts'; diff --git a/packages/vedic/src/vedic-provider.ts b/packages/vedic/src/vedic-provider.ts new file mode 100644 index 0000000..efae38c --- /dev/null +++ b/packages/vedic/src/vedic-provider.ts @@ -0,0 +1,35 @@ +import { WARNING_CODES, makeWarning } from '@ming/contracts'; +import type { EngineWarning, NormalizedBirthData, VedicSettings } from '@ming/contracts'; + +/** + * Vedic (Jyotish) provider — P1 skeleton only (ADR 0013, Status: Proposed). + * + * Golden rules honored here: the engine NEVER fabricates a chart value, a + * provider ref, a ruleset ref or a precision claim for a system it cannot + * actually compute. Until the P2 slice lands (precise Lahiri ayanamsha, nine + * grahas, mean/true Rahu, Ketu opposition, Lagna — all behind an independent + * ≤1′ Swiss golden), this provider returns null plus one structured warning, + * exactly like the pre-provider phases of the other three systems. + * + * The `settings` parameter is accepted (contract-validated upstream) but + * deliberately unused: `nodes` and `dashaYear` are unresolved owner decisions + * (ADR 0013 Open questions 1–2) and must not influence anything yet. + */ +export function computeVedic( + _normalized: NormalizedBirthData, + _settings: VedicSettings, +): { result: null; warnings: EngineWarning[] } { + return { + result: null, + warnings: [ + makeWarning( + WARNING_CODES.SYSTEM_NOT_YET_IMPLEMENTED, + 'vedic', + 'The vedic provider is a P1 skeleton (ADR 0013): contracts are reserved, but no ' + + 'calculation exists yet. Nothing is fabricated; graha/Lagna/panchanga/dasha ' + + 'values arrive only after the P2/P3 slices pass their independent goldens.', + { severity: 'info' }, + ), + ], + }; +} diff --git a/packages/vedic/test/vedic-provider.test.ts b/packages/vedic/test/vedic-provider.test.ts new file mode 100644 index 0000000..6950b43 --- /dev/null +++ b/packages/vedic/test/vedic-provider.test.ts @@ -0,0 +1,53 @@ +// Synthetic fixture — fictional data only; not a real person. +import { describe, expect, it } from 'vitest'; +import { parseBirthInput } from '@ming/contracts'; +import { normalizeBirthData } from '@ming/time-location'; +import { computeVedic } from '@ming/vedic'; + +/** + * Vedic P1 provider skeleton tests (ADR 0013): the provider must NEVER fabricate. + * It always returns null plus exactly one structured SYSTEM_NOT_YET_IMPLEMENTED + * warning — no result, no provider ref, no ruleset ref, no numbers. + */ +const input = parseBirthInput({ + calendar: 'gregorian', + localDate: '1984-11-05', + localTime: '06:45', + timeAccuracy: 'exact', + timezone: 'Asia/Kolkata', + location: { latitude: 19.07, longitude: 72.87, source: 'user' }, + settings: { systems: ['vedic'] }, +}); + +describe('computeVedic (P1 skeleton)', () => { + it('always returns null + a single SYSTEM_NOT_YET_IMPLEMENTED warning', () => { + const normalized = normalizeBirthData(input); + const { result, warnings } = computeVedic(normalized, input.settings.vedic); + expect(result).toBeNull(); + expect(warnings).toHaveLength(1); + const warning = warnings[0]!; + expect(warning.code).toBe('SYSTEM_NOT_YET_IMPLEMENTED'); + expect(warning.system).toBe('vedic'); + expect(warning.severity).toBe('info'); + }); + + it('fabricates nothing: no detail payload, no numeric claims in the message', () => { + const normalized = normalizeBirthData(input); + const { warnings } = computeVedic(normalized, input.settings.vedic); + expect(warnings[0]).not.toHaveProperty('detail'); + // No degree/longitude-like fabricated values in the pending message. + expect(warnings[0]!.message).not.toMatch(/\d+\.\d+°|\d+°\d+/); + }); + + it('ignores the reserved knobs: same outcome with or without nodes/dashaYear', () => { + const normalized = normalizeBirthData(input); + const plain = computeVedic(normalized, input.settings.vedic); + const knobbed = computeVedic(normalized, { + ...input.settings.vedic, + nodes: 'true', + dashaYear: 'savana-360', + }); + expect(knobbed.result).toBeNull(); + expect(knobbed.warnings).toEqual(plain.warnings); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7480da..1a0a43c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: '@ming/time-location': specifier: workspace:* version: link:../time-location + '@ming/vedic': + specifier: workspace:* + version: link:../vedic '@ming/western': specifier: workspace:* version: link:../western @@ -138,6 +141,22 @@ importers: specifier: workspace:* version: link:../test-fixtures + packages/vedic: + dependencies: + '@ming/contracts': + specifier: workspace:* + version: link:../contracts + devDependencies: + '@ming/time-location': + specifier: workspace:* + version: link:../time-location + + packages/vedic-rules: + dependencies: + '@ming/contracts': + specifier: workspace:* + version: link:../contracts + packages/western: dependencies: '@ming/contracts': diff --git a/skills/calculate-birth-charts/scripts/dist/engine.mjs b/skills/calculate-birth-charts/scripts/dist/engine.mjs index 26583f8..aba6458 100644 --- a/skills/calculate-birth-charts/scripts/dist/engine.mjs +++ b/skills/calculate-birth-charts/scripts/dist/engine.mjs @@ -39871,7 +39871,7 @@ var WARNING_CODES = { RULESET_VARIANT_DEFAULTED: "RULESET_VARIANT_DEFAULTED" }; var WarningSeverity = external_exports.enum(["info", "warning"]); -var EngineSystem = external_exports.enum(["time", "western", "bazi", "ziwei", "engine"]); +var EngineSystem = external_exports.enum(["time", "western", "bazi", "ziwei", "vedic", "engine"]); var EngineWarning = external_exports.object({ code: external_exports.enum(Object.values(WARNING_CODES)), severity: WarningSeverity, @@ -39974,12 +39974,25 @@ var Provenance = external_exports.object({ rulesets: external_exports.array(RulesetRef) }); +// packages/contracts/src/vedic.ts +var VedicSettings = external_exports.strictObject({ + rulesetId: external_exports.string().default("vedic-parashara-lahiri@0.1.0"), + /** Rahu node model. No default: owner decision pending (ADR 0013 Open question 1). */ + nodes: external_exports.enum(["mean", "true"]).optional(), + /** Vimshottari year model. No default: BLOCKED owner decision (ADR 0013 Open question 2). */ + dashaYear: external_exports.enum(["julian-365.25", "savana-360", "sidereal"]).optional() +}); +var VedicChartResult = external_exports.object({ + rulesetId: external_exports.string(), + provider: ProviderRef +}); + // packages/contracts/src/birth-input.ts var DATE_RE = /^\d{4}-\d{2}-\d{2}$/; var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/; var CalendarSystem = external_exports.enum(["gregorian", "lunar"]); var TimeAccuracy = external_exports.enum(["exact", "approximate", "unknown"]); -var ChartSystem = external_exports.enum(["western", "bazi", "ziwei"]); +var ChartSystem = external_exports.enum(["western", "bazi", "ziwei", "vedic"]); var FoldChoice = external_exports.enum(["earlier", "later"]); var GeoLocation = external_exports.strictObject({ displayName: external_exports.string().optional(), @@ -40013,10 +40026,12 @@ var ZiweiSettings = external_exports.strictObject({ useApparentSolarTime: external_exports.boolean().default(false) }); var CalculationSettings = external_exports.strictObject({ + /** Default stays the three implemented systems; 'vedic' is opt-in only (ADR 0013 P1). */ systems: external_exports.array(ChartSystem).min(1).default(["western", "bazi", "ziwei"]), western: WesternSettings.prefault({}), bazi: BaziSettings.prefault({}), - ziwei: ZiweiSettings.prefault({}) + ziwei: ZiweiSettings.prefault({}), + vedic: VedicSettings.prefault({}) }); var BirthInput = external_exports.strictObject({ schemaVersion: external_exports.string().default(SCHEMA_VERSION), @@ -40465,6 +40480,9 @@ var EvidenceKind = external_exports.enum([ "bazi-rule", "western-rule", "ziwei-rule", + // Reserved for the Vedic P4 slice (ADR 0013); no producer emits these yet. + "vedic", + "vedic-rule", "time" ]); var InterpretationEvidence = external_exports.object({ @@ -40882,6 +40900,8 @@ var ChartBundle = external_exports.object({ western: WesternChartResult.optional(), bazi: BaziChartResult.optional(), ziwei: ZiweiChartResult.optional(), + /** Reserved slot (ADR 0013 P1): never populated until the Vedic provider computes for real. */ + vedic: VedicChartResult.optional(), warnings: external_exports.array(EngineWarning), provenance: Provenance }); @@ -51771,6 +51791,21 @@ function computeWestern(normalized, settings) { return { result, warnings }; } +// packages/vedic/src/vedic-provider.ts +function computeVedic(_normalized, _settings) { + return { + result: null, + warnings: [ + makeWarning( + WARNING_CODES.SYSTEM_NOT_YET_IMPLEMENTED, + "vedic", + "The vedic provider is a P1 skeleton (ADR 0013): contracts are reserved, but no calculation exists yet. Nothing is fabricated; graha/Lagna/panchanga/dasha values arrive only after the P2/P3 slices pass their independent goldens.", + { severity: "info" } + ) + ] + }; +} + // packages/orchestrator/src/calculate.ts function pad23(n) { return String(n).padStart(2, "0"); @@ -51878,6 +51913,12 @@ function calculate(input, options = {}) { providers.push(result.provider); rulesets.push(parseRulesetId(result.rulesetId)); } + } else if (system === "vedic") { + const { result, warnings: vedicWarnings } = computeVedic(normalized, resolved.settings.vedic); + warnings.push(...vedicWarnings); + if (result !== null) { + bundle.vedic = result; + } } else { warnings.push(pendingSystemWarning(system)); } diff --git a/tools/vedic-docs.test.ts b/tools/vedic-docs.test.ts index 6c8ef75..7cf255f 100644 --- a/tools/vedic-docs.test.ts +++ b/tools/vedic-docs.test.ts @@ -111,6 +111,13 @@ describe('vedic docs gate: no premature capability claims on user-facing surface expect(offenders, `remove vedic claims from: ${offenders.join(', ')}`).toEqual([]); }); + it('CLI --systems all still expands to exactly the three implemented systems (P5 owns the flip)', () => { + const cli = read('skills/calculate-birth-charts/scripts/ming-chart.mjs'); + // The literal all-expansion list must stay three-system until P5 ships Vedic for real. + expect(cli).toContain("['western', 'bazi', 'ziwei']"); + expect(cli).not.toMatch(/vedic|jyotish/i); + }); + it('README mentions vedic only as an explicit plan, never as a current system', () => { const readme = read('README.md'); const mentions = readme.match(/^.*(vedic|jyotish).*$/gim) ?? [];