| name | horosa-dev |
| description | Horosa (星阙) project dev, verification & release operations. Use when building/previewing/running the local stack, doing end-to-end verification, releasing to GitHub, or working on the "AI分析" feature — especially per-technique context mounting (命盘/事盘 snapshots), adding/modifying an astrology technique, or the chat Markdown rendering. Covers build/preview/backend commands (incl. the embedded-Python gotcha), the headless technique-recompute architecture, the live verification procedure, and the macOS desktop release runbook. |
Horosa dev / verify / release
Horosa (星阙) is a macOS desktop astrology app. Repo root holds five maintenance lines; the product itself is
Horosa-Web/ (Umi3 + React 17 frontend astrostudyui/, Java Spring Boot backend astrostudysrv/, Python
chart service astropy/). The desktop shell + release pipeline live in Horosa_Desktop_Installer/ (Tauri).
For the full change history and the detailed release runbook, read
docs/ai-analysis-context-and-markdown.md. This skill is the quick operational guide.
固定流程(process discipline — 每次进场必做)
强制,针对每一次改动 Horosa:
- 先读这两份文档,再动手:本 skill(
.claude/skills/horosa-dev/SKILL.md)+ 仓内 harness 文档
Horosa-Web/AGENTS.md。两者是「会反复踩」的坑清单 + dev/release 流程;跳过 = 重付一轮 debug。
- 解决任何新问题后立刻回写:根因 + 修法写进
Horosa-Web/AGENTS.md(机制坑)或本 skill(dev/发布坑),
并优先在 release_preflight.sh 加 code guard。institutional memory 只靠「每次回写」维持,不写就漂移。
- 发布:严格按下面 runbook 顺序;build/sign/publish/push 是 consequential、需用户确认后再跑。
Critical gotchas (read first)
- Embedded Python, not the venv (now auto-handled by the launcher).
runtime/mac/python/bin/python3 (note:
runtime/, not .runtime/) is the complete embedded interpreter with every chart dep. The old
.runtime/mac/venv/bin/python3 is a broken symlink into miniconda base missing cn2an/sxtwl/cnlunar,
which crashed the Python service (:8899) on boot. start_horosa_local.sh was fixed to (a) prefer the
embedded runtime, and (b) reject any python whose readiness check is missing those load-bearing deps — so a
plain ./start_horosa_local.sh now self-resolves the right interpreter. .claude/settings.local.json still
sets HOROSA_PYTHON as an explicit override (optional now). If the launcher prints "python runtime not ready",
the chosen interpreter lacks a chart dep — install it or point HOROSA_PYTHON at the embedded runtime.
- Build needs the legacy OpenSSL flag.
npm run build / npm run start already export
NODE_OPTIONS=--openssl-legacy-provider (umi3 on modern Node). If you invoke umi/webpack directly, set it.
- Backend ports are derived. Frontend
ServerRoot (astrostudyui/src/utils/constants.js) for localhost =
http://127.0.0.1:<pagePort + 1999>. Dev server on :8000 → backend :9999 (Java) + :8899 (Python).
In a fresh preview browser you can force it: set localStorage.horosaLocalServerRoot='http://127.0.0.1:9999'
and horosaLocalServerRootMode='manual', then reload (or append ?srv=http://127.0.0.1:9999).
umi is not on PATH. Build via npm run build (or npx umi build), never a bare umi.
- No ESLint gate in the build, so unused vars won't fail CI — keep code clean yourself. (There are some
intentionally-retained-but-unused 事盘 time-recompute helpers in
aiAnalysisContext.js; safe to prune later.)
- Umi builds are sequential. Do not run
npm run build and npm run build:file in parallel; both write
src/.umi-production and can corrupt generated files. If generated syntax errors appear there, clear the cache
and rerun the builds one at a time. Cache deletion is intentionally not auto-approved by the harness; ask before
using destructive cleanup commands.
- Claude harness JSON is part of the project.
settings.json, settings.local.json, and launch.json must
parse before handoff. settings.local.json stays ignored because it contains absolute machine paths.
- Kentang/kin engines (奇门/太乙/金口/三式合一/术数) live on the chart service, not separate ports. They're
mounted onto the single Python chart service (
CHART_PORT, default :8899) by
astropy/websrv/webchartsrv.py → mount_kentang_services, and the release verifier
verify_kentang_runtime_endpoints.py --root http://127.0.0.1:${CHART_PORT} checks them there. Two things had
to be true for them to work locally: (a) vendor/ on the Python path (now in start_horosa_local.sh
PYTHONPATH_ASTRO) so import kinqimen etc. succeed; (b) the frontend resolving local kentang to the chart
port — integrations/kentang/serviceRoot.js now maps a local :9999 backend to :8899 (the per-engine
defaultLocalPort 8898/8895/… in KENTANG_SERVICE_CONFIG are legacy and were the cause of the
sanshi.qimen.kinqimen_unavailable error). If a kentang technique shows "unavailable" locally, verify
curl -s -XPOST http://127.0.0.1:8899/qimen/pan -d '{...}' returns "source":"kinqimen", then check the
frontend resolved to 8899 (not 8898). Production (srv.horosa.com) is unaffected — same host routes by path.
- 八字盘走前端本地计算,不是后端 (a bazi display fix must touch the frontend). The 八字 chart is rendered
from a local JS calc —
utils/baziLunarLocal.js → buildLocalBaziResult (lunar-javascript based), called by
cntradition/BaZi.js fetchBaziCached/fetchBaziDirectCached. The Java backend /bazi/birth
(astrostudycn BaZi.java / BaZiDirect) is only an edge-case fallback when the local calc throws.
So fixing what the bazi chart displays means changing baziLunarLocal.js (and the display components),
not just the backend. Time-display contract (mirrored in both the local calc and BaZi.java):
nongli.clockTime = raw input / clock time (stable); nongli.solarTime = apparent solar time
(longitude + equation-of-time corrected, independent of timeAlg); nongli.birth = the pillar calc
basis (changes with timeAlg). Any time row that reads nongli.birth will "jump" when the user toggles
时间算法 — read clockTime/solarTime instead. Off the +08:00 (120°E) meridian, solarTime ≠ clockTime.
The 4 display sites are cntradition/{PaiBaZi,BaZiAppInfoPanel,BaZiLegacyView,BaZi}.js. Full detail +
multi-longitude verification: docs/bazi-time-display-fix.md.
- Backend Java changes require rebuilding
astrostudyboot.jar — the release does NOT recompile it.
package_runtime_payload.sh only copies astrostudysrv/astrostudyboot/target/astrostudyboot.jar (falling
back to the committed runtime/mac/bundle/astrostudyboot.jar); nothing in the release runs Maven. There is
no root reactor pom — modules are standalone with fixed versions (boundless 1.2.1.2, astrostudy/
astrostudyboot 1.0.0). To ship a backend change, with JDK 17 (/usr/libexec/java_home -v 17):
cd Horosa-Web/astrostudysrv && mvn -f boundless/pom.xml install -DskipTests && mvn -f astrostudy/pom.xml install -DskipTests && mvn -f astrostudyboot/pom.xml clean package -DskipTests. The clean is mandatory —
a plain package sees astrostudyboot's own sources unchanged and reuses the stale fat jar without rebundling
the updated dependency jars (it bundles BOTH astrostudy AND astrostudycn — either can be stale; if you
touched astrostudycn/** e.g. ChartController, also mvn -f astrostudycn/pom.xml install before the boot
package). Verify by extracting + javap-ing the controller classes for the new routes/methods — do NOT trust
the BUNDLED dep-jar mtime: BOOT-INF/lib/astrostudy-1.0.0.jar carries a FIXED Maven reproducible-build
outputTimestamp (an old date) regardless of rebuild, so its mtime never reflects content freshness; the FAT
jar's own mtime IS reliable. (v2.4.0 nearly mis-diagnosed a freshly-rebuilt jar as stale from the bundled
mtime; javap confirmed dist()/agepoint()/greatconj() were present.)
- AI 分析 provider rules (
AIAnalysisProxyService.java + AIAnalysisMain.js). (a) OpenAI reasoning models
(gpt-5.x/o1/o3/o4, detect via isOpenAIReasoningModel) reject non-default temperature and require
max_completion_tokens not max_tokens — omit/translate for those, leave gpt-4.1 etc. untouched.
Maintenance: the prefix list in isOpenAIReasoningModel (gpt-5/gpt-6/o1/o3/o4) must be extended when OpenAI
ships new reasoning families (gpt-7, o5, …), or those models regress to sending temperature → 400.
(b) Surface real upstream errors: backend ensureSuccess must include the response body; the frontend stream
onEvent must handle the error event (not only delta) or errors collapse into "模型未返回可用内容".
(c) Never echo credentials in errors/logs — HttpUriRequestHystrixCommand redacts auth headers + strips URL
query strings. Full detail: docs/ai-provider-compat-and-error-surfacing.md.
(v2.1.5) The active provider is driven by modelSelection (profileId::model); the provider card click +
"设为当前" set it — never assume providerProfiles[0]. buildAuthHeaders omits Bearer for gemini (it uses
?key=) and honors providerOptions.authHeaderName/authPrefix. Per-version sync detail is in
docs/windows-sync-handoff.md (a per-release Windows sync ledger — keep appending to it each release).
Commands
cd Horosa-Web/astrostudyui
npm run build
npm run build:file
cd ../..
python3 -m json.tool .claude/settings.json >/dev/null
python3 -m json.tool .claude/settings.local.json >/dev/null
python3 -m json.tool .claude/launch.json >/dev/null
test -x runtime/mac/python/bin/python3
runtime/mac/python/bin/python3 -c 'import cn2an, kerykeion, sxtwl, cnlunar; print("embedded python ok")'
cd Horosa-Web/astrostudyui
node -e 'const p=require("@babel/parser");const fs=require("fs");
["src/utils/aiAnalysisContext.js"].forEach(f=>{p.parse(fs.readFileSync(f,"utf8"),{sourceType:"module",
plugins:["jsx","classProperties","objectRestSpread","optionalChaining","nullishCoalescingOperator","dynamicImport"]});console.log("OK",f);});'
npm test -- --runInBand src/utils/__tests__/aiAnalysisContext.test.js
npm test -- --runInBand src/integrations/kentang/__tests__/serviceRoot.test.js src/utils/__tests__/aiAnalysisSelection.test.js
cd ../..
cd Horosa-Web
HOROSA_SKIP_UI_BUILD=1 ./start_horosa_local.sh
./stop_horosa_local.sh
lsof -nP -iTCP:9999 -sTCP:LISTEN && lsof -nP -iTCP:8899 -sTCP:LISTEN
env -u HOROSA_PYTHON -u PYTHONPATH HOROSA_SKIP_UI_BUILD=1 ./start_horosa_local.sh
./stop_horosa_local.sh
cd ..
python3 scripts/browser_horosa_aianalysis_check.py
AI分析 context-mounting architecture
The chat mounts a structured-text "snapshot" per selected technique into the right-side "本轮挂载上下文" panel,
then buildPromptContext layers it for the LLM. Core: Horosa-Web/astrostudyui/src/utils/aiAnalysisContext.js;
UI: src/components/aianalysis/AIAnalysisMain.js (+ .less).
Two opposite rules, branched by source.sourceType in buildTechniqueContext():
- 命盘 (chart) → recompute every selected technique from the chart's stored birth data.
regenerateChartTechniqueSnapshot(record, key) dispatches per technique; for chart-derived ones it fetches via
fetchChartResultForRecord(record, {includePrimaryDirection}).
- 事盘 (case / divination) → mount ONLY the technique it was cast with, from the case's own stored
payload.
Never recompute from time (a cast = a one-time coin/dice/manual draw). Selecting a technique the cast
doesn't have → "缺失". The time-based regenerateCaseTechniqueSnapshot path is intentionally NOT called.
Hard invariant — never mount the wrong chart/cast. pickSnapshotCandidate() drops any candidate whose
compatible === false (birth/cast signature mismatch via isSnapshotMetaCompatible). A stale global module
cache (horosa.ai.snapshot.module.v1.<module>) can never leak into a different chart. If you touch candidate
selection, preserve this filter.
Wired chart techniques (recompute from birth): astrochart (via buildChartContext), bazi, ziwei,
indiachart, firdaria, primarydirect, guolao, suzhan, germany. Each component exports a headless
builder (buildBaziSnapshotForParams, buildZiweiSnapshotForParams, buildIndiaSnapshotForFields,
buildFirdariaSnapshotText/buildPrimaryDirectSnapshotText, buildGuolaoSnapshotForFields,
buildSuzhanSnapshotText, buildGermanySnapshotForFields).
Not wired (safe fallback only): profection / solararc / solar-/lunar-return / givenyear (need a target
date), jieqi, and the DOM-/iframe-bound ones (primarydirchart, zodialrelease, decennials, cntradition,
fengshui). Never recomputable: otherbu (random dice) and relative (needs two charts). All of these show
correct cached data or "缺失" — never wrong data.
How to add a technique (headless recompute)
- In the technique's component,
export a headless builder that takes the chart's form fields (or derived
params), does its own fetch, and returns snapshot text. Reuse the existing genParams/fieldsToParams and the
existing buildXxxSnapshotText. planetDisplay = null ⇒ shows the full traditional star set.
- Import it in
aiAnalysisContext.js and add a case to regenerateChartTechniqueSnapshot.
- For settings, prefer the user's stored prefs (e.g. 七政四余 reads
getStoredGuolao*) or sensible code defaults.
- Parse-check, build, then verify live (below). Watch for circular imports (these components must not import
aiAnalysisContext).
- An async builder that fetches MUST
return '' on empty/no data. If it always pushes a table header, the
technique reads as available: true, and aiAnalysisContext.test.js's "unwired/unselected techniques must be
missing" contract goes red. Register the new key in BOTH ANALYSIS_CHART_TECHNIQUES and the technique-label map.
(2026-05-29: 界推运/Huber added this way — buildDistributionsSnapshotText/buildAgePointSnapshotText fetch
/predict/dist & /predict/agepoint; empty → ''. Run npm test -- src/utils/__tests__/aiAnalysisContext.test.js.)
- Astro 事盘 / aux charts (世俗盘 etc.) do NOT use this recompute path — they mount the case's stored
payload.aiSnapshot (extractCaseSnapshotText 'ready'), else a raw JSON dump. To give them a formatted snapshot,
store it at save time: pass an optional buildAiSnapshot(chart,fields,extra) prop to DivinationChartShell
(keeps the shell generic) → divinationCaseSave writes payload.aiSnapshot. MundaneMain passes
ingress-header + buildAstroSnapshotContent. Judgment sections (12分度/主宰星链/寿命格局) instead live in
astroAiSnapshot.js buildAstroSnapshotContent + aiExport.js preset lists (bump both version consts).
- AI导出 (
aiExport.js) is a SEPARATE system from AI分析挂载 — wiring挂载 does NOT wire导出. A new technique that
should be exportable (predictions like primarydirect/firdaria are) must be added to every map or
aiExport.test.js's getAIExportAuditMatrix goes red (it asserts each technique has presetSections + options +
extractionKind + structuredSnapshotKeys). For a direction-subtab prediction: AI_EXPORT_TECHNIQUES,
AI_EXPORT_PRESET_SECTIONS, predictiveLabelMap, predictiveMap (domain predictive_raw), isPredictiveExportKey,
and the predictiveKeys array (~L4842). Export text is captured via the horosa:refresh-module-snapshot window
event — the component must listen and write evt.detail.snapshotText (see AstroDistributions/AstroAgePoint;
AstroDirectMain does it for primarydirect). For an auxchart/事盘 (世俗盘): add to auxchartMap + presets, set
getExtractorKindByExportKey→'predictive' (NOT 'astro', which grabs the MAIN chart not the aux chart) +
getStructuredSnapshotKeysByExportKey→non-empty; the shell (DivinationChartShell) answers the refresh event via
its buildAiSnapshot prop. (2026-05-29: 界推运/Huber/世俗 all wired this way.) Run npm test — full 140 must pass.
Chat Markdown rendering
Assistant messages render through renderMarkdownToHtml() (marked@4 → DOMPurify@2 sanitize → .markdownBody).
User messages stay plain (.messageText, pre-wrap). Keep the DOMPurify sanitize step — it guards against
malicious HTML from the model. Styling lives in .markdownBody rules in AIAnalysisMain.less.
Acceptance / verification procedure (do this for AI分析 changes)
Compile is necessary but NOT sufficient — verify behavior in the running app.
- Build green:
npm run build exits 0.
If you are preparing a release, also run npm run build:file after npm run build, sequentially.
- Focused tests green: run the AIAnalysis context tests and kentang service-root tests above. The kentang
local test must expect
:9999 → :8899, not legacy per-engine ports.
- Harness green: JSON parse
.claude/settings.json, .claude/settings.local.json, and .claude/launch.json;
verify .claude/settings.local.json is ignored by git and its HOROSA_PYTHON points at an existing embedded
runtime interpreter.
- Bring up the stack: start backend (above), confirm
:9999 + :8899 listen. Start preview horosa-ui.
- Point the preview at the backend: via
preview_eval, set localStorage.horosaLocalServerRoot +
…Mode='manual', reload; switch to the AI分析 view.
- Seed a known chart (so birth data is controlled) into
localStorage['horosa.localCharts.v1'] (JSON array;
record needs cid,name,birth:'YYYY-MM-DD HH:mm:ss',zone,lat,lon,gpsLat,gpsLon,gender,group:'[]',updateTime).
Click 刷新案例.
- Select the chart + techniques. Read the mounted panels via
preview_eval over .ant-collapse-item
(header = title+status+signature; expand to read .ant-collapse-content-box). Assert every technique's
signature matches the seeded birth date — this is the no-串盘 check. Confirm non-empty content + "已就绪".
For the current chart side this means the 9 wired techniques:
astrochart, indiachart, bazi, ziwei, firdaria, primarydirect, guolao, suzhan, germany.
- 事盘 check: a 六爻 case shows its卦; adding 奇门 shows "缺失"; never a fresh re-cast.
- Markdown check: assistant messages render through
marked + DOMPurify; verify <strong>/<h*>/<ul>/<table>
appear and there are zero literal ** markers in the rendered report. Do not remove sanitize.
- Layout check: 系统提示 sits in the right column above 挂载上下文; the toolbar buttons (刷新案例/新对话/…)
sit at the bottom-left of 发送分析.
Release to GitHub
This is a manual, macOS-signed pipeline (no CI auto-release on tag). Full ordered runbook + checklists are in
docs/ai-analysis-context-and-markdown.md (§ Release runbook). Summary:
- Bump version in lockstep:
Horosa_Desktop_Installer/{package.json, src-tauri/Cargo.toml, src-tauri/Cargo.lock, src-tauri/tauri.conf.json}
CITATION.cff; bump Horosa_Desktop_Installer/config/release_config.json runtimeVersion (-runtime<N>,
reset to -runtime1 on app bump). release_preflight.sh 还门禁两处易漏的版本(v2.3.0 踩过):Horosa_Desktop_Installer/web/app.js
的 const APP_VERSION = '<ver>'(⚠ 它被打进 .app 启动器 UI + 显示「pkg/runtime 」——必须在 build_desktop_release.sh 之前 bump,
否则要整包重建+重签+重公证)和 Horosa_Desktop_Installer/scripts/verify_launcher_console_states.py 的「来源 pkg 」「本机组件版本 」断言。
全量自检:grep -rn "2\.2\.1" Horosa_Desktop_Installer/{package.json,src-tauri/Cargo.toml,src-tauri/Cargo.lock,src-tauri/tauri.conf.json,web/app.js,config/release_config.json,scripts/verify_launcher_console_states.py} CITATION.cff 应为空。 Append a UPGRADE_LOG.md entry. If the backend (astrostudysrv/**) changed,
rebuild astrostudyboot.jar (gotcha #10). Write per-version highlights to
Horosa_Desktop_Installer/config/release_notes/{version}.md (e.g. 2.1.4.md) — publish injects it into the
release page's "本版更新 / What's new" section; without it the page shows only the generic product overview.
Every fix MUST also ship (process rule): a tech doc docs/<topic>-v{version}.md (root cause + change +
verification; state whether astrostudyboot.jar needs rebuilding) and a docs/windows-sync-handoff.md entry
(top of file, newest first: what changed / what Windows must do / verification, linking the tech doc). The Windows
repo is a platform fork that syncs by reading these on main; release_preflight.sh gates the windows-sync entry
for the version (so a fix can't ship without Windows being able to read it).
- Run the pre-release gates: harness JSON, focused tests, clean sequential
npm run build then npm run build:file,
browser AIAnalysis smoke, clean-env local startup smoke.
- If you edited any Rust (
src-tauri/**, esp. main.rs), run cargo fmt BEFORE committing. CI's "Desktop
Shell Check" runs cargo fmt --check; a non-fmt edit fails it, and publish_github_release.sh's preflight [6]
requires CI = success → it aborts the publish. (v2.4.0 hit this: edited main.rs + ran cargo check not
cargo fmt → push → CI Desktop Shell Check failed → publish aborted; fix = cargo fmt + new commit + wait for
CI green + re-publish.) fmt collapsing func(&arg, …) to one line does NOT break the [9]A/D sentinels (they
grep a string literal / a comment, both fmt-stable). Backend Maven + Frontend CI jobs are independent — check
gh api .../commits/<sha>/check-runs to see exactly which job failed before assuming it's fmt.
git commit -m "release: prepare vX.Y.Z beta" and push main after validation, unless the user explicitly asks
for an earlier main push.
cd Horosa_Desktop_Installer && ./scripts/check_apple_signing_prereqs.sh.
HOROSA_PUBLIC_DISTRIBUTION=1 ./scripts/build_desktop_release.sh (the single build + sign + notarize + staple step).
HOROSA_DESKTOP_SKIP_REBUILD=1 ./scripts/verify_desktop_packaging.sh,
./scripts/verify_runtime_backend_boot.sh --timeout 300, and
./scripts/verify_public_distribution_readiness.sh.
- Gotcha:
verify_desktop_packaging.sh hard-exits (exit 1) unless it finds a python with Playwright
installed — it drives a headless chromium for the launcher console-state check. None of the default pythons
ship it. Make a throwaway venv (python3 -m venv /tmp/pw && /tmp/pw/bin/pip install playwright && /tmp/pw/bin/python -m playwright install chromium) and pass HOROSA_PLAYWRIGHT_PYTHON=/tmp/pw/bin/python.
The other two gates need no Playwright.
HOROSA_PUBLIC_DISTRIBUTION=1 ./scripts/publish_github_release.sh (needs GITHUB_TOKEN or a working
git credential for github.com; uploads existing assets and creates/updates both the app tag/release and runtime
tag/release). Do not manually git tag afterward unless this script explicitly failed before creating tags.
- Publish re-runs
verify_desktop_packaging.sh (so it needs Playwright again) unless you pass
HOROSA_SKIP_VERIFY=1. Safe to skip only when you just ran that gate on the same unmodified dist/ bytes;
HOROSA_PUBLIC_DISTRIBUTION=1 still enforces the fast signed-readiness check before upload.
- A brand-new
-runtimeN tag uploads cleanly. HOROSA_FORCE_RUNTIME_UPLOAD=1 is only needed when the
release_config.json runtime tag already exists remotely and its payload sha changed.
- Post-publish verification is mandatory:
git ls-remote --heads --tags origin main refs/tags/vX.Y.Z refs/tags/vX.Y.Z-runtimeN,
gh release view vX.Y.Z, gh release view vX.Y.Z-runtimeN, fetch the latest
horosa-latest.json, then run ./scripts/verify_github_release_end_to_end.sh.
Build/sign/publish/push are consequential and deliberately NOT auto-approved in .claude/settings.json — confirm
with the user before running them. Never git push --force to main.
Safeguards & notes (added v2.1.4):
- Run
Horosa_Desktop_Installer/scripts/release_preflight.sh before publishing — publish_github_release.sh
now auto-runs it (HOROSA_SKIP_PREFLIGHT=1 overrides). It encodes every gap from the v2.1.4 process review as
pass/fail checks: version lockstep across all files, per-version config/release_notes/{version}.md present,
UPGRADE_LOG entry, docs/windows-sync-handoff.md has a {version} entry (every fix must be Windows-readable),
.claude/settings.local.json not git-tracked + all harness JSON parseable, astrostudyboot.jar
/ dist-file newer than sources, and CI green for HEAD. When you discover a new release gap, add a check here.
package_runtime_payload.sh fails if astrostudyboot.jar or dist-file is older than its sources — prevents
silently shipping stale backend/frontend. Rebuild the stale artifact, or set HOROSA_SKIP_FRESHNESS_GUARD=1 if
you're certain it's intentional.
- ⚠️ git checkout/merge 顶乱 mtime → freshness guard 误报 stale(v2.3.0 踩过)。
git checkout <branch> /
git merge(如发布前 ff main)会把切换涉及的文件 mtime 刷到「现在」。若 build 已做完、之后才做 branch 操作,
dist-file / astrostudyboot.jar 会内容是新的、mtime 却比源旧 → guard 报「比源旧」。正确顺序:所有 git
branch 操作(commit / ff main)做完后,再 cd Horosa-Web/astrostudyui && npm run build && npm run build:file
重建 dist-file;jar 若在 branch 操作前 find Horosa-Web/astrostudysrv -name '*.java' -newer <jar> 为空(内容已确认当前),
touch target 与 runtime/mac/bundle 两个 jar 即可,否则按 gotcha #10 重编。别无脑 HOROSA_SKIP_FRESHNESS_GUARD=1 跳过(那会把真 stale 一起放过)。
- Release gates cover signing/boot/install/kentang/chart, not feature behavior (bazi / AI analysis). Feature-logic
regressions are caught by CI on push (
mvn test in boundless + astrostudy, npm test in astrostudyui) —
keep CI green; add tests there for new logic. True end-to-end feature checks still need ad-hoc testing (e.g. a real
provider key for AI).
- The in-app auto-update path (vX→vY) is not exercised per release; only
horosa-latest.json consistency is
(verified by the e2e gate).
main is normally pushed after validation; for strict main/release consistency you may push it only after a
successful publish.
- GitCode mirror — DEPRECATED / DO NOT RUN (user decision 2026-05-27). Releases go to GitHub only. Do
NOT run
scripts/mirror_to_gitcode.sh, do not create GitCode 发行版, do not remind the user about GitCode sync.
(Historical: it was a read-only Pull mirror HoraceDong_C137/<repoName> with a create-only notes API; kept here
only so a future agent knows it was intentionally retired, not forgotten.)
Self-improvement — do this EVERY release (the harness must get more reliable, not drift):
After each release, spend a few minutes re-auditing the whole dev→release flow for gaps you have not seen before
(stale artifacts, generic/boilerplate release notes, untested code paths, leaked secrets, missing CI coverage,
inconsistent versions, …). For every new finding: (a) prefer a CODE GUARD over a doc note — a check in
release_preflight.sh or a hard-fail in the relevant script beats relying on memory; (b) add the check to
release_preflight.sh; (c) record the gotcha here and in the relevant docs/*. This section and the preflight
script are the institutional memory — keep growing them.
Gotcha (v2.2.1) — 晚子时·时柱起干 has FOUR independent hour-stem code paths; a fix must touch all of them.
The late-zi hour-stem rule lives in BaZiHelper.java ×3 (getTimeColumn two overloads + getTimeGanziStr +
getTimeStartGan) AND in each technique's frontend fetch payload/cache key. The 六壬/金口 displayed 时柱 comes from
NongliHelper.getNongLi → getTimeGanziStr (NOT getTimeColumn, which feeds 八字/果老/OnlyFourColumns) — fixing only
getTimeColumn leaves 六壬/金口 wrong. Frontend: each fetchXxxPan/cache-key must carry after23NewDay +
lateZiHourUseNextDay or the switch needs a reload (GuoLaoChartMain.normalizeChartParams, JinKouCalc.fetchJinKouPan,
TaiYiCalc.fetchTaiyiPan, DunJiaCalc.fetchQimenPan+getQimenOptionsKey). Pinned (1,0) 27日23:30 → 壬寅 戊子
across ALL techniques. Detail: docs/v2.2.1-session-addendum.md.
Gotcha (v2.2.1) — "AI mounts the rule" can be a lie if the helper is never called. aiAnalysisContext.js
buildDayBoundaryMeta was defined-but-never-invoked for a whole release cycle, so the day-boundary rule was NOT actually
in the AI context despite release notes claiming it. Lesson: when wiring metadata into AI export/analysis, grep for the
call site, not just the definition. Now wired in buildContextLayers + aiExport.js header.
Self-check additions (run these for any 排盘/AI/更新/偏好 change before release):
- 23:30 four-pillar matrix, ALL techniques consistent: at 2026-05-27 23:30,
(after23,lateZi) →
(1,1)壬寅庚子 (1,0)壬寅戊子 (0,1)辛丑庚子 (0,0)辛丑戊子; verify 八字/六壬/金口/太乙/奇门/果老 agree
(Java techniques regressed to 庚子 on (1,0) before the getTimeGanziStr/getTimeColumn symmetric fix). NO-OP at 22:30/00:30.
- In-app update non-blocking: desktop shell, simulate a higher version in
horosa-latest.json → bottom-right
non-modal card → 更新 downloads in background, minimizable, app stays usable → "重启更新" is user-initiated. Web build: UpdateNotifier renders nothing (no window.__TAURI__).
- Preferences dialogs: open 全局设置/AI导出设置/排盘设置/星盘参数/AI分析设置 + 关于星阙 in light AND dark; semantic colors unchanged.
- AI provider: Anthropic (incl. relay) 测试连接 must succeed (content blocks carry
type:"text"); unauthenticated 401 shows an actionable message, not a raw dump.
占星地图 / ACG(地理占星,辅盘→占星地图 locastro;分支 feature/acg-map-upgrade)。 Standard astrocartography
rebuilt to D3-geo + bundled GeoJSON + analytic RA/Dec lines (replacing the old AMap + iterative search). Stack:
AstroAcg.js→/location/acg(+/location/acgpoint)→Java AcgController/AstroHelper→Python AcgSrv→acg/ACGraph.py.
Renderer AcgD3Map.js (no new npm deps — reuses d3@7's d3-geo); world map = bundled world.geo.json (Natural Earth
110m, open data). Full detail: docs/acg-map-地理占星.md. Four traps that WILL bite (each cost a debugging round):
- Full-width horizontal lines render as nothing. A
LineString [[-180,lat],[180,lat]] is degenerate on the
antimeridian — d3.geoPath clips it to two zero-length dots, and a Δlon=360 split computes t=0/0=NaN. Equator /
tropics / parans must be multi-point sampled (-179..179). Vertical 2-point MC/IC meridians are fine.
- ASC/DSC curves break / don't close. Near the rise/set cutoff (
|φ|=90-|δ|) longitude changes very fast → 1°
sampling false-splits at ±180 and the rising/setting hooks don't meet the MC/IC meridians. Fix = 0.5° sampling +
exact band-edge endpoints (H=0→MC lon, H=180→IC lon) in _ascDescLines, and splitAtDateline interpolates an
edge point at ±180 so segments reach the map edge (no gap).
- SVG label glyphs inherit a global
text { stroke } (≈1px #162033). Chart glyphs share ywastro; a global rule
strokes all SVG text. A presentation attr (attr('stroke','none')) does NOT win — must use inline
style('stroke','none') on the label <text>, else glyphs look muddy/outlined. (Verify with computed style, not
the attribute.)
getAcg uses requestNoCache. Default request() caches by params for a day, so editing ACGraph.py shows
stale results for the same chart. Because that's a Java change, rebuild astrostudyboot.jar (gotcha #10).
- Theme: map reads
:root[data-horosa-appearance] (light/dark) via MutationObserver → light=pale terrain+deepened
planet colours+white casing; dark=dark terrain+bright lines. Label chips flip (white-bg/dark-frame ↔ dark-bg/light-frame).
- Parans default to luminary (Sun/Moon) pairs, 1°-deduped + faint; toolbar cycles 关/日月/全部. 4 basemap styles
(
STYLES: 标准/简约/政区/单色) orthogonal to light/dark.
- Self-check (visual): 辅盘→占星地图 in light AND dark — lines smooth & connected (polar hooks close, no dateline gap),
chip glyphs crisp with no stroke, equator/黄道/回归线 visible, Parans 关/日月/全部, click→落点分析 with 迁移四轴 +
解读. Theme toggle: map follows instantly.
- Self-check (algorithm gold standard): after ANY change to
ACGraph.py analytic math, run
runtime/mac/python/bin/python3 Horosa-Web/astropy/astrostudy/acg/validate_acg.py → must print
ACG alignment PASS (worst < 1e-3°, actual 0.000000°; exit 0). It independently re-checks every four-axis line against
swisseph.azalt() (its own sidereal time — same engine pro tools use): ASC/DESC points at true-altitude 0, MC/IC on the
meridian. This is the precise "aligned with the standard" proof — far better than eyeballing any external map.
Gotcha (v2.3.0) — 更新后重启「卡在 100% 很久」= 更新后首启的全量慢路径 + 两个复发 bug。
症状:在线更新后第一次重启,进度停在 100% 很久才进主界面(平常重启很快)。根因:Tauri main.rs runtime_bootstrap
检测到 update-complete.txt 标记就走故意放慢的全量校验路径(trusted_runtime=0/fast_path=false/300s 超时),
且「100%」是进度最后一格、其后还要 window.location.replace 冷加载前端(更新后 .app 整包被换→OS 缓存冷 + Gatekeeper 重验)。
再叠加两个 bug:① 标记仅成功时消费→首启一失败就残留→下次仍走 300s 慢路径反复卡;② pid 仅判存在→死 pid 误拦截启动。
实测:平常 6–8s vs 更新后首启 27–29s。修法(纯 Tauri 外壳 main.rs + start_horosa_local.sh,无 Java、不重编 jar):
① 标记「读取即消费」consume_update_complete_marker_into_state(解析通知缓存进 AppState.pending_update_notice 后立即删标记,
show_post_update_notice_if_needed 改从内存取);② prune_stale_pid_file(kill -0 判存活,死 pid 自动清);
③ 首启 emit_indeterminate_progress(不确定动画 +「约 30–60 秒」文案,launcher web/app.js 已支持 indeterminate);
④ warmup 后台非阻塞(( … ) >/dev/null 2>&1 &,外层重定向切断 stdout/stderr 继承否则卡住 command.output())+ 轮询与 trusted
解耦 0.2s + mongo 一律 skip ping。完整校验(fast_path=false/全量 verify)原样保留不动。 实测脚本慢路径 29s→14s。
防回归:release_preflight.sh [9] 哨兵。详见 docs/更新后启动卡顿修复-v2.3.1.md。
Self-check (更新 / 启动 改动前后必跑): 本地模拟更新后首启
env -u HOROSA_PYTHON HOROSA_SKIP_UI_BUILD=1 HOROSA_REQUIRE_EMBEDDED_RUNTIME=1 HOROSA_TRUSTED_RUNTIME=0 HOROSA_DESKTOP_MONGO_SKIP_PING=1 HOROSA_SKIP_RUNTIME_WARMUP=0 ./start_horosa_local.sh
应:diagnostics 日志为 runtime warmup begin (background) 且 run end 不等其 done;先 echo 999999 > .horosa_py.pid 再启动应被自动清除(rc=0)、服务在跑时再启动应被拦截(rc=1)。功能零降级靠这三套(在提速后 trusted=0 路径下):verify_kentang_runtime_endpoints.py --root http://127.0.0.1:8899(17 引擎全 200)、HOROSA_SERVER_ROOT=http://127.0.0.1:9999 node astrostudyui/scripts/verifyHorosaRuntimeFull.js(exit 0)、… node astrostudyui/scripts/verifyPrimaryDirectionRuntime.js(exit 0)。
Gotcha (v2.3.1, Win #10「服务不稳定」) — SSE 并发竞态 + SSE 标志跨请求污染,打挂 AI + 排盘 + predict。
两个独立的共享后端 bug:(A) withHeartbeat 心跳线程(每 15s emitter.send(keep-alive)+ 失败自行 complete())与读流线程(sendEvent→emitter.send)并发写非线程安全 SseEmitter → ResponseBodyEmitter has already completed(sendEvent:995)→ AI「几句话后停止」。(B) TransData.setSSE(true) 实为 request.setAttribute("__sse__")(绑 request 对象、非 ThreadLocal,pureClearTransData 只清 5 个 map、碰不到它),Tomcat 池化复用 HttpServletRequest 致 __sse__=true 残留 → 排盘/predict 在 RequestHeaderInterceptor.complete():595/afterCompletion:744 被 isSSE() 误判(且排在「handler 返回类型是否 SseEmitter」的可靠判定之前)走 event-stream → 间歇 signature.error/「本地排盘服务未就绪」/predict 200 报错。修法:(A) 线程安全内部类 SseChannel 收口所有 emitter 写(单锁+幂等)、心跳不再自行 complete;(B) preHandle 非 REQUEST dispatch 早返回(免 async re-dispatch 用空 body 重复验签)+ REQUEST 进来 TransData.setSSE(false) 归零。纯 Java(astrostudy + boundless)→ 必重编 astrostudyboot.jar(gotcha #10)。 验证:三套(kentang/Full/主限)全过证明验签未破坏+零降级(它们都过 preHandle);SSE 真实场景靠 Windows 真机实测。preflight [10] 哨兵。详见 docs/服务不稳定-SSE并发与签名污染修复-v2.3.1.md。