diff --git a/INSTALL.md b/INSTALL.md index 89ccdc8..a062f0a 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -82,6 +82,16 @@ > 后续发布必须使用新的不可变 tag 与独立 ZIP;创建 Release、上传资产并完成重下 SHA-256 校验后,才可更新清单。不得使用 `latest/download`,不得复用已撤下的历史发布地址。 +## 从 v0.1.x 迁移到 v0.2.0 + +v0.2.0 的主要变化: + +1. **西方宫制规则版本**:默认 rulesetId 从 `western-tropical-placidus@0.1.0` 升级为 `western-tropical-placidus@0.2.0`。Koch 宫制计算已修正(旧版有最大 36° 偏差)。显式传入旧 `@0.1.0` 将返回 `RULESET_UNSUPPORTED` 错误,不会静默映射也不会继续计算错误结果。迁移方法:删除输入中的 `rulesetId` 字段(使用默认值),或显式改为 `western-tropical-placidus@0.2.0`。 +2. **validate-answer 契约**:仅接受 `reading-draft/v2`;v1 返回 `UNSUPPORTED_CONTRACT_VERSION`。迁移方法见 `references/answer-contract.md`。 +3. **引擎版本**:engineVersion 从 0.1.1 升至 0.2.0(Koch 宫制修正 + Swiss Ephemeris 独立金标覆盖)。 + +已发布的 v0.1.x Release 保持不变,作为历史可复现边界。 + ## ZIP 解压安全配额 所有宿主 Agent 在解压安装包时必须执行以下配额检查(基于 inflate 前的 central directory 元数据): diff --git a/docs/STATUS.md b/docs/STATUS.md index bf2e8cc..5c697fe 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` | 680 tests / 33 files ?all passing (all systems + JPL Horizons 独立 golden + interpret + 吉凶 + 合婚 + reading-lint/空话/重复/越界 + validate-answer v2 结构与措辞门禁(约束引用事实豁免+全可见文本安全扫?资源上限+有界解析入口,非语义正确性证明) + western-rules/ziwei-rules 语义规则 + 版本迁移/回滚/目标白名?+ PII 隐私护栏 green) | +| `pnpm run test` | 685 tests / 34 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 271980c..017d4aa 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: **680 tests / 33 files ?all passing**. The Western provider +- Typecheck: clean. Tests: **685 tests / 34 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 cb9b324..3d5c2ec 100644 --- a/packages/contracts/src/birth-input.ts +++ b/packages/contracts/src/birth-input.ts @@ -30,8 +30,11 @@ export const GeoLocation = z.strictObject({ export type GeoLocation = z.infer; /** Western defaults: tropical zodiac, Placidus houses, true node (handoff §5.1). */ +export const WESTERN_RULESET_CURRENT = 'western-tropical-placidus@0.2.0'; +/** Retired ruleset ids that are no longer computed (returns RULESET_UNSUPPORTED). */ +export const WESTERN_RULESET_RETIRED = ['western-tropical-placidus@0.1.0'] as const; export const WesternSettings = z.strictObject({ - rulesetId: z.string().default('western-tropical-placidus@0.1.0'), + rulesetId: z.string().default(WESTERN_RULESET_CURRENT), zodiac: z.enum(['tropical', 'sidereal']).default('tropical'), /** Sidereal ayanamsha model; only applied when zodiac = 'sidereal'. */ ayanamsha: z.enum(['lahiri', 'fagan-bradley']).default('lahiri'), diff --git a/packages/contracts/src/version.ts b/packages/contracts/src/version.ts index 37e2840..244ffbb 100644 --- a/packages/contracts/src/version.ts +++ b/packages/contracts/src/version.ts @@ -12,7 +12,7 @@ export const SCHEMA_VERSION = '0.1.0'; * corrected to the accurate "astronomy-engine/VSOP87+NOVAS" attribution, which changes emitted * provenance text (not any numeric result), hence a new engine version. */ -export const ENGINE_VERSION = '0.1.1'; +export const ENGINE_VERSION = '0.2.0'; /** Human-facing engine name. */ export const ENGINE_NAME = 'ming-engine'; diff --git a/packages/western/src/western-provider.ts b/packages/western/src/western-provider.ts index ccfab73..7237d51 100644 --- a/packages/western/src/western-provider.ts +++ b/packages/western/src/western-provider.ts @@ -1,4 +1,10 @@ -import { WARNING_CODES, makeWarning } from '@ming/contracts'; +import { + WARNING_CODES, + makeWarning, + ERROR_CODES, + EngineError, + WESTERN_RULESET_RETIRED, +} from '@ming/contracts'; import type { EngineWarning, NormalizedBirthData, @@ -48,6 +54,16 @@ export function computeWestern( normalized: NormalizedBirthData, settings: WesternSettings, ): { result: WesternChartResult | null; warnings: EngineWarning[] } { + // Guard: retired ruleset versions are rejected with a stable error code. + // The caller explicitly requested a version that is no longer computed + // (Koch was corrected in 0.2.0; the old erroneous results are not reproducible). + if ((WESTERN_RULESET_RETIRED as readonly string[]).includes(settings.rulesetId)) { + throw new EngineError( + ERROR_CODES.RULESET_UNSUPPORTED, + `Ruleset "${settings.rulesetId}" has been retired. Migrate to western-tropical-placidus@0.2.0 (see INSTALL.md migration notes). Historical results from v0.1.x releases remain available in those published releases.`, + { requestedRuleset: settings.rulesetId, currentRuleset: 'western-tropical-placidus@0.2.0' }, + ); + } const warnings: EngineWarning[] = []; const dateMs = normalized.utcInstantMs; const latitude = normalized.location.latitude; diff --git a/packages/western/test/western-ruleset-version.test.ts b/packages/western/test/western-ruleset-version.test.ts new file mode 100644 index 0000000..872ccfb --- /dev/null +++ b/packages/western/test/western-ruleset-version.test.ts @@ -0,0 +1,83 @@ +// Tests for the western-tropical-placidus ruleset version lifecycle: +// - Default rulesetId is 0.2.0 +// - Explicit 0.2.0 works +// - Retired 0.1.0 is rejected with RULESET_UNSUPPORTED +// - Error message structure is stable and does not leak internals +import { describe, expect, it } from 'vitest'; +import { + ERROR_CODES, + EngineError, + WESTERN_RULESET_CURRENT, + WESTERN_RULESET_RETIRED, + WesternSettings, +} from '@ming/contracts'; +import { computeWestern } from '../src/western-provider.ts'; +import { NormalizedBirthData } from '@ming/contracts'; + +/** A minimal valid NormalizedBirthData for exercising the provider (synthetic). */ +const SYNTHETIC_INPUT = NormalizedBirthData.parse({ + schemaVersion: '0.1.0', + calendar: 'gregorian', + timeAccuracy: 'exact', + timeKnown: true, + localDate: '2000-01-01', + localTime: '20:00:00', + localCivilIso: '2000-01-01T20:00:00+08:00', + timezone: 'Asia/Shanghai', + timezoneOffsetMinutes: 480, + utcInstant: '2000-01-01T12:00:00.000Z', + utcInstantMs: Date.UTC(2000, 0, 1, 12, 0, 0), + ambiguity: { status: 'unambiguous', candidateCount: 1 }, + location: { latitude: 30.0, longitude: 120.0, source: 'user' }, + solar: null, + tzdb: { version: '2024a', source: 'moment-timezone' }, +}); + +describe('western ruleset version lifecycle', () => { + it('default rulesetId is western-tropical-placidus@0.2.0', () => { + const parsed = WesternSettings.parse({}); + expect(parsed.rulesetId).toBe('western-tropical-placidus@0.2.0'); + expect(parsed.rulesetId).toBe(WESTERN_RULESET_CURRENT); + }); + + it('explicit western-tropical-placidus@0.2.0 computes successfully', () => { + const settings = WesternSettings.parse({ rulesetId: 'western-tropical-placidus@0.2.0' }); + const { result } = computeWestern(SYNTHETIC_INPUT, settings); + expect(result).not.toBeNull(); + expect(result!.rulesetId).toBe('western-tropical-placidus@0.2.0'); + }); + + it('retired western-tropical-placidus@0.1.0 is stably rejected', () => { + const settings = WesternSettings.parse({ rulesetId: 'western-tropical-placidus@0.1.0' }); + let thrown: unknown; + try { + computeWestern(SYNTHETIC_INPUT, settings); + } catch (e) { + thrown = e; + } + expect(thrown).toBeInstanceOf(EngineError); + const err = thrown as EngineError; + expect(err.code).toBe(ERROR_CODES.RULESET_UNSUPPORTED); + // Error includes migration guidance + expect(err.message).toContain('0.2.0'); + expect(err.message).toContain('retired'); + }); + + it('error from retired ruleset does not leak internal implementation details', () => { + const settings = WesternSettings.parse({ rulesetId: 'western-tropical-placidus@0.1.0' }); + let message = ''; + try { + computeWestern(SYNTHETIC_INPUT, settings); + } catch (e) { + message = (e as Error).message; + } + // Must not contain stack traces, file paths, or internal function names + expect(message).not.toMatch(/\.ts:|at \w+|node_modules/); + // Must not contain the raw internal array reference + expect(message).not.toContain('WESTERN_RULESET_RETIRED'); + }); + + it('WESTERN_RULESET_RETIRED contains 0.1.0', () => { + expect(WESTERN_RULESET_RETIRED).toContain('western-tropical-placidus@0.1.0'); + }); +}); diff --git a/skills/calculate-birth-charts/scripts/dist/engine.mjs b/skills/calculate-birth-charts/scripts/dist/engine.mjs index 33afa74..26583f8 100644 --- a/skills/calculate-birth-charts/scripts/dist/engine.mjs +++ b/skills/calculate-birth-charts/scripts/dist/engine.mjs @@ -25279,7 +25279,7 @@ var require_lib2 = __commonJS({ // packages/contracts/src/version.ts var SCHEMA_VERSION = "0.1.0"; -var ENGINE_VERSION = "0.1.1"; +var ENGINE_VERSION = "0.2.0"; var ENGINE_NAME = "ming-engine"; var SUPPORTED_YEAR_MIN = 1901; var SUPPORTED_YEAR_MAX = 2100; @@ -39989,8 +39989,10 @@ var GeoLocation = external_exports.strictObject({ /** Where the coordinates came from; geocoder use requires separate consent. */ source: external_exports.enum(["user", "geocoder", "import"]) }); +var WESTERN_RULESET_CURRENT = "western-tropical-placidus@0.2.0"; +var WESTERN_RULESET_RETIRED = ["western-tropical-placidus@0.1.0"]; var WesternSettings = external_exports.strictObject({ - rulesetId: external_exports.string().default("western-tropical-placidus@0.1.0"), + rulesetId: external_exports.string().default(WESTERN_RULESET_CURRENT), zodiac: external_exports.enum(["tropical", "sidereal"]).default("tropical"), /** Sidereal ayanamsha model; only applied when zodiac = 'sidereal'. */ ayanamsha: external_exports.enum(["lahiri", "fagan-bradley"]).default("lahiri"), @@ -51617,6 +51619,13 @@ function round6(n) { return Math.round(n * 1e6) / 1e6; } function computeWestern(normalized, settings) { + if (WESTERN_RULESET_RETIRED.includes(settings.rulesetId)) { + throw new EngineError( + ERROR_CODES.RULESET_UNSUPPORTED, + `Ruleset "${settings.rulesetId}" has been retired. Migrate to western-tropical-placidus@0.2.0 (see INSTALL.md migration notes). Historical results from v0.1.x releases remain available in those published releases.`, + { requestedRuleset: settings.rulesetId, currentRuleset: "western-tropical-placidus@0.2.0" } + ); + } const warnings = []; const dateMs = normalized.utcInstantMs; const latitude = normalized.location.latitude; diff --git a/tools/lib/host-config.ts b/tools/lib/host-config.ts index d08c710..3886989 100644 --- a/tools/lib/host-config.ts +++ b/tools/lib/host-config.ts @@ -73,9 +73,9 @@ export const PUBLISHED_RELEASE_TAG: string | null = 'v0.1.6'; // --- CANDIDATE: the next unpublished build produced by package:hosts. --- /** Engine semver of the candidate: same audited engine as the source root (no engine change pending). */ -export const CANDIDATE_ENGINE_VERSION = '0.1.1'; +export const CANDIDATE_ENGINE_VERSION = '0.2.0'; /** Install-package release version of the candidate (next packaging release over the 0.1.1 engine). */ -export const CANDIDATE_RELEASE_VERSION = '0.1.7'; +export const CANDIDATE_RELEASE_VERSION = '0.2.0'; /** Immutable tag the CANDIDATE build targets (never `latest/download`). */ export const CANDIDATE_RELEASE_TAG = `v${CANDIDATE_RELEASE_VERSION}`; /** Sub-directory under `releases/` for the candidate build (gitignored). */