Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/RULESETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 1 addition & 1 deletion docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/contracts/src/birth-input.ts
Original file line number Diff line number Diff line change
@@ -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}$/;
Expand All @@ -12,7 +13,12 @@ export type CalendarSystem = z.infer<typeof CalendarSystem>;
export const TimeAccuracy = z.enum(['exact', 'approximate', 'unknown']);
export type TimeAccuracy = z.infer<typeof TimeAccuracy>;

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<typeof ChartSystem>;

/** DST fall-back disambiguation: which of two identical wall clocks to keep. */
Expand Down Expand Up @@ -63,10 +69,12 @@ export const ZiweiSettings = z.strictObject({
export type ZiweiSettings = z.infer<typeof ZiweiSettings>;

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<typeof CalculationSettings>;

Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/chart-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
});
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/interpretation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EvidenceKind>;
Expand Down
46 changes: 46 additions & 0 deletions packages/contracts/src/vedic.ts
Original file line number Diff line number Diff line change
@@ -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<typeof VedicSettings>;

/**
* 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<typeof VedicChartResult>;
2 changes: 1 addition & 1 deletion packages/contracts/src/warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof WarningSeverity>;

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<typeof EngineSystem>;

export const EngineWarning = z.object({
Expand Down
50 changes: 50 additions & 0 deletions packages/contracts/test/vedic-contracts.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
1 change: 1 addition & 0 deletions packages/orchestrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions packages/orchestrator/src/calculate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
}
Expand Down
72 changes: 72 additions & 0 deletions packages/orchestrator/test/vedic-dispatch.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
16 changes: 16 additions & 0 deletions packages/vedic-rules/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
10 changes: 10 additions & 0 deletions packages/vedic-rules/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions packages/vedic/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
5 changes: 5 additions & 0 deletions packages/vedic/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading