diff --git a/plans/260818-2158-amlich-converter-improvements/phase-01-lunar-core-helpers.md b/plans/260818-2158-amlich-converter-improvements/phase-01-lunar-core-helpers.md new file mode 100644 index 0000000..7b4becd --- /dev/null +++ b/plans/260818-2158-amlich-converter-improvements/phase-01-lunar-core-helpers.md @@ -0,0 +1,75 @@ +--- +phase: 1 +title: Lunar core helpers +status: completed +effort: S +priority: P2 +dependencies: [] +--- + +# Phase 1: Lunar core helpers + +## Overview + +Add the two pure helpers the handlers need: the disputed-boundary set + membership check, and +leap-month existence probing. No behavior change to existing conversion functions. + +## Requirements + +- Functional: `nearDisputedBoundary(jdn int) bool` reports whether the lunar month containing + solar day `jdn` starts or ends on one of the 7 disputed month boundaries. +- Leap-variant existence needs NO new helper: handlers probe `lunarToSolar(day, month, year, true)` + and treat `err == nil` as "leap variant exists" (DRY — reuses existing validation; also naturally + suppresses the hint when e.g. day 30 doesn't exist in the 29-day leap month). +- Non-functional: helpers stay in `lunar.go`, unexported, zero allocations on the hot path + (package-level set built once). + +## Architecture + +- `disputedMonthStarts` — package-level `map[int]bool` built in a `var` initializer from + `jdFromDate` on the 7 dates recorded in `docs/amlich-known-issues.md`: + 09/12/2072, 15/11/2077, 07/05/2130, 26/05/2150, 17/05/2159, 22/01/2175, 26/01/2199. +- `nearDisputedBoundary(jdn int) bool` — mirror `solarToLunar`'s month-start search: + +```go +k := floorInt((float64(jdn) - jdNewMoonEpoch) / newMoonCycle) +monthStart := getNewMoonDay(k + 1) +for monthStart > jdn { + k-- + monthStart = getNewMoonDay(k + 1) +} +return disputedMonthStarts[monthStart] || disputedMonthStarts[getNewMoonDay(k+2)] +``` + + Rationale for checking next start too: if the *end* boundary of the containing month is disputed, + the month's length (and day numbers near its end) is what may shift. + +## Related Code Files + +- Modify: `internal/modules/amlich/lunar.go` +- Modify: `internal/modules/amlich/lunar_test.go` + +## Implementation Steps + +1. Add `disputedMonthStarts` set with a comment explaining provenance (new moon within ±2 min of + UTC+7 midnight; see docs/amlich-known-issues.md) — no plan/audit labels in comments. +2. Add `nearDisputedBoundary`. +3. Test: each of the 7 JDs is an actual month start per the engine — for each date, assert + `solarToLunar(d,m,y)` returns lunar day 1. If any assertion fails ±1 day, the doc date and the + engine's boundary disagree: adjust the set entry to the engine's month start and flag the doc + discrepancy in the phase report. +4. Test `nearDisputedBoundary`: true for a mid-month day of the month starting 09/12/2072, true for + a day in the month *before* it (whose end boundary is disputed), false for an ordinary 2072 date + far from the boundary and for a 2024 date. + +## Success Criteria + +- [ ] 7 set entries verified as engine month starts by test +- [ ] `nearDisputedBoundary` true/false cases covered as above +- [ ] Existing lunar tests untouched and green + +## Risk Assessment + +- Doc dates might be the full-Meeus engine's boundaries rather than the current engine's (off by + one day). Mitigation: step 3's pin test resolves it mechanically; the ±1-day neighborhood is the + same disputed lunation either way. diff --git a/plans/260818-2158-amlich-converter-improvements/phase-02-handler-replies.md b/plans/260818-2158-amlich-converter-improvements/phase-02-handler-replies.md new file mode 100644 index 0000000..8693f1b --- /dev/null +++ b/plans/260818-2158-amlich-converter-improvements/phase-02-handler-replies.md @@ -0,0 +1,66 @@ +--- +phase: 2 +title: Handler replies +status: completed +effort: S +priority: P2 +dependencies: + - 1 +--- + +# Phase 2: Handler replies + +## Overview + +Wire the leap-month hint into `/duonglich` and the razor-edge caveat into both commands. Reply +sentences gain optional trailing lines only; existing first-line format is unchanged. + +## Requirements + +- Functional (`/duonglich` hint): after a successful conversion with `leap == false`, if + `lunarToSolar(day, month, year, true)` returns nil error, append: + `Lưu ý: năm âm lịch có tháng nhuận — thêm "nhuan" nếu ý bạn là tháng nhuận.` +- Functional (caveat, both commands): if `nearDisputedBoundary(jd)` — where `jd` is the *solar* JD + of the queried/resulting date — append: + `Lưu ý: ngày này gần ranh giới tháng âm lịch chưa chắc chắn; kết quả có thể lệch 1 ngày so với lịch chính thức sau này.` +- Hint NOT shown when `nhuan` was explicit (user already disambiguated) or when the exact leap date + doesn't exist. Both lines may co-occur (hint first, then caveat), each on its own line after the + main sentence, separated by `\n`. +- Non-functional: no change to usage strings, error paths, or year-range checks. + +## Architecture + +- `/amlich`: `jd := jdFromDate(day, month, year)` (already-parsed solar input) → caveat check. +- `/duonglich`: caveat check on `jdFromDate(solarDay, solarMonth, solarYear)` from the conversion + result; hint check via the leap-variant probe before assembling the reply. +- Message constants live next to the usage constants in `handlers.go`. + +## Related Code Files + +- Modify: `internal/modules/amlich/handlers.go` +- Modify: `internal/modules/amlich/handlers_test.go` + +## Implementation Steps + +1. Add the two message constants (hint as format string taking year + month). +2. `/duonglich`: after successful `lunarToSolar`, build reply, conditionally append hint (probe + with `leap=true` only when input `leap == false`), then conditionally append caveat. +3. `/amlich`: conditionally append caveat after the main sentence. +4. Tests (extend existing fake-reply pattern in `handlers_test.go`): + - `/duonglich 5/5/2028` → hint present (2028 has leap 5); `/duonglich 5/5/2028 nhuan` → absent; + `/duonglich 5/5/2027` → absent (no leap 5 in 2027). + - `/amlich` on a date inside the disputed month at 09/12/2072 → caveat present; ordinary date → + absent. `/duonglich` case whose result lands in that month → caveat present. + - Assert exact full reply strings (repo test style asserts exact text — keep it strict). + +## Success Criteria + +- [ ] Hint behavior matches the three `/duonglich` cases above +- [ ] Caveat fires for disputed-month dates in both commands, silent elsewhere +- [ ] All pre-existing handler tests pass without assertion loosening + +## Risk Assessment + +- Double-probe cost: one extra `lunarToSolar` call per leap-year query — microseconds, irrelevant. +- Wording is user-visible contract; if the user wants different Vietnamese phrasing, only the + constants change. Flag wording in the PR description for review. diff --git a/plans/260818-2158-amlich-converter-improvements/phase-03-golden-table-testdata-and-docs.md b/plans/260818-2158-amlich-converter-improvements/phase-03-golden-table-testdata-and-docs.md new file mode 100644 index 0000000..3c0874d --- /dev/null +++ b/plans/260818-2158-amlich-converter-improvements/phase-03-golden-table-testdata-and-docs.md @@ -0,0 +1,75 @@ +--- +phase: 3 +title: Golden-table testdata and docs +status: completed +effort: S +priority: P3 +dependencies: [] +--- + +# Phase 3: Golden-table testdata and docs + +## Overview + +Freeze the verified 1800–2199 month structure as committed testdata, and close the resolved open +questions in `docs/amlich-known-issues.md`. Independent of phases 1–2. + +## Requirements + +- Functional: a test recomputes every lunar year's structure from the engine and compares + byte-exact against `testdata/lunar-years-1800-2199.txt`; `go test -run TestGoldenTable -update` + regenerates the file (standard `-update` flag idiom). +- File format, one line per lunar year: + `YYYY L n1 n2 ... nN` — `L` = leap month number (0 = none), `n*` = month lengths (29/30) in + chronological order, tháng 1 first, leap month inserted in sequence after month `L` + (12 entries normal year, 13 leap year). +- Non-functional: file lives in `internal/modules/amlich/testdata/`; generation logic lives in the + test file only (no production code). + +## Architecture + +- Build each year from `lunarToSolar` month starts: for lunar year Y, JDs of tháng 1..12 (+ leap + where `lunarToSolar(1, m, Y, true)` succeeds), sorted chronologically; lengths = successive + start-JD differences, last month's length from tháng 1 of Y+1. Simpler than sweeping days and + exercises the lunar→solar direction the round-trip test already covers from the other side. +- Rationale over round-trip: `TestSolarLunarRoundTrip` proves self-consistency only; a future + engine change that shifts a boundary consistently in both conversions passes it. The golden file + pins the actual verified placement. + +## Related Code Files + +- Create: `internal/modules/amlich/testdata/lunar-years-1800-2199.txt` +- Modify: `internal/modules/amlich/lunar_test.go` (or new `golden_test.go` if it crowds the file) +- Modify: `docs/amlich-known-issues.md` + +## Implementation Steps + +1. Write `TestGoldenTable` with `var update = flag.Bool("update", false, ...)`; generator builds the + full table string; when `-update`, write file and skip compare; else compare byte-exact with a + diff-friendly failure message (first differing line). +2. Generate the file once; eyeball spot checks: 2025 leap 6, 2028 leap 5, 2033 leap 11, 1944 + leap 4, 1967 no leap month (leap numbers already pinned by `TestLeapMonthTable` — must agree; + the doc's "30/5 Đinh Mùi" for 1967 is day 30 of the regular month 5, not a leap month). +3. Commit the generated file. Edge-case years must round-trip with the existing suite untouched. +4. Update `docs/amlich-known-issues.md`: + - Open question 1 → resolved: caveat line added, months touching the boundary only. + - Open question 2 → closed (don't build): add the South-Vietnam wrinkle (UTC+7 until 1959, + UTC+8 1960–67 per Hồ Ngọc Đức's historic-calendar page) — a single UTC+8 mode would be wrong + for the South 1955–59, strengthening the existing conclusion. + - "`/duonglich` defaults inside leap months" item → note the hint now self-disambiguates. + - Keep questions 3 (ΔT) and 4 (future official tables) open; add one line noting the 2026 CGPM + leap-second-abolition vote as a further far-future timescale uncertainty in the same bucket. + +## Success Criteria + +- [ ] Golden file committed; regeneration from HEAD is byte-identical +- [ ] Golden leap months agree with `TestLeapMonthTable` pins +- [ ] Docs updated; no stale claims left in the two resolved items +- [ ] `docs/amlich-known-issues.md` stays under docs.maxLoc (800) + +## Risk Assessment + +- Ordering bug when inserting the leap month (nhuận m sorts after regular m) — chronological + sort by start JD avoids hand-rolled index math. +- `flag.Bool` at package scope collides if a flag named `update` ever exists elsewhere in the + package's tests — it doesn't today; keep the name. diff --git a/plans/260818-2158-amlich-converter-improvements/plan.md b/plans/260818-2158-amlich-converter-improvements/plan.md new file mode 100644 index 0000000..d558c08 --- /dev/null +++ b/plans/260818-2158-amlich-converter-improvements/plan.md @@ -0,0 +1,64 @@ +--- +title: >- + Amlich converter improvements: leap-month hint, razor-edge caveat, + golden-table tests +description: >- + Three merged non-breaking improvements to internal/modules/amlich from + brainstorm decision +status: completed +priority: P2 +branch: main +tags: + - amlich + - telegram-bot +blockedBy: [] +blocks: [] +created: '2026-08-18T15:04:11.318Z' +createdBy: 'ck:plan' +source: skill +--- + +# Amlich converter improvements: leap-month hint, razor-edge caveat, golden-table tests + +## Overview + +Implement the three improvements selected in +`plans/reports/brainstorm-decision-260818-2158-amlich-improvements-selection-report.md` +(research: `plans/reports/research-brainstorm-260818-2147-amlich-converter-improvements-report.md`): + +1. `/duonglich` leap-month ambiguity hint — when the entered month is also that year's leap month + and no `nhuan` flag given, append a one-line hint. +2. Razor-edge caveat — both commands warn when the result's lunar month touches one of the 7 + disputed month boundaries from 2072 on (documented in `docs/amlich-known-issues.md`). +3. Golden-table regression testdata — freeze the verified 1800–2199 month-structure output. + +Explicitly out of scope (rejected with verification in the research report): ΔT model update, +pre-1968 historic mode, table-driven engine rewrite, range extension, extra calendar features. + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [Lunar core helpers](./phase-01-lunar-core-helpers.md) | Completed | +| 2 | [Handler replies](./phase-02-handler-replies.md) | Completed | +| 3 | [Golden-table testdata and docs](./phase-03-golden-table-testdata-and-docs.md) | Completed | + +## Dependencies + +None cross-plan. Phase 2 depends on phase 1; phase 3 is independent. + +## Acceptance Criteria (whole plan) + +- All existing tests pass unchanged — especially `knownDates` pins (20/6/1944, 7/7/1967), + `TestSolarLunarRoundTrip`, `TestLeapMonthTable`. +- `/duonglich 5/5/2028` (leap-5 year) shows the hint; `/duonglich 5/5/2028 nhuan` and non-leap + years do not. +- Conversions inside a lunar month adjacent to the 09/12/2072 boundary show the caveat (both + commands); ordinary dates do not. +- Golden file regenerated from HEAD is byte-identical to the committed one. +- No public-contract changes; reply format only gains optional trailing lines. +- `go vet ./...` and `staticcheck` clean (repo standard). + +## Validation + +`go test ./internal/modules/amlich/` after each phase; full `go test ./...` + lint at the end. diff --git a/plans/reports/brainstorm-decision-260818-2158-amlich-improvements-selection-report.md b/plans/reports/brainstorm-decision-260818-2158-amlich-improvements-selection-report.md new file mode 100644 index 0000000..445eb3d --- /dev/null +++ b/plans/reports/brainstorm-decision-260818-2158-amlich-improvements-selection-report.md @@ -0,0 +1,72 @@ +# Brainstorm Decision: amlich Converter Improvements — Selected Scope + +Date: 2026-08-18 21:58 (+07) +Basis: `plans/reports/research-brainstorm-260818-2147-amlich-converter-improvements-report.md` +Mode: user delegated choice ("choose the best, merge if mergable"). + +## Decision + +All three recommended items merged into one change set — they are complementary (input UX, +output honesty, test hardening), not alternatives. Rejections from the research report stand. + +### In scope + +1. **Leap-month hint in `/duonglich`** (highest user value, do first) + - Trigger: resolved month == that lunar year's leap month AND `nhuan` flag absent. + - Output: append "Năm nay có tháng X nhuận — thêm 'nhuan' nếu ý bạn là tháng nhuận." + - Impl: `leapMonthOf(year int) (month int, ok bool)` helper in `lunar.go` reusing + `getLunarMonth11` + `getLeapMonthOffset`; hint assembled in `handlers.go`. + - NOT triggered when `nhuan` explicit (resolves research report open question 2: no). + +2. **Razor-edge caveat in both commands** + - Data: 7 disputed month-start JDs (new moons of 09/12/2072, 15/11/2077, 07/05/2130, + 26/05/2150, 17/05/2159, 22/01/2175, 26/01/2199) as a package-level set in `lunar.go`, + JD values derived at implementation time and pinned by test. + - Trigger: containing month start OR next month start is in the set → append one-line + caveat: result near disputed lunar-month boundary, may differ ±1 day from future + official tables. + - Scope: months touching the boundary only, not the whole lunar year (resolves research + report open question 1: months-only; year-wide is alarmist). + - Impl: `nearDisputedBoundary(jdn int) bool` helper; handlers call it with the solar JD. + +3. **Golden-table regression testdata** + - `internal/modules/amlich/testdata/lunar-years-1800-2199.txt`: one line per year — + year, leap-month index (0 = none), month lengths in order (12 or 13 entries). + - Test recomputes from engine, compares byte-exact; `-update` flag idiom regenerates. + - Rationale: round-trip test proves self-consistency only; golden table freezes the + verified boundary placement against silent drift. + +### Out of scope (rejected, verified in research report) + +- ΔT model update — breaks bit-compatibility with ecosystem; verified by prior 400-year diff. +- Pre-1968 historic mode — ground truth fragments (North UTC+8 1945–67; South UTC+7→UTC+8 1960). +- Table-driven engine rewrite — no authoritative source beyond current engine; test-data-only instead. +- Range extension beyond 1800–2199; can-chi day names, tiết khí, holiday lookup. + +## Touchpoints + +- `internal/modules/amlich/lunar.go` — `leapMonthOf`, `nearDisputedBoundary`, disputed-JD set. +- `internal/modules/amlich/handlers.go` — hint + caveat lines in both command replies. +- `internal/modules/amlich/lunar_test.go`, `handlers_test.go` — new cases; existing pins untouched. +- `internal/modules/amlich/testdata/` — new golden file. +- `docs/amlich-known-issues.md` — close open questions 1–2 with these resolutions. + +## Acceptance Criteria + +- All existing tests pass unchanged, incl. `knownDates` (20/6/1944, 7/7/1967) and full round-trip. +- `/duonglich 5/5/2028` (leap-5 year) shows hint; `/duonglich 5/5/2028 nhuan` and non-leap years don't. +- `/amlich` for a date inside a month adjacent to 09/12/2072 boundary shows caveat; ordinary dates don't; + `/duonglich` symmetric. +- Golden file regenerated from HEAD is byte-identical to committed version. +- ~100 LOC total incl. tests; no public-contract changes. + +## Risks + +- Caveat JD derivation error → wrong months flagged. Mitigation: pin the 7 JDs in a test that + recomputes them from `getNewMoonDay`. +- Reply strings are user-visible contract for handler tests — update expected strings, don't loosen asserts. + +## Unresolved Questions + +- None blocking. Post-leap-second timescale question stays parked in `docs/amlich-known-issues.md` + open question 4. diff --git a/plans/reports/research-brainstorm-260818-2147-amlich-converter-improvements-report.md b/plans/reports/research-brainstorm-260818-2147-amlich-converter-improvements-report.md new file mode 100644 index 0000000..512bd2f --- /dev/null +++ b/plans/reports/research-brainstorm-260818-2147-amlich-converter-improvements-report.md @@ -0,0 +1,128 @@ +# Research + Brainstorm Report: Improving the amlich/duonglich Converter + +Date: 2026-08-18 21:47 (+07) +Scope: `internal/modules/amlich` — what improvements remain, ranked; what to explicitly reject. +Inputs: repo code + `docs/amlich-known-issues.md` (prior 400-year Meeus-vs-HND diff), 5 web lookups (2 WebSearch, 3 WebFetch). + +## Executive Summary + +Algorithm question is settled and should stay settled: the truncated Hồ Ngọc Đức port is bit-compatible +with the de-facto Vietnamese ecosystem and empirically beat a full Meeus ch.49 engine on both +historically verifiable razor-edge dates. No engine change is justified by any new evidence found. + +Remaining improvement space is small and mostly UX/honesty, not math: +(1) caveat line for the 7 razor-edge lunations 2072+, (2) leap-month ambiguity hint in `/duonglich`, +(3) optional golden-table regression hardening. Everything else researched — ΔT model update, +pre-1968 historic mode, table-driven rewrite, range extension — should be rejected; reasons below. + +## New Research Findings (2026) + +- **ΔT trend confirms doc's suspicion, changes nothing.** Earth set rotation-speed records in 2024–2025; + IERS added no leap second in 2024; ~30% chance of a first-ever *negative* leap second before 2035. + ΔT is flat-to-declining vs the polynomial's predicted growth — so the code's ΔT branch overestimates + future ΔT. But this only matters inside the already-documented razor-edge windows. See "Reject: ΔT". +- **Leap-second abolition adds a *new* far-future uncertainty.** CGPM votes Oct 2026 to replace the leap + second (possibly retiring it as early as 2027). If UTC stops tracking UT1, civil UTC+7 slowly drifts + from the astronomical time the algorithm models — same bucket as ΔT: only razor-edge relevant, + unresolvable until Vietnam's authorities say which timescale the calendar follows. +- **No official Vietnamese tables exist past ~2100.** Nothing from a state calendar bureau covering + 2072+ disputes surfaced. Hồ Ngọc Đức remains the de-facto ground truth; his site claims historic + reliability "since 1301" and notes official/astronomical calendars coincide since 1976. +- **Pre-1968 is messier than "UTC+8".** Per HND's historic-calendar page: North used UTC+8 1945–67; + South used UTC+7 until 1959 then UTC+8 1960–67; pre-1945 rests on dynastic tables (Bách trúng kinh + 1624–1799, Khâm định vạn niên thư 1554–1903). A single "UTC+8 historic mode" (open question 2 in + known-issues) would be *wrong for South Vietnam 1955–59* — the idea is even weaker than documented. +- **Go ecosystem check.** Other Go ports (hungtrd/amlich, buichuongvnua/amlich, go-dyn/vcalendar) are + straight HND ports without the day-0 overshoot fix or input validation this repo already has. + Nothing to borrow; this implementation is ahead of them. + +## Brainstorm: Evaluated Approaches + +### Recommend — small, honest, non-breaking + +**1. Razor-edge caveat in bot replies** (resolves known-issues open question 1) +- What: hardcode the 7 disputed month-boundary JDs (09/12/2072, 15/11/2077, 07/05/2130, 26/05/2150, + 17/05/2159, 22/01/2175, 26/01/2199). When a conversion's month start or next-month start is one of + them, append one line: result near a disputed lunar-month boundary, may differ ±1 day from future + official tables. +- Affects ~413 of 146,097 days; zero risk to correct output; converts a silent known-wrongness into + stated uncertainty. Cost: a small table + one condition + tests. +- Trade-off: nobody realistically queries 2130 from a Telegram bot — pure-YAGNI reading says skip. + But the cost is ~30 LOC and it closes a documented open question permanently. + +**2. Leap-month ambiguity hint in `/duonglich`** (fixes the "correct output reported as bug" item) +- What: when the resolved (possibly defaulted) month equals that lunar year's leap month and no + `nhuan` flag was given, append a hint: "Năm nay có tháng X nhuận — thêm 'nhuan' nếu ý bạn là + tháng nhuận." Detection is one `getLeapMonthOffset` call on the already-computed a11. +- Alternatives considered: (a) reply with *both* conversions — noisier, two answers where user wants + one; (b) leave as-is — keeps a documented user-confusion source. Hint is the KISS winner: + single authoritative answer + self-service disambiguation. + +**3. Golden-table regression corpus** (optional hardening) +- What: generate once, from the current verified engine, a compact per-year record (leap-month index + + 12/13 month lengths) for all 400 years; commit as testdata; test decodes and compares. +- Why round-trip isn't enough: `TestSolarLunarRoundTrip` proves *self-consistency*; a future change + could shift a month boundary consistently in both directions and pass. `knownDates` + + `TestLeapMonthTable` pin samples only. A golden table freezes the full verified behavior and makes + any future engine experiment a reviewable one-file diff. +- Cost: ~1 generator run + ~4 KB testdata + one test. Verdict: worth it, do alongside #1. + +### Reject — with reasons pinned + +**ΔT model update.** New IERS data makes the polynomial *more* wrong, yet updating it is still wrong to +do: the module's value is bit-compatibility with the reference algorithm every Vietnamese app runs. +A "better" ΔT flips razor-edge dates away from ecosystem consensus → user-visible mismatches +reported as bugs, with no authority to say we're right. Revisit only if official 2072+ tables appear +(known-issues open question 4). Approach #1 (caveat) is the correct treatment of this uncertainty. + +**Pre-1968 historic mode.** Ground truth fragments by government (North/South differ 1955–67, dynastic +before 1945); a correct implementation is a research project, not a module feature; the proleptic +astronomical calendar is what every reference source shows for those years anyway. Current behavior +already matches the published record on both verifiable disputes. Keep documented, don't build. +This *strengthens* the known-issues answer to open question 2: even a user-reported mismatch should +trigger a doc note, not a UTC+8 mode. + +**Table-driven rewrite.** A table must be generated from something. From this engine → just a cache of +identical output (Go float64 JD math has ~4.5e-10-day ulp vs 0.0014-day decision margins; no +platform-flip risk to cache away). From official tables → they don't exist past ~2100. Table-driven +is how you'd start from scratch; with a verified engine it adds a second representation to keep in +sync (DRY violation) for zero accuracy. The useful 20% of this idea is #3 (table as *test* data). + +**Range extension beyond 1800–2199.** Bound exists because published references stop there; claims +outside are unverifiable. Nothing found changes that. + +**Feature creep** (ngày can-chi, tiết khí, giờ hoàng đạo, holiday lookup): out of scope until a user +asks. Noted so it isn't re-brainstormed. + +## Success Criteria (if #1–#3 are implemented) + +- All existing tests pass unchanged, incl. `knownDates` pins (20/6/1944, 7/7/1967). +- Caveat appears for a 2072+ razor-edge date, absent for ordinary dates (both directions of conversion). +- `/duonglich 5/5/2028` (leap-5 year) shows hint; `/duonglich 5/5/2028 nhuan` and non-leap years don't. +- Golden table regenerated from HEAD is byte-identical to committed testdata. + +## Next Steps + +1. Decide which of #1/#2/#3 to implement (recommendation: all three; #2 first — most user-visible). +2. `/ck:plan` with this report as context if proceeding; scope is small enough for a single phase. +3. Update `docs/amlich-known-issues.md` open questions 1–3 with the resolutions above once implemented. + +## Sources + +- [Hồ Ngọc Đức — Vietnamese lunar calendar](https://www.xemamlich.uhm.vn/vncal_en.html) +- [Hồ Ngọc Đức — Historic Vietnamese lunar calendar](https://www.xemamlich.uhm.vn/histcal.html) +- [Vietnamese calendar — Wikipedia](https://en.wikipedia.org/wiki/Vietnamese_calendar) +- [IERS: no leap second in 2024 — DCD](https://www.datacenterdynamics.com/en/news/no-leap-seconds-added-to-universal-time-in-2024-iers-says/) +- [Earth rotation records spur Oct 2026 CGPM vote — TechTimes](https://www.techtimes.com/articles/320185/20260711/earth-rotation-records-spur-october-vote-avert-negative-leap-second.htm) +- [Negative leap second outlook — timeanddate](https://www.timeanddate.com/time/negative-leap-second-maybe.html) +- [Earth rotation acceleration analysis — Astronomy Reports 2024](https://arxiv.org/html/2404.06343v3) +- Go ports surveyed: [hungtrd/amlich](https://github.com/hungtrd/amlich), [buichuongvnua/amlich](https://pkg.go.dev/github.com/buichuongvnua/amlich), [go-dyn/vcalendar](https://pkg.go.dev/github.com/go-dyn/vcalendar) + +## Unresolved Questions + +1. Caveat wording/threshold for #1: flag only the two months touching a disputed boundary (proposed), + or the whole lunar year? Proposed: months only — year-wide is alarmist. +2. Should the leap-month hint (#2) also fire when month+`nhuan` *was* given but the defaulted year + was filled in (user may have meant a different year)? Proposed: no — over-engineering. +3. If leap seconds are abolished (Oct 2026 vote), does Vietnam's calendar follow civil UTC+7 or + UT1+7? Unanswerable today; park with known-issues open question 4.