Compare commits

..

78 Commits

Author SHA1 Message Date
Jesse_Chen 901cdb96ef fix(rectification): expand quality-gate pytest globs before argv
Independent Staging Quality Gate / validate (pull_request) Successful in 13m37s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
subprocess.run does not shell-expand tests/test_rectification_*.py, so a
string pin could stay green while the suite never ran. Expand glob targets
to real files and fail closed on zero matches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 19:13:38 +08:00
Jesse_Chen c48a965640 fix(rectification): close round A/2 tail gaps in tests, CI, and stale-score reuse
Independent Staging Quality Gate / validate (pull_request) Failing after 6m13s
Independent Staging Quality Gate / publish (pull_request) Has been skipped
Window_scan assertions now match the public from_sign/to_sign contract, the
staging quick gate runs the rectification Python suite, and compare-candidates
rescores when stored policy lags the live engine identity.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 18:17:33 +08:00
Jesse_Chen 26ea3f06cb docs(rectification): add round A/2 tail-work task brief
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
存量断言修复(window_scan from_sign/to_sign)、CI 漏跑 Python 测试
排查补跑、真实环境点测单、存量快照按新引擎重算的触达方案。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-01 09:58:06 +00:00
Jesse_Chen 8743dcb105 fix(rectification): separate adjacent minutes with transition proximity
Independent Staging Quality Gate / validate (push) Successful in 11m56s
Independent Staging Quality Gate / publish (push) Has been cancelled
Day-level events now score Vimshottari/Narayana transition closeness so
nearby candidate minutes can diverge, with gated quality probes and
answer-prior ranking so high-base-rate existence questions stay out.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 17:41:06 +08:00
Jesse_Chen 20df8c6022 fix(rectification): let engine-ready cases adopt a representative minute
Independent Staging Quality Gate / validate (push) Successful in 10m34s
Independent Staging Quality Gate / publish (push) Successful in 13m34s
deliveryCapability no longer ties adoption to minute separation or holdout;
those stay on the exact-minute confirmation gate so users can save a range.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 16:52:01 +08:00
Jesse_Chen de84f06940 docs(rectification): add round-2 engine convergence task brief
Independent Staging Quality Gate / validate (push) Successful in 10m2s
Independent Staging Quality Gate / publish (push) Failing after 11m19s
相邻分钟可区分:换运日期贴近度打分(P0)、known_event_quality
受门控升级为区分题、基础率校正的期望信息增益、簇内不可分时
的前瞻验证登记。依赖 Round A(provisional-adopt)先行合入。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-01 08:17:39 +00:00
Jesse_Chen 422fc65b22 docs(rectification): add provisional-adopt task brief
Independent Staging Quality Gate / validate (push) Successful in 15m33s
Independent Staging Quality Gate / publish (push) Has been cancelled
采用门对齐引擎语义:provisional 采用成为一等成功出口。
记录三权威冲突根因、经授权推翻的旧红线、以及本地 1993
分钟级收敛系答案泄漏(不构成 web 端目标)的核查结论。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-01 08:01:25 +00:00
Jesse_Chen 75fc456d7e fix(rectification): drop duplicate collect cards and false run_failed
Independent Staging Quality Gate / validate (push) Successful in 10m23s
Independent Staging Quality Gate / publish (push) Failing after 8m59s
Spoken collect no longer renders a second visual prompt; choice legends stay screen-reader only and live cards share the assistant inset. Exhaustion collect avoids colliding with the opening question id, and a successful billed turn no longer surfaces run_failed after the exit gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 15:19:00 +08:00
Jesse_Chen e404b6f42b fix(rectification): guarantee nonterminal turn exits
Independent Staging Quality Gate / validate (push) Successful in 13m25s
Independent Staging Quality Gate / publish (push) Successful in 9m26s
2026-09-01 13:35:51 +08:00
Jesse_Chen 15877069fc docs(rectification): add non-terminal exit task brief
Independent Staging Quality Gate / validate (push) Successful in 13m36s
Independent Staging Quality Gate / publish (push) Successful in 10m51s
The previous pass closed the "adoptable on one event" hole but left no
forward path: case 645ba774 ends with can_adopt false and
current_question null, so the user is stuck with neither a result nor a
next question. Two causes: answer_choice never reaches
ensureNonTerminalTurnExit or persistNextInterviewIfIdle, all of whose
call sites sit in the message/opening branches; and once every probe is
answered or dropped, decideRectification returns offer_provisional_range
before the holdout branch, ignoring unused oos_blind prompts and the
nakshatra boundary question.

Same defect class as BUG-456, which was fixed by patching one branch
rather than gating every exit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-31 19:18:25 +00:00
Jesse_Chen 857f9dc9b9 docs(rectification): record skill bump identity pitfalls
Independent Staging Quality Gate / validate (push) Successful in 15m49s
Independent Staging Quality Gate / publish (push) Successful in 10m28s
The registry packagePath points at versions/<version>/, but the root
SKILL.md copy makes it easy to hash the package root instead. Hashing
the root pulls the entire versions/ subtree into the digest and still
returns a value, so the mistake surfaces only as a failing gate later.
Records BUG-459 and corrects the task brief's D-2 table, which listed
the root SKILL.md as the hash target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-31 18:25:32 +00:00
Jesse_Chen 4b1331776d fix(rectification): fail closed UI and question ownership
Independent Staging Quality Gate / validate (push) Successful in 12m32s
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-09-01 02:06:53 +08:00
Jesse_Chen 9f011194f2 fix(rectification): centralize delivery authority
Independent Staging Quality Gate / validate (push) Successful in 13m0s
Independent Staging Quality Gate / publish (push) Successful in 10m58s
2026-09-01 01:13:04 +08:00
Jesse_Chen 9aebd0474a docs(rectification): add decision-authority refactor task brief
Independent Staging Quality Gate / validate (push) Successful in 36m36s
Independent Staging Quality Gate / publish (push) Failing after 15m31s
The engine receipt says acceptance_allowed/selection_allowed false with
reasons insufficient_events and insufficient_domain_diversity, yet the
top-level projection flips both to true and renders adoptable candidates
after a single piece of evidence. Records the four structural causes:
dual decision authority where overlayPublicDecision lets TS override the
engine, insufficient-evidence sharing an exit with exhausted, capability
flags hardcoded across eleven branches, and several fail-open paths
including holdout "unavailable" granting adoption.

Also folds in the skill bump (D-2) after verifying that existing cases
bind their own skill_version and deprecated packages still resolve, so
no case migration is required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-31 14:37:26 +00:00
Jesse_Chen 67101f0397 test(billing): make renewal timing deterministic
Independent Staging Quality Gate / validate (push) Successful in 14m5s
Independent Staging Quality Gate / publish (push) Successful in 48m2s
2026-08-31 20:49:59 +08:00
Jesse_Chen afb8305ca1 fix admin feature pricing visibility
Independent Staging Quality Gate / validate (push) Failing after 26m23s
Independent Staging Quality Gate / publish (push) Has been skipped
2026-08-31 19:57:08 +08:00
Jesse_Chen 47b4b06bfb fix(rectification): surface pricing failures and persist opening question
Independent Staging Quality Gate / validate (push) Successful in 12m42s
Independent Staging Quality Gate / publish (push) Successful in 22m34s
2026-08-31 15:16:23 +08:00
Jesse_Chen 29e331bc0d docs(rectification): add billing failure and question-slot task brief
Independent Staging Quality Gate / validate (push) Successful in 9m55s
Independent Staging Quality Gate / publish (push) Successful in 11m16s
Traces the staging run_failed on the first user message to a missing
feature_pricing row: the opening turn is free and skips reserve(), so
the first `message` action is the first request to hit
resolve_feature_pricing, which raises feature_pricing_missing and maps
to the generic run_failed fallback. Also records the independent
opening-turn defect that leaves current_question null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-31 06:05:50 +00:00
Jesse_Chen 9492bae15f docs: record round-1 audit and open round-2 brief
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 9m32s
Verified both round-1 briefs against the code rather than the progress
notes. Billing 0/1/2/3/6 and rectification 0/1/2/3/6 are in and clean, and
neither the confirmation gate nor the sealed holdout was loosened.

Two gaps remain. Prompt caching only emits its marker for Anthropic, so on
a DeepSeek-class provider it buys observability and no cost reduction, and
the membership fair-use numbers are untouched — correctly so, since no
real unit cost has been measured yet. The round-2 brief gates those on a
staging measurement pass and pulls the per-answer narrowing table forward,
since it is the deliverable and depends on nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-31 05:00:55 +00:00
Jesse_Chen 3a4396a4b7 fix(rectification): adopt evidence-based stop semantics
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-08-31 12:50:30 +08:00
Jesse_Chen f8aea88bc3 docs(rectification): archive session records and add delivery-gap tasks
Independent Staging Quality Gate / validate (push) Successful in 11m54s
Independent Staging Quality Gate / publish (push) Successful in 11m1s
Two real local sessions are archived. In them the agent states twice that
its minute came from a report already in the upstream repo rather than
from the user's answers, and the follow-up "fix" wired that answer into
the scoring chain; upstream now carries a Narayana tie-break derived from
that single case. This repo was checked and is clean, so two red lines now
keep it that way and gate any upstream sync.

The same records show what the answers genuinely bought: a 30-minute
window narrowed to 3. Tasks 7-10 add what makes that deliverable — the
per-answer narrowing table, an explain surface, mid-case window changes
with evidence retained, and the batched first round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-31 03:05:11 +00:00
Jesse_Chen 6abe2a5899 test: sync capability audit route contract
Independent Staging Quality Gate / validate (push) Successful in 10m27s
Independent Staging Quality Gate / publish (push) Successful in 8m59s
2026-08-31 10:36:38 +08:00
Jesse_Chen 30ef6afea7 docs(rectification): archive upstream playbook and add stop-rule task
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
The upstream interview playbook and evidence thresholds resolve task 0.
Upstream stops and reports when evidence is thin — fewer than three dated
events, fewer than two domains, or a tie — while this repo treats the same
kind of thresholds as a confirmation gate and keeps asking when they are
not met. Its label ladder has no confirmed rung at all, and every contract
test asserts candidate_range_not_birth_time_truth.

Task 1 capped the interview by round count, which is half of it. Task 6
adds the evidence-state stop rules, reframes exhausted as a normal
delivery, adopts the upstream label ladder and closing wording, and keeps
the 4/3 confirmation gate separate from the 3/2 delivery floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-31 02:27:32 +00:00
Jesse_Chen 53450f5bf3 Merge remote-tracking branch 'origin/staging' into codex/billing-pricing-20260830
Independent Staging Quality Gate / validate (push) Failing after 8m27s
Independent Staging Quality Gate / publish (push) Has been skipped
2026-08-31 10:00:36 +08:00
Jesse_Chen aafcef06df docs(billing): define membership fair-use semantics 2026-08-31 10:00:03 +08:00
Jesse_Chen 3a762e1fca feat(billing): record prompt cache usage 2026-08-31 09:59:53 +08:00
Jesse_Chen e8bd3a5208 feat(rectification): expose candidate result reports
Independent Staging Quality Gate / validate (push) Successful in 16m46s
Independent Staging Quality Gate / publish (push) Successful in 19m37s
2026-08-31 05:52:38 +08:00
Jesse_Chen 6cbf1f22e2 fix(rectification): separate question slots from model prose 2026-08-31 05:50:28 +08:00
Jesse_Chen 86ba17ee18 fix(rectification): enforce persisted convergence budgets 2026-08-31 05:50:17 +08:00
Jesse_Chen 79304c31fc fix(rectification): keep model confirmations separate from question slots 2026-08-31 04:50:39 +08:00
Jesse_Chen a499c3444e feat(admin): add read-only pricing simulator 2026-08-31 04:30:40 +08:00
Jesse_Chen 6e13949d3b feat(billing): resolve chat and rectification pricing by feature 2026-08-31 04:29:45 +08:00
Jesse_Chen 8edae211ff test(billing): cover durable report settlement 2026-08-31 04:29:38 +08:00
Jesse_Chen a73d996f65 feat(billing): charge personal reports with durable settlement 2026-08-31 04:08:20 +08:00
Jesse_Chen aaaa5fb73c feat(billing): add feature pricing configuration 2026-08-31 03:37:44 +08:00
Jesse_Chen db6716e76c docs(rectification): rewrite convergence brief after upstream comparison
Independent Staging Quality Gate / validate (push) Successful in 9m29s
Independent Staging Quality Gate / publish (push) Successful in 20m8s
Comparing against the upstream skill repo overturned v1's premise. The
three methodology files are byte-identical across both repos, and the
production v5 scorer imports the same upstream event engine the v4 ranker
does, so swapping engines is off the table and v1's three-way bake-off is
dropped.

What productization actually dropped is the stopping mechanism: upstream
terminates on a fixed 8-question bank and a one-shot adjudication, while
the production decision path discriminates as long as a probe exists. The
round budget is computed but never read by that path, and the "exhausted"
branch it needs already exists. Tasks now wire that budget in, restore the
upstream next-step summary, and keep the report readable from turn zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-30 19:35:52 +00:00
Jesse_Chen da4c03a857 feat(billing): add usage cost aggregation report 2026-08-31 03:06:15 +08:00
Jesse_Chen 7db2dd2de0 docs(rectification): add convergence refactor task brief
Independent Staging Quality Gate / validate (push) Successful in 11m4s
Independent Staging Quality Gate / publish (push) Successful in 9m2s
Audit found three unrelated rectification scoring paths: the local-agent
methodology references, the production v5 server scorer, and the v4 fact
ranker that only the eval scripts import. The sealed holdout numbers
measure the third one, so neither the production path nor the local path
has ever been evaluated.

Task 0 is therefore a blind three-way bake-off on one case set, gating
every later task. The remaining tasks cover a single convergence function
with property tests, moving question authority fully to the server, making
the deliverable exist from turn zero, and the calibration data shortage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-30 18:45:30 +00:00
Jesse_Chen e7226bbfb5 docs(billing): add pricing and billing closure task brief
Independent Staging Quality Gate / validate (push) Successful in 9m25s
Independent Staging Quality Gate / publish (push) Successful in 9m9s
Records the billing gaps found by reading the live code paths: report.full
has no authorizeUsage call at all, rectification charges model.creditCost
once per case, and pricing is bound to the model rather than the feature.

Seven tasks, with task 0 as a measurement gate so no price lands in the
database from an estimate. Task 6 adds an admin pricing simulator next to
the model config page so margins stay computable against live data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-30 18:24:01 +00:00
Jesse_Chen ff5998f184 fix(rectification): render adoption carrier on deterministic choice path
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-08-31 02:15:12 +08:00
Jesse_Chen 79bfa73da3 fix(rectification): prevent silent collect focus stalls
Independent Staging Quality Gate / validate (push) Successful in 20m38s
Independent Staging Quality Gate / publish (push) Successful in 24m14s
2026-08-30 23:08:10 +08:00
Jesse_Chen 0ae3e2d796 feat(reports): generate personal reports by section
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-08-30 22:59:17 +08:00
Jesse_Chen 1d1c1d3d24 docs(reports): record task 0 staging observation 2026-08-30 20:57:56 +08:00
Jesse_Chen 37c0c13965 Merge remote-tracking branch 'origin/staging' into staging
Independent Staging Quality Gate / validate (push) Successful in 10m45s
Independent Staging Quality Gate / publish (push) Successful in 21m49s
2026-08-30 20:11:12 +08:00
Jesse_Chen 82f30880e4 chore(staging): retrigger release gate 2026-08-30 20:10:53 +08:00
Jesse_Chen 1f1fdb1d15 feat(reports): record personal report generation telemetry
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-08-30 20:10:10 +08:00
Jesse_Chen f9e9b03a7e Merge remote-tracking branch 'origin/staging' into staging
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
2026-08-30 19:50:23 +08:00
Jesse_Chen 40684eec6d fix(rectification): prevent collect focus dead-end after choice answers 2026-08-30 19:47:42 +08:00
Jesse_Chen a96df75473 fix(frontend): stop the personal report rendering white on white in dark mode
Independent Staging Quality Gate / validate (push) Successful in 10m50s
Independent Staging Quality Gate / publish (push) Has been cancelled
.personal-report-reader declared --report-paper, --report-rule and
--report-accent on itself. A custom property resolves from the nearest
element that declares it, so an element-local declaration beats :root
regardless of media queries or source order — the dark override added
with the theme was dead the moment it was written.

The inks did follow the theme, because they live on :root. So dark mode
put --color-ink (#f2f0ea) on paper that stayed #f8f5ee: 1.05:1 for
headings and 1.22:1 for body. The report was effectively invisible.

Printing had the same defect from the other side: the print block forces
white backgrounds but never reset the inks, so printing from a dark
screen produced a blank page. It now pins the palette back to light —
paper is always paper.

Every report ink now clears AA on report paper in both themes, and the
new contract test forbids re-declaring the report palette anywhere but
:root, which is the trap that caused this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-30 11:29:58 +00:00
Jesse_Chen 85b14b13ae fix(rectification): close non-converging range offer without an exit
Independent Staging Quality Gate / validate (push) Successful in 11m3s
Independent Staging Quality Gate / publish (push) Successful in 8m48s
When coverage still blocks adopt and no renderable discriminator remains, narrate the numeric range and persist a spoken collect instead of promising a time card that cannot be clicked. Recompute probe information gain on active candidates and drop zero-split probes explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 18:35:16 +08:00
Jesse_Chen 79f65ac8b0 fix(frontend): restore staging quality gate contracts
Independent Staging Quality Gate / validate (push) Successful in 10m34s
Independent Staging Quality Gate / publish (push) Successful in 10m11s
2026-08-30 17:01:01 +08:00
Jesse_Chen a671986a1d fix(frontend): complete settings MVP structure
Independent Staging Quality Gate / validate (push) Failing after 10m21s
Independent Staging Quality Gate / publish (push) Has been skipped
2026-08-30 16:32:58 +08:00
Jesse_Chen f5e6d51646 feat(frontend): bind sessions to chart profiles 2026-08-30 15:55:45 +08:00
Jesse_Chen 5c1d095f69 feat(frontend): use offline global birth place hierarchy 2026-08-30 15:55:45 +08:00
Jesse_Chen f9a00dc568 fix(frontend): preserve owner profile when switching charts 2026-08-30 15:55:45 +08:00
Jesse_Chen 796e4cf8cf feat(frontend): clarify chart profile management 2026-08-30 15:55:45 +08:00
Jesse_Chen f4fb39e0f3 feat(frontend): separate profile and chart settings 2026-08-30 15:55:45 +08:00
Jesse_Chen 52477306cc fix(rectification): server-append spoken collect stems after free-text turns
Independent Staging Quality Gate / validate (push) Successful in 10m43s
Independent Staging Quality Gate / publish (push) Successful in 9m8s
Choice path already wrote spokenFollowupForUser into the body; free-text
dropped that stem and only filled empty answers, so a new collect_spoken
focus stayed invisible after “记下了”.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 15:49:20 +08:00
Jesse_Chen 04b2ccdbe2 feat(rectification): expand dated dasha_boundary probe supply
Independent Staging Quality Gate / validate (push) Successful in 12m41s
Independent Staging Quality Gate / publish (push) Successful in 9m33s
Distinguish-stage reverse-verify questions were exhausting after 1–3
dated probes. Union boundary windows across representative pairs, keep
multiple years per domain, raise the public cap, and allow activation
fallback without relaxing MIN_BOUNDARY_DAYS or scoring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 11:46:30 +08:00
Jesse_Chen e8fa1ce4ea fix(frontend): raise dark contrast, expand hit areas, and surface input limits
Independent Staging Quality Gate / validate (push) Successful in 9m19s
Independent Staging Quality Gate / publish (push) Successful in 9m54s
Dark muted surfaces missed WCAG AA; action and tertiary tokens plus a 32-pair contract close that. Hit targets, remaining-count, and Enter-to-send follow the interaction audit without changing visual sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 11:19:50 +08:00
Jesse_Chen b43808b016 fix(rectification): stop yearless ungrounded varga contrast from minting cards
Independent Staging Quality Gate / validate (push) Successful in 9m10s
Independent Staging Quality Gate / publish (push) Successful in 8m43s
Only sign-bound varga_style questions may omit a concrete period. Remaining-layer existence and quality probes now drop as yearless_ungrounded_contrast instead of scoring by group order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 10:55:11 +08:00
Jesse_Chen 95f5851261 fix(rectification): rewrite D9/D10 style options and stem spacing
Independent Staging Quality Gate / validate (push) Successful in 9m28s
Independent Staging Quality Gate / publish (push) Successful in 7m26s
Choice cards were showing type-table tokens as options, dating long-term style questions, and stacking the stem on the first option because legend is outside the fieldset grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 10:01:32 +08:00
Jesse_Chen ea0fbd4406 fix(rectification): ask spoken collect questions in the agent body
Independent Staging Quality Gate / validate (push) Successful in 9m32s
Independent Staging Quality Gate / publish (push) Successful in 6m53s
The standalone collect prompt bar reused the choice-card chrome and
duplicated the question. Visibility now comes from the spoken reply,
with an empty-body fallback that posts the persisted focus prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:39:47 +08:00
Jesse_Chen 02d4c91626 fix(rectification): stop occupation coverage from locking questions and the range exit
Independent Staging Quality Gate / validate (push) Successful in 11m40s
Independent Staging Quality Gate / publish (push) Successful in 9m36s
Occupation answers were stored as career, so coverage never closed, yearless
cards never fired, and the decision layer suppressed an engine-allowed range.
Normalize occupation-collect writes, allow yearless cards once training is
open, offer a range when no distinguish card remains, and align the public
house table to the representative minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 03:05:28 +08:00
Jesse_Chen f5c02924c9 fix(rectification): show spoken collect questions in the chat UI
Independent Staging Quality Gate / validate (push) Successful in 10m32s
Independent Staging Quality Gate / publish (push) Successful in 8m46s
Spoken collect prompts lived only in GET current_question. The chat never
parsed that field, Agent projections returned null after evidence writes,
and active_focus followups collapsed the questionId. Render the parsed
prompt, keep choiceReady on real cards, and give collect focuses a stable
domain-scoped id.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 00:51:14 +08:00
Jesse_Chen 3a0319694e fix(rectification): keep same-sentence dated events after structured answers
Independent Staging Quality Gate / validate (push) Successful in 13m7s
Independent Staging Quality Gate / publish (push) Successful in 29m19s
The choice and collect fast paths applied the answer then returned,
so a dated event in the same utterance never reached the evidence
ledger. Idle persist also prechecked follow-up with a hardcoded
collect_evidence outcome instead of the dossier decision.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 23:18:07 +08:00
Jesse_Chen 078c1cdc0d fix(rectification): keep collection denials from stalling the interview
Independent Staging Quality Gate / validate (push) Successful in 23m37s
Independent Staging Quality Gate / publish (push) Has been cancelled
A spoken no neither scored nor declined coverage, so relatives never closed and free-text turns left current_question null. Prefer a same-domain yearless scoring card before an unscoreable collect, resolve explicit collect denials as declined, and persist the next followup after an idle agent turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 22:37:32 +08:00
Jesse_Chen 187ef6ae24 feat(frontend): put a light/dark/system control in the account menu
Independent Staging Quality Gate / validate (push) Successful in 10m4s
Independent Staging Quality Gate / publish (push) Successful in 24m59s
Appearance is a radio group inside the avatar menu rather than a loose
button, so arrow keys reach it and the current choice is announced.
Picking one keeps the menu open, so the change is visible where it was
made.

"跟随系统" removes data-theme instead of writing a third value — the
media query has nothing to match otherwise. A synchronous script at the
top of <head> re-applies a pinned choice before the first paint; going
through next/script with any strategy would defer it and bring the flash
straight back, so the test asserts a plain script tag. Blocked storage
degrades to following the OS instead of throwing.

The stored value is browser state, so it is read through
useSyncExternalStore rather than synced into React state in an effect,
which also trips the cascading-render lint rule. localStorage only fires
`storage` in other tabs, so a same-tab write notifies its own listeners
and every open tab stays in step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-29 12:12:06 +00:00
Jesse_Chen c2d705ef6e feat(frontend): add a warm dark theme and close the DESIGN.md drift
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
The palette is restated for dark rather than inverted: elevation reads
through lightness on dark and through darkness on light, so the floor is
the darkest surface here and the second-lightest there. The clay hue is
kept and lifted, because #85432f is 2.1:1 on a dark ground. All 37
themeable tokens are covered, in an OS-preference block and a data-theme
block that a contract test keeps identical, and ink, action, danger,
success and warning are asserted at 4.5:1 against the dark canvas.

Four raw colors that would have stayed light-theme values are tokenised
(the avatar hairline, the sheen sweep, a one-off shadow, a literal
warning hex). The QR keeps literal white in both themes, since scanners
need light modules to be light, and print keeps white paper.

The four root boundary pages cannot read a token, so they restate the
handful they need in both themes. forbidden.tsx also stops painting a
bespoke near-black page in four colours that appear nowhere in the
palette, which broke the rule that dark ink is never a page-scale
surface.

Also fixes what the audit found in DESIGN.md itself: two ink values that
had drifted from the code, a motion tier documented at 360ms that was
never implemented, a breakpoint section claiming three tiers where the
stylesheet has eleven, an undocumented report-paper palette, and an admin
section describing a bespoke panel that antd + Refine replaced. Five
zero-reference admin rules go with it.

The sidebar gets the accent, opaque drawer, heading rank and empty-state
guidance settled earlier, and fenced code blocks finally get a container.

BUG-439.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-29 12:01:50 +00:00
Jesse_Chen d02fa8bbf3 fix(frontend): left-align the sidebar nav items on the mobile drawer
Independent Staging Quality Gate / validate (push) Successful in 11m52s
Independent Staging Quality Gate / publish (push) Successful in 16m19s
The nav buttons defaulted to centered for the desktop icon rail and were
flipped back by `[data-state="expanded"]`. That attribute tracks the
desktop open state, while the mobile drawer runs off `openMobile`, so on
a phone the labels rendered but the override never matched: 新建对话 and
我的报告 floated mid-drawer while every other row sat flush left.

Left alignment is the base layer now, correct for both the drawer and the
expanded desktop sidebar, and centering moved into the >=768px collapsed
block beside the rest of the rail rules. That was the only top-level
`[data-state=...]` selector left in the stylesheet.

personal-report-entry asserted `justify-content: center` on the report
button, which is the defect itself; the value is updated and the rest of
that assertion stands.

BUG-438.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-29 11:22:39 +00:00
Jesse_Chen 3cecb25f42 fix(rectification): stop a stale candidate-set prefix from hiding the choice card
Independent Staging Quality Gate / validate (push) Successful in 11m31s
Independent Staging Quality Gate / publish (push) Successful in 14m41s
Scoring scoped the contrast packet to the previous round's candidate set while
minting probes for the new one, so every later read re-prefixed the stored hash
and the persisted focus schema could never match. Mint one prefix per set and
compare splits by probe identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-08-29 08:51:19 +00:00
Jesse_Chen 0044d4f358 fix(frontend): style the paywall header and guard undefined class names
Independent Staging Quality Gate / validate (push) Successful in 10m8s
Independent Staging Quality Gate / publish (push) Successful in 9m39s
The onboarding paywall wrote `dialog-header`, a class no stylesheet
defines, so its close button stacked under the title instead of sitting
beside it. The session-delete confirm buttons had the same problem, and a
`message-entry` wrapper added in August had quietly killed the
`.message + .message` spacing rule in the main chat.

Fixes all three against the patterns already in the codebase, then adds a
contract test so the next undefined class fails instead of shipping: it
scopes itself to our own class families so Tailwind utilities stay out,
strips CSS comments before deciding what counts as defined, and carries
an allowlist of the fifteen deliberate no-op modifiers that must shrink
rather than grow.

Also collapses the paywall's duplicate display heading into the intro
sentence, leaving one heading in the dialog.

BUG-434, BUG-435, BUG-436.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-29 05:40:29 +00:00
Jesse_Chen c960691ee5 fix(rectification): pass birthDate into the choice-path gate
Independent Staging Quality Gate / validate (push) Successful in 10m27s
Independent Staging Quality Gate / publish (push) Successful in 8m35s
The post-choice decision used a looser plan than the followup builder.
Load compute once in persistApplied and feed the same birthDate to both.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 12:57:10 +08:00
Jesse_Chen 57e5f92d40 fix(rectification): keep spoken collect from silencing the agent
Independent Staging Quality Gate / validate (push) Successful in 22m22s
Independent Staging Quality Gate / publish (push) Successful in 10m14s
Collect focus was leaking discriminator probes into the agent prompt and
inheriting choice identity. Mark spoken questions, hide current_probe
unless a real choice card exists, strip fallback identity, log persist
failures, and align the decision probe with the plan including birthDate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 11:47:57 +08:00
Jesse_Chen a50c62a66c fix(frontend): restore root 404 styles and unify inline wait spinners
Independent Staging Quality Gate / validate (push) Successful in 9m45s
Independent Staging Quality Gate / publish (push) Successful in 9m4s
Root error/not-found pages lost all CSS after globals left the shared
layout. Inline the same token fallbacks as global-error so admin stays
off the chat stylesheet. Collapse three LoaderCircle spins into one
InlineSpinner, and build greeting fixtures in local time so UTC CI
does not hit the night-time copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 10:56:04 +08:00
Jesse_Chen 86d6923e6e fix(rectification): stop discriminator followup from dropping user evidence
Independent Staging Quality Gate / validate (push) Successful in 10m10s
Independent Staging Quality Gate / publish (push) Successful in 7m25s
Decision and question ranking now share contrast option completion, so a
missing style card cannot deadlock the interview with a dead-end reply.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 10:23:34 +08:00
Jesse_Chen 31b54aa71d fix(frontend): say the database is missing, not Supabase
Independent Staging Quality Gate / validate (push) Successful in 9m34s
Independent Staging Quality Gate / publish (push) Successful in 7m21s
Self-hosted PostgreSQL is the runtime. A missing APP_DATABASE_URL
must not tell users the retired hosted service is unconfigured.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 09:44:53 +08:00
Jesse_Chen 3198fb6b2b perf(frontend): split chat streaming, load Inter, isolate admin CSS
Independent Staging Quality Gate / validate (push) Successful in 10m14s
Independent Staging Quality Gate / publish (push) Successful in 11m5s
Settled messages no longer rebuild on every token, Inter is actually
requested, and admin routes drop the 33 KB chat stylesheet. Root
force-dynamic is gone so public shells can prerender without changing
the no-store Cache-Control contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 03:39:43 +08:00
503 changed files with 25049 additions and 2068 deletions
+21
View File
@@ -1,9 +1,24 @@
# BLOCKED
## 客户端交互收尾(2026-08-30,分支 `codex/interaction-20260830`
- **`.message-actions button` 不能做到 44×44。** 四个 26×26 按钮,`gap: 1px`,中心距 27px;下方 follow-up 芯片只有 8px 底边距。44×44 伪元素会水平重叠 17px,并在有 follow-up 时垂直吃掉 1px 点击。热区做到不重叠的最大值 **27×34**(水平吃掉 1px 缝,垂直用掉 4px 底边距)。距 44×44 还差 17px 宽、10px 高。把 `gap` 拉到 18px 才能水平到 44,但那会改变视觉留白,本轮红线禁止。
- **`.chart-nav-chip``.auth-links button` 停在 40px。** 两者视觉高度 32px,容器 `gap: 8px`chip 还会换行)。`inset: -4px` 把热区扩到 40px,刚好填满间距、互不重叠。再扩 4px 就会在换行后上下吃点击。距 44px 还差 4px。
- 以上两处都写进 `frontend/tests/touch-target-contract.test.ts`,禁止在间距不变时把伪元素硬撑到 44px。
- **登录页当前关密码通道,`.auth-links` 不渲染。** 热区用夹具 + 注入节点量的。产品登录是 OTP,「发送验证码」已是 44px。
- **主对话计数 / 引导框中文 IME 实机未录。** 无受控登录会话,headless Chrome 无输入法。四处计数与三处 `isComposing` 由合同测试锁定。
- 真实收信端到端验收:执行环境没有可识别的 staging 测试邮箱/收件箱变量,仓库只记录发信配置而未提供受控测试邮箱。按任务硬规则不使用他人邮箱;代码、测试和部署继续,部署后的注册、验证码登录与忘记密码真实收信步骤待具备受控邮箱后补验。
- PostgreSQL 事务反向测试:当前执行环境没有 `docker``postgres``initdb``psql`、Podman/Colima/Lima。`frontend/tests/admin-database.test.ts` 已实现审计触发器故意失败并断言兑换码行数仍为 0 的红灯证据,但本地执行在启动 fixture 前以 `spawnSync docker ENOENT` 阻塞;交由 exact-SHA staging quality gate 的 Docker 环境运行。全量 `npm test` 因同一缺失 Docker 共阻塞 11 项数据库/部署测试,另有 1 项既有真实 DOM 测试因缺 Playwright headless Chromium 阻塞;其余 1031 项通过,skipped/todo=0。
- staging 两角色浏览器冒烟:已确认受控 admin 测试账号存在且是 `user,admin`,但当前执行环境没有其密码或已登录会话;也未提供受控 viewer 账号。不得读取/猜测凭据或使用他人账号。已完成匿名 shell、5 个资源 401、写请求 401 的服务端冒烟;admin/viewer 登录后浏览器冒烟待授权人员提供受控会话后补验。
## 前端优化九条(2026-08-29,分支 `codex/frontend-optimization-20260828`
- **`web/index.html``web/rectification.html``web/evidence_packet.html` 不得按“无人引用”删除。** `scripts/jyotish_api_server.py` 仍把它们当作 `/``/evidence``/rectification` 的调试页;`tests/test_api_async_job_contract.py` 锁路径。任务书全仓检索漏了 Python API。移到 `scripts/` 必须改 `tests/**` 既有断言,本轮禁止。它们不是产品 UI,继续留在 `web/`
- **admin 已登录目视(`/admin/users|orders|roles|feature-flags`)未做。** 执行环境没有受控后台会话,与上方 staging 两角色冒烟同一缺口。本轮用 `next build` 客户端 CSS 清单代替:上述四页不再包含 globals(`message-list`)那 33.7 KB gzip,只剩 admin.css 1.8 KB + Inter `@font-face` 0.8 KB = 2.5 KB。登录后的视觉塌陷仍需人补一眼。
- **`error.tsx` / `not-found.tsx` 不能 import `site-styles`。** 它们挂在根布局段,import globals 会把聊天 CSS 重新打进每一条 admin 路由。样式用内联 `style` + fallback 色值(与 `global-error.tsx` 同一模式),不必另开布局链,也不必移动 `app/page.tsx`。见 2026-08-29 收尾与 BUG-433。
- React Compiler 仍关闭,见下方 2026-08-17 记录。本轮未重开。
## React Compiler 接管 page.tsx2026-08-17,分支 codex/react-compiler-20260817
- **~~`Home` 的编译失败原因无法定位~~ 已解除并已定位(2026-08-17,用户授权新增依赖后完成)。** 结论:`Home` 的失败全部来自上游编译器未实现的语法与内部断言失败,**没有一条是本仓库代码写错**。稳定版 `babel-plugin-react-compiler@1.0.0` 报 24 条错误、三类全带 `Todo:`(编译器标记「尚未实现」):13 条 `try/finally`、9 条 `try/catch` 内的 `throw`、2 条 `MemberExpression cannot be safely reordered`。更新的 experimental 版已修好前两类,`Home` 随即撞上两个连续的编译器内部断言失败(`Invariant:`,即编译器 bug):`page.tsx:2666``catch (error)` 绑定被闭包捕获,以及 `page.tsx:1837` 的提升函数 `refreshAccount`。诊断用的依赖已卸载、锁文件已用 `npm ci` 权威还原、`git diff` 为空,不进交付——因为它买不到任何东西,见下一条。
@@ -14,3 +29,9 @@
- **顺手活一律未做,登记在此:** 其一,`frontend/src/lib/skill-package-registry.ts:523``readFileSync(currentRegistryPath, "utf8")` 触发 Turbopack 构建警告「Dynamic filesystem access causes tracing of the whole project」,会把整个项目(含 `public/`)打进 server 产物,影响部署体积;升级前后都存在,与本轮无关,未改。其二,`npx eslint` 有 4 个既有 `no-unused-vars` warning,分别在 `birth-time-candidate-result.tsx:143``birth-time-candidate-completion.ts:10,11``tests/identity-auth-factory.test.ts:48`,未改。其三,`eslint-config-next` 仍是 16.2.10、与 next 16.3.1 版本号不同步,但 eslint 实测 0 error,按「不许顺手升别的依赖」未动。
- **测试环境噪声(非阻塞,已自行消化):** `frontend/tests/rectification-v9-database.test.ts` 的「v9 migration applies on a fresh database and re-applies idempotently」在全量并发下偶发失败(`database migration failed`,1 !== 0),单独重跑 7/7 通过、全量重跑 1592/1592 通过。靠 Docker 起临时 Postgres,判定为资源争用型 flake,与 Next 版本无关。未改任何测试文件。
- **文件名偏离:** 任务书要求新建 `PROGRESS.md`,但根目录已有受版本控制的 `progress.md`(1025 行)且本机文件系统大小写不敏感,写 `PROGRESS.md` 等于覆盖清单外的文件,故进度记录落在 `PROGRESS-react-compiler-20260817.md`
## 生时校正收敛重构任务 02026-08-31,分支 `codex/rectification-convergence-impl-20260830`
- 无法获取任务书要求的上游 `interview_playbook.md``evidence_thresholds.md`:任务书所指的 `~/.workbuddy/skills/jyotish-birth-time-rectification/` 在当前执行环境不存在,仓库内只有测试对该外部路径的引用;未伪造文件,也没有可验证的上游来源可供导入。
- 无法获取一次真实本地校正会话完整记录:当前仓库没有可证明为真实线上会话的完整原始记录,执行环境也没有受控会话/上游维护者提供的记录。因此无法可靠回答轮数、最终区间宽度、`confidence``can_apply`
- 该信息收集缺口不阻塞任务 1–3,按 v2 任务书继续实现并在进度文件中标记为未验证;不得据此声称已验证“固定题数后停止”的上游机制。
+30
View File
@@ -1,5 +1,35 @@
# 印度占星 Skill 更新日志
## 2026-08-30 — 非收敛区间提议不再是无出口死胡同(Skill 10.0.13 不变)
训练门已开、覆盖层仍挡住确认、可渲染区分题又问不出来时,助手会说「可以先按当前区间看盘,也可以再补一件记得时间的经历」,但时间卡、下一问、区间数值都不给。本轮把这句话改成与真实承载一致。
- 正文写出可信区间与代表分钟,并沿用既有「代表分钟不是已确认的唯一出生分钟」口径。
- 可执行出口落一条未用过的 family / education / finance 口述采集题(`current_question` 非 null);不放开 `canAdopt` / confirmation gate,不出时间卡。
- 带年份反推题不再因为该领域已有别的年份证据而被整域丢掉;信息量按当前活跃候选重算,对活跃集合零区分力的探针写入 `dropped_probes``no_split_among_active`)。
## 2026-08-30 — 口述采集题由服务器接在正文后(Skill 10.0.13 不变)
自由文本记下经历后,`collect_spoken` 题已经落库,但助手只说「记下了:…」,用户看不到下一问。点选路径本来就会把 `spokenFollowupForUser` 写进正文;自由文本路径把这句题干丢了,而且只在正文为空时补。
- 用本轮是否新建 active focus 决定补不补,不看正文有没有问号。
- 题干仍是普通一段,接在同一条助手消息后面;不新造容器,不复用选择卡。
- 撤回「模型必须自己把口述题问出来」,避免服务器补了模型再问一遍。
## 2026-08-30 — 反推前事探针供给扩容(Skill 10.0.13 不变)
区分阶段在收窄无年份分盘对比题之后,带年份的 `dasha_boundary` 反推题每个 case 只剩 1–3 条。本轮只加供给,不放宽判定门槛、不改计分口径。
- 边界窗改为多对代表分钟求并集(时间排序后的相邻对 + 首尾对),仍执行 `MIN_BOUNDARY_DAYS = 45`
- 同领域按 `information_gain` 保留前 N=3 条年份(去重键仍是 `(domain, year, month, source)`)。
- 公开上限 `MAX_PROBES` 提到 8:约 4–6 个领域竞争时,可让约 3 个领域各留两年并再加少量 activation,前端出题层仍按信息量全局排序。
- 采集上限仍是 `MAX_COLLECTION_PROBES = 3`,避免采集题被连带放宽。
- 即使该领域已有边界探针,只要还没到每域 N,也可以再补一条带年份的 `dasha_activation`(仍走 blocked_years / holdout / 契约校验)。
- 每域评估预算 K=8:先评均匀抽样的 K 个年月,按信息量保留;若 K 次全部未命中再扫描剩余窗直到第一击,避免漏掉唯一能区分的月份。K 与 N、MAX_PROBES 的关系:N ≤ K,公开列表再按全局信息量截到 MAX_PROBES。
- 未改 `_evaluate_contexts` / `LEVEL_P` / `match_level` / information_gain 算法,未放宽 confirmation gate,未新增「已确认唯一分钟」路径。
同一 fixture(两簇、Moon 100 vs 101、多层分盘差)实测:公开探针 3 → 7 条;三次打分中位 0.2878s → 0.0108sK 截断减少 `_evaluate_contexts` 次数)。
## v6.9.142026-06-21)—— 发布卫生、CI门禁与包产物校验
> **验证**65 techniques registry validate PASS55 covered / 10 complete / 0 partial / 0 missing);475 pytest PASSlegacy runner 102/102 PASSfrontend Vite build PASSwheel/sdist build PASStwine check PASS。
+142
View File
@@ -0,0 +1,142 @@
# 计费闭环与功能级定价进度(2026-08-30)
## 任务 0 · 真实单位成本门控
已完成只读聚合端点:`frontend/src/app/api/admin/usage/aggregate/route.ts`
- 权限:`billing.orders.read`
- 窗口:最近 30 天
- 输出:每个预留功能的 `runs``avg``p50``p95``max`,覆盖 `cost_microusd``input_tokens``output_tokens``duration_ms`
- 无数据:`hasData=false`,统计值为 `null`,不会静默伪造为 0
- `report.full`:当前账本 0 行,这是报告接入计费前的已知空缺
### 当前可见数据库观测
本机 `jyotisha-local-preview-postgres-1`(不是 staging/production)截至 2026-08-30 的最近 30 天账本:
| feature_key | runs | cost p50 | cost p95 | input p50 | output p50 | duration p50 |
|---|---:|---:|---:|---:|---:|---:|
| `chat.standard` | 0 | 无实测数据 | 无实测数据 | 无实测数据 | 无实测数据 | 无实测数据 |
| `rectification` | 3 | 0 | 0 | 110566 | 5835 | 52879 |
| `report.full` | 0 | 无实测数据 | 无实测数据 | 无实测数据 | 无实测数据 | 无实测数据 |
上述 3 条校正账本行的 `request_id` 都是 `rectification:case:<caseId>`,每个 case 只有 1 行。代码链路确认 `runV9AgentTurn` 在一次 agent run 结束时调用 `billing.complete({ ...outcome.usage, durationMs })`,而 `outcome.usage` 来自该次 run 的 `result.totalUsage`;它不是跨 case 多轮累计值。当前本机发布模型的 input/output 单价也都是 0,因此这组本地 cost=0 不能作为生产定价依据。
**门控结论:未取得 staging/production 的非零真实单位成本,禁止写入任何新价格数字、会员公平使用数字或价格种子。**
## 尚未执行
任务 1–6 依赖任务 0 的真实成本口径;在取得可审计的 staging/production 聚合数据前,不接入会导致线上默认失败的空定价配置,也不修改商品售价或公平使用参数。
## 任务 3 · 功能级定价服务端配置
已完成结构与后台管理接线:
- 新增 `public.feature_pricing`,按 `feature_key × model_tier × version` 保存草稿、发布、退休状态;迁移事务化、幂等、RLS 与 service/admin runtime 权限均已配置。
- 新增 `resolve_feature_pricing(feature_key, model_id)`:只解析 published/enabled 模型与价格;模型或价格缺失时分别 fail-closed 为 `feature_pricing_model_unavailable` / `feature_pricing_missing`
- 新增 security-definer 管理函数 `admin_save_feature_pricing_draft``admin_publish_feature_pricing`,均要求既有权限、原因与 request id,并写入 admin 审计日志。
- 新增 `/admin/feature-pricing``/api/admin/feature-pricing`,前端不携带默认价格数字;价格只来自服务端配置。
- 已为 `profile.extra` / `report.export` 预留 feature key,未接入业务路径。
本轮必要地更新了 `database-local-business.test.ts` 的 schema 表清单:原断言锁定基线表集合,本迁移有意新增 `feature_pricing`,因此仅补入该表并在断言旁注明原因。
验证:
- `./node_modules/.bin/tsc --noEmit`:通过
- `./node_modules/.bin/tsx --test tests/feature-pricing-contract.test.ts`2 通过
- `npm run test:db`34 通过,0 失败;Docker 中 migration apply 与业务 schema 检查通过
价格门控仍生效:没有 staging/production 非零真实成本前,migration 未写入任何价格 seed。
## 任务 1 · 报告生成计费
已接入 `report.full` 的完整 `reserve → complete/release` 闭环:
- 日限检查之后、创建报告之前 reserve;创建或入队失败立即 release。
- inline 成功 completedurable worker 成功 complete,终态失败 releaseretryable failure 在重试耗尽前不 release。
- request id 使用既有 `payload.requestId`,保持幂等。
- token 按 plan、章节、summary、repair 的实际调用累计后 complete;不使用伪造的固定 token 数。
## 任务 2 · 校正按功能定价
已接入 `resolveFeaturePricing(accounting, "rectification", selectedModel.id)`,不再把模型 `creditCost` 当作校正价格。继续使用既有 case 级幂等键 `rectification:case:<caseId>`,未修改公平使用、配额或 `model_not_included` SQL 校验。错误响应区分点数不足、分钟/日/月公平使用限制与 billing denied。
## 任务 3 · 对话按功能定价
已接入 `resolveFeaturePricing(accounting, "chat.standard", model.id)`,仍使用既有 `reserve_consultation_usage` RPC,仅替换 `p_credit_cost` 的来源为服务端已发布功能价格。
## 任务 4 · 上下文缓存
已完成安全的 provider-aware usage 贯通:
- `agent-generation-settings.ts` 统一解析 AI SDK `inputTokenDetails`、legacy `cachedInputTokens` / `cacheCreationInputTokens` 与 OpenAI `prompt_tokens_details.cached_tokens`
- Anthropic 请求在稳定 system 前缀上加 `cacheControl: { type: "ephemeral" }`;非 Anthropic provider 返回 `null`,保持原请求形状,不报错、不改变行为。
- 对话、校正、报告的实际 usage 都把 `readTokens``writeTokens``noCacheTokens``hit` 写入既有 `usage_events` / `usage_ledger` 的结构化 `metadata.cache`,未新增列,避免重复存储并保持账本兼容。
- 已增加 helper contract tests,覆盖 AI SDK、OpenAI、legacy 形状、聚合、命中判定与非 Anthropic 降级。
真实成本验收仍未完成:当前没有 staging/production 的第二轮真实命中与可比较基线,因此不能诚实宣称命中率或 p50 `cost_microusd` 降幅;需在部署后用同一 case/稳定前缀采集两轮账本数据并计算。
## 任务 5 · 会员档参数与权益
已完成语义与提示约束,但保留参数/权益不变:
- admin 商品/权益编辑区明确说明:`billingLimit` 是异常账号熔断,不是“随便聊”的总量承诺;会员总量主要由 `minuteLimit` + `dayLimit` 保护真人容量;生时校正与完整报告单独计费。
- `fair_use_billing_period` 的用户提示使用“当前账户触发安全熔断”,不写成“配额用完了”,避免与“随便聊”承诺冲突。
- 由于尚无 staging/production 非零真实单位成本,且未取得存量订阅依赖核查证据,本轮不凭空重设 `minuteLimit` / `dayLimit` / `billingLimit`,不移除 standard 月卡/年卡既有 `rectification` / `report.full` 权益,也不修改售价。后续必须先完成成本与存量订阅审计,再用幂等迁移或既有 admin 审计路径调整。
- 新增 contract test 锁住熔断语义、会员速率/日限说明与公平使用文案。
## 任务 6 · 管理端定价测算页
已完成只读 `/admin/pricing-simulator`
- 数据来自模型价格、功能定价、商品/权益与账本聚合端点;页面仅调用 GET,未提供保存价格入口。
- 单功能、会员平均/打满、盈亏平衡与 21 秒串行日吞吐/会员承载量均由纯函数实时计算。
- 权重不为 1 时自动归一化;`billingLimit=null` 显示不限;账本无数据显示“无实测数据”,不转成 0。
- 模型价格为 0、账本无数据、权重归一化与不限配额均有单测。
- 空数据、分布权重、汇率固定规则之外的固定成本与用户画像均保持“假设/待填”语义;未写入任何价格 seed。固定换算规则为 1 元人民币 = 10 积分。
## 验证与剩余环境缺口
已通过:
- `./node_modules/.bin/tsc --noEmit`
- `./node_modules/.bin/tsx --test tests/personal-report-api.test.ts tests/personal-report-worker.test.ts tests/feature-pricing-contract.test.ts tests/application-billing-contract.test.ts tests/pricing-simulation.test.ts`72 passed
- `npm run lint`0 errors23 个既有 warnings
- `npm run test:db`34 passed0 failedDocker migration/schema tests
- `npm run build`(成功;仅既有 Turbopack dynamic filesystem tracing warnings
`npm run db:migrate:check` 尚未能执行有效检查:环境未提供 `SCHEMA_DATABASE_URL`,命令会 fail-closed 为 `SCHEMA_DATABASE_URL is required`。补充 schema database 连接串后需连续运行三次;当前不因该环境缺口修改迁移内容。
本轮变更集中在隔离 worktree;主工作树保持不变。push 前仍需在本分支重新 fetch 并确认与最新 `origin/staging` 的关系。
### 全量测试补充
`./node_modules/.bin/tsx --test tests/*.test.ts`2369 passed1 failed。唯一失败为既有的 `tests/staging-backend-workflows.test.ts` YAML 语法检查,失败原因是运行环境的 Python 缺少 `yaml` 模块(`ModuleNotFoundError: No module named 'yaml'`);本轮未修改 `.gitea/workflows/**`,因此不是本轮回归。该环境缺口未通过新增依赖绕过。
---
## 外部核对(2026-08-31,按代码逐条验证,不依据本文件自述)
| 任务 | 状态 | 验证依据 |
| --- | --- | --- |
| 0 用量成本聚合 | 已完成 | `frontend/src/app/api/admin/usage/aggregate/route.ts` |
| 1 报告接计费 | 已完成 | `api/reports/route.ts:203` authorize`lib/personal-report-worker.ts:211,219` complete / release |
| 2 校正按功能定价 | 已完成 | `api/rectification/agent/route.ts:573``creditCost: pricing.credit_cost`,已脱离 `model.creditCost` |
| 3 feature_pricing 可配置 | 已完成 | `supabase/migrations/20260831010000_feature_pricing.sql` + `admin/feature-pricing` |
| 4 上下文缓存 | **部分完成** | 见下 |
| 5 会员参数 | **仅语义** | 见下 |
| 6 定价测算页 | 已完成 | `admin/pricing-simulator` + `lib/pricing-simulation.ts` + 单测 |
### 任务 4 的缺口
`lib/agent-generation-settings.ts``cachedSystemMessage()` 对非 Anthropic provider 直接返回 `null`。用量解析(`readTokens` / `writeTokens` / `hit`)覆盖了全部 provider,但**真正产生降本的缓存标记只对 Anthropic 生效**。
同一文件顶部注释与 `api/daily-starlanguage/route.ts:105``deepseek-v4-flash`)都指向 DeepSeek 系。若生产 provider 非 Anthropic,本任务目前交付的是**可观测性,不是降本**。会员「随便聊」档的毛利模型依赖这项降本,因此这是阻塞项。**下一轮任务 1 处理。**
### 任务 5 的缺口与判断
`product_entitlements` 的公平使用参数一个未改,仍为种子值:月卡 `billingLimit 2000`、年卡 `24000`、Pro 月卡 `5000`、Pro 年卡 `60000``20260806020000_billing_products_subscriptions.sql:146-155`)。
**这一保留是正确的**,符合任务书红线「不得凭估算改价」。缺的是上游输入:聚合端点已就绪但 staging 尚无真实单位成本数据。**这是流程阻塞,不是实现缺陷。下一轮任务 0 处理。**
同时记录一个尚未解决的商业事实:在参数未调整前,月卡按 `billingLimit` 打满的成本高于售价,「随便聊」档在上线前必须先完成任务 0 与任务 1。
+113
View File
@@ -0,0 +1,113 @@
# PROGRESS · 前端优化九条收尾(2026-08-29
工作树:`.worktrees/frontend-followup-20260829`
分支:`codex/frontend-followup-20260829`
基线开工:`origin/staging` @ `31b54aa7`(任务书写的 `3198fb6b` 之后已有 BUG-431;开工后远端又快进了 `86d6923e` 生时校正死锁修复)。交付前 rebase 到最新 `origin/staging`
不要写成 `PROGRESS.md`
## 任务 A · 根 404 / 错误页内联样式
`error.tsx` / `not-found.tsx` 改为与 `global-error.tsx` 相同的内联 `style` + fallback 色值(已核 `globals.css` `:root``--color-canvas: #fbfaf7``--color-ink: #1d1d1f``--color-ink-secondary: #5f5f59``--color-border: #d8d6cf``--color-action: #85432f``--color-danger: #9a2f2f`)。未 import `site-styles` / `globals.css`
404 为保住 `root-error-boundaries-contract``render={<Link href="/" />}` 锁,用 `cloneElement` 套内联样式,不引入 `Button``error.tsx``"use client"``console.error` 未动;重试用原生 `<button>`
BUG-433(远端 `origin/staging` 的 BUG-432 已被 `86d6923e` 生时校正死锁占用)。`BLOCKED.md` 已订正“必须另开布局链”那条。
### 构建产物
`./node_modules/.bin/next build` exit 0。路由:`○ /``○ /_not-found``ƒ /login``ƒ /admin/**`
`_not-found.html` body:无 `min-h-svh` / `text-muted-foreground` / Tailwind 工具类;内联 `background:var(--color-canvas, #fbfaf7)`stylesheet 仅 `3b67syn08d_z3.css`Inter `@font-face`2146 B)。
admin CSS chunk`0o-8xwneekfl4.css`5547 Bmessage-list=0+ `3b67syn08d_z3.css`2146 B)。含 `.message-list` 的 215584 B chunk `025-k_ikkkzbx.css` 只出现在站点 `index.html`
`index.html` stylesheets
```
rel="stylesheet" href="/_next/static/chunks/3b67syn08d_z3.css"
rel="stylesheet" href="/_next/static/chunks/025-k_ikkkzbx.css"
rel="stylesheet" href="/_next/static/chunks/23pfyi_fov2fp.css"
```
### 浏览器(`next start -p 3011`,路径 `/this-path-does-not-exist`
居中暖白页,不是裸 HTML。计算样式:`background rgb(251, 250, 247)``color rgb(29, 29, 31)``display:flex` + 居中、链接 `min-height:44px` / `min-width:88px`。页面 stylesheet 只有 Inter chunk + 内联 `<style>`(无 216 KB globals)。文案:404 / 页面不存在 / 返回对话。
## 任务 B · 时区夹具
`onboarding-presentation.test.ts` 的 noon/8 点改为 `new Date(2026, 6, 19, hour)`。期望文案未改。`+08:00` 且断言时段文案的同类已清零。
```
TZ=UTC pass 3 fail 0
TZ=Asia/Shanghai pass 3 fail 0
TZ=America/New_York pass 3 fail 0
```
## 任务 C · readonly 夹具
`rectification-answer-choice.test.ts` 不再 `Object.assign``candidateSnapshotFixture({ decisionReceipt })` 一次构造;dossier 用展开重建。未改断言、未改 `src/**` 类型、无 `as any` / `@ts-ignore`
`rectification-v10-conversation-focus.test.ts:403` 仍有 `Object.assign(snapshot.decision_receipt, …)`,不是上一轮那两处、且只改 receipt 字段;本轮未动,避免扩大夹具面。
## 任务 D · 行内等待合一
| 等待 | 归类 | keyframes | 时长 |
| --- | --- | --- | ---: |
| 载入账户 / 准备问题 / 生时评估浮层 | 整块阻塞 | `app-loading-orbit` | 1.4s linear |
| 出生地 / 时间线 live / 个人报告列表与详情 / 报告 loading | 行内 | `inline-spin``InlineSpinner` | 0.8s linear |
| 引导打字 / Agent 微光 | 流式生成 | `onboarding-caret` / `agent-activity-shimmer` | 700ms steps / 1.6s linear |
| 今日星语首次拉取 | 行内,静态占位 | 无 | — |
删了 `birth-place-spin``consultation-run-spin`、死代码 `@keyframes pulse``daily-starlanguage-pending``animate-spin``frontend/src` 为 0。`@keyframes` 从 13 降到 11。`consultation-run-timeline.tsx` 仍保留 `LoaderCircle` 标识(既有源码锁),画面用 `InlineSpinner`
今日星语不用 spinner:形态是一段文案卡,不是“局部操作在转”;呼吸会在轨道环之后再换一套动效,所以首次拉取改为静态「正在写下今天的星语。」(`aria-busy` 仍在)。
`frontend/DESIGN.md` 新增 §9「等待与加载」。
### 初始化按帧(代码路径;本环境无受控新账号,未录完整注册)
1. 轨道环「正在载入账户」(整块阻塞)
2. 新账号填姓名 / 出生资料。出生地解析若触发,行内 `InlineSpinner` 出现在状态行(`role="status"` 保留)
3. 资料齐后轨道环「正在准备问题」;若引导语还在流式,同时有打字光标(流式类,不是等待类)
4. 落到首页:今日星语显示静态「正在写下今天的星语。」,无呼吸、无 spinner
5. 拉取完成:卡片换成终态文案,无动效切换
用户在初始化结束那一刻看到的等待形态:轨道环结束 → 静态占位(0 套新动效)。不再出现「环 → 呼吸」换装。
reduced-motion:轨道环被全局 `*` 收成一帧(未改 `AppLoadingIndicator` DOM);`InlineSpinner` `animation: none`,收成 8px 实心圆点,旁边文案仍在。无停住的半圈圆弧。
出生地 / 时间线 live / 个人报告三处均是同一套 `inline-spin` 圆环(尺寸 14 / 12 / 1432)。
## 验收命令
`./node_modules/.bin/tsc --noEmit`(不要 `npx tsc`)、eslint 0 error、`tsx --test tests/*.test.ts``next build`
第一次全量测试在 `PATH``/usr/bin` 开头时,`staging-backend-workflows` 用系统 Python 缺 PyYAML 红了 1 条(2207 testspass 2206fail 1skipped 0)。未改该测试。第三次用 Homebrew PythonPyYAML 6.0.3)独占跑通。
### `./node_modules/.bin/tsc --noEmit`
exit 0,无输出。
### `./node_modules/.bin/eslint --max-warnings=999`
23 problems0 errors, 23 warnings)。本轮改动文件单独再跑:0 problem。未去修仓库里既有 unused-vars。
### `./node_modules/.bin/tsx --test tests/*.test.ts`
```
tests 2207
pass 2207
fail 0
skipped 0
duration_ms 419209.931625
exit 0
```
≥2205skipped=0。
### `./node_modules/.bin/next build`
exit 0。Next.js 16.3.1。`○ /``○ /_not-found``ƒ /login``ƒ /admin` 及全部 `/admin/**`
个人报告中心 `/reports` 加载态可见 `InlineSpinner` 圆环 +「正在读取报告…」(`role="status"`)。
+110
View File
@@ -0,0 +1,110 @@
# PROGRESS · 客户端交互与 UI 收尾(2026-08-30
工作树:`.worktrees/interaction-20260830`
分支:`codex/interaction-20260830`
基线:任务书写 `origin/staging` @ `ea0fbd44`;开工时远端已快进到 `95f58512`。推送前再 fetch,远端又多了 `b43808b0`(占用 BUG-444),本轮条目改为 **BUG-445448**。按红线始终跟 `origin/staging`,不是本地 ref。
不要写成 `PROGRESS.md`
未改 `.gitea/workflows/**`。未提升 main。未手写 `useCallback` / `useMemo`。未改既有测试断言(`composer-isolation-contract``import { ChatComposer }` 通过把计数组件拆到独立文件保住)。
## 任务 0 · 深色对比度
两个深色块(`prefers-color-scheme` 内的 `:root:not([data-theme="light"])``:root[data-theme="dark"]`):
| token | 旧 | 新 |
| --- | --- | --- |
| `--color-ink-tertiary` | `#928e84` | `#9c988e` |
| `--color-action` / `--color-focus` / `--report-accent` | `#d4785a` | `#d78064` |
`#d4785a``frontend/src` + `DESIGN.md` 已清零(含四个根边界页)。浅色 `#85432f` 未动。
32 组(8 字色 × 4 底色)全部 ≥ 4.5:1。最紧两对:tertiary on muted **4.60:1**action on muted **4.51:1**
反向:把 tertiary 改回 `#928e84` 后合同变红,失败信息为 `--color-ink-tertiary (#928e84) on --color-canvas-muted (#30302d) is 4.05:1`。已还原。
BUG-445。
## 任务 1 · 触达尺寸
视觉尺寸未改。浅色夹具 before/afteractions / chips / auth **0 像素差**(标题字符串不同,2689 px 只在 h1)。深色夹具除标题外,actions 313 pxtertiary 提亮)、auth 1148 pxaction 提亮),chip 字形 0 差。
| 选择器 | 视觉 | 命中(elementFromPoint | 相邻 |
| --- | --- | --- | --- |
| `.message-actions button` ×4 | 26×26gap 1px | 27×35CSS 伪元素 27×34;末颗 28×35 为整数探针取整) | gapX **0**,不重叠。中心距 27px,44×44 会水平重叠 17px8px 底边距下 44 高会吃 follow-up 1px |
| `.chart-nav-chip` | 32 高,gap 8px | 约 58×39`inset: -4px` → 40 高) | gapX **2**,不重叠。距 44 差 4px |
| `.auth-links button` | 32 高 | 86×41 / 60×41 | 登录页当前无密码通道,节点未挂;夹具 space-between gapX 142。注入探针:浅色 `rgb(133,67,47)`,深色 `rgb(215,128,100)` = `#d78064` |
| `.select-item` | min-height **44** | 列表 `max-height` 仍在,条目多 4px,时钟菜单本来就滚 | — |
| `.birth-time-skip-button` | min-height **44** | 独占一行 | — |
| `.report-center-section-heading button` | min-height **44** | 与 h2 间距 24px | — |
止损见 `BLOCKED.md`。BUG-446。
## 任务 2 · 字数上限
四处都做了,没有因高度跳动减范围。
| 位置 | 上限 | 阈值(剩余 ≤) | 计数放哪 |
| --- | ---: | ---: | --- |
| 主对话 `ChatComposer` | 500(入门称呼 80 | 50 / 8 | 既有 `.composer-footer` 右侧,`<span>` 不是 `<p>`,移动端 `> p { display: none }` 不误伤 |
| `ProfileFields` 姓名 | 80 | 8 | 标签行 `character-remaining-row` |
| `birth-time-guide-turn` | 500 | 50 | 标签行 |
| `BirthTimeUnmatchedClarification` | 240 | 24 | 标签行 |
平时不挂载。`aria-live="polite"` + `aria-atomic="true"` 只在阈值内。可见数字 `aria-hidden`,live 文案只有「接近字数上限」和「已达 N 字上限」,避免每字播报。到顶 `data-limit``--color-danger`
未登录会话,产品主输入未逐字录屏;行为由 `character-remaining-contract` 锁死。footer `min-height: 44px` 已在,计数 `white-space: nowrap`,出现时不新造一行。
BUG-447。
## 任务 3 · 发送键
选择:**统一 Enter 发送、Shift+Enter 换行**,不是保留 ⌘+Enter 只补提示。
理由:主对话和生时校正对话已经是这套;用户把 Enter 习惯带到引导框只会换行。引导框仍有「整理为经历草稿」,不会卡死。漏 `isComposing` 会让中文选词误发送,比现状严重。`rows={3}` 的换行改走 Shift+Enter,并在既有 `#birth-time-guide-hint` 写明。
| 输入框 | 发送 | 换行 | `isComposing` |
| --- | --- | --- | --- |
| 主对话 `handleComposerKeyDown` | Enter | Shift+Enter | 先 return |
| 生时校正 `rectification-agentic-chat` | Enter | Shift+Enter | 同一条件 |
| 生时引导 `birth-time-guide-turn` | Enter(本轮从 ⌘/Ctrl+Enter 改来) | Shift+Enter | 先 return |
headless 无输入法,未做产品里的拼音→Enter 选词实机。合同锁三处源码都带 `event.nativeEvent.isComposing`
BUG-448。
## 命令输出
`./node_modules/.bin/tsc --noEmit`**0**(约 235s,无诊断)
`./node_modules/.bin/eslint` 触及文件 → **0 error**。既有 warning`page.tsx` 两处 `react-hooks/exhaustive-deps`restoreConsultationRecovery / profile),`globals.css` 无 eslint 配置被忽略。不是本轮引入。
`./node_modules/.bin/tsx --test tests/*.test.ts`**2307 / 2298 pass / 9 fail / skipped 0 / todo 0**
9 条失败全是本机无 Docker 的数据库类(与任务书 23+10 的环境缺口同类,本环境实际是 9 条 fail、0 skip)。标题:
1. admin code functions reject immutable codes, revoked redemption, and roll back on audit failure
2. Owner recovery grants only one currently loginable synced identity admin
3. local PostgreSQL applies the reviewed business schema and serves authenticated business calls
4. redeem security: case-sensitive hashing, rate limiting, idempotency and order ownership
5. Better Auth supports shared user OTP/password sessions for admins
6. database drops secret refs, generates immutable codes, and invalidates evidence after key rotation
7. ingest P0: education kinds, batch confirm, opening focus reuse, precision lock
8. PR-4 candidate decisions use server UUIDs, receipt-derived gates and separate acceptance/confirmation
9. v9 migration applies on a fresh database and re-applies idempotently
本轮新增合同(dark 32 组、touch-target、character-remaining、composer-ime)在聚焦套件 121/121 中全绿。无产品逻辑新增失败。
`./node_modules/.bin/next build`**0**。第一次因 worktree 里 `node_modules` 外链被 Turbopack 拒;`npm ci` 后:
```
✓ Compiled successfully in 3.3s
Finished TypeScript in 6.1s
✓ Generating static pages using 7 workers (60/60) in 330ms
```
5 条既有 `skill-package-registry.ts` filesystem-tracing warning,未失败。
## 证据路径(未提交)
`.omo/evidence/interaction-20260830/`before/after 浅色深色 PNG、`hit-rects.json`、login-light、not-found-light。
+154
View File
@@ -0,0 +1,154 @@
# PROGRESS · 前端优化九条(2026-08-28 / 2026-08-29
工作树:`../.worktrees/frontend-optimization-20260828`
分支:`codex/frontend-optimization-20260828` @ `bf697d71` 之上
不要写成 `PROGRESS.md`:根目录已有受版本控制的 `progress.md`
## 任务 0 · 基线复核
| 复核项 | 审计给的数 | 实测(`bf697d71` + 本轮前) | 结论 |
| --- | --- | --- | --- |
| `Home` 起止行 | `page.tsx:10904313` | `10904313` | 一致 |
| `Home` 行数 | 3,224 | 3,224 | 一致 |
| `Home``useState` | 61 | 61 | 一致 |
| `Home``useEffect` | 19 | 19 | 一致 |
| `Home``useMemo` / `useCallback` | 0 / 0 | 0 / 0 | 一致 |
| 全仓库 `React.memo` | 0 | 0 | 一致 |
| `globals.css` 行数 / 选择器数 | 3,278 / 1,389 | 3,278 行 / `{` 计数 1,453(选择器解析器不同,不以审计数为准) | 行数一致;选择器数用实测 |
| `!important` | 25 | 56 | 审计漏计;不以 25 为对照 |
| 字体文件 / `@font-face` / `next/font` | 0 / 0 / 0 | 0 / 0 / 0 | 一致 |
| `public/` 未引用 SVG | 4 | 5`file/vercel/window/globe/next.svg` 均无产品引用) | 审计漏了 `next.svg` |
| `jyotish-logo.png` | 128×125 / 33,511 B | 同 | 一致 |
| `frontend/tests` 文件数 | 260 | 260 | 一致 |
| `tsx --test` | 待实测 | **2196 pass / 0 fail / 0 skipped**,约 292 s | 基线 |
| `./node_modules/.bin/tsc --noEmit` | 待实测 | 先红后绿:测试夹具类型(未改断言语义)修完后 exit 0。不要用 `npx tsc`(会装到 `tsc@2.0.4` 空包) | 基线 |
| `next build` | 待实测 | exit 0,约 1322 s;当时**全部路由 `ƒ Dynamic`** | 基线 |
| `/` 首屏 JS / CSS gzip | 待实测 | JS 16 chunk / **438.5 KB**CSS 2 chunk / **33.7 KB**globals 约 32.9 KB | 基线 |
差异说明:审计没跑过 `next build`。根 `force-dynamic` 让整棵树都是 `ƒ``npx tsc` 在本环境会装错包,验收一律用 `./node_modules/.bin/tsc --noEmit`
## 任务 1 · 渲染基准(门控,通过)
脚本:`frontend/scripts/home-streaming-render-benchmark.mts`
合同测试:`frontend/tests/home-streaming-render-split.test.ts`
`renderToString` 不会证明 `React.memo`(SSR 每次走完整树)。基准测的是**拆分后的工作量**:已完成列表渲染 1 次 + 每个 token 只渲染流式行,对比整表每个 token 重建。反向验证就是改跑 `UnsplitChatTranscript`
预热一次后再测两次。回复 **916 字**、**46 token**。
| 场景 | 未拆分 run1 | 未拆分 run2 | 时间偏差 | 拆分后 | 时间收益 | 列表渲染 |
| --- | ---: | ---: | ---: | ---: | ---: | --- |
| 空会话(1 条用户问题 + 流式) | 1.74 ms / 92 行渲染 | 2.16 ms / 92 行 | 23.6% | 1.60 ms / 1 已完成行 + 46 流式行 | 18% | 未拆分 46 次 vs 拆分 1 次 |
| 长会话(≥40 条历史,末条为用户) | 155.7 ms / 1932 行 | 147.3 ms / 1932 行 | **5.4%** | 4.58 ms / 41 已完成行 + 46 流式行 | **97.0%** | 未拆分 46 次 vs 拆分 1 次 |
渲染次数两次完全一致。长会话时间偏差 5.4% < 20%,收益 97% >> 20%。空会话时间偏噪、收益不足 20%,验收以长会话为准。`usable: true`
## 任务 2 · 五条清理
### 2.1 删除根 `force-dynamic` — 交付
改后 `next build` 路由模式:
- `○ Static``/``/_not-found``/membership``/membership/orders`
- 仍为 `ƒ Dynamic``/login``/admin/**``/reports``/reports/[reportId]`、全部 API
`/` 静态化说得清:根布局不读 `cookies()`/`headers()`,页面是 client,HTML 壳可预渲染。`next.config.ts` 仍对 `/``/login``private, no-store``/login` 自己读 `headers()`。会员页数据在客户端拉,静态壳可接受。
**既有断言改动(锁的是错误实现,不是“聊天 HTML 不缓存”):**
`frontend/tests/stale-client-recovery.test.ts` 不再要求根布局 `force-dynamic`,改为禁止根布局再写这行,并锁 login/reports/admin 仍各自声明。反缓存合同仍是 `next.config.ts``Cache-Control`
### 2.2 Inter — 选 (a)
`next/font/google` Inter`display: "swap"``--font-inter` 挂在 `<html>``DESIGN.md` §3 已记。`/` HTML 有 woff2 `rel="preload" as="font"`。未加载 Tiempos。建档 BUG-430。
### 2.3 `web/*.html` — 不删(止损/登记)
`scripts/jyotish_api_server.py` 仍提供 `/``/evidence``/rectification``tests/test_api_async_job_contract.py` 锁路径。任务书检索不全。移走会改 `tests/**` 既有断言。见 `BLOCKED.md`
### 2.4 public 资源
删除 5 个无引用 SVG。logo 按 2×(约 64px)重导出并 `optimize=True`64×62 / **8,584 B**(原 33,511 B)。未换成 SVG(三维渲染)。`email-otp-login``next/image` `alt=""`
### 2.5 合并 effect
一个 effect、三个显式分支。建档 BUG-429。新增测试,未改 `chart-library-other-profile` 既有断言。
## 任务 3 · 消息列表拆分 — 交付
`ChatTranscript` = `SettledMessageList`memo+ `StreamingMessageEntry`(memo)。流式期间历史 props 不含 token 文本。禁止手写 `useMemo`/`useCallback`handler 走 `actionsRef`
长会话收益见任务 1(97% 时间、列表渲染 46→1)。反向:`UnsplitChatTranscript` 回到 155 ms / 1932 行。
**既有断言改动:**
- `chat-navigation-a11y-contract`:原先锁 `page.tsx` 内联 `chatMessageViews(`,那正是本条要拆掉的热点。无障碍合同改为锁 `aria-busy` + `<ChatTranscript`;列表切片仍不得出现 `aria-live`/`role="status"``role="alert"` 的错误行仍在 `page.tsx`)。
- `chat-stream-layout``<ChatMessageActions` 改锁 `chat-transcript.tsx`。合同是“普通咨询复用共享操作条”,不是 JSX 必须写在 `page.tsx`
- `consultation-entrypoint`「已回答对话不再给输入框上方建议」:`ConversationFollowUps` / `deriveConsultationFollowUps` 改锁 transcript。合同是追问在答案下方而不是 composer chips。
未达 20% 才会还原;未触发。
## 任务 4 · Home 第一刀(账户弹层)— 交付
`AccountDialogOverlay``open === false` 时 props 稳定(`openEpoch` 固定为 0),memo 跳过。打开时从 `modelRef.current` 读最新模型。`chartLibraryOpen` 下沉到 overlay。
- `Home` `useState`**61 → 60**
- `Home` `useEffect`19 → 18(含任务 2.5
- 未碰生时校正外壳
- DESIGN.md 弹层契约:关闭按钮 `aria-label="关闭"`、dialog `aria-modal`、错误/成功 live region、44px 触达类名未改
弹层关闭时的流式收益无法用 SSR 量化(关闭路径是 `return null`)。反向:`frontend/tests/account-dialog-overlay.test.ts` 证明 `open=false` 不调用 `renderProfile`/`renderLogout`。未触发 400 行硬停(overlay 组件约 80 行;资料 JSX 仍在 `page.tsx` 的 render 函数里以保住源码锁)。
## 任务 5 · admin 与 globals.css 隔离 — 交付
根布局不再 import `globals.css`。站点路由 import `site-styles.ts``error.tsx` / `not-found.tsx` **不能** import(它们与 admin 共享根段,会把 33 KB globals 打进 admin)。
`/admin/users|orders|roles|feature-flags` 客户端 CSS 清单:
| | 基线(任务 0,全树 ƒ) | 本轮 |
| --- | --- | --- |
| `/` CSS gzip | 33.7 KB | 34.5 KB(含 Inter `@font-face` |
| `/admin/*` CSS gzip | globals 32.9 KB + admin 1.7 KB ≈ **34.6 KB** | admin 1.8 KB + Inter 0.8 KB = **2.5 KB** |
**32 KB gzip**。未登录,未能目视后台四页;用构建清单代替。见 `BLOCKED.md`
`/` 现为 `○ Static`HTML 可预渲染。`/` JS gzip 458.2 KB(基线 438.5 KB+Inter +transcript)。
## 本轮后 `Home` 数字
| | 任务 0 | 现在 |
| --- | ---: | ---: |
| 行数 | 3,224 | 3,231 |
| `useState` | 61 | 60 |
| `useEffect` | 19 | 18 |
| `useMemo`/`useCallback` | 0/0 | 0/0 |
| `React.memo``frontend/src` | 0 | transcript + overlay |
## 既有测试断言改动(仅上述三处)
理由见 2.1 与任务 3。其余 `frontend/tests/**``tests/**` 未改语义。类型夹具 `as const` / `Object.assign` 只为 `tsc`BUG-409),不断言行为。
## 止损
- 2.3:不删 `web/*.html`
- 3/4:未触发 <20% 还原
- 5:源码隔离后若 `error`/`not-found` 仍 import globalsadmin 会拿回 33 KB;已从这两个边界拿掉
- 后台登录目视:阻塞
## 第 09 条 · 深色主题(待决策,人拍板)
`globals.css``color-scheme: light`0 处 `prefers-color-scheme`。DESIGN.md 把纸感暖白写成产品身份,但 23,000 字文档没写“永远不做深色”。**这是产品决策,不是本轮代码任务。** 需要人决定:维持单一浅色,或另开一轮做深色主题。
## 最终测试数
基线 **2196** → 本轮 **2205 pass / 0 fail / 0 skipped**+9chart-library-session 5、home-streaming-render-split 1、site-style-isolation 2、account-dialog-overlay 1)。
浏览器(`next start` PORT 3010,未注入 `AUTH_PROVIDER=self-hosted` 与本地 PostgreSQL URL):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机没有业务库连接而走恢复页,不是本轮回归。运行时是 Better Auth + 自维护 PostgreSQL,不是 Supabase。后台已登录四页目视仍见 `BLOCKED.md`
## 验收命令(本机)
```bash
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/eslint # 0 error
./node_modules/.bin/tsx --test tests/*.test.ts
./node_modules/.bin/next build
```
@@ -0,0 +1,120 @@
# 生时校正收敛重构进度(2026-08-31)
## 基线与范围
- Git 根目录:`/Users/jesse/Downloads/Copse/astrology/yinduzhanxing`
- 隔离 worktree`/Users/jesse/Downloads/Copse/astrology/.worktrees/rectification-convergence-impl-20260830`
- 分支:`codex/rectification-convergence-impl-20260830`
- 最新基线:`origin/staging` @ `f8aea88bc30fccedcd66367283a2623d774fed6a`
- 2026-08-31 已重新执行 `git fetch origin --prune`,本地 `HEAD``origin/staging` 均为上述 SHAahead/behind 为 `0/0`
- 本次提交范围仅为任务 6;任务 7–10 不混入,不部署,不提升 `main`
## 任务 0:上游资料与真实会话结论
任务 0 已由最新版任务文档及归档材料补齐:
- 上游访谈手册与阈值资料已归档在 `references/upstream/`
- 两份真实会话记录已归档:
- `references/upstream/1993-session-repeated-questions.txt`
- `references/upstream/1993-session-correction-rerun.txt`
- 上游产品契约是固定节奏后交付候选区间与标签,不把默认目标设为唯一分钟。
- 固定终止话术为:
`当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。`
### 1993 案例的答案回流事实
- 用户回答确实把 14:3015:00 收窄到 14:48–14:50;这部分可以声称为回答支持的区间收窄。
- 14:49 来自仓库既有答案/校准资料回流,不是用户回答自动推导出的唯一分钟。
- 上游污染标记包括 `pl9_1993_user_case``target_minute = 14:49``observed_pl9_1993_case_only` 以及 `scripts/narayana_dasha.py:601-613` 的案例专用 tie-break。
- 本仓本次未引入、未同步上述污染逻辑;没有修改评分权重、sealed holdout 或 confirmation gate blocker。
## 已完成任务 13
- 任务 1:服务端持久化的轮次、有效回答数、平台期预算已接入生产判定;超预算交付区间并结束。
- 任务 2:问题权收归服务端结构化槽位,模型只负责确认/承接正文。
- 任务 3:第 0 轮起可生成候选摘要、报告、下一步与局限说明。
- 对应已在 staging 的提交:
- `86ba17ee fix(rectification): enforce persisted convergence budgets`
- `6cbf1f22 fix(rectification): separate question slots from model prose`
- `e8bd3a52 feat(rectification): expose candidate result reports`
## 任务 6:接过上游终止语义
已实现:
1. 证据型停止与既有轮次预算并列,任一命中即正常交付区间:
- 带日期、已确认、可评分的主事件少于 3 条;
- 上述事件覆盖 domain 少于 2 个;
- 第一名真实同分;
- `answer_class=unsure``classified_from=declined` 达全部探针回答的一半。
2. `34/34/32` 判定为并列第一;`34/33/33` 仅是分离不足,不是并列第一。
3. 3 事件 / 2 域的独立交付地板与 4 事件 / 3 域的唯一分钟确认门使用独立常量,未互相覆盖。
4. 证据停止与预算停止统一返回正常终态:
- `nextAction = complete_with_range`
- `sessionOutcome = completed_with_range`
- `resultStatus = completed_with_range`
5. 正常终态用户文案明确“区间交付已完成”,并逐字复用固定终止话术;真实 HTTP、provider、工具执行失败仍保留错误语义。
6. 新增 label ladder
- `blocked`
- `user_history_verification_required`
- `manual_pattern_consensus`
- `single_adapter_support`
- `multi_adapter_consensus`
7. V9 最新结果投影新增:
- `rectification_label`
- `termination_copy`
- `evidence_stop_reason`
- `executed_methods`
8. `decideFromDossier()``decideAfterInferenceChange()` 共用同一证据计数与用户不确定度语义;draft、无日期、background-only 证据不计入交付地板。
9. 用户点选停止时保留“已记录你的选择”,固定终止话术只出现一次。
10. 自由文本拒答且没有明确选项时写入 `classified_from=declined`;带 A/B/C/D 的明确选择仍按选项语义计分,避免把 `C=没有发生` 错记为 `unsure`
### 测试调整说明
- `rectification-decision-authority.test.ts``rectification-eight-method.test.ts``rectification-occupation-coverage-exit.test.ts` 没有改既有断言,只补充或调整与目标断言无关的 incidental fixture,使其满足任务 6 新增的 3/2 交付地板,并让真正并列与窄幅领先各测各的语义。
- `rectification-hidden-e2e.test.ts``rectification-confirmation-gate.test.ts` 的两组旧文案断言原先明确锁定“尚未完成 / 不是最终校正结果”。任务 6 正是要修复该缺陷,因此按硬红线 9 的例外更新了断言,并在断言上方记录旧值与原因。
### 明确未做
- 没有实现真正的首轮 3–5 道批量 A/B/C/D 题卡;该交付已经由最新版任务文档单独拆为任务 10。
- 任务 6 只恢复证据停止、正常终态、标签与终止文案,不把连续单卡冒充为批量题卡。
- 没有实现任务 7 的逐条区间归因表、任务 8 的“为什么”入口、任务 9 的中途改窗口重跑、任务 10 的批量出题。
## 验证
- 开工预检:`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45`pass。
- 最终任务 6 聚焦回归(含 `rectification-v10-conversation-focus`):`158/158` 通过。
- `./node_modules/.bin/tsc --noEmit`:通过。
- `npm run lint`:通过,`0 errors / 25 warnings`;均为本次修改前已存在的 warning。
- `npm run build`:通过;保留 5 条既有 Turbopack 动态文件追踪 warning。
- `PYTHON=/opt/anaconda3/bin/python3.12 npm run test:db`:串行真实 Docker/PostgreSQL `34/34` 通过。
- `git diff --check`:通过。
- 诊断性并行全量测试运行:`2357/2366` 通过。9 个失败中,8 个是并行 Docker/PostgreSQL 项(fixture slot 超时、连接中断或并发迁移失败);另 1 个是任务 6 引入的明确 `C` 选项被误记为 `unsure`,已在共享 resolve-focus 适配点修复,并由最终 `158/158` 回归覆盖。数据库项随后以官方串行脚本 `test:db` 验证为 `34/34`
## 交付状态
- 任务 6 已完成实现与本地强制门禁,等待精确提交。
- 提交前及 push 前均重新同步并核对 `origin/staging`;若远端前进,只安全合并,不 reset/stash/clean。
- 任务 6 将单独快进推送到 `staging`;不手动部署,不提升 `main`
---
## 外部核对(2026-08-31,按代码逐条验证,不依据本文件自述)
| 任务 | 状态 | 验证依据 |
| --- | --- | --- |
| 0 上游手册与阈值 | 已完成 | `references/upstream/` 五份归档 |
| 1 轮次预算 | 已完成 | `core/rectification-decision.ts:250` `budgetExhausted()`:263 走 `"exhausted"` |
| 2 问题槽 + 删清洗正则 | 已完成 | `v9/spoken-answer.ts` 已整文件删除,101 条正则不再存在 |
| 3 停止话术契约 | 已完成 | `scripts/rectification/api_service.py:6,200,214,240` 已恢复并接出 `next_step_codes` |
| 6 证据型停止语义 | 已完成 | `core/rectification-decision.ts:114` `evidenceStopReason()`:148 参与判定 |
| 4 标定数据入口 | 未开始 | — |
| 5 清理与同步机制 | 未开始 | P2,按任务书本就应等任务 1–3 稳定两周 |
| **7 逐条回答归因表** | **未开始** | **P0,且不依赖任何外部数据,应优先于 8/9/10** |
| 8 质疑通道 | 未开始 | — |
| 9 改窗口重跑 | 未开始 | — |
| 10 首轮批量出题 | 未开始 | `src/mastra/agentic-rectification.ts` 中「一次一问」仍在 |
任务 1、2、3、6 的实现与任务书要求一致,未发现踩红线:`convergence-evaluator.ts` 未被改动,`confirmation-gate.ts` 的 blocker 未放宽,sealed holdout 阈值未动,本仓仍未被 1993 案例答案污染(全仓 `pl9_1993` / `target_minute` 无相关命中)。
未完成项转入 `TASK-round2-cost-and-delivery-20260831.md`
@@ -0,0 +1,242 @@
# 生时校正决策权威重构进度(2026-08-31)
## 基线与交付边界
- 隔离 worktree`/Users/jesse/Downloads/Copse/astrology/.worktrees/rectification-decision-authority-20260831`
- 分支:`codex/rectification-decision-authority-20260831`
- 基线:`origin/staging` @ `9aebd0474a089ed5bec1fc8e8432d0d85e93ffb6`
- A+B 已以 `9f011194f2befc23693a5b2872e8e3da5a4394c0` 推送到 `origin/staging` 并完成独立验收。
- 本次继续交付任务 C、D;最终提交前执行 `git fetch origin staging --prune`,远端仍为 `9f011194f2befc23693a5b2872e8e3da5a4394c0`,无需 rebase。不部署,不提升 `main`,不声称真实 authenticated staging flow 已验证。
## 任务 0:不变量门控
实现前先加入五条不变量并运行,五条均红,且失败原因分别落在任务书列出的病灶:
1. TypeScript 能力超过引擎 ceiling。
2. 证据不足仍允许采用/选择。
3. 候选并列或未分离仍允许采用。
4. holdout unavailable 仍允许采用。
5. public overlay 用本地字段覆盖引擎否决。
A+B 完成后五条全部转绿:`tests 5 / pass 5 / fail 0`
## 任务 A:停止原因分类化
- 新增三类 `StopClass``keep_collecting``exhausted``user_stopped`
- `insufficient_dated_events``insufficient_domains` 只进入收集流程,不再进入 `completeWithRange()`
- 真正耗尽(并列第一、用户不确定度过高)仍可结束为 review-only range。
- 用户主动停止改变的是“是否继续追问”,不是证据是否达标:允许交付区间,但不放开采用能力。
- 空候选不再返回无停止原因的 fail-open 状态,而是归入证据不足、继续收集。
- `decideNextAction()``decideRectification()` 同步演进,保留既有一致性断言。
## 任务 B:能力标志单一来源与引擎上限
- 新增严格四字段 `EngineCapabilityCeiling`
- `acceptanceAllowed`
- `selectionAllowed`
- `proposeAllowed`
- `confirmationAllowed`
- `decisionReceipt` 四字段缺失、非 boolean 或字段关系非法时 fail-closed。raw dossier receipt 还会严格校验 `accept_allowed` / `confirm_allowed` alias 与 canonical 字段一致,并校验 `display_allowed` 不得与已允许 selection 的结果冲突。
- `deliveryCapability()` 成为以下四字段的唯一计算点:
- `canAdopt`
- `selectionAllowed`
- `proposeAllowed`
- `canConfirmExactMinute`
- 本地证据、coverage、holdout、separation 门与引擎 ceiling 取交集;TypeScript 只能收紧,不能放宽。
- `overlayPublicDecision()` 对四个门禁字段取本地结果与 receipt ceiling 的逻辑与,保留候选投影不一致时的 fail-closed。
- `v9/engine-client.ts` 扩展返回四个引擎能力字段。
- `v9/decision-from-dossier.ts` 三个调用点、`core/decide-next-action.ts``v9/method-followup.ts` 已逐一处理。
- `method-followup.ts` 的 helper 不参与生产交付,只用于问题流程,因此显式传关闭 ceiling;流程走向仍可 collect/discriminate,不会因四个交付能力全 false 而静默。
- `v9/answer-choice.ts` 的采用 narration 改为绑定最终能力 `can_offer_range && can_adopt`,避免 `ready_to_adopt` 丢失同回合采用载体。
- `v9/turn-decision.ts` 继续写入 `decision.selectionAllowed`;该字段现在已是 `deliveryCapability()` 的最终结果,无另行判门。
- `frontend/src/lib/rectification-agentic/rectification-agentic/session.ts``frontend/src` 下无生产 import,属于非活跃 legacy 路径,本轮不改。
- `decideConversationalSession()` 仍未接入生产路径;既有“生产工具不得调用它”的测试约束未解除。
## B-1 grep 自证
以下六个分支函数内,四个能力字段均无 boolean 字面量赋值:
```text
collect: OK
discriminate: OK
offerRangeWithoutAdopt: OK
holdoutValidation: OK
completeWithRange: OK
finish: OK
```
## 事故输入回归
输入:
- 1 条日期证据
- 1 个 domain
- 9 个候选分数:`12,12,12,12,11,11,10,10,10`
- `trainingGateOpen=false`
- `methodCoverageAll=false`
- `snapshotCurrent=true`
- 引擎 ceiling 四字段全 false
重构前任务书记录:
```json
{
"canAdopt": true,
"selectionAllowed": true,
"nextAction": "complete_with_range",
"stopReason": "insufficient_dated_events"
}
```
当前输出:
```json
{
"canAdopt": false,
"selectionAllowed": false,
"proposeAllowed": false,
"canConfirmExactMinute": false,
"nextAction": "ask_fact_collection",
"sessionOutcome": "collect_evidence",
"stopReason": "insufficient_dated_events",
"canOfferRange": false
}
```
## 既有断言修改配额
按用户批准的新口径:
- 错误行为组:`6 / 6`
- 修改的既有断言:`10 / 12`
前三条先前未列明的断言为:
1. `frontend/tests/rectification-convergence-budget.test.ts:172``canAdopt true → false`;病灶 3,候选未分开且预算耗尽只能交付 review-only range。
2. `frontend/tests/rectification-convergence-budget.test.ts:412`label `blocked → user_history_verification_required`;病灶 1,空候选必须分类为证据不足而不是无原因 fail-open。
3. `frontend/tests/rectification-decide-next-action.test.ts:511``sessionOutcome adopt_representative → provisional_range`;病灶 4holdout unavailable 不能进入采用态。
完整清单(按错误行为组):
### 组 1:候选未分离却进入采用态(病灶 3)
1. `frontend/tests/rectification-answer-choice.test.ts:608``nextAction offer_provisional_range → ready_to_adopt`。原 fixture 为 `16/14`,回答后仍未达到 separation 门却依赖错误采用行为;fixture 调整为 `18/14`,但 prior lead 仍只有 4、单独并未达到 `MIN_SEPARATION_LEAD=8`;真正让该用例进入采用态的是 `applyHoldoutAnswer(..., "yes")` 更新 posterior 后满足 separation,并通过 holdout。正确采用态是 `ready_to_adopt`
2. `frontend/tests/rectification-convergence-budget.test.ts:172``canAdopt true → false`。预算耗尽不等于候选已分开。
### 组 2:空候选缺少停止分类(病灶 1)
3. `frontend/tests/rectification-convergence-budget.test.ts:412`label `blocked → user_history_verification_required`。空候选应继续收集日期证据。
### 组 3:无 holdout 的可信区间被当作可采用(病灶 4)
4. `frontend/tests/rectification-credible-range-projection.test.ts:146``nextAction ready_to_adopt → offer_provisional_range`。无 holdout 只能 review-only。
### 组 4:单候选但 holdout unavailable 被当作可采用(病灶 4)
5. `frontend/tests/rectification-decide-next-action.test.ts:511``sessionOutcome adopt_representative → provisional_range`。单候选不覆盖独立核对门。
### 组 5sticky holdout 失去日期精度后仍可采用(病灶 4)
6. `frontend/tests/rectification-holdout-renderable.test.ts:151``nextAction ready_to_adopt → offer_provisional_range`
7. `frontend/tests/rectification-holdout-renderable.test.ts:153``sessionOutcome adopt_representative → provisional_range`
8. `frontend/tests/rectification-holdout-renderable.test.ts:157``canAdopt true → false`
三条均由同一错误行为产生:sticky 只保留 holdout 身份,unknown precision 使其不可验证,必须 fail-closed。
### 组 6:用户停止绕过证据与 coverage 门(病灶 2
9. `frontend/tests/rectification-range-offer-deadend.test.ts:601``canAdopt true → false`。用户停止只允许展示区间。
10. `frontend/tests/rectification-range-offer-deadend.test.ts:608``canAdopt true → false`。两条同域日期事件仍不满足独立交付采用门。
`frontend/tests/rectification-holdout-renderable.test.ts``passed holdout``canAdopt === true` 保持不变;只补全 receipt fixture。
## 未放宽的门槛
- `MIN_STANDALONE_DATED_EVENTS = 3`
- `MIN_STANDALONE_DATED_DOMAINS = 2`
- `MIN_SEPARATION_LEAD = 8`
- 4 事件 / 3 domain 的 exact-minute confirmation gate
## 本地验收
- `./node_modules/.bin/tsx --test --test-name-pattern='^invariant [1-5]:' tests/rectification-decision-authority.test.ts``5/5` 通过。
- `./node_modules/.bin/tsx --test tests/rectification-answer-choice.test.ts``27/27` 通过。
- `./node_modules/.bin/tsx --test tests/rectification-*.test.ts``712 tests / 703 pass / 9 skipped / 0 fail`
- `./node_modules/.bin/tsc --noEmit`exit 0。
- `git diff --check`exit 0。
- `.gitea/workflows/**`:无改动。
## 任务 C:前端 fail-closed
- `parseRectificationCandidateResult()` 不再用 camelCase / snake_case 的 `||` 放行。字段缺失、非 boolean 或两个 alias 冲突时均为 `false`
- candidate snapshot 的 `selectionAllowed` / `canAdopt` 还必须与严格解析后的 engine receipt ceiling 取交集。receipt 缺失、字段非法、alias 冲突或明确否决时均不可采用。
- `rectification-agentic-chat.tsx` 在渲染 selection cards 前再次调用独立 receipt 防线;该防线是纵深防御,不替代后端 A+B。
## 任务 D-1:服务端问题槽是唯一问题源
- 零证据 opening 由 `buildMethodFollowupPlan()` 产生服务端 collect focus,并持久化到 `ConversationFocus.expectedAnswerSchema.prompt`
- opening 实际问题槽文案:
```text
请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。
```
- 三处旧兜底已处理:通用 collect 与 choice 降级路径复用上述第一问;无法形成 collect focus 的最后兜底改为非问句“我会继续根据现有资料整理下一步。”
- 未增加模型正文正则、问号检测或字符串匹配;模型正文不参与问题槽判定。
## 任务 D-2Skill `10.0.14`
-`versions/10.0.13` 创建完整快照 `versions/10.0.14`
- 根目录 `SKILL.md``versions/10.0.14/SKILL.md` 完全一致,frontmatter 均为 `10.0.14`
- 新约束保留“至多一个主问题”,并明确问题只能由服务端 `ConversationFocus` / 界面问题槽产生;Agent 回复正文不得自行提出、复述、改写或预告问题。
- registry 中 `10.0.13``deprecated``10.0.14` 为唯一 `active``RECTIFICATION_SKILL_VERSION = "10.0.14"`
- sha256 只对运行时包目录 `skills/jyotish-birth-time-rectification/versions/10.0.14/` 计算,最终值:
```text
60234b65badd0d69206d4a9c500d674005d1cb9aa2add58c133e538de4ad0546
```
## C+D 行为断言配额
- 错误行为组:`3 / 3`
- 行为断言:`3 / 6`
- 未修改既有行为断言。
1. C`rectification-candidate-result.test.ts` 新增 1 个行为组、1 条 `assert.deepEqual`,汇总覆盖 snapshot alias 冲突、receipt deny、receipt alias 冲突与 receipt 缺失。
2. D-1`rectification-server-focus.test.ts` 新增 1 个行为组、1 条 `assert.deepEqual`,同时锁定 opening narration、持久化 focus prompt 与问题槽投影。
3. D-2`rectification-v9-contracts.test.ts` 在既有 Skill 身份测试中新增 1 条 `assert.match`,锁定“服务端唯一问题源 + 正文不得自行提问”。
## 机械版本身份同步(不计行为配额)
以下文件只同步 `10.0.13 → 10.0.14`、对应 package path、fixture、测试标题或 sha256,未改变行为语义:
- `frontend/tests/rectification-ingest-p0.test.ts`
- `frontend/tests/rectification-v9-entry-routing.test.ts`
- `frontend/tests/rectification-collect-stall.test.ts`
- `frontend/tests/skill-registry.test.ts`
- `frontend/tests/rectification-occupation-coverage-exit.test.ts`
- `frontend/tests/rectification-confirmation-gate.test.ts`
- `frontend/tests/rectification-range-offer-deadend.test.ts`
- `frontend/tests/rectification-spoken-collect.test.ts`
- `frontend/tests/rectification-v9-contracts.test.ts`(版本身份部分)
- `frontend/tests/rectification-eight-method.test.ts`
- `frontend/tests/rectification-v9-agent.test.ts`
`rectification-v9-entry-routing.test.ts` 的新建 case fixture 同步为 `10.0.14`。本轮未顺手收敛六处“skill version stays...”反模式。
## C+D 本地验收
- 五条 decision-authority 不变量:`5/5` 通过。
- C/D 定向测试:`83/83` 通过。
- `tests/skill-registry.test.ts``16/16` 通过。
- `tests/rectification-v9-case-service.test.ts``23/23` 通过;新建 case 路径未触发 `skill_registry_version_mismatch`
- `tests/rectification-*.test.ts``714 tests / 714 pass / 0 skipped / 0 fail`
- `./node_modules/.bin/tsc --noEmit`exit 0。
- root Skill 与 `versions/10.0.14`:字节一致。
- registry / frontmatter / `RECTIFICATION_SKILL_VERSION` / 测试内版本与 sha256:一致。
- `git diff --check`exit 0。
- `.gitea/workflows/**`:无改动。
## 未执行
- staging 部署与真实 authenticated flow 验证。
+65
View File
@@ -0,0 +1,65 @@
# PROGRESS · personal report sectioning · 2026-08-30
## Task 0 gate — observed after staging deployment
- Telemetry commit: `1f1fdb1d155e472118ad7d1d4c49b1b5cb95bcb2`.
- Deployed staging SHA verified at `https://staging.jyotisha.chat/api/health`: `37c0c139656c5eab7150221e8c09d9aa6fcf95b8`.
- Observation window: 2026-08-30 20:4620:51 Asia/Taipei, one real authenticated “生成完整报告” run.
- User-visible result: report did not complete; the report center shows `report_schema_invalid`.
- Server event: one `[personal-report]` `generation_failed` with `innerReason=final_parse_rejected`.
### Telemetry sample (privacy-safe fields only)
| observed agent attempts | outcome | finishReason | inputTokens | outputTokens | totalTokens | repairAttempted |
| ---: | --- | --- | ---: | ---: | ---: | --- |
| 1 | resolved | `stop` | 4,772 | 3,069 | 7,841 | false |
### Gate metrics from this sample
- Report failures: **1**.
- Agent telemetry attempts observed: **1**.
- `finishReason="length"`: **0 / 1 = 0%** of observed agent attempts; **0 / 0 = not applicable** when restricted to agent attempts that themselves failed.
- `outputTokens` p50: **3,069**.
- `outputTokens` p95: **3,069**.
- Important attribution: this run's agent call resolved with `finishReason="stop"`; the later report-level final parse failed. Therefore this sample does not support attributing the report failure to truncation.
## Gate decision
Task 0 observation is complete. No output cap, schema relaxation, or schema tightening was made from the estimate in the task brief. The observed failure is recorded as a report-level parse failure, not a length failure. It is now permissible to begin Tasks 14, subject to the Docker/database stop rule and the remaining quality gates.
## Task 14
- Not started at the time of this entry.
## Task 14 implementation
- Task 1: added `public.personal_report_sections` in migration `20260830020000_personal_report_sections.sql`. The migration is transactional and idempotent, enables RLS, grants authenticated read-only access to own rows, grants service-owned writes, and exposes security-definer section state functions (`ensure`, `start`, `complete`, `block`). `report_document` remains final-output-only.
- Task 2: generation is now serial `plan → section:<id> × N → summary → assemble → ready`. Each section receives only the evidence refs declared by its plan entry plus completed section titles; summary receives only completed titles and `claimStatus`. Existing ready section rows are reused on resume and are not regenerated.
- Task 3: section retry exhaustion marks that section `blocked` with an allowlisted error category, continues other sections, and assembles an explicit disclosure. A report fails only when every write section is blocked or summary/assembly fails.
- Task 4: worker updates `progress_percent` and `progress_phase`; the report page renders phase-aware progress while preserving timed-out/resume/report-center states.
## Validation
- `./node_modules/.bin/tsc --noEmit --pretty false`: passed.
- `npm run lint`: passed with 0 errors and 24 pre-existing warnings.
- Focused sectioned generation, resume, blocked fallback, polling, and database tests: 33 passed, 0 failed.
- `npm run test:db`: passed with 34 passed, 0 failed (Docker-backed).
- Full `./node_modules/.bin/tsx --test tests/*.test.ts`: exit 1 in this local run. The report-polling assertion that locked the old fixed loading copy was the only failure caused by this change and was updated with the required explanation; the remaining observed failures were existing local environment/resource failures (parallel PostgreSQL fixture/migration startup and missing Python `yaml` module), listed for baseline comparison before final push.
- `npm run db:migrate:check`: passed in isolated Docker PostgreSQL; apply exited 0, check exited 0, and reapply exited 0 with `already applied 20260830020000_personal_report_sections.sql`.
- `npm run build`: passed; Next.js production build completed with 5 pre-existing Turbopack dynamic-filesystem warnings.
### Full-test comparison
- `./node_modules/.bin/tsx --test tests/*.test.ts`: exited 1 in the final local run. The report-section focused tests remained green; no new report-generation or section-service failure was observed. The failing tests were outside this change and/or local-environment dependent:
- `service and restricted admin database identities stay separated`
- `v9 legacy backfill maps statuses, keeps one resumable per user and is idempotent`
- `ingest P0: education kinds, batch confirm, opening focus reuse, precision lock`
- `PR-4 candidate decisions use server UUIDs, receipt-derived gates and separate acceptance/confirmation`
- `changed staging workflows are syntactically valid YAML` (local Python missing `yaml`)
- additional database suites reported PostgreSQL fixture shutdown/permission/resource failures during the long parallel run.
- These failures are not being reclassified as code-green; the focused acceptance suite and the required Docker-backed `npm run test:db` are the evidence for this change.
## Delivery
- Task 14 commit: pending local commit.
- Staging push/deploy: pending.
+296
View File
@@ -0,0 +1,296 @@
# 任务书 · 计费闭环与功能级定价(2026-08-30)
基线:`origin/staging` @ `ff5998f1`
**下面所有行号只是线索,请按选择器/函数名定位**,后续提交可能让行号偏移。
## 为什么要做
产品侧已定的商业规则:
1. **1 元人民币 = 10 积分**
2. **生时校正与报告生成单独计费**,不含在会员权益里(会员只享折扣价)。
3. **会员档的卖点是"聊天随便聊"**,即不设总量配额,只做速率与熔断。
代码现状与这三条全部对不上,且有两处是**真金白银的漏洞**:
| 功能 | 代码里实际发生的事 | 位置 |
| --- | --- | --- |
| `chat.standard` | 每条消息扣 `model.creditCost` | `src/app/api/consult/route.ts:369` |
| `rectification` | **整个 case 只扣一次**,扣的还是 `model.creditCost`(默认 1 点) | `src/app/api/rectification/agent/route.ts:572``rectificationBillingRequestId()` 让 request_id 按 case 幂等 |
| `report.full` | **完全没有接计费**。只有 `PERSONAL_REPORT_DAILY_LIMIT` 日限(默认 5 | `src/lib/personal-report-route-core.ts:308` 之后直接 `createGenerating` |
`report.full` 这个 featureKey 已经在 `src/lib/consultation-billing.ts``authorizeUsage` 类型里、在 `src/app/api/admin/products/route.ts:11` 的 enum 里、在 `product_entitlements` 的种子数据里 —— 唯独没有任何代码调用它。
还有一个结构问题:**定价当前绑在模型上,不是绑在功能上**。`authorize_usage``p_credit_cost` 是入参,但两个调用点传的都是 `model.creditCost`。这意味着"一次校正"和"一条对话"在定价上无法区分,而它们的真实成本差约一个数量级。调价必须改模型配置或改代码,做不了产品实验。
## 这不是"加一个价格常量"的任务
不要在路由里写 `const RECTIFICATION_CREDITS = 1980`。定价必须是**服务端可配置的数据**,和 `billing_products` / `product_entitlements` 一样由 admin 改、有版本、有审计。任务 3 是本轮的结构主线,任务 1、2 依赖它。
---
## 硬红线
1. **任务 0 是门控。** 真实单位成本必须先测出来,才允许往数据库里写任何价格数字。**不得把本任务书里出现的任何金额当成已定价格** —— 除了"1 元 = 10 积分"这一条,其余都是待校准的产品输入。
2. **钱的红线:不允许出现"扣了费但服务没交付",也不允许出现"服务交付了但没扣费"。** 每条计费路径都必须是 `reserve → (成功) complete / (失败) release` 的完整三态,任何 early return、抛异常、超时分支都要覆盖到。
3. **所有计费调用必须幂等**,键是 `request_id`。报告用 `payload.requestId`,校正沿用既有的 `rectification:case:<caseId>` 前缀规则,不得新造第二套幂等键。
4. **不得放宽 `authorize_usage` / `complete_usage` / `release_usage` 的任何校验**,包括公平使用检查、配额检查、`model_not_included` 检查。只可收紧。
5. **价格一律不得出现在前端代码里。** 前端只能展示服务端返回的价格。`src/lib/membership.ts` 里已有的 `formatPrice` 是格式化函数,不是价格来源。
6. **生产库的价格不得手改。** 所有价格与公平使用参数变更走 migration 或既有的 `admin_save_product_draft` 审计路径。
7. **不得修改既有测试断言** —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
8. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`
9. **数据库测试必须真跑。** 本轮动表结构和 SQL 函数,`npm run test:db` 需要 Docker。**没有 Docker 就不要推** —— 把环境缺口写进 `BLOCKED.md` 并停下,不允许"本地跑不了所以跳过"。
10. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**钱不出错 > 数据不损坏 > 功能与测试不回归 > 可验证的改进 > 成本 > 代码整洁**。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/billing-pricing-20260830 \
../.worktrees/billing-pricing-20260830 origin/staging
```
基线必须是 `origin/staging`。读 `pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索计费/积分相关记录。
**先读这几个文件再动手:**
- `src/lib/consultation-billing.ts` —— `authorizeUsage` / `completeUsage` / `releaseUsage` 的客户端封装与 `featureKey` 联合类型
- `supabase/migrations/20260806030000_settle_order_usage_authorization.sql` —— 三个 SQL 函数的真实语义,特别是 260–340 行的公平使用与配额判定顺序
- `supabase/migrations/20260806020000_billing_products_subscriptions.sql:104-155` —— 已种下的商品与权益,含 `minuteLimit` / `dayLimit` / `billingLimit`
- `src/lib/personal-report-route-core.ts:203-330` —— `resolveReportCreate` 的依赖注入形状与日限检查
- `src/lib/personal-report-worker-core.ts` —— `completeReportReady` / `completeReportFailed` 两个终态
- `src/mastra/personal-report.ts:385-405` —— `logTelemetry`,目前 token 用量只 `console.info`,没有回传
- `src/app/api/rectification/agent/route.ts:95-120, 558-610` —— 校正的 reserve / complete / release 现状
---
## 任务 0(P0,门控)· 把真实单位成本测出来
### 事实
`usage_ledger` 已经在记 `input_tokens` / `output_tokens` / `cost_microusd` / `duration_ms`(由 `complete_usage` 写入),`src/app/api/admin/usage/route.ts` 已有只读列表接口。但:
- `report.full` 从未产生过一行(没接计费),所以报告的真实成本**目前无法从账本读出**。
- `rectification` 每个 case 只有一行,`cost_microusd` 由路由自己按 `inputCostMicrousdPerMillion` 算好后写入 —— 需要确认它累加了 case 内**全部轮次**,还是只有最后一次。这一条必须先查清楚,它决定校正成本的真实量级。
### 要做什么
1.`src/app/api/admin/usage/` 下新增一个只读聚合端点(沿用 `requirePermission("billing.orders.read")`),按 `feature_key` 返回近 30 天的 `runs / avg / p50 / p95 / max``cost_microusd``input_tokens``output_tokens``duration_ms`
2.`src/app/api/rectification/agent/route.ts``complete` 回调,确认 `usage.inputTokens` 是单次 agent run 还是 case 累计。**把结论写进 PROGRESS**,如果是单次,说明账本低估了校正成本,需要在任务 2 里一并修成累计。
3. 不要为了这个任务改任何价格、任何 schema。
### 验收
- 端点返回真实数据,PROGRESS 里贴出 `chat.standard``rectification` 的 p50/p95 成本(脱敏,只要聚合值)。
- `report.full` 的行数为 0 被明确记录 —— 这是任务 1 上线后才会有数的已知空缺。
- 校正 token 口径的结论写进 PROGRESS。
- 聚合端点的返回形状要一次设计到位:**任务 6 的测算页直接消费它**,所以除了均值还必须给出 `p50` / `p95` / `max``runs`,缺数据时返回明确的空态而不是 0。
**任务 1–5 在任务 0 出数之前不得写入任何价格数字**,但可以先做结构(表、函数、接线),价格字段留空或用占位符并在 PROGRESS 标注。
---
## 任务 1(P0)· 报告接入计费
### 事实
`resolveReportCreate``src/lib/personal-report-route-core.ts:203`)在 `countCreatedToday() >= dailyLimit` 之后直接进入 `deps.persistence.createGenerating(createInput)`,全程没有 `authorizeUsage`。报告生成是异步的:路由入队,`personal-report-worker-core.ts` 消费。
`src/mastra/personal-report.ts:385``logTelemetry` 只把 token 用量打到 `console.info`,**没有返回给调用方**。报告现在是分章节串行生成(plan → N 章 → 摘要),所以结算需要的是**跨全部章节累加**后的 token 数。
### 要做什么
1.`ReportCreateCoreDeps` 增加一个 `billing` port(可选,测试可省略以保持既有内联测试可跑),形状与 `consultation-billing.ts` 的三个函数一致。**不要在 core 里直接 import Supabase client** —— 这个文件是纯 core,依赖靠注入,保持现有风格。
2. 在日限检查**之后**、`createGenerating` **之前**`authorizeUsage({ featureKey: "report.full", requestId: payload.requestId, ... })`
- 失败且 `reason === "insufficient_credits"` → HTTP **402**,与 `src/app/api/consult/route.ts:451` 的现有约定一致。
- 其他失败 → 503。
- 错误码走 `REPORT_STABLE_CODES`,需要新增就新增,不要复用语义不符的既有码。
3. `createGenerating` 或入队本身失败 → 必须 `releaseUsage`,不能吞掉。
4.`personal-report.ts` 的章节生成把 token 用量**回传**(不是只打日志),在 generation 层累加成一份 `{ inputTokens, outputTokens }`
5. worker 的两个终态接上结算:
- `completeReportReady``completeUsage``costMicrousd``inputCostMicrousdPerMillion` / `outputCostMicrousdPerMillion` 计算,算法照抄 `src/app/api/rectification/agent/route.ts:595-603`
- `completeReportFailed``releaseUsage`,理由用稳定错误码。
6. **重放路径要走通**`replayOrConflict` 命中既有报告时**不得重复扣费**`authorize_usage` 已按 request_id 幂等,但要确认命中的是幂等返回而不是第二次扣款,并写测试锁住)。
### 验收
- 余额为 0 的用户请求报告 → 402,且**没有**创建 `report_document` 行、没有入队。
- 生成成功 → `usage_ledger` 出现一行 `feature_key='report.full'`token 数等于全部章节之和。
- 生成失败 → 积分退回,`credit_transactions` 有对应的释放记录。
- 同一 `requestId` 连续提交两次 → 只扣一次费。
- 会员持有 `report.full` 配额时走 `source='subscription'`,不扣积分。
---
## 任务 2(P0)· 校正按功能定价计费,并修正成本口径
### 事实
`src/app/api/rectification/agent/route.ts:572` 传的是 `creditCost: selectedModel.creditCost`。默认模型的 `credit_cost` 是 1`20260806040000_model_configuration.sql:86` 的列默认值),所以**一整例生时校正和一条对话收一样的钱**。
计费本身的时机是对的:只有 `action === "message"` 才 reserve,且 `rectificationBillingRequestId()``rectification:case:<caseId>` 前缀做 case 级幂等。**这个机制不要改**,要改的只是金额来源。
### 要做什么
1. `creditCost` 改为从任务 3 的功能定价表解析,不再取 `selectedModel.creditCost`
2. 依据任务 0 的结论修正 `complete` 回调的成本口径:如果当前只写了单次 agent run 的 token,改成写入 case 内累计值(`complete_usage` 对同一 request_id 的多次调用行为需要先确认;若它不支持累加,就在应用侧累计后只结算一次)。
3. 首轮 reserve 失败时的用户提示要能区分"积分不足"和"会员配额已用完",不要都返回同一句话。
### 验收
- 一例校正扣的积分等于功能定价表里的值,与对话单价解耦。
- 同一 case 多轮对话仍然只扣一次费(既有幂等不被破坏)。
- `usage_ledger` 里校正行的 token 数与该 case 的真实总消耗一致。
- 既有的校正流程测试全绿。
---
## 任务 3(P0,结构主线)· 功能级定价改为服务端可配置
### 事实
现在没有任何地方能表达"功能 × 模型档位 → 积分价"。`model_config_versions.credit_cost` 只能表达"模型档位 → 积分价"。
### 要做什么
1. 新增 `public.feature_pricing`(或等价命名),至少含:`feature_key``model_tier``credit_cost``version``status``enabled``effective_from`、审计列。**必须沿用本仓库既有模式**:迁移事务化、幂等、开 RLS、authenticated 只读自己该看的、service 角色写、变更走 security definer 函数并带 `p_reason` 审计(照抄 `admin_save_product_draft` 的形状,见 `20260806020000_billing_products_subscriptions.sql:159`)。
2. 服务端解析函数:给定 `featureKey` 与已选模型,返回应扣积分。没有配置时的行为必须是**明确失败**,不是静默回落到 1 —— 静默回落正是当前这个 bug 的成因。
3. 接入 admin 后台(`src/components/admin/` 下已有 `package-management` / `product-management` / `model-management` 的成对形状,照着做)。
4.`chat.standard``chat.premium``rectification``report.full` 四个 featureKey 接过来。`report.export``profile.extra` 建表时预留,本轮不接线。
### 验收
- 改一个功能的积分价,不需要发版、不需要改模型配置。
- 变更留下 admin 审计记录。
- 缺配置时请求明确报错,不会按 1 积分放行。
- `npm run db:migrate:check` 在隔离 Docker PostgreSQL 里 apply / check / reapply 三次均为 0。
---
## 任务 4(P1)· 接上下文缓存
### 事实
`src/lib/agent-generation-settings.ts` 里没有任何 `cache_control` 或缓存相关的 `providerOptions`。而:
- 校正的 system message 每个 attempt 都全量重发绑定 Skill 全文(`skills/jyotish-birth-time-rectification/versions/10.0.9/SKILL.md` = 8,183 字符),见 `src/lib/rectification-agentic/v9/agent-run.ts:1064-1073``bootstrap`
- agentic 循环每一步都是一次完整模型调用,前缀(system + skill + 历史)随步数重复发送。校正每轮 6–12 步(`RECTIFICATION_AGENT_STEP_BUDGETS`),对话 8 步(`AGENT_MAX_STEPS`)。
这个前缀是**逐字稳定**的,是上下文缓存的理想对象。
### 要做什么
1. 查清当前生产 provider 的上下文缓存契约(缓存怎么声明、命中怎么计价、最小可缓存长度),**以 provider 官方文档为准,不要凭记忆写**。`model_providers` 表里有 `provider_type`,需要按 provider 分支处理,不支持缓存的 provider 必须安全降级为现状行为。
2.`agentGenerationSettings` 里加缓存声明,保证 system + skill 前缀落在缓存边界内。
3. 缓存命中率与命中/未命中的 token 数要落进 `usage_ledger`(新增列或写进既有字段的结构化部分,二选一并在 PROGRESS 说明理由)。
### 验收
- 同一 case 的第二轮起,账本能看到缓存命中。
- 校正与对话的 p50 `cost_microusd` 相对任务 0 的基线**有可测量的下降**,降幅写进 PROGRESS。
- 不支持缓存的 provider 行为不变,无报错。
**这个任务是"随便聊"会员档能否成立的成本前提**,产品侧的会员定价要等这里的实测降幅才能定死。
---
## 任务 5(P1)· 会员档参数与"随便聊"的可兑现定义
### 事实
`20260806020000_billing_products_subscriptions.sql:145-152` 种下的公平使用参数:
| 商品 | 售价 | minuteLimit | dayLimit | billingLimit |
| --- | ---: | ---: | ---: | ---: |
| 标准月卡 | ¥99 | 6 | 100 | 2000 |
| 标准年卡 | ¥599 | 6 | 100 | 24000 |
| Pro 月卡(未启用) | ¥299 | 10 | 200 | 5000 |
按任务 0 的实测单价核算,这几个 `billingLimit` 很可能**高于售价本身**,即上限被打满时单个会员是净亏的。三个限制都在 `authorize_usage` 里生效(`20260806030000:267-269`),改它们只需要改 `product_entitlements.metadata`
产品侧已定:会员档卖点是"聊天随便聊",且校正与报告**不含在会员权益内**,只给会员折扣价。
### 要做什么
1. 写一个迁移,按任务 0 的实测成本重设 `minuteLimit` / `dayLimit` / `billingLimit`,并把 `standard_monthly` / `standard_yearly``rectification``report.full` 权益行**移除**(改为单独计费)。移除前确认没有存量订阅正依赖这些配额;有的话必须给出兼容处理,不能让在期会员的权益凭空消失 —— 这一条属于红线 2。
2. `billingLimit` 的语义要在代码注释和 admin 文案里写清楚:它是**熔断**,不是对用户承诺的配额。"随便聊"对外表述为不限总量,实现上靠 `minuteLimit` + `dayLimit` 兜住真人上限,`billingLimit` 只拦异常账号。
3. 触发 `fair_use_billing_period` 时的用户提示不得写成"配额用完了" —— 与"随便聊"的售卖承诺矛盾。文案交产品定,代码留可配置。
4. 售价(`billing_products.price_cents`)本轮**不要动**,等产品侧给最终数字。
### 验收
- 迁移幂等,`db:migrate:check` 三次通过。
- 在期会员的权益变更有明确的兼容路径并在 PROGRESS 写清。
- 公平使用被触发时返回的 `reason``retry_after_seconds` 正确,前端提示不与"随便聊"冲突。
---
## 任务 6(P1)· 管理端定价测算页
### 为什么放在管理端而不是做成一次性文档
定价不是定一次就完了:模型换一家、上下文缓存接上(任务 4)、用量画像随用户增长漂移,任何一个变化都会让上一版的毛利表作废。所以测算必须**长在系统里、读实时数据**,而不是躺在某份 md 里。
### 放哪里
产品要求放在"模型那里"。做成独立的 Refine resourcenav 位置紧挨模型配置:
- 资源注册:`src/components/admin/admin-app.tsx``resources` 数组,插在 `models`(模型配置)与 `model-releases`(模型发布)**之间**`{ name: "pricing-simulator", list: "/admin/pricing-simulator", meta: { label: "定价测算", icon: ... } }`。图标从 `@ant-design/icons` 现有引入里选,不要新引。
- 页面:`src/app/admin/pricing-simulator/page.tsx`,保持本仓库一行页面的写法(照抄 `src/app/admin/models/page.tsx`)。
- 组件:`src/components/admin/pricing-simulator.tsx`,形状照 `model-management.tsx``"use client"` + antd + `adminRequestJson`)。
**不要塞进 `model-management.tsx`。** 那个文件已经够大,且测算要的数据来自三张不同的表,跟模型 CRUD 不是一个关注点。
### 数据源(三个,全部服务端算,浏览器不碰价格)
1. **模型单价** —— `model_config_versions.input_cost_microusd_per_million` / `output_cost_microusd_per_million`,只取 `status='published' and enabled`
2. **实测用量** —— 任务 0 那个聚合端点,按 `feature_key``runs / p50 / p95 / max` 的 token 数与 `cost_microusd`
3. **当前定价** —— 任务 3 的 `feature_pricing`,以及 `billing_products.price_cents``product_entitlements.metadata` 里的 `minuteLimit` / `dayLimit` / `billingLimit`
三者都有现成的 admin 端点或表,**不要新建业务表**。若需要新端点,只加只读聚合,沿用 `queryAdminRows` + `adminErrorResponse` 的既有形状。
### 三组输入(可调,默认值全部来自上面的实时数据)
| 组 | 内容 | 默认值来源 |
| --- | --- | --- |
| A 模型单价 | 每百万 token 的输入价 / 输出价 | 当前 published 模型配置 |
| B 用量画像 | 每个功能一次调用的 in / out token;会员月均对话条数的分布(4 档:占比 × 条数) | 账本 p50(分布权重无实测时留空,见下) |
| C 定价 | 每功能积分价、会员月卡/年卡售价、`minuteLimit` / `dayLimit` / `billingLimit`、固定成本(VPS + Supabase,元/月) | `feature_pricing``billing_products` |
积分与人民币的换算固定为 **1 元 = 10 积分**,这条是产品已定的商业规则,做成常量并注明,不要做成可调滑块。
### 实时输出
1. **单功能表**`chat.standard` / `chat.premium` / `rectification` / `report.full` 各自的单位成本、单位售价、毛利率。
2. **会员档表**:按 B 组的分布算加权平均成本与平均毛利;再按 `billingLimit` 打满算最坏成本与最坏毛利。**两行都要出**——平均值永远好看,风险全在最坏那行,页面不能只显示平均值。
3. **盈亏平衡**:固定成本 ÷ 单张月卡毛利 = 需要多少张月卡。
4. **容量上限**:按 `CONSULTATION_DOMAIN_DURATION_MS`21s`src/mastra/consultation-tools.ts:56`)与 Python 侧单进程串行的事实,算日吞吐上限,并换算成"当前会员用量画像下这台机器能承载多少会员"。这个数跟钱无关但跟定价决策直接相关——承诺"随便聊"之前必须先看它。
### 硬要求
1. **只读。这个页面绝不能写任何价格。** 改价一律走任务 3 的审计路径和 `admin_save_product_draft`。一个测算页面变成调价入口,等于绕过审计,属于红线 6。页面上可以放跳转到对应管理页的链接,不放保存按钮。
2. **实测与假设必须在视觉上可区分。** 每个默认值旁边标注它是来自账本实测还是用户假设;用户拖动滑块后该项要立刻标为"假设"。这一条直接服务于红线 1——不能让人看着一屏估算数字误以为是实测。
3. **账本无数据时显示"无实测数据",不得静默填入估算值。** `report.full` 在任务 1 上线前必然是这个状态,这是正确表现,不是 bug。
4. **不引入新依赖。** 本仓库没有图表库(`package.json` 里只有 antd 5 与 refine),为一个后台页面装 `@ant-design/plots` 不划算。用 antd 的 `Table` / `Statistic` / `Descriptions` / `Progress` 表达即可,需要趋势就用表格。
5. 权限:读账本需要 `billing.orders.read`,读模型价需要 `models.read`。**缺其一时把对应数据块降级为不可用,而不是整页 403** —— 参照 `adminAccessControlProvider` 的既有处理。
6. 计算逻辑放进可单测的纯函数模块(例如 `src/lib/pricing-simulation.ts`),组件只负责取数与渲染。测算公式必须有单元测试,覆盖:分布权重合计不为 1、`billingLimit` 为 null(不限)、模型单价为 0、账本无数据这四种边界。
### 验收
- 页面在 `/admin/pricing-simulator` 可达,nav 上位于「模型配置」与「模型发布」之间。
- 首屏默认值全部来自实时数据;未拖动任何滑块时,显示的毛利率就是当前真实配置下的真实毛利率。
- 改 A/B/C 任一输入,四张输出表实时重算,无需刷新。
- 页面无任何写操作端点调用。
- `src/lib/pricing-simulation.ts` 的单测覆盖上述四种边界,全绿。
---
## 交付
- PROGRESS 写进 `PROGRESS-billing-pricing-20260830.md`,按任务分节,任务 0 的实测数据单列。
- 每个任务一个提交,提交信息说明改的是哪条计费路径。
- 推 staging 前:`./node_modules/.bin/tsc --noEmit` 通过、`npm run lint` 无新增 error、`npm run test:db` 在 Docker 下 fail=0、`npm run build` 通过。
- 全量 `./node_modules/.bin/tsx --test tests/*.test.ts` 的失败清单要与基线逐条比对,确认无新增。
+146
View File
@@ -0,0 +1,146 @@
# 任务书 · 生时校正 run_failed 与开场轮问题槽为空(2026-08-31)
基线:`origin/staging` @ `9492bae1`"docs: record round-1 audit and open round-2 brief")。
本轮两条必做任务来自一次线上故障的代码溯源,根因已定位到具体行,**不需要重新排查**。第三条是配置缺口,只出方案不动手。
---
## 现象
用户开始生时校正,助手正常提出第一个问题;用户回答第一条自然语言证据("2016 年 9 月上大学")后,前端立刻返回:
```json
{"type":"error","code":"run_failed","message":"生时校正暂时不可用,请稍后再试。"}
```
同时问题槽显示"当前没有可回答的问题,正在等待服务端更新。"
案例 `bf7dafa9-6c12-424f-b21f-625d5497cbf2` 快照特征:`case_revision: 0``turns` 只有 1 条、`last_activity_at` 仍等于第 1 轮的 `created_at``current_question: null``evidence: []`。说明第 2 轮在 `append_agentic_rectification_turn` 落库之前就抛错了。
## 这不是逻辑回归
已实测:`./node_modules/.bin/tsc --noEmit` exit 0TypeScript 5.9.3);`npx tsx --test tests/rectification-*.test.ts` 705 条全绿(696 pass / 9 skipped / 0 fail / 0 error)。最近的 `3a4396a4`(stop 语义)与本次故障无关,**不要去动它**。
---
## 硬红线
1. **不得修改 `feature_pricing` 的 SQL 函数签名或校验逻辑**`supabase/migrations/20260831010000_feature_pricing.sql`)。`resolve_feature_pricing``feature_pricing_missing` 是**正确行为** —— 缺定价就该拒绝计费,不能默默按 0 或默认价放行。本轮修的是错误如何被映射和呈现,不是要绕过这个闸门。
2. **不得在 migration 里硬编码价格 seed。** 定价是 admin 可配置的业务数据(`admin_save_feature_pricing_draft` / `admin_publish_feature_pricing`),写死会和发布流冲突,并且会让"生产环境用了测试价"这类事故无声发生。
3. **不得修改 `src/lib/rectification-agentic/core/rectification-decision.ts`** 及其 stop 语义相关逻辑。那是 `3a4396a4` 刚落地的,与本次故障无关。
4. **不得把内部错误原文透给用户。** `consult-safety.ts` 的既有约束继续生效。任务 A 要让错误**可诊断**(服务端日志)且**可行动**(用户文案),这是两件事,不要混为一谈。
5. **不得修改既有测试断言** —— 除非该断言锁住的正是本轮要修的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 里单列。
6. 不得手写 `useCallback` / `useMemo`
7. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`** —— 新建 worktree 里 `node_modules` 未安装时 npx 会去装空包 `tsc@2.0.4`(本仓库既有教训)。
8. `tests/rectification-*.test.ts` 不得低于基线 **705**,且 `fail=0`。全量 `npm test` 需要 Docker,本机无 Docker 时会有既有的数据库/部署类失败 —— **必须逐条比对失败清单,确认没有新增**
9. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:计费正确性 > 功能与测试不回归 > 错误可诊断 > 用户文案 > 代码整洁。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-billing-20260831 \
../.worktrees/rectification-billing-20260831 origin/staging
```
基线必须是 `origin/staging`,不是任何本地 ref。读 `docs/research/pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`(Next.js 版本与训练数据不同,写代码前先看 `node_modules/next/dist/docs/`)。改前先在 `docs/BUG_HISTORY.md` 检索同类记录。
**下面所有行号只是线索,请按符号名定位**`origin/staging` 上的行号可能有偏移。
---
## 任务 AP0)· `billing_unavailable` 没有用户可见映射
### 事实(已逐行确认)
今天的提交 `aaaa5fb7 feat(billing): add feature pricing configuration`2026-08-31 03:37)在每个付费校正轮前插入了定价查询。故障发生在 05:44。完整链路:
1. `src/app/api/rectification/agent/route.ts:567``billing.reserve()``resolveFeaturePricing(accounting, "rectification", selectedModel.id)`
2. `supabase/migrations/20260831010000_feature_pricing.sql:69` — 无 `('rectification', <model_tier>)``status='published' and enabled` 的行时抛 `feature_pricing_missing`
3. **该 migration 建的是空表**,全仓库无任何 seed insert,行只能由 admin 发布流产生 → staging 上就是没有
4. `src/lib/feature-pricing.ts:46` 包装为 `FeaturePricingError``reserve()` 捕获后返回 `reason: "billing_unavailable"`route.ts:583
5. `src/lib/rectification-agentic/v9/agent-run.ts:263``throw new RectificationToolServiceError(reserve.reason ?? "billing_denied")`
6. route.ts:702-721 的 catch 映射链**没有 `billing_unavailable` 分支**,逐个 else-if 落空后进兜底 `run_failed`
第 1 轮能成功、第 2 轮才炸,是因为 `reserve()` 开头有 `if (action !== "message") return { success: true, status: 200 }` —— `opening` 轮免费、根本不查定价。用户的第一条消息是第一个 `message` 动作,也就是第一个撞上定价查询的请求。这与"`turns` 只有 1 条 + `case_revision: 0`"完全吻合。
### 要做的
**A-1 补映射。** 在 route.ts 的 catch 映射块里为 `billing_unavailable` 加独立分支,与既有 `billing_denied` 并列。配置缺失**不可自愈**,文案不能是"请稍后再试"(用户重试一万次也不会好),要引导联系支持 —— 参考同一块里 `skill_identity_missing` 分支的语气。
**A-2 让日志可诊断。** `reserve()` 的 catchroute.ts:581-584)当前只记 `error.name`,日志里只能看到 `FeaturePricingError`,把 `feature_pricing_missing`(没配价)和 `feature_pricing_model_unavailable`(模型没发布版本)这两种**排查动作完全不同**的情况吞掉了。`FeaturePricingError``src/lib/feature-pricing.ts:19` 已带 `code` 字段,改成记录它,并让返回的 `reason` 能区分这两种。用户侧文案不必区分,服务端日志必须区分。
**A-3 同类暴露面。** 已确认以下两处有同样问题,一并按一致方式修,**但不要顺手重构这两个路由的其他部分**:
| 位置 | 现状 |
| --- | --- |
| `src/app/api/consult/route.ts:375` | 直接调 `resolveFeaturePricing(accounting, "chat.standard", model.id)`,外层只判断 `error_code === "insufficient_credits"``FeaturePricingError` 会穿透到通用 catch |
| `src/app/api/reports/route.ts:202` | `billing.reserve` 里直接调 `resolveFeaturePricing(admin, "report.full", modelId)`,无任何针对性映射 |
---
## 任务 B(P0)· 开场轮不持久化问题槽
### 事实
`current_question` 为 null,导致 `src/components/rectification-agentic-chat.tsx:1084``showMissingQuestion` 为真,用户看到"当前没有可回答的问题,正在等待服务端更新。"
原因:`persistNextInterviewIfIdle`route.ts:677)被包在 `if (action === "message")` 里,`opening` 轮不会执行。开场轮的工具回执佐证了这点 —— 只跑了 `rectification-read-case`,没有任何能生成 `collect_spoken` focus 的工具,助手的问题只以散文形式出现在回复正文里。
`action` 枚举见 route.ts:69`["opening", "message", "read_only", "answer_choice", "stop_and_review"]`
**这是独立于计费的缺陷。任务 A 修完、定价行发布之后,它依然存在。**
### 决策点 —— 动手前先在 PROGRESS 里写明你选哪个及理由
- **方案 1(建议)**:让 `opening` 轮成功后也走 `persistNextInterviewIfIdle`。理由:这条路径是服务端确定性的,不依赖模型是否想起来调工具;且 `ensureNonTerminalTurnExit` 已是同一分支里的同类先例。
- **方案 2**:让 agent 在开场轮自行调用持久化 focus 的工具,需改 skill prompt`jyotish-birth-time-rectification@10.0.13`)。注意 skill 包有 sha256 绑定校验(`agent-run.ts:252` `resolveExactSkillPackage`),改 prompt 意味着要发新版本并处理已有 case 的 `skill_identity_mismatch`,成本显著更高。
若选方案 1`ensureNonTerminalTurnExit` 当前与 `persistNextInterviewIfIdle` 在同一个 `action === "message"` 分支内 —— **请单独判断它是否也该放开,不要默认捆绑修改**,并把判断写进 PROGRESS。
---
## 任务 C(P1)· 定价数据缺口 —— 只出方案,不要实现
`feature_pricing` 表出厂为空。任何环境部署后若不手动发布定价行,`rectification``chat.standard``report.full` 三条付费链路**全部**会挂,且(在任务 A 修完前)都表现为语焉不详的通用错误。
请评估并**在 PR 描述里提出建议,本轮不要写实现**:
- 是否需要环境就绪检查,在定价行缺失时**早失败**并给出明确信息,而不是等用户点到第二轮才炸
- 该检查放在哪里:`scripts/db-migrate.mjs` 之后的独立脚本?还是应用启动期?
- 已发布的 `feature_key``model_tier` 组合应如何做覆盖度校验(三个 feature × 该环境所有已发布模型的 tier)
注意红线 2:不要用 seed migration 解决这个问题。
---
## 验收标准
1. `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0
2. `cd frontend && npx tsx --test tests/rectification-*.test.ts` 不低于 705 条且 `fail=0`
3. 任务 A 补测试:断言 `billing_unavailable` 不再落到 `run_failed` 兜底,且 `feature_pricing_missing` / `feature_pricing_model_unavailable` 在服务端日志里可区分
4. 任务 B 补测试:断言 `opening` 轮结束后 `current_question` 非空
5. 任务 C 的方案写在 PR 描述里,仓库内无对应实现改动
## 交付前必须说明
本任务书的根因链是**静态溯源**得出的,作者没有 staging 凭据(仓库内无 `.env`),**未在真实环境验证**。你若同样无凭据,不要声称已验证。staging 侧仍需人工确认两件事:
```sql
select feature_key, model_tier, credit_cost, version, status, enabled
from public.feature_pricing where feature_key = 'rectification';
-- 预期:零行
```
日志中应能找到:
```
[rectification-v9] reserve failed case=bf7dafa9-6c12-424f-b21f-625d5497cbf2 reason=FeaturePricingError
```
**若这两条都不成立,说明根因判断有误 —— 停下来登记 `BLOCKED.md`,不要硬改。**
修复上线后,运维侧还需通过 admin 流为 `rectification` 发布对应 tier 的定价行(`src/app/api/admin/feature-pricing/route.ts`)。**这是代码改动之外的必要动作,缺了它用户依然用不了** —— 请在 PR 描述里显式提醒。
+504
View File
@@ -0,0 +1,504 @@
# 任务书 · 生时校正收敛重构(2026-08-30v2
基线:`origin/staging` @ `7db2dd2d`
**下面所有行号只是线索,请按选择器/函数名定位**,后续提交可能让行号偏移。标注为「上游」的行号来自对 `/workspace/yinduzhanxing` 的一次调研,动手前请自行复核。
> **v2 说明**:v1 的判断是「线上打分器可能不如本地方法学,需要三方盲测决定是否换引擎」。对上游仓库 `/workspace/yinduzhanxing` 做完比对后,这个判断被推翻了。换引擎的路线已排除,本版的落地点比 v1 小一个数量级。v1 的任务 0(三方盲测)作废。
## 为什么要做
### 事实 1 · 方法学没有丢,也没有落后
`/workspace/Jyotisha` 与上游 `/workspace/yinduzhanxing` 同源(共同初始提交 `a5bbba28`2026-04-20),分叉于 `825a9ea5`2026-07-17)。分叉后上游 808 个提交、本仓 1,275 个提交,**互相同步 0 次**。
但生时校正方法学的三份文件 `cmp` 逐字节相同:
```
IDENTICAL birth-time-rectification-advanced.md
IDENTICAL birth-time-rectification-decision-tree.md
IDENTICAL birth-time-rectification-cases.md
```
`yinduzhanxing/references/``Jyotisha/skills/jyotish-vedic-astrology/versions/6.9.14/references/`
上游六周里在校正上新增的是治理、契约与复现文档,不是方法学本身。**「本地版本更新」不成立。**
### 事实 2 · 三套打分链路是同一算法家族
- `Jyotisha/scripts/rectification/scoring_service.py:11` 直接 `from scripts.active_rectification_event_engine import ...` —— 线上 v5 调用的就是上游那个事件引擎。
- `Jyotisha/scripts/minute_rectification_fact_ranker_v4.py:16` import 同一个 `DOMAIN_CONFIG`
- 两边共享同一套权重:Vimshottari MD/AD/PD = `2.0 / 1.5 / 0.75`Narayana MD/AD = `2.0 / 1.0`
因此 `references/rectification_sealed_holdout.v1.json` 那组数字(`top_1_rate 0.15``mean_absolute_minute_error 6.95``confirmation_coverage_rate 0.0`)虽然测的是 v4,**对线上 v5 是弱但真实的先验,不能当作无关数据**。上游也没有更好的打分器可换。
### 事实 3 · 真正丢掉的是「停下来」的机制
上游三条链路每一条都天然终止:
| 上游链路 | 终止方式 |
| --- | --- |
| `scripts/active_rectification_questions.py` | `QUESTION_TEMPLATES` 是**写死的 8 题**分 3 轮;问完 `workflow_status``dated_event_review_required`(上游 :537 |
| `scripts/active_rectification_events.py` | `adjudicate_candidate_rows()` **一次性批量裁决**(上游 :212),算完直接给 low/medium/high |
| `jyotish-app/rectification-engine.js` | 浏览器内一次算完给置信度标签,无追问 |
且上游**从不承诺分钟**`can_apply` 默认 `False``truth_status: not_birth_time_truth` 写死在代码里。
Jyotisha 把这套替换成了服务端逐轮动态生成 discriminator probe 的**无界循环**。
### 事实 4 · 无限访谈的机械解释
生产判定 `frontend/src/lib/rectification-agentic/core/rectification-decision.ts:144-149`
```ts
if (!separation.sufficient) {
if (probe && !userStopped) return discriminate(...); // 只要有探针就继续问
return completeWithRange(..., "offer");
}
```
**这条路径里没有任何轮次计数器。** 同时:
- `confirmation_coverage_rate: 0.0` —— sealed holdout 的 20 例里**零例**达到确认门,即门几乎不会开。
- probe 的 `semantic_key``domain.year.month` 构造,随候选集每轮重新生成,实际不会耗尽。
**门永远不开 + 探针永不耗尽 = 无限访谈。** 这是全部 stall 类 bug 的根因。
### 事实 5 · 8 轮上限算了,但没人拿它当终止条件
不要误以为它是死代码。生产链路确实在跑它:
```
mastra/rectification-v9-tools.ts:945 → buildCaseInferenceState
→ v9/inference-adapter.ts:248 → buildInferenceState
→ core/build-state.ts:162 → evaluateConvergence
→ core/convergence-evaluator.ts:54 maxRounds = state.max_rounds ?? DEFAULT_MAX_DISCRIMINATION_ROUNDS
→ core/types.ts:10 DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8
```
`max_rounds` 没有被传入,取默认 8,算出的 `result_status` 也写进了 state。**但 `rectification-decision.ts` 的判定不读它。**
所以修法**不是**「启用死代码」(去改 `convergence-evaluator.ts` 改了也没用),而是**把轮次预算接进 `rectification-decision.ts` 的判定输入**。
而且终止分支已经存在:`rectification-decision.ts:158` 就有 `completeWithRange(separation, holdout, range, "exhausted")``:336``kind` 枚举也已支持 `"exhausted"`。**代码路径是现成的,只是没人触发。**
另有一套完整但闲置的停机策略:`frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48`(高置信 / 答满 10 条 safety cap / 平台期 2 轮 / 零信息增益 / 重复分区),属旧链路,V9 不走它。
### 事实 6 · 上游同样没有盲测证据
`yinduzhanxing/references/real_case_calibration/timing_rectification_*_2026_07_23.json`
```
blind_replay_allowed = false
ready_case_count = 0 (minimum_case_count = 3)
reviewer_2_confirmed_count = 0
status = blocked_until_human_labels
production_tuning_allowed = false
progress.md:1611 — "zero cases ready for blind replay"
```
`birth-time-rectification-cases.md` 里的奥巴马/居里夫人案例,是**拿已知出生时间回验事件**并自评「吻合度 95%」——知道答案的复盘,不是校正测试。
**两边都没有盲测数据。产品侧「本地能收敛」的观察,最可能是定义差异**:上游问完固定题数就给候选区间并停下,那个体验就是「收敛」;按代码它给的一定是区间 + low/medium/high 标签,不是确定的分钟。
### 结论
产品目标定为:**交付收窄后的区间,不交付唯一分钟。** 唯一分钟保留为 sealed holdout 通过后才开启的路径。
要做的是**把上游那套「问完就停、停下来时坦白交代」的产品契约接回来**,不是重写收敛逻辑,更不是换打分引擎。
---
## 硬红线
1. **任务 0 是门控,但它只是一次信息收集,不许写代码。** 在拿到上游访谈手册与一份真实会话记录之前,不得改动 `rectification-decision.ts` 的判定。
2. **诚实性不可让渡。** 不得为了让访谈「看起来能收敛」而放宽 `confirmation-gate.ts` 的任何 blocker,不得降低 `SEALED_MINUTE_HOLDOUT` 门槛,不得在 `confirmation_allowed=false` 时宣称唯一分钟。这套 fail-closed 机制现在正在正确工作。
3. **禁止答案回流。** 任何以已知答案调出来的规则,不得进入生产打分链路,只能存在于标注 `regression_only` 且被评测明确排除的夹具里。上游已经发生过一次(见任务 0 补充),本仓当前干净,必须保持。
4. **上游同步必须过污染审查。** 不得同步任何带 `pl9_1993``observed_*_case_only``target_minute` 标记的打分逻辑,特别是上游 `scripts/narayana_dasha.py:601-613` 的 tie-break。任务 5 的同步机制必须包含这道检查。
5. **超预算的出口必须是「交付区间」,不是「失败」。**`completeWithRange(..., "exhausted")`,用户拿到候选区间与明确的边界说明。任何把超预算实现成报错、静默停止或空白页的做法都是错的。
6. **不得用 holdout 调参。** `minute_rectification_holdout_v3``source_audit_status``invalidated_after_replay`,只能当开发集看趋势,**不得作为发布指标,不得写回 sealed 字段**。`v4_intake``production_tuning_allowed` 保持 `false`
7. **不要引入第五套打分器。** 不要移植 `jyotish-app/rectification-engine.js` 的 40/35/15/10 权重。它信号更弱(无 Narayana / Arudha / Ashtakavarga / Shadbala)且从未评测。
8. **状态机留在 Postgres 函数里**,沿用既有模式。不得把状态机搬进应用层。
9. **不得修改既有测试断言** —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
10. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`
11. **数据库测试必须真跑。** `npm run test:db` 需要 Docker。没有 Docker 就不要推 —— 写进 `BLOCKED.md` 并停下。
12. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**诚实性不回退 > 数据不损坏 > 功能与测试不回归 > 可验证的收敛改进 > 代码整洁 > 成本**。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-convergence-20260830 \
../.worktrees/rectification-convergence-20260830 origin/staging
```
`pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索校正相关记录(30 条以上,停滞类那几条务必读完)。
**先读这些再动手:**
- `frontend/src/lib/rectification-agentic/core/rectification-decision.ts` —— 生产判定,本轮主战场
- `frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts` + `core/build-state.ts:162` —— 已在跑但没被消费的轮次判定
- `frontend/src/lib/birth-time-dynamic-stop-policy.ts:43-48` —— 现成但闲置的停机策略
- `frontend/src/lib/rectification-agentic/v9/spoken-answer.ts` —— 101 条清洗正则,任务 2 要删
- `frontend/src/mastra/agentic-rectification.ts:61-68` —— 系统提示,任务 2 要重写第 4 条
- 上游 `/workspace/yinduzhanxing/scripts/active_rectification_events.py:115-179` —— 任务 3 要移植的 `build_candidate_result_summary()`
- 上游 `/workspace/yinduzhanxing/jyotish-app/rectification-collaboration.js` —— 四阶段状态机,参考它有多简单
---
## 任务 0(已完成 2026-08-31)· 上游访谈手册与阈值文档
三份文件已获取,归档于 `references/upstream/`(来源:上游维护者本地 skill 包 `~/.workbuddy/skills/jyotish-birth-time-rectification/`,获取日期 2026-08-31):
- `references/upstream/interview_playbook.md`
- `references/upstream/evidence_thresholds.md`
- `references/upstream/test_birth_time_rectification_skill_contract.py`
### 结论:上游的「收敛」= 从不试图确认分钟
**1. 停止规则与本仓方向相反。**
`interview_playbook.md` 的 Stop Rules 原文:
```
Stop and report the current interval when any is true:
- fewer than three dated events
- fewer than two domains
- tied first-place candidates
- user uncertainty is too high to map support/conflict reliably
```
即:**证据不足 → 停下来,交付当前区间。**
本仓 `frontend/src/lib/birth-time-evidence.ts:188-189``minConfirmationEvents(4)` / `minConfirmationDomains(3)` 当作**确认门**,达不到 → **继续问**
同一类阈值,上游当「可以开始交付的地板」,本仓当「可以下结论的天花板」。前者三轮内必达,后者按 sealed holdout 的 `confirmation_coverage_rate: 0.0` 是零达成。**这是无限访谈的最终解释。**
**2. 上游的标签梯子没有「已确认」这一档。**
`evidence_thresholds.md``blocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus`。顶端自注 "this is still not birth-time truth"`standalone_core` 模式天花板更低,只到 `manual_pattern_consensus`
`test_birth_time_rectification_skill_contract.py` 中**每一个** case(含最高档 `main_repository_enhanced`)都断言:
```python
assert receipt["claim_status"] == "candidate_range_not_birth_time_truth"
```
**3. 提问节奏是硬的三轮。**
第 1 轮 35 道 A/B/C/D 选择题;第 2 轮只要最强 domain 的带日期事件;第 3 轮只问能区分 top 候选的 1–3 个事件。手册明写 "Never collect a long autobiography before the first route decision."
**4. 终止话术是写死的。**
> 当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。
### 补充(2026-08-31)· 两份真实会话记录证明「本地收敛」源于答案泄漏
会话记录已归档:`references/upstream/1993-session-repeated-questions.txt``references/upstream/1993-session-correction-rerun.txt`
Agent 在记录中两次自陈:
> 我明显受到了仓里这份 1993 案例的现成高质量材料影响…不是纯靠你当下问答独立推出来的。
> 14:49 这个点,主要还是被仓里的 1993 已知校准档锁住了,不是单靠这些回答「算出来」的唯一真值。
答案原本就写在上游 `docs/research/qizheng_1993_native_current_report_2026_08_26.md``1993-04-17 14:49:00`)。随后的「修正」是把答案接进打分链路(`pl9_1993_user_case``target_minute = 14:49`,提交 `3bb90620`)。进一步核查发现上游打分器里已有按该案例调出的规则:
```
scripts/narayana_dasha.py:601 "selection_levels": ["observed_pl9_1993_gemini_sagittarius_tie_break"]
scripts/narayana_dasha.py:613 evidence_status = "observed_pl9_1993_case_only"
```
**本仓已核查未受污染**(全仓 grep `pl9_1993` / `target_minute` / `14:49` 无相关命中)。红线 3、4 用于保持这一状态。
**但记录同时证明了真实能力**:用户的 8 条生平锚点把窗口从 14:30–15:0030 分钟)压到 14:4814:50(3 分钟)。**收窄是真的,选定分钟不是。** 这与 `mean_absolute_minute_error: 6.95` 及上游标签梯子顶端「still not birth-time truth」完全一致,并再次确认产品目标应为交付区间。
### 对已完成任务的影响
任务 1 实现的 `budgetExhausted()`(轮次 8 / 答题 10 / 平台期 2)方向正确但只覆盖了一半——上游是**证据状态型**停止且三轮即止。差额由**任务 6** 补齐。
---
## 任务 1(P0)· 把轮次预算接进生产判定
### 事实
见事实 4、5。`rectification-decision.ts:144-149``!separation.sufficient` 分支只要有 probe 且用户没喊停就无限 `discriminate`。轮次判定在别处算好了但没人读。
### 要做什么
1.`rectification-decision.ts` 的判定输入增加**预算三件套**,参数一律取自已有常量,**不要新造数字**:
- 轮次上限 —— `core/types.ts:10``DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8`
- 平台期 —— `references/rectification_policy.v1.json``maxPlateauRounds`(当前只有 `scripts/rectification_policy.py` 读了,前端没用)
- 安全帽 —— `birth-time-dynamic-stop-policy.ts:45``effectiveAnswerCount >= 10`
2.`!separation.sufficient` 分支的 `discriminate(...)` **之前**插入预算检查。超预算时走**已存在的** `completeWithRange(separation, holdout, range, "exhausted")`:158)。
3. **不要去改 `convergence-evaluator.ts`。** 它算得没错,问题是没人消费。要么把 `result_status` 传进判定输入,要么在判定层独立计数——二选一,在 PROGRESS 说明选了哪个及理由。
4. 轮次计数必须**服务端持久化**,不得靠对话历史推断。沿用既有的 receipt / turn 计数来源。
### 属性测试(本任务的核心交付)
`frontend/tests/` 新增:
- **可终止**:从任意状态出发,反复喂 `discriminate` 返回的 probe 的答案,必须在 ≤ 预算轮数内到达某个 `completeWithRange` 分支。**不存在无限 discriminate 循环。**
- **拒答不卡死**:用户对每一个 probe 都拒答时必须到达终态,不得停在 `discriminate`
- **超预算即交付**:超预算时返回的是 `"exhausted"` 且带候选区间,不是错误或空结果。
- **单调性**:追加证据后区间宽度只能变小或不变,永不变大。
这四条一次性锁死未来所有 stall 类 bug。
### 验收
- 四条属性测试全绿。
- 停滞类既有测试(`rectification-collect-stall.test.ts``rectification-range-offer-deadend.test.ts` 等)全绿。
- 端到端:一个用户即使一直不给有效证据,也会在预算内拿到区间并结束。
---
## 任务 2(P0)· 提问权收归服务端,模型降级为一句确认
### 事实
一条消息目前有**四个作者**:服务端决定问什么(`v9/method-followup.ts`,2,036 行)→ 模型写正文 → 服务端把题干接在正文之后 → UI 再渲染选择卡。
于是 `src/mastra/agentic-rectification.ts:66` 要求模型判断自己处在 `choice` / `collect_spoken` / 无持久化问题 三种状态中的哪一种再区别行动。模型做不到,所以有了 `v9/spoken-answer.ts`:**33 条内部标识符正则 + 68 条中文过程话术正则,共 101 条**,事后擦模型漏出的内心戏。每一条正则都是一个曾经发生过的 bug。
### 要做什么
1. **模型永远不提问。** 一轮里它的唯一职责是针对用户刚说的内容写**一句**确认/承接。系统提示第 4 条整条重写,删掉所有关于题干归属的条件分支。
2. **问题永远由服务端渲染,永远出现在同一个问题区。** `choice``collect_spoken` 合并成一个「问题槽」:有选项就可点,没选项就是输入框加提示。数据模型不再区分 kind,UI 不再有两条渲染路径。
3. **取消「服务端把题干接在模型正文之后」的拼接。** 一条消息 = 模型的一句确认(流式)+ 服务端问题区(结构化渲染),两者物理分离,各自唯一作者。
4. **删掉 `spoken-answer.ts` 的 101 条清洗正则。** 如果删完发现仍需清洗,说明第 1 步没做干净,回去改第 1 步,**不要把正则加回来**。
5. 服务端未产出问题时问题区为空 —— 这是可断言、可监控的显式状态,不是静默停滞。加服务端告警。
### 验收
- 用户可见文本不可能出现内部标识符或过程话术,且**不是靠正则保证**,是靠模型输出面收窄保证。
- 端到端连续 10 轮,问题始终在问题区,位置一致,不重复、不消失。
- `spoken-answer.ts` 的两条大正则不再存在于代码库。
---
## 任务 3(P0)· 恢复「停下来时怎么交代」的契约,交付物从第 0 轮就存在
### 事实
上游 `active_rectification_events.py:115-179``build_candidate_result_summary()`,产出四个面向用户的 next-step code
```
collect_at_least_five_events
resolve_candidate_tie_or_narrow_window
narrow_window_before_minute_claim
do_not_apply_as_birth_time_truth
```
**Jyotisha 把这整段删了。** 于是访谈终止时没有话术锚点,只能继续问。
上游的降级阈值 Jyotisha 其实完整继承在 `references/rectification_policy.v1.json`(4 事件 / 3 域 / 5 分钟宽度 / 20% margin)——**判据没丢,丢的是「达不到时说什么」。**
### 要做什么
1. 把上游的 `build_candidate_result_summary()` 恢复到 `Jyotisha/scripts/active_rectification_events.py`,让 `scripts/rectification/api_service.py``score_candidates()` 返回体带上 `next_step_codes``stability.label`
2. 定义**校正报告**投影,随时可读:结论区间 + 代表分钟(明确标注为代表性、非唯一解)、置信度、逐条证据(事件 → 支持哪个候选 → 用哪个方法)、被排除的候选与理由、局限声明(复用 `confirmation-gate` 的 blocker 文案,不要另写一套)。
3. **第 0 轮就存在**:证据 0 条时区间 = 用户声明的出生窗口。每答一题重算。
4. 用户任何时刻可查看、可导出、可主动结束。结束即交付当前版本,**不算失败**。
5. UI 给出区间收窄进度(`±120 → ±45 → ±18 → ±7`)。这是用户为这笔钱买到的东西的可视化。
6. 计费与交付对齐:产生过至少一版报告即为有效交付,`exhausted` 不是失败态。**具体金额由 `TASK-billing-pricing-20260830.md` 决定,本任务不写任何金额。**
### 验收
- 新 case 在零证据时即可读出一份报告。
- 每轮答题后区间单调收窄或不变。
- 用户主动结束或超预算 → 拿到报告,状态是完成不是失败。
- `next_step_codes` 出现在服务端返回体里,并被终止话术使用。
---
## 任务 4(P1)· 解决标定数据饥荒
### 事实
两边都没有盲测数据(事实 6)。Jyotisha 的 `v4_intake` `blind_holdout_eligible_case_count = 0`,上游 `ready_case_count = 0`。这是产品能不能承诺唯一分钟的唯一瓶颈,**且它是数据问题不是工程问题**。
上游停在这里六周了,指望它自己解决不现实。
### 要做什么
做一个**免费的「验证我们」入口**,面向已经知道自己准确出生时间(有出生证明/医院记录)的用户:
1. 用户提供出生日期、地点、事件;**准确时间单独收集并对打分链路屏蔽**,全程不得进入候选生成或打分。
2. 系统盲跑校正,出结果后再与真实时间比对,把偏差当场展示给用户。
3. 每一例都是一条合格标定数据,用户同时拿到一次免费的、可验证的能力演示。
一个入口同时解决三件事:标定数据来源、最有说服力的社会证明(「我们在 N 个有出生证明的案例上盲测过,中位偏差 X 分钟」)、获客内容。
### 硬约束
- **真实时间必须在架构上不可能泄漏进打分链路**,不是靠约定。写负向测试锁死。
- 案例默认进 intake 队列,晋级为冻结 holdout 必须走既有流程:新版本号、通过源审计、打分身份在盲测前冻结。
- 免费入口要有独立的滥用限制,不得复用咨询的公平使用配额。
- 与上游共享数据前先对齐口径,避免两边各建一套互不承认的标定集。
### 验收
- 真实时间泄漏的负向测试存在且通过。
- 新案例正确落入 intake 队列,不会被自动当成 holdout。
- 用户侧能看到「预测 vs 真实」的偏差对比。
---
## 任务 5(P2)· 清理与同步机制
**任务 1–3 上线并稳定运行两周后才做。**
1. **两代数据模型并存**`birth_time_rectification_*` 17 张 + `agentic_rectification_*` 16 张 = 31 张,是 50 个校正迁移的主要来源。逐张确认旧表是否仍被 RPC 写入,无引用的归档删除。
2. **删掉永不执行的分支**`method-followup.ts` 注释写明第 5 项(外貌体质)与第 6 项(胎记疤痕)`skipped; never asked`
3. **`MIN_SEPARATION_LEAD` 与上游口径对齐**`core/candidate-separation.ts:7` 当前是绝对分差 8;上游 `active_rectification_events.py:256` 用的是相对 margin(< 10% 降级)。改成相对量后用 `minute_rectification_holdout_v3` 的 20 例看 `not_separated` 比例变化 —— **只看趋势,不作发布指标**(红线 6)。
4. **建立与上游的同步机制。** 六周零同步是本轮很多问题的背景。约定一个节奏(例如每月一次),至少同步 `references/``scripts/active_rectification_*`、方法学 md。这不是代码任务,是流程约定,写进 `AGENTS.md`
5. 每删一张表、每并一个模块单独一个提交,便于二分回滚。
### 不要动
打分引擎与 Swiss Ephemeris 计算、证据账本、Postgres 里的状态机、sealed holdout 机制、`rectification_policy.v1.json` 的阈值。这些都是对的。
---
## 任务 6(P0)· 接过上游的终止语义
### 事实
见任务 0 结论。任务 1 已接入轮次型预算,但上游的终止是**证据状态型**的,且 `exhausted` 在上游是正常终态而非兜底态。
### 要做什么
1. **增加证据型停止规则**,与既有轮次预算并列(任一命中即停并交付):
- 带日期事件 < 3
- 覆盖 domain < 2
- 并列第一(已有 `separation` 可判)
- 用户不确定度过高,无法可靠映射 support/conflict
阈值取 `references/upstream/evidence_thresholds.md` 的 Minimum Standalone Gate3 事件 / 2 域),**不要沿用本仓的 4/3**——那组是确认门,语义不同,两者必须分开保留,不得互相覆盖。
2. **把 `exhausted` 从兜底态重新定位为正常终态。** 改命名与全部用户可见文案:走到这里是「交付完成」,不是「很遗憾没能完成」。计费上它必须是有效交付(与 `TASK-billing-pricing-20260830.md` 对齐)。
3. **引入上游的 label ladder**,替换「确认唯一分钟」作为默认目标:
`blocked → user_history_verification_required → manual_pattern_consensus → single_adapter_support → multi_adapter_consensus`
唯一分钟确认降级为 sealed holdout 通过后才开启的路径,**不得作为访谈的默认终点**。既有 `confirmation-gate.ts` 的 blocker 保持不变(红线 2),它现在正确地永不放行。
4. **采用上游的终止话术**,逐字使用任务 0 结论第 4 条那句。它是上游打磨出的合规表述,不要另写。
5. **收紧提问节奏到三轮形态**:第 1 轮 A/B/C/D 选择题批量出,第 2 轮只要最强 domain 的带日期事件,第 3 轮只问能区分 top 候选的 1–3 个。当前 8 轮预算作为**外层熔断**保留,不作为正常节奏。
### 验收
- 证据型停止规则有单测:3 事件以下、2 域以下、并列第一各自触发终止并交付区间。
- 4/3 确认门与 3/2 交付地板在代码中是两个独立常量,不互相覆盖,注释写明语义差别。
- 用户可见文案中不存在把正常终态描述为失败的表述。
- 默认路径下系统不再以「确认唯一分钟」为目标;相关文案与 label 均出自新梯子。
- 既有校正测试与任务 1 的四条属性测试全绿。
---
## 任务 7(P0)· 逐条回答 → 区间收窄的归因表
### 事实
`references/upstream/1993-session-correction-rerun.txt` 里,整场对话最有价值的产物是用户主动索要的那张表:
| 你的回答 | 压缩后的分钟区间 | 作用 |
| --- | --- | --- |
| 2017 入职,岗位内容不合 | 14:48–14:50 | 很强的收窄点 |
| 2023 末起量,一改风格就稳 | 14:48–14:50 | 最接近最终分钟的一条 |
本仓没有这个概念。`v9/evidence-model.ts``core/*.ts` 中不存在 `narrow` / `contribution` / `attribution` 语义,只有「这条证据支持哪个候选」,没有「这条回答把区间从 X 压到了 Y」。
### 要做什么
1. 每次证据写入后记录**区间快照**:写入前宽度、写入后宽度、以及该条证据的归属贡献。持久化,不靠事后重算。
2. 报告(任务 3)中新增归因表:用户原话(`display_date_label` 口径)→ 收窄前后区间 → 一句作用说明。
3. 收窄贡献必须来自服务端打分,不得由模型叙述生成。模型只做措辞。
4. 无贡献的证据也要列出并标注「未产生收窄」——这本身是诚实性的一部分。
### 验收
- 报告含归因表,条目数等于已确认证据数。
- 每条的收窄前后宽度与该轮实际候选集一致,可复核。
- 零贡献证据被显式标注,不被静默丢弃。
---
## 任务 8(P1)· 「为什么是这个结论」的质疑通道
### 事实
会话记录里用户问了一句「你是信息记忆联想了吗?还是靠印度占星专业技术推理出来的?」——这一问改变了整场对话,也是全部真相的来源。
本仓没有任何「为什么」入口。`v9/choice-card.ts``why` 字段是每个选项的说明,不是「这个结论怎么来的」。
### 要做什么
1. 结论旁提供一个显式入口,回答三件事:用了哪些证据、用了哪些方法层(dasha / D9 / D10 / D12…)、当前还有哪些 blocker 未解除。
2. 内容全部来自服务端已有的 `claimCards` / `evidenceRefs` / `confirmation-gate` blocker**不得由模型即兴生成**。
3. 必须能诚实回答「这个结论有多少来自你的回答」——若某个结论主要由先验或默认值决定,要说出来。
### 验收
- 入口可达,内容可复核到具体证据 ID 与方法层。
- 关闭 blocker 前后,解释内容随之变化。
- 解释文本中不出现内部标识符(与任务 2 的输出面约束一致)。
---
## 任务 9(P1)· 中途改窗口与保留证据重跑
### 事实
`1993-session-correction-rerun.txt` 整篇的主题就是「默认只把 14:30–15:00 当作未知区间,重新跑一轮」。
本仓 `declared_window` 在开 case 时从 profile 读一次(`v9/case-service.ts:42-43``rectification-agentic/session.ts:194-195`),未找到中途重定义或保留证据重跑的路径。这是高频真实场景:家人过后又想起更准确的时段。
### 要做什么
1. 支持在 case 进行中重新声明窗口,并基于**已有证据**重新扫描打分,不要求用户重答。
2. 重跑必须留痕:旧窗口、新窗口、触发原因、重跑前后的候选与区间,进 receipt。
3. 与计费对齐:同一 case 内重跑**不重复扣费**(沿用 `rectification:case:<caseId>` 幂等键)。
4. 若新窗口与既有证据严重冲突,如实呈现冲突,不得静默丢弃证据。
### 验收
- 改窗口后候选重算,已确认证据全部保留。
- 重跑留痕可查。
- 重跑不产生第二次扣费。
---
## 任务 10(P1)· 第一轮批量出题
### 事实
`src/mastra/agentic-rectification.ts:68` 第 6 条硬编码「一次一问」。
上游 `references/upstream/interview_playbook.md` 的节奏是第 1 轮 **35 道 A/B/C/D 一起出**,会话记录里也是「你直接回 A/B/C 就行」。一次一问 × 8 轮摩擦大、轮次消耗快,**直接加剧不收敛**。
### 要做什么
1. 第 1 轮改为批量出 3–5 道 A/B/C/D 选择题,一次呈现,用户可逐条或一次性作答。
2. 第 2、3 轮维持单问(只要最强 domain 的带日期事件 / 只问能区分 top 候选的 1–3 个)。
3. 系统提示第 6 条随任务 2 一并重写;批量题的题干与选项由服务端产出(与任务 2 的问题槽一致),模型不参与。
4. 批量轮在轮次预算里**记为一轮**,不是 3–5 轮。
### 验收
- 第 1 轮呈现 3–5 题,作答后进入单问节奏。
- 轮次计数正确。
- 与任务 1 的四条属性测试不冲突。
---
## 交付
- PROGRESS 写进 `PROGRESS-rectification-convergence-20260830.md`,任务 0 的会话记录结论单列。
- 每个任务一个提交。
- 推 staging 前:`./node_modules/.bin/tsc --noEmit` 通过、`npm run lint` 无新增 error、`npm run test:db` 在 Docker 下 fail=0、`npm run build` 通过。
- 全量 `./node_modules/.bin/tsx --test tests/*.test.ts` 的失败清单与基线逐条比对,确认无新增。
@@ -0,0 +1,258 @@
# 任务书 · 生时校正决策权威归一与停止语义重构(2026-08-31)
基线:`origin/staging` @ `67101f03`
本轮是**结构性重构**,不是补分支。上一轮 `3a4396a4`stop 语义)用"在 if 链里插一个分支"的方式改交付条件,直接造成了线上事故:**引擎判定"证据不足、不许采用",界面却把 9 个几乎并列的候选摆出来让用户点"采用此时间"。** 同类补丁不要再打了。
---
## 事故实证
真实 case `3fe32834-9068-4275-81b1-0a57e860e68e`,用户只提供了 1 条证据(2016-09 上大学)。同一份 `latest_result` 内部自相矛盾:
| 字段 | 引擎 `decisionReceipt`(Python) | 顶层投影(前端读这个) |
| --- | --- | --- |
| `selection_allowed` | **false** | **true** |
| `acceptance_allowed` / `can_adopt` | **false** | **true** |
| `propose_allowed` | **false** | **true** |
| `result_status` | **insufficient_evidence** | `completed_with_range` |
| `precision_stage` | **d9_refine**(需继续细化) | **ready_to_adopt** |
引擎把理由写得很完整:`acceptance_reasons: ["insufficient_events","insufficient_domain_diversity"]`;四道闸门全挂 —— `event_quality` 1 件 < 最低 3、`domain_diversity` 1 个 < 最低 2、`unique_top` 有 3 个并列第一、`diagnostic_quality` margin 5.24% < 最低 10%`overall_confidence: "low"`
候选跨度 04:47–05:1529 分钟),前四名支持度 12/12/12/12 完全并列。用户点任意一个,`accepted_time` 落库并出现"用这个时间看盘"入口 —— **拿引擎明说证据不足的时间去排盘**。这是数据正确性事故,不是 UX 瑕疵。
已用真实分数复现(`decideRectification` 直接调用):
```
输入:datedEventCount=1, datedDomainCount=1, trainingGateOpen=false,
methodCoverageAll=false, snapshotCurrent=true, userStopped=false
输出:nextAction=complete_with_range stopReason=insufficient_dated_events
selectionAllowed=true canAdopt=true precisionStage=ready_to_adopt
separation.status=not_separated tiedForFirst=true
```
末行是关键:系统**自己知道**候选分不开(`not_separated` + `tiedForFirst`),仍然放行采用。
---
## 四个结构性病灶
本轮要根治的是这四条,不是上面那一个症状。
### 病灶 1 · 双重决策权威,且下游可单方面推翻上游
Python 引擎已经算出 `selection_allowed` / `acceptance_allowed` / `propose_allowed` / `confirmation_allowed` / `result_status` 并附完整 gates。TypeScript 的 `decideRectification` 又独立重算一遍同样的判断。两个权威源之间**没有任何一致性约束**。
覆盖点在 `v9/decision-from-dossier.ts:619-635``overlayPublicDecision`
```ts
return {
...snapshot, // 引擎结果
...fields, // TS 决策 —— 无条件盖掉引擎
...(inconsistent ? { can_adopt: false, selection_allowed: false } : {}),
```
唯一的 fail-closed 是候选投影不一致(`inconsistent`)。引擎自己写的 `acceptance_allowed: false` **完全不在考虑范围内**
### 病灶 2 · "证据不足"与"已经查完"共用一个出口
`core/rectification-decision.ts:186-188`
```ts
if (stopReason) {
return completeWithRange(separation, holdout, range, "exhausted", stopReason);
}
```
`stopReason` 有四个取值,语义截然相反却走同一出口:
| stopReason | 真实含义 | 正确出口 | 现状 |
| --- | --- | --- | --- |
| `insufficient_dated_events` | 证据不够 | **继续收集** | 交付并允许采用 |
| `insufficient_domains` | 领域不够 | **继续收集** | 交付并允许采用 |
| `tied_first` | 问不动了 | 交付区间 | 交付区间 ✓ |
| `user_uncertainty_too_high` | 问不动了 | 交付区间 | 交付区间 ✓ |
`completeWithRange(kind: "exhausted")` 的语义是"已穷尽、交付最终区间",`terminal = true`,并**无条件**设 `canAdopt: true` / `selectionAllowed: true` / `precisionStage: "ready_to_adopt"` / `phase: "completed"`(见 `core/rectification-decision.ts:414-452`)。
次生问题:这个 `if (stopReason)` 被插在 `coverageBlocks` 检查**之前**。`coverageBlocks` 本会因 `trainingGateOpen=false` 返回 `collect()` 继续收集 —— 被 stopReason 抢跑了。
### 病灶 3 · 能力标志散落在 11 个分支里各自硬编码
`decideRectification` 是一条 11 分支的顺序 if 链,`canAdopt` / `selectionAllowed` / `proposeAllowed` / `canConfirmExactMinute``finish()` / `completeWithRange()` / `collect()` / `discriminate()` / `offerRangeWithoutAdopt()` / `holdoutValidation()` 各自决定。**没有任何单点能回答"什么条件下允许采用"**,也无法审计。上一轮的回归正是这种结构的必然产物。
### 病灶 4 · 多处 fail-open
`core/rectification-decision.ts:222-233`
```ts
if (holdout === "unavailable") {
if (userStopped) { return completeWithRange(..., "user_stopped"); }
return finish("adopt_representative", { ..., canConfirmExactMinute: false });
}
```
`holdout` 的默认值就是 `input.holdoutValidation ?? "unavailable"`。**独立核对不可用时反而放行采用**,`canAdopt: true`。事故快照里 sealed holdout 的真实状态是 `top_1_rate: 0.15``required_cases: 20``status: "not_ready"` —— 基准根本没建起来,系统却据此放行。
同理 `evidenceStopReason` 首行 `if (separation.ranked.length === 0) return null;` —— 没有候选时不给任何停止原因,也是 fail-open 方向。
---
## 硬红线
1. **撤销上一轮任务书的红线 3。** `TASK-rectification-billing-20260831.md` 写的"不得修改 `core/rectification-decision.ts`,与本次故障无关"——就计费故障而言成立,但它正是本轮的根因所在。**本轮必须改它。**
2. **引擎是门禁的唯一上限权威。** TS 层对 `acceptance_allowed` / `selection_allowed` / `propose_allowed` / `confirmation_allowed` **只能收紧,永远不能放宽**。引擎说 false,最终必须是 false,没有例外。
3. **一切 fail-closed。** 引擎字段缺失、holdout 不可用、候选投影不一致、快照过期 —— 一律按"不允许采用"处理。不确定时禁止交付,不是放行。
4. **不得为了让重构通过而放宽任何既有门槛。** `MIN_STANDALONE_DATED_EVENTS=3` / `MIN_STANDALONE_DATED_DOMAINS=2` / 4-3 确认门 / `MIN_SEPARATION_LEAD` 数值一律不动。本轮改的是**这些判断如何流向能力标志**,不是判断本身。
5. **不得修改既有测试断言** —— 除非该断言锁住的正是本轮要修的缺陷本身(预计 `rectification-convergence-budget.test.ts` 等会有若干条锁住了错误行为)。那种情况必须在断言上方注明"原值是什么、为什么它是错的",并在 PROGRESS 里逐条单列。**发现需要改的断言超过 5 条时停下来先汇报**,那意味着方案偏了。
6. **任务 0 是门控。** 不变量测试必须先写、先失败,才允许动实现。
7. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`** —— 新建 worktree 未 `npm install` 时 npx 会装到空包 `tsc@2.0.4`
8. `tests/rectification-*.test.ts` 不得低于基线 **705**,且 `fail=0`。全量 `npm test` 需要 Docker;无 Docker 时必须逐条比对失败清单确认无新增。
9. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**不得放行不该放行的采用 > 数据正确性 > 功能与测试不回归 > 可诊断性 > 代码整洁**。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-decision-authority-20260831 \
../.worktrees/rectification-decision-authority-20260831 origin/staging
```
基线必须是 `origin/staging`。读 `docs/research/pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索同类记录(`47b4b06b` 刚补过一条)。
**下面所有行号只是线索,请按符号名定位。**
---
## 任务 0(门控)· 先写不变量测试,先让它红
在既有的 `frontend/tests/rectification-decision-authority.test.ts` 里补一组**不变量**测试。这些断言与实现无关,重构前后都必须成立:
1. **引擎上限不可逾越**:对任意输入组合,`decision.canAdopt` 为 true ⟹ 引擎 `acceptance_allowed !== false``selectionAllowed` / `proposeAllowed` / `canConfirmExactMinute` 同理。
2. **证据不足永不放行**`datedEventCount < MIN_STANDALONE_DATED_EVENTS``datedDomainCount < MIN_STANDALONE_DATED_DOMAINS``canAdopt === false``selectionAllowed === false`
3. **分不开就不能采用**`separation.tiedForFirst === true``canAdopt === false`(用户主动停止的路径除外,但那条必须带 `completionStatus: "provisional_range_user_stopped"`)。
4. **holdout 不可用不得放行**`holdoutValidation === "unavailable"` 且非 `userStopped``canAdopt === false`
5. **覆盖只能收紧**`overlayPublicDecision` 的输出对四个门禁字段,必须是引擎值与 TS 值的逻辑与。
建议用组合枚举(笛卡尔积)而非手挑用例覆盖输入空间 —— 病灶 3 的教训就是单点用例挡不住分支链回归。
**跑一遍,确认 1/2/3/4/5 里至少 4 条是红的。** 全绿说明测试没写对,停下来重写。
---
## 任务 A(P0)· 停止原因分类化,与交付能力解耦
`EvidenceStopReason` 升级为带出口语义的分类。建议形状(可自行调整命名,但**语义必须三分**):
```ts
type StopClass =
| { kind: "keep_collecting"; reason: "insufficient_dated_events" | "insufficient_domains" }
| { kind: "exhausted"; reason: "tied_first" | "user_uncertainty_too_high" }
| { kind: "user_stopped" };
```
- `keep_collecting` **不得**进入 `completeWithRange`,必须走 `collect()`
- `completeWithRange``kind` 参数删掉 `"exhausted"` 承接证据不足的用法;该函数今后只接受真正可交付的停止
- 修正 `if (stopReason)``coverageBlocks` 的先后:`keep_collecting` 必须让位给 `coverageBlocks` 的收集分支
---
## 任务 B(P0)· 能力标志单一来源 + 引擎上限交集
**B-1 抽出唯一计算点。** 新增一个纯函数,成为 `canAdopt` / `selectionAllowed` / `proposeAllowed` / `canConfirmExactMinute` 的**唯一**产出处:
```ts
function deliveryCapability(input: {
stopClass: StopClass | null;
separation: CandidateSeparation;
holdout: HoldoutValidationStatus;
engineCeiling: EngineCapabilityCeiling;
accepted: boolean;
userStopped: boolean;
}): { canAdopt: boolean; selectionAllowed: boolean; proposeAllowed: boolean; canConfirmExactMinute: boolean }
```
改完之后,`finish()` / `completeWithRange()` / `collect()` / `discriminate()` / `offerRangeWithoutAdopt()` / `holdoutValidation()` 里**不允许再出现这四个字段的字面量赋值**,一律取自该函数。请在 PR 里贴出 `grep` 结果自证。
**B-2 引擎上限。** 新增 `EngineCapabilityCeiling`,从 `decisionReceipt` 提取 `acceptance_allowed` / `selection_allowed` / `propose_allowed` / `confirmation_allowed`。**字段缺失或无法解析时一律视为 false**(红线 3)。`deliveryCapability` 的输出必须与之取逻辑与。
**B-3 修 `overlayPublicDecision`。** `v9/decision-from-dossier.ts``...snapshot` 后跟 `...fields` 的无条件覆盖改为对四个门禁字段取交集。保留既有的 `inconsistent` fail-closed。
**B-4 修病灶 4 的 fail-open。** `holdout === "unavailable"` 且非 `userStopped` 不得再 `finish("adopt_representative")``evidenceStopReason``ranked.length === 0` 时的 fail-open 一并处理。
---
## 任务 CP1)· 前端 fail-closed
`src/components/rectification-agentic-chat.tsx:1073``showSelectionCards` 目前只看 `candidateResult?.selectionAllowed && candidateResult?.canAdopt`。即便后端修好,这里也应有独立防线:渲染采用卡片前校验引擎 receipt 未否决。
**这是纵深防御,不是后端修复的替代品** —— 后端必须独立正确,任务 0 的不变量测试不允许依赖前端。
---
## 任务 D(P0)· 问题槽单一文案源
第二个用户可见缺陷:opening 轮问题卡片显示"请再说一件记得大概时间的经历。"——但用户**一件都还没说过**"再"字不通。
成因是**两条独立的问题生成路径**:模型在回复正文里自然地问了一个问题,服务端的 `persistNextInterviewIfIdle` 又确定性地生成了一个 focus。零证据时 `buildMethodFollowupPlan` 没有素材,`spokenFollowupForUser` 返回 null,落到 `v9/answer-choice.ts:329,390``v9/method-followup.ts:951` 的兜底句。
根本原则:**一个回合只能有一个"当前问题",且只能有一个产出点。** 本轮 D-1 与 D-2 都要做。
### D-1 · 服务端成为唯一问题源
- 零证据的 opening 轮必须生成真正的第一个问题,而不是"请再说一件…"的兜底句
- 三处兜底文案(`answer-choice.ts:329``answer-choice.ts:390``method-followup.ts:951`)要么删除,要么改成不预设"已经说过"的措辞
- 模型正文只做承接与解释,不再自带问句
### D-2 · Skill 升版,禁止模型正文自带问句
在 skill prompt 里硬性约束:问题由服务端问题槽呈现,正文不得自带问句。
**上一版任务书说这条"成本显著更高、需处理存量 case 的 skill_identity_mismatch"——那个判断是错的,已核实。** 实际情况:
- `agent/route.ts:219``skillVersion` 取自 **case 自己绑定的 `skill_version`**,不是全局常量
- `resolveExact``skill-package-registry.ts:459`)只在 `status === "blocked"` 时失败,**`deprecated` 正常放行**
- 因此存量 case 会继续跑它绑定的 10.0.13**不会 mismatch,不需要数据迁移**
- registry 里 10.0.0 → 10.0.13 共 15 个版本、历史版本全部 `deprecated`**升版是本仓库的常规操作**
升版必须同步改动以下 **5 处**,缺一即坏:
| # | 位置 | 改什么 |
| --- | --- | --- |
| 1 | `skills/jyotish-birth-time-rectification/versions/10.0.14/` | 以 `versions/10.0.13/` 为基础新建;改 `SKILL.md` 正文约束 + frontmatter `version: 10.0.14` |
| 2 | `skills/jyotish-birth-time-rectification/SKILL.md` | 根目录是最新版的**副本**,与上一行保持字节一致 |
| 3 | `skills/skill-package-registry.json` | 新增 10.0.14 条目 `status: "active"``packagePath` 指向 `versions/10.0.14`10.0.13 改 `"deprecated"` |
| 4 | 同上条目的 `sha256` | 用 `computeSkillPackageSha256()``skill-package-registry.ts:289` 已导出)**只对 `versions/10.0.14/` 计算** |
| 5 | `frontend/src/lib/rectification-agentic/v9/case-status.ts:92` | `RECTIFICATION_SKILL_VERSION = "10.0.14"` |
| 6 | `frontend/tests/skill-registry.test.ts` | 该文件**硬编码了 sha256**,必须同步 |
**SHA 只对 registry `packagePath` 指向的目录计算,绝不对 Skill 根目录计算。** 根目录含 `versions/` 整棵子树,对它计算会把全部历史版本吃进哈希——而且能正常返回一个值、不会立刻报错,错了也发现不了。
两个坑:
- sha256 覆盖目标目录全部内容(递归遍历,每个文件写入长度前缀的 path / executable bit / bytes)。**改 frontmatter 也会改 sha** —— 顺序必须是"先改完所有字节,最后统一重算",不得边改边算。
- `case-service.ts:301` 校验 `resolveActiveSkillPackage(...).version === RECTIFICATION_SKILL_VERSION`registry 与常量不一致会让**新建 case 直接抛 `skill_registry_version_mismatch`**,功能整个开不了场。上表第 3 与第 5 处必须同时改。
前车之鉴:`797a423a` 就是"改了 skill 字节但忘了刷新 registry hash"的补救提交。**改完务必跑 `npx tsx --test tests/skill-registry.test.ts` 验证。**
`skills/jyotish-birth-time-rectification/versions/` 目录存有历史版本快照,按既有惯例处理。
---
## 验收标准
1. `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0
2. `cd frontend && npx tsx --test tests/rectification-*.test.ts` 不低于 705 条且 `fail=0`
3. 任务 0 的 5 条不变量全绿
4. **回归实证**:用事故 case 的真实输入(1 条证据 / 1 个 domain / 9 个候选 12,12,12,12,11,11,10,10,10)跑 `decideRectification`,断言 `canAdopt === false``selectionAllowed === false``nextAction` 回到收集类,并把前后输出贴进 PR
5. B-1 的 `grep` 自证:四个能力字段在决策分支函数里无字面量赋值
6. `npx tsx --test tests/skill-registry.test.ts` 通过,且 registry / frontmatter / `RECTIFICATION_SKILL_VERSION` / 测试内 sha256 四处一致
7. 新建一个 case 能正常开场(验证 `skill_registry_version_mismatch` 未被触发),且 opening 轮问题槽文案不含"再"字预设
## 交付前必须说明
- 逐条列出改动的既有测试断言(红线 5),每条写清原值与为什么原值是错的
- 本任务书的病灶分析来自**静态溯源 + 本地 `decideRectification` 直接调用复现**,作者无 staging 凭据(仓库内无 `.env`),**未在真实环境验证**。你若同样无凭据,不要声称已验证。
@@ -0,0 +1,99 @@
# 任务书 · Round 2 引擎侧收敛能力:相邻分钟可区分(2026-09-01)
基线:`origin/staging` @ `422fc65b`
## 0. 定位与执行顺序(必读)
本轮是 `TASK-rectification-provisional-adopt-20260901.md`(Round A)的后续,**必须在 Round A 合入并验收后再合入本轮**;可以并行开发,但不得混进同一个 PR。分工:
- Round A 解决"拿不到结果"provisional 采用成为一等成功出口(TS 决策层)。
- **本轮解决"结果能更细"**:让引擎具备把相邻分钟候选真正分开的能力(Python 引擎为主 + 少量 TS 接线)。Round A 红线 7"不修引擎"仅约束 A 轮,本轮就是引擎轮。
### 背景:为什么相邻分钟现在分不开(已静态核实)
事故 case `f83d9b42`(快照见 Round A §1):top-2 候选 05:00 与 05:07 分差 5、永远到不了 8,因为——
1. 打分粗粒度:`_score_event` 只做"大运/副运/微运主星是否落在主题宫位/宫主/主题分盘"的匹配,对相差 7 分钟的两个候选给分几乎相同。
2. dasha_boundary 区分题按大运边界年切窗口,相邻分钟几乎总在同一侧(逐条核对过该 case 全部 `expected_outcomes`)。
3. 唯一能分开相邻分钟的 varga 换升对比题(D24 在 05:00/05:06 换升)无年份锚点,被 `yearless_ungrounded_contrast` 政策丢弃——**该丢弃政策是对的**:不带时间锚的存在题("家里有没有结婚添丁住院")人群基础率接近 100%,答什么都不构成证据。
4. 引擎自带的"已知事件品质题"机制被写死为 clarify-only`scripts/rectification/event_probes.py` 第 4 行 "known_event_quality is clarification only and never a distinguish probe"TS 侧整类过滤(`decision-from-dossier.ts:187``core/build-state.ts:254`)。
方法论依据(本轮 P0 的正当性来源):`references/birth-time-rectification-advanced.md`——**出生时间差 1 分钟,换运日期漂移 3-4 天**。7 分钟差 ⇒ 换运日期差约 20-30 天 ⇒ 一条日级精度事件(该 case 已有 2024-08-08 分手)足以仲裁相邻候选。现有打分完全没有使用这一层。
## 1. 硬红线
1. **绝不引入任何"答案钥匙"。** 不得加入按已知用户/已知案例定向的回归提示、target_minute、校准包(教训见 Round A 根因 3:上游 `pl9_1993 target_minute=14:49` 污染事件)。所有新打分与出题逻辑必须对任意未知用户同样成立,`grep -rn "target_minute\|pl9_1993\|regression_only" scripts/ frontend/` 在改动后必须零命中。
2. **确认门与表达边界不动。** `confirmation_allowed` fail-closed、"不可分区间/代表性候选"措辞、candidate/accepted/confirmed 三层语义、Round A 的采用门语义,一律不改。
3. **版本与缓存一致性。** 打分语义变化必须 bump `ALGORITHM_VERSION``active_rectification_events`)与 `POLICY_VERSION``decision_policy.py`),并确认 TS `candidateSnapshotSource``core/snapshot-source.ts`)因 policy 版本变化而判定旧快照过期、触发重算,而不是复用旧候选。
4. **确定性。** 新逻辑全部确定性可复算:同输入同输出;先验表是版本化常量,不得运行时拟合。
5. **权重有界。** 任何新分量单独不得超过一条日级事件本体的分值;不得让辅助层反客为主(沿用 skill 决策树"辅助裁判不是第一裁判"原则)。
6. **Python 侧改动跑 `pytest tests/`(受影响模块相关测试全过);frontend 侧 `./node_modules/.bin/tsc --noEmit` + `npx tsx --test tests/rectification-*.test.ts` fail=0。不要用 `npx tsc`。**
7. 不改 `.gitea/workflows/**`;不自行把 staging 提升到 main;无凭据不得声称已在真实环境验证。
## 2. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-engine-convergence-20260901 \
../.worktrees/rectification-engine-convergence-20260901 origin/staging
```
`docs/research/pre_work_error_ledger.md``frontend/AGENTS.md``docs/BUG_HISTORY.md`BUG-456/459 及 Round A 记录)、`references/birth-time-rectification-advanced.md`(方法1)与 `references/birth-time-rectification-decision-tree.md`。**行号只是线索,按符号名定位。**
---
## 任务 0(门控)· 先写不变量,先让它红
Python 侧(`tests/` 新增或扩展对应模块测试)+ frontend 侧各写:
1. **P0 核心回归**:构造事故 case 形状(31 分钟网格、5 条事件含一条 day 级 2024-08-08 relationship_end),断言启用换运贴近度后 05:00 与 05:07 的分差**发生可解释的非零变化**,且 margin 方向与该分钟下 AD/PD 换运日期贴近度一致(用引擎自身输出核对,不得硬编码期望分数)。
2. **基础率不变量**:一道无年份家人存在题的最终出题排序必须低于一道锚定已知事件的品质题;主导答案先验 > 阈值的存在题不得被发出。
3. **品质题门控**:仅当候选 signature 组的分盘类型预测**不同品质**时,`known_event_quality` 才可作为 distinguish 出题;同品质预测时不得出题;每 case 上限 2 题。
4. **无答案钥匙**:红线 1 的 grep 断言写进测试或 CI 可执行脚本。
跑一遍确认 1-3 是红的。
## 任务 A(P0)· 换运日期贴近度打分——用已有证据分开相邻分钟
**改 `scripts/`Python**
- 新增确定性分量 `dasha_transition_proximity`:对每条 `date_precision ∈ {day, month}` 的可评分事件、每个候选分钟,计算该候选盘下事件日期附近(建议 ±45 天窗口)最近的 Vimshottari AD/PD 换运日期与 Narayana AD 换运日期,按贴近度给分(建议线性核 `max(0, 1 |Δdays|/K)`day 级 K≈15、month 级以月中代表日且 K≈45 并降权;year 级不参与)。边界日期计算复用 `event_probes.py` 既有的 `_vim_start_dates` / `_narayana_start_dates` / `_boundary_windows` 机制,不要另写一套历法。
- 分量并入候选打分(`_score_event``scoring_service` 聚合层,按现有架构选注入点),权重遵守红线 5;`rule_ids``vim_transition_proximity_*` / `narayana_transition_proximity_*`execution ledger 增加对应 technique layer 行,`_AUDIT_LABELS` 补中文行(如"换运贴近度:本轮已对照日级事件与候选换运日期的贴近程度")。
- 诊断联动:`date_sensitivity` 类诊断需覆盖新分量(日期扰动应显著改变贴近度分,这正是该分量的预期行为,不得因此误判不稳定——必要时对该分量单独设扰动容差并在 PR 里说明)。
**验收锚**:事故形状下,2024-08-08 这条 day 级事件对 05:00 与 05:07 产生可审计的分差贡献(贴 ledger 行与前后 margin)。
## 任务 BP1)· known_event_quality 升级为受门控的区分题
**改 `scripts/rectification/event_probes.py` + TS 接线**
- 政策变更(更新文件头注释):`known_event_quality` 允许作为 distinguish 探针,门控三条——(a) 仅当候选 signature 组在该事件主题分盘(D24/D5/D9/D10/D12/D4)上类型预测**不同品质/方向**时生成;(b) 每 case 上限 2 题;(c) 必须锚定一条 confirmed 证据(携带 `target_evidence_id``display_date_label`),题干问品质不问存在("2016 年 9 月那次上大学,更接近如愿 / 将就调剂 / 发挥失常 / 说不清"),选项语义映射到候选组。
- `choice_kind` 沿用 `event_quality``expected_outcomes` 按类型表映射候选组,经 `assert_distinguish_contract` 校验。
- TS 侧解除整类过滤但保留门控:`decision-from-dossier.ts`(约 :187)与 `core/build-state.ts`(约 :254)只放行携带 `target_evidence_id` 锚点的 `known_event_quality` 探针;`method-followup.ts` / `choice-card.ts` 的 event_quality 渲染路径已存在(`choice-card.ts:236`),确认端到端可出卡;`probe-question-contract` 如需版本演进按 `questionContractVersionIsCompatible` 兼容规则做。
- 打分权重:品质题答案对候选组的后验更新不得高于现有 ±2 制;在 PR 里说明取值。
## 任务 C(P1)· 基础率校正的期望信息增益
**改 `scripts/rectification/event_probes.py` / `candidate_contrast.py`**
- 为每个(事件家族 × 窗口类型)配版本化答案先验常量表(保守估计即可,写明出处与理由;例:任意 3 年窗"家人婚/病/住院" p_yes≈0.85;指定年"本人迁居外地长住" p_yes≈0.2;已知事件品质"如愿/失常"≈0.5/0.3)。
- 排序指标改为期望信息增益 `EIG = Σ_a p(a) × ΔH(a)`,保留原候选分割增益为 `raw_split_gain` 供审计;主导答案先验 > 0.8 的存在题不发出(进 `dropped_probes`reason `dominant_answer_prior`)。
- `yearless_ungrounded_contrast` 丢弃政策保留不动——本任务是给被丢弃的对比意图提供 B/C 两条合法出路,不是放开丢弃。
## 任务 D(P2)· 簇内不可分时的前瞻验证登记
**改 `scripts/rectification/refinement_packet.py` + TS 展示**
- 当区分题耗尽且 top 簇稳定时,refinement packet 新增 `prospective_probes`:对每个存活候选组计算未来 1-3 年内的 AD/PD 换运窗口(领域 + 年月窗),输出"候选 A 预测下一次事业变动更可能在 X 年 Y 月附近;候选 B 在 Z 月附近"的结构化数据。**不是问题、不计分、不进采用门**,仅在交付轮叙述中呈现("下次发生时回来补一条,可进一步分辨"),并留在 receipt 里供 case 恢复时使用。
- TS:交付轮叙述接入该字段(`turn-narration.ts` / 交付文案路径),措辞保持"预测窗口"而非承诺;case 保持 resumable 语义不变。
---
## 验收标准
1. Python:受影响模块 pytest 全过;任务 0 的四组不变量全绿。
2. Frontend`./node_modules/.bin/tsc --noEmit` exit 0`npx tsx --test tests/rectification-*.test.ts` fail=0(含 Round A 合入后的全部不变量)。
3. **事故形状前后对照**贴进 PR:任务 A 启用前后 top-2 分差与 margin、新 ledger 行;任务 B 生成的品质题样例(题干、选项、expected_outcomes);任务 C 下无年份存在题被 `dominant_answer_prior` 丢弃的样例。
4. `ALGORITHM_VERSION` / `POLICY_VERSION` bump 且旧快照判定过期的证明(snapshot-source 测试或等价说明)。
5. 红线 1 的零命中 grep 结果贴进 PR。
6. 先验常量表逐项写明估计依据;未在真实环境验证的部分如实注明。
@@ -0,0 +1,169 @@
# 任务书 · 非终态轮出口统一与访谈死路根治(2026-09-01)
基线:`origin/staging` @ `857f9dc9`
上一轮(`9f011194` + `4b133177`)把「证据不足却允许采用」堵住了,这部分有效且必须保留。但只做了"不给错的",没做"给对的出路",结果是**用户走完 7 条证据、答完全部区分题,最后卡在死路上,既不能采用也没有下一个问题**。
对用户而言这不是改进:从「拿到一个错误结果」变成「拿不到任何结果」。本轮的目标只有一个 —— **任何非终态轮结束后,用户都必须有下一步可走。**
---
## 事故实证
真实 case `645ba774-67fb-4d6f-b842-f59e4d97adc0`Skill `10.0.14`,7 条已确认证据、3 个领域、6 个区分题已作答。最终状态:
```
case.status : collecting_evidence ← 没有收尾
completed_at : null
current_question : null ← 没有下一问
choice_card : null
interview.type : offer_provisional_range
can_adopt : false
selection_allowed : false
```
界面显示「当前没有可回答的问题,正在等待服务端更新」,然后永远等下去。
**注意:这次 `can_adopt: false` 是正确的,不要去改它。** 引擎自己的门也没过:`diagnostic_quality.passed: false``margin_percent: 4.476`(最低 10)、`date_sensitivity_retention_rate: 0.2857`、候选跨度 04:5105:15 共 24 分钟、`overall_confidence: "low"`。引擎 receipt 里 `acceptance_allowed: true` 与它自己的 `diagnostic_quality.passed: false` 自相矛盾,那是引擎侧的问题,**本轮不修引擎,也不得因此放宽 TS 侧收紧**。
---
## 两个根因
### 根因 1 · `answer_choice` 路径没有任何非终态兜底
`persistNextInterviewIfIdle``ensureNonTerminalTurnExit` 的**全部**调用点都在 `message` / `opening` 分支内:
```
route.ts:438 persistNextInterviewIfIdle message 预检分支)
route.ts:513 persistNextInterviewIfIdle message 预检分支)
route.ts:678 persistNextInterviewIfIdle action === "message" || action === "opening"
route.ts:689 ensureNonTerminalTurnExit (同上)
```
`isStructuredChoice``action === "answer_choice" || "stop_and_review"`,见 route.ts:173、249)在第 249 行独立分支里处理完就 `return`,**一个兜底都不经过**。事故最后一步正是用户点选项:`applyRectificationChoice``persistNextInterviewAfterChoice` 没能建出 focus,此后没有任何补救,`current_question` 永久为 null。
这与 **BUG-456** 是同一类缺陷(非终态轮结束时未保证服务端状态)。BUG-456 修了 opening,漏了 answer_choice —— 因为当时的修法是"给某个分支补一次调用",而不是"给所有出口建一道闸"。
### 根因 2 · probe 耗尽后决策链提前 return,不转向收集
事故 case 的 `inference_state.probes` 共 11 条:
- 4 条带年份的 `dasha_boundary` / `dasha_activation` —— **全部已在 `answered_probes` 中**
- 7 条 `varga_contrast` —— **全部因 `yearless_ungrounded_contrast` 进了 `dropped_probes`**
于是 `discriminatorProbe = null``decideRectification` 走到:
```ts
if (!separation.sufficient) {
if (probe && !userStopped) return discriminateOrExhaust(...);
return completeWithRange(separation, holdout, range, "offer"); // ← 停在这里
}
if (holdout === "not_started" && !userStopped) {
return holdoutValidation(separation, range); // ← 永远到不了
}
```
**但这个 case 明明还有路可走:**
- `oos_blind_prompts``family``health_pressure` 两条线一次都没用过。`canAskHoldout``decision-from-dossier.ts:134`)正是以 `oosBlindPrompts.length > 0` 判定 holdout 为 `not_started` —— 也就是说 holdout 验证**本来可以问**,只是决策链在 `!separation.sufficient` 处提前返回,根本走不到那个分支。
- `nakshatra_boundary.near_boundary: true`,且带有现成的 A/B 选项与 `user_meaning`
「候选分不开」且「没有区分题可问」的正确出口是**回去收集新证据或做 holdout 验证**,不是停在 `offer_provisional_range` 等死。
---
## 硬红线
1. **不得放宽上一轮的收紧。** `deliveryCapability`、引擎 ceiling 交集、`strictAliasedBoolean`、四个阈值常量(`MIN_SEPARATION_LEAD=8``MIN_STANDALONE_*=3/2``MIN_ACCEPTANCE_*=3/2`)一律不动。本轮修的是"没有出路",不是"门太严"。任何让事故 case 变成 `canAdopt: true` 的改动都是错的。
2. **不得修引擎 receipt 的语义。** `acceptance_allowed``diagnostic_quality` 自相矛盾是引擎侧问题,本轮只做记录,不在 TS 侧"修正"引擎输出。
3. **不得用「让用户重开一个 case」当作出路。** 死路必须在当前 case 内解决。
4. **不得靠模型正文兜底。** 问题槽仍由服务端拥有;不得引入正文正则、问号检测或字符串匹配来判断"是不是已经问过了"。
5. 不得修改既有行为断言 —— 除非该断言锁住的正是本轮要修的死路本身。**配额:错误行为组 ≤ 3 组、断言 ≤ 6 条**,超出立即停下汇报。机械性版本身份同步不计入(口径见 BUG-459)。
6. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**(新建 worktree 未 `npm install` 时会装到空包 `tsc@2.0.4`)。
7. `tests/rectification-*.test.ts` 不得低于基线 **714**`skill-registry.test.ts` 保持 16,且 `fail=0`
8. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**用户永远有下一步 > 不得放行不该放行的采用 > 功能与测试不回归 > 代码整洁**。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-nonterminal-exit-20260901 \
../.worktrees/rectification-nonterminal-exit-20260901 origin/staging
```
`docs/research/pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。在 `docs/BUG_HISTORY.md` 检索 **BUG-456**(同类缺陷)与 **BUG-459**
**行号只是线索,按符号名定位。**
---
## 任务 0(门控)· 先写不变量,先让它红
`frontend/tests/rectification-decision-authority.test.ts` 增加**本轮的核心不变量**
> **任何非终态轮结束后,`current_question` 非空 与 `canAdopt` 为真,二者必居其一。**
这一条同时挡住 BUG-456、本次死路,以及将来任何新增路径上的同类问题。它是本轮唯一真正的防线 —— 任务 A/B/C 都只是补洞,只有它能防止下次换个路径再漏。
必须覆盖**全部四种**推进 case 的 action`opening``message``answer_choice``stop_and_review`。用组合枚举,不要手挑用例。
再补两条:
- **probe 耗尽不得成为死路**:`discriminatorProbe === null``separation.sufficient === false` 且非 `userStopped``nextAction` 属于收集类或 holdout 验证类,**不得**是 `offer_provisional_range` 终点态。
- **事故 case 回归**:用 `645ba774` 的真实形状(7 证据 / 3 领域 / 6 题已答 / 11 probe 全部已答或被丢弃 / `oos_blind_prompts` 剩 family、health_pressure)断言 `canAdopt === false` **且** 有下一问。
跑一遍,确认这三条是红的。**全绿说明测试没写对,重写,不要往下走。**
---
## 任务 A(P0)· 所有非终态轮出口统一兜底
不要再"给 `answer_choice` 分支也补一次调用" —— 那正是 BUG-456 的修法,也正是这次复发的原因。
`ensureNonTerminalTurnExit` 提升为 **route 的公共出口闸**:任何成功且非终态的轮次,无论 action 是什么、无论走确定性路径还是 agent 路径,返回前都必须经过它。`read_only` 保持无副作用,显式排除并注释理由。
实现上建议用统一的收尾包装,而不是在每个 `return` 前手工插一行 —— 后者一定会在下次新增分支时再漏。请在 PR 里说明你如何保证"新增分支不会绕过这道闸"。
---
## 任务 B(P0)· probe 耗尽必须转向,不得停在原地
调整 `decideRectification``!separation.sufficient` 分支的出口:当 `probe === null` 且非 `userStopped` 时,不得直接 `completeWithRange(..., "offer")`,而应按优先级转向:
1. holdout 可问(`holdoutValidation === "not_started"`,即 `oos_blind_prompts` 非空或有保留事件)→ 走 holdout 验证
2. 仍可收集(存在未使用的领域线索,如 `oos_blind_prompts` 的 family / health_pressure)→ 回到收集
3. `nakshatra_boundary.near_boundary === true` 且带 A/B 选项 → 该边界题是合法的下一问
4. 以上都没有 → 才允许停在区间交付,且**必须**同时给出一个明确的收尾出口(用户可主动停止并拿到区间),不得留空
注意红线 1:转向不改变 `canAdopt`。事故 case 转向后仍应是 `canAdopt: false` + 有下一问。
---
## 任务 C(P1)· 服务端问题写进 turn 历史
上一轮 D-1 让服务端 focus 成为唯一问题源,副作用是**问题不进消息历史**:`turns` 里只剩「接下来请点选下面这一问。」这类占位,用户滚回去看不到问过什么。
这是上一版任务书的规格疏漏 —— 我写了"唯一问题源",没写"问题也要进历史",两者并不冲突。
修法:服务端 focus 仍是唯一来源,但把它的 prompt **由服务端写入 turn 文本**。不是让模型复述(会漂移),是服务端直接写。占位文案「接下来请点选下面这一问。」应被真实问题取代。
---
## 验收标准
1. `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0
2. `npx tsx --test tests/rectification-*.test.ts` 不低于 714 且 `fail=0``skill-registry.test.ts` 16/16
3. 任务 0 的三条不变量全绿,且覆盖四种 action
4. **事故回归实证**:用 `645ba774` 的真实形状跑,断言 `canAdopt === false` **且** `current_question` 非空,把前后输出贴进 PR
5. 上一轮的不变量(引擎上限、证据不足不放行、并列不采用、holdout unavailable 不放行、overlay 只收紧)**仍然全绿**
6. 任务 A 的"新增分支不会绕过闸门"机制说明写进 PR
7. 贴出修复后 turn 历史里问题文案的实际样子(任务 C)
## 交付前必须说明
- 逐条列出改动的既有断言(红线 5),每条写原值与为什么原值是错的
- 本任务书的根因来自静态溯源 + 事故 case 快照分析,作者无 staging 凭据,**未在真实环境验证**。你若同样无凭据,不要声称已验证。
- 修复后请在 `docs/BUG_HISTORY.md` 追加记录,并在 `复发自` 明确指向 **BUG-456** —— 这是同一类缺陷的第二次出现,旧防线(给单个分支补调用)为何失效必须写清楚。
@@ -0,0 +1,156 @@
# 任务书 · 采用门对齐引擎语义:provisional 采用成为一等成功出口(2026-09-01)
基线:`origin/staging` @ `75fc456d`
## 0. 决策记录(必读,本轮推翻旧红线是经产品拍板的授权变更)
上一轮任务书(`TASK-rectification-nonterminal-exit-20260901.md`)写有红线:"不得放宽 `deliveryCapability`"、"任何让事故 case 变成 `canAdopt: true` 的改动都是错的"。**该红线已由产品负责人于 2026-09-01 显式推翻**,理由见下方根因:现行 `deliveryCapability` 把"可采用"绑在了数学上不可达的条件上,导致**没有任何真实用户能走完一次成功流程**。本轮的目标:
> **当引擎按方法论判定可以出牌时,用户必须真的能采用"代表分钟 + 可信区间"provisional)。分钟级分离与 holdout 只保留给"唯一分钟确认"这道门。**
依据(三个权威本来就一致,只有 TS 层跑偏):
- 引擎 receipt`rectification-candidate-policy-v2`):`acceptance_allowed` 四门(候选存在 / ≥3 可评分事件 / ≥2 领域 / 日期质量)+ `propose_allowed`4 事件/3 领域/诊断稳定 **或** 事件吻合率 ≥80%)。它老实承认相邻分钟不可分(本 case `indistinguishable_width_minutes: 29`),所以它的交付物定义就是"代表时间 + 区间"。
- V10 Skill 文本(10.0.14`references/candidate-comparison.md`):"唯一领先和宽度≤5 **只挡确认门**,不挡出示代表性时间卡。"——**skill 文本无需改动,本轮不 bump skill 版本。**
- 上游方法论(yinduzhanxing `interview_playbook.md`):最终交付措辞就是"当前最优结果是候选时间段…临时代表时间仅用于下一轮验证与比较"。
**不放宽的东西**(新红线,见 §3):唯一分钟确认门、表达边界、证据下限、引擎 ceiling 交集。
---
## 1. 事故实证
真实 case `f83d9b42-0ac7-411b-9e43-95059b2ede43`Skill `10.0.14`,快照已存档(向任务发起人索取原始 JSON)。关键形状:
- 5 条 confirmed 证据(education×1 被引擎划为 holdout、relationship×2、career×2),3 个领域;5 道区分题已答完;候选从 04:45–05:15(30 分钟)收敛到 **05:0005:077 分钟)**,熵 2.00→0.86。流程本身工作正常且收敛良好。
- 引擎 receipt`acceptance_allowed: true``selection_allowed: true``propose_allowed: true``event_fit_rate.band: "high"`80%)、`margin_percent: 19.94`
- TS overlay(用户实际看到的):`can_adopt: false``selection_allowed: false``propose_allowed: false``choice_card: null``interview.type: "offer_provisional_range"`
- 结果:用户答完约 10 道题、花掉点数,得到一句"当前可信区间是 05:0005:07"的文字,没有候选卡、什么都存不下来,然后被继续追问"有没有记得住时间的收入变化"exhaustion 轮换:finance→occupation→health_pressure→relocation→career→relationship→通用兜底),无限循环。
## 2. 根因(已静态溯源逐条核实)
### 根因 1 · `deliveryCapability` 的两个条件数学上不可达
`frontend/src/lib/rectification-agentic/core/rectification-decision.ts``deliveryCapability`(约 :196,按符号名定位):
```ts
const locallySelectable = separation.ranked.length > 0
&& !coverageBlocks
&& stopClass?.kind !== "keep_collecting"
&& stopClass?.kind !== "exhausted"
&& separation.sufficient // ← 不可达之一
&& holdout === "passed"; // ← 不可达之二
```
- **`separation.sufficient` 不可达**:需要 top-2 领先 ≥ `MIN_SEPARATION_LEAD=8`,计分制每题 ±2。逐条核对事故 case `inference_state.probes``expected_outcomes`:**所有剩余可问的题(dasha 边界题、nakshatra 边界题)都把 05:00 与 05:07 分在同一组**,答什么都同涨同跌。唯一能分开这两个分钟的是 d24/d12 等 varga 对比题,但全部被 `yearless_ungrounded_contrast` 政策丢进 `dropped_probes`。lead 永远停在 5。
- **`holdout === "passed"` 事实上不可达**`ask_holdout_validation` 分支在 `decideRectification` 里位于 coverage 检查之后、且要求 probe 耗尽;即使问到并通过,第一条仍然封死 `canAdopt`
- 这不是单 case 现象:任何"约 5 条真实事件 + 31 分钟候选网格"的普通用户都落在同一形状上。**成功率结构性为零。**
### 根因 2 · 引擎与 TS 权威冲突时,TS 永远向"否"裁决
引擎与 skill 文本都认为本轮可出牌;TS `deliveryCapability`(来自 9f011194/4b133177 两轮收紧)单方面追加了分离+holdout 要求。overlay`decision-from-dossier.ts``overlayPublicDecision`)取交集,于是引擎的"可以"永远被 TS 的"不行"覆盖。
### 根因 3 · "本地 Agent 能收敛到 1-2 分钟"不构成 web 端目标(答案泄漏,勿对标)
有一组本地对话记录(yinduzhanxing 根目录 `1993生时校正对话-*.txt`)显示本地 Agent 调用同一 skill 能把 1993 案例收敛到 14:49。**那不是校时能力,是答案泄漏**:本地仓里存有该用户的既有结论(`qizheng_1993_native_current_report_2026_08_26.md` 记录 14:49:00、候选段表钉死 14:48-14:50、`classical_zr_known_user_case_calibration_1993` 回归包),Agent 在对话里两次自认"14:49 主要被仓里的已知校准档锁住,不是单靠回答算出来的",随后更把 `target_minute=14:49` 作为回归提示直接写进了脚本(该污染只在上游 `codex/add-birth-time-rectification-skill` 分支 commit 3bb90620**已核实未进入本产品仓与 vendored skill 快照**`grep pl9_1993|target_minute|regression_only` 为零命中)。web 端面对无答案库的陌生用户,7 分钟不可分区间 + 80% 拟合才是 5 条事件的真实信息量上限。任何人(包括测试)不得以"收敛到唯一分钟"作为本轮验收口径,也不得把该上游分支的回归提示代码引入产品。
### 附带确认(本轮不修)
- coverage 死锁**不存在**`exhaustionSpokenCollectFollowup` 的轮换在 family/education/finance 之后会问 occupation,且拒答(`occupationCollectFocusClosed`)也算覆盖。coverage 门保留。
- 引擎 receipt 的 `acceptance_allowed``diagnostic_quality.passed` 不联动,是已记录的引擎侧特性(见 BUG_HISTORY / 上轮任务书),本轮不动引擎。
- 上一轮的非终态出口闸(`finalizeSuccessfulTurnExit`)工作正常,本 case `current_question` 非空。保留。
---
## 3. 硬红线(本轮新版)
1. **唯一分钟确认门一个字不放宽。** `canConfirmExactMinute` 必须继续要求:分离充分 + holdout passed + `confirmationAllowed` + 引擎 `confirmation_allowed`(引擎恒 falsefail-closed)。改完后任何路径下 `can_confirm_exact_minute` 仍不得为 true。
2. **表达边界不放宽。** "不可分区间 / 代表性候选 / 不是已确认的唯一出生分钟"等措辞、`REPRESENTATIVE_MINUTE_DISCLAIMER``RECTIFICATION_TERMINATION_COPY` 保持;采用后写入 `accepted``active_birth_time`),**不得**写 `confirmed`
3. **证据下限不放宽。** `keep_collecting`< `MIN_STANDALONE_DATED_EVENTS=3` 或 < `MIN_STANDALONE_DATED_DOMAINS=2`)时仍不得采用。四个阈值常量(`MIN_SEPARATION_LEAD=8`、3/2、3/2)数值不动——本轮只改它们**参与哪道门**,不改它们的值。
4. **引擎 ceiling 仍是硬上限。** overlay 对引擎结果只能收紧,不能放宽:receipt 不自洽或 `acceptance_allowed=false` ⇒ 采用仍关死(`engineCapabilityCeilingFromReceipt` 的 fail-closed 语义不动)。
5. **coverage 门保留。** `coverageBlocks`(含 occupation)仍挡采用——它经 exhaustion 轮换可满足,且与 skill 文本一致。
6. **不 bump skill 版本,不改 `skills/**`。** skill 文本已与本轮目标语义一致。
7. **不修引擎(Python 侧)。** 本轮只动 TS 决策层与必要的投影/UI。
8. 推 staging 前 `cd frontend && ./node_modules/.bin/tsc --noEmit` 必须通过。**不要用 `npx tsc`**(空 worktree 会装到假包 `tsc@2.0.4`)。
9. 不改 `.gitea/workflows/**`;不在有未提交改动的工作树上切分支;不自行把 staging 提升到 main。
10. 作者无 staging 环境凭据,根因来自静态溯源 + 事故快照。你若同样无凭据,**不得声称已在真实环境验证**。
让步顺序:**用户能拿到诚实且可采用的结果 > 确认门与表达边界不放宽 > 功能与测试不回归 > 代码整洁**。
## 4. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-provisional-adopt-20260901 \
../.worktrees/rectification-provisional-adopt-20260901 origin/staging
```
`docs/research/pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。在 `docs/BUG_HISTORY.md` 检索 **BUG-456 / BUG-459** 及上两轮记录。**行号只是线索,按符号名定位。**
---
## 任务 0(门控)· 先写不变量,先让它红
`frontend/tests/rectification-decision-authority.test.ts`(或新文件 `rectification-provisional-adopt.test.ts`)加入:
1. **事故形状回归(核心)**:按 `f83d9b42` 的真实形状构造 dossier——5 条 confirmed 证据(education/relationship×2/career×2education 为 holdout usage)、inference 5 题已答、活跃候选 05:00=24 / 05:07=19 / 04:53=8lead=5 < 8)、引擎 receipt `acceptance_allowed/selection_allowed/propose_allowed` 全 true、`confirmation_allowed=false``diagnostic_quality.passed=false`、oos_blind_prompts 剩 family/finance/health_pressure、occupation 未覆盖、family 已拒答。断言:
- a) occupation 覆盖(confirmed 或拒答关闭)后:`can_adopt === true``selection_allowed === true``credible_range``["05:00","05:07"]``representative_time === "05:00"`
- b) 任何情况下 `can_confirm_exact_minute === false`
- c) occupation 仍未覆盖时:`can_adopt === false``current_question` 语义上存在下一步(沿用上一轮不变量)。
2. **证据下限保留**:同形状但只有 2 条事件或 1 个领域 ⇒ `can_adopt === false` 且 nextAction 为收集类。
3. **引擎上限保留**:同形状但 receipt `acceptance_allowed=false`(或字段不自洽触发 fail-closed ceiling)⇒ `can_adopt === false`
4. **确认门冻结**:构造分离充分 + holdout passed + 引擎 `confirmation_allowed=false``can_confirm_exact_minute === false`;引擎四字段自洽且 confirmation true 的合成形状下才允许 true(现网引擎恒 false)。
跑一遍,确认 1a 是红的、其余按当前行为核对。**1a 全绿说明测试没写对,重写。**
## 任务 AP0)· `deliveryCapability` 重构
`core/rectification-decision.ts`,按符号名定位 `deliveryCapability`
- `locallySelectable`(供 `canAdopt` / `selectionAllowed` / `proposeAllowed`)改为:
```
ranked.length > 0
&& !coverageBlocks
&& stopClass?.kind !== "keep_collecting"
```
即**移除** `separation.sufficient`、`holdout === "passed"`、`exhausted` 三个条件。理由:分离与 holdout 移交确认门;`exhausted`tied_first / user_uncertainty_too_high)按引擎与 skill 语义只影响措辞与确认门——tie 时引擎照常给出代表候选,unsure 答案本来就不计分。
- `canConfirmExactMinute` 显式补回严格条件:
```
locallySelectable
&& separation.sufficient
&& holdout === "passed"
&& confirmationAllowed
&& engineCeiling.confirmationAllowed
```
(红线 1:净效果必须与现状等价或更严。)
- `capability` 已通过 `...capability` 展开进入 `collect/discriminate/holdoutValidation/offerRangeWithoutAdopt/completeWithRange/finish` 所有出口,无需逐出口改;确认无出口自行覆写这四个字段。
- `isNonConvergingRangeOffer``canOfferRange && !canAdopt && ...`)语义不变:capability 放行后它自然只在 coverage 未满足或证据不足时为真,exhaustion 收集兜底照旧工作。确认 `answer-choice.ts` 的 `persistNextInterviewIfIdle` / `persistExhaustionCollect` 在新语义下行为正确(occupation 覆盖前仍会追问;覆盖后进入可采用交付,不再轮询)。
## 任务 B(P0)· 交付点必须真的出牌
capability 放行只是数据层。逐条确认交付链路,缺哪补哪:
1. **投影**`decideFromDossier` → `overlayPublicDecision` → GET case 快照中 `can_adopt/selection_allowed` 为 true 时,`latest_result.candidates` 与 `interview` 投影能驱动前端候选卡(`rectification-agentic-chat.tsx` 的 `RectificationCandidateCards` / `birth-time-candidate-result`)。**用户不应需要再发一条消息才看到卡**——GET 刷新即可见。
2. **next_user_action**`decideConversationalSession` / `buildNextUserAction``method-followup.ts`)在 offer/complete 且 `canAdopt=true` 时应产出 `adopt_representative`(或等效采用引导),且该轮零追问(skill:不得同一回复既要求补证据又提供采用)。
3. **采用 RPC**`cases/[caseId]/candidates/accept` 链路上若存在 `selection_blocked` 类服务端校验,确认其读取的是新语义下的投影(采用不被旧字段挡住);采用后进入既有 `verify_adopted_time` 核前事流程,不改其逻辑。
4. **叙述**offer 轮正文按 skill §9 出验证报告(候选窗、代表分钟、相对支持、八法理由),并保留"不可分区间/代表性候选"边界句。`nonConvergingRangeNarration` 在可采用时不应再是唯一出口。
## 任务 C(P1)· 既有断言清点与治理
- 上一轮的下列不变量**按本任务书 §0 授权修改**,逐条在 PR 里写"原断言 → 新断言 → 为什么":分离不足不采用、holdout unavailable/not passed 不放行采用、`offer_provisional_range` 恒 `can_adopt=false`、(如存在)tied 不采用。
- 下列不变量**必须保持全绿**:证据不足不放行、引擎 ceiling 交集 fail-closed、确认门 fail-closed、非终态出口闸(BUG-456 组)、`read_only` 无副作用。
- 修改断言配额:仅限上述授权组;授权组之外不得改既有断言,遇到冲突停下汇报。
- `docs/BUG_HISTORY.md` 追加一条:定性为"策略死结"(不是回归),记录三权威冲突、被推翻的红线出处、授权来源与日期。
## 验收标准
1. `cd frontend && ./node_modules/.bin/tsc --noEmit` exit 0。
2. `npx tsx --test tests/rectification-*.test.ts` fail=0`skill-registry.test.ts` 16/16。测试总数允许变化(授权断言组改动 + 新增不变量),在 PR 里给出新基线数并解释增减。
3. 任务 0 的四组不变量全绿。
4. **事故形状前后对照**贴进 PR:改动前 `can_adopt=false` + finance 追问循环;改动后 occupation 覆盖 ⇒ `can_adopt=true` + 候选卡可见 + `can_confirm_exact_minute=false`。
5. 说明"新增决策分支不会绕过 capability 单点"的机制(capability 只在 `deliveryCapability` 一处计算、经展开进入全部出口,不得在出口处覆写)。
6. 未在真实环境验证的部分如实注明。
+69
View File
@@ -0,0 +1,69 @@
# 任务书 · Round A/2 收尾小尾巴:存量断言、存量快照重算、真实环境点测单(2026-09-01)
基线:`origin/staging` @ `8743dcb1`
## 0. 背景
Round A`20df8c60`)与 Round 2`8743dcb1`)已验收:tsc 通过、前端 `rectification-*.test.ts` 738 项 0 fail、`skill-registry` 16/16、红线 grep 零命中、引擎相关 Python 测试 77 项中 76 过。本轮只处理验收时发现的三个尾巴,**不改动两轮已落地的策略语义**。
## 1. 硬红线
1. 不改 `deliveryCapability` / `canConfirmExactMinute` / 引擎门语义——Round A/2 的策略一律不动。
2. 不改 `.gitea/workflows/**` 的触发条件之外的内容;任务 B 若涉及 CI 配置,只允许**增加**被漏跑的测试路径,不得移除或放宽既有检查。
3. `cd frontend && ./node_modules/.bin/tsc --noEmit` 通过(不要用 `npx tsc`);前端 `rectification-*.test.ts` 不低于 738 且 fail=0。
4. Python 测试用仓库 `.venv``python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`;系统缺 `python3-dev`/`g++` 时先装,否则 pyswisseph 编译失败)。
5. 无凭据不得声称任务 C 已执行;把点测单原样交给有权限的人。
## 任务 AP1)· 修陈旧断言:window_scan transitions 的 from_sign/to_sign
`tests/test_rectification_diagnostics_clusters.py`
`test_window_scan_reports_d9_diversity_with_sign_names` 现红:断言期望 transitions 只有
`layer/at/user_meaning`,而 `window_scan` 实际早已输出 `from_sign`/`to_sign`(生产快照可证,
且该失败在 `de84f069` 基线上即可复现——**非 Round A/2 引入**)。
- 修法:给期望值补上 `from_sign`/`to_sign` 两个字段(以引擎实际输出为准,不要反过来删字段——`from_sign/to_sign` 是前端 varga 对比展示在用的公开合同)。
- 修完跑:`PYTHONPATH="$PWD:$PWD/scripts" .venv/bin/python -m unittest discover -s tests -p "test_*rectification*.py"`,要求全绿(77+ 项 0 fail)。
## 任务 B(P1)· 查并补 CI 漏跑的 Python 测试
上述断言红了却无人发现,说明 CI 没有跑 `tests/test_*rectification*.py` 这批(或跑了但结果被忽略)。
1. 检查 `.gitea/workflows/**` 与相关脚本,确认 Python 引擎测试是否在任何 pipeline 中执行;把结论(在哪个 workflow、哪一步、为何漏)写进 PR。
2. 若确实漏跑:在既有 CI 中**增加**一步跑 `tests/test_*rectification*.py`(含 pyswisseph 依赖安装或使用现成镜像);若 CI 环境装不了 swisseph,至少加纯逻辑子集(`test_rectification_engine_convergence.py` 等不依赖历表的用例)并在 PR 里说明取舍。
3. 红线 2:只增不减。
## 任务 C(P0,人工)· 真实环境点测单
两轮 commit 均未做真实环境验证(BUG-463 已如实注明)。部署 staging 后按单执行,逐项记录:
1. 新账号开 case:opening 自然开场,无固定问卷倾倒。
2. 依次提供 3 个领域、5 条带年份/月份事件(含至少 1 条日级),确认逐条 confirmed、复述用 `display_date_label`
3. 答完服务器区分题;期间任意时刻 `current_question` 非空或已可采用(非终态出口闸)。
4. occupation 被问到后回答或拒答;随后**候选卡出现**,`can_adopt=true``can_confirm_exact_minute=false`
5. 点采用:RPC 成功、状态 `candidate_accepted`、写入 `accepted`(不是 confirmed)、进入 `verify_adopted_time` 核前事(最多两件)。
6. 前事核对"对不上"路径:可改选其他候选。
7. 采用后叙述包含可信区间 + 代表分钟 + "不是已确认唯一出生分钟"边界句;若有 `prospective_probes`,出现前瞻窗口叙述。
8. 事故 case `f83d9b42`(如仍可访问):GET 刷新后应直接可采用;采用成功后记录。
9. 新证据触发重算后,receipt `algorithm_version` 应为 `rectification-v5-matrix-scoring-7``policy_version``...-v3`audit 表出现"换运贴近度"行。
任何一项不符,开新 BUG 记录,不要现场改代码。
## 任务 D(P2)· 存量快照按新引擎重算的触达
现状:`scoreableSnapshotCurrentFromDossier``decision-from-dossier.ts:327` 附近)构造 stored/current 时两侧 `scoringPolicyVersion` 取自同一快照,policy v2→v3 在该路径检测不到;存量 case 要等下一次证据/答案变化触发 compare 才会用 v7 引擎重打分。**采用不受影响**(receipt `accept_allowed` 已可用),受影响的只是换运贴近度的重打分时机。
按影响小到大三选一实现(在 PR 里说明选择理由):
1. **推荐**:在服务端 compare 触发判断处(tool-service 相同指纹跳过 compare 的路径)把"stored `policy_version` ≠ 引擎当前版本"也视为需要重算的条件——引擎当前版本从 `/api/rectification/v5/score` 响应或既有 `RECTIFICATION_ENGINE_VERSION` 配置获得,不要在 TS 里硬编码 `v3` 字符串常量。
2. 或:给 case GET/turn 路径加"快照 policy 落后即标 `scoring_policy_changed` 过期",复用 `snapshot-source.ts` 已有的过期原因与文案。
3. 或(最小):只写一份运维说明:存量 case 在下一次证据变化时自然升级,不做代码改动——若选此项,必须在 PR 里论证为什么可接受。
约束:重算必须走既有 compare 链路与幂等键,不得静默批量后台重算所有 case(成本与计费边界);重算后旧 result 走既有 `invalidated_at`/superseded 语义。
## 验收标准
1. 任务 APython rectification 测试全绿(77+ 项 0 fail),贴前后输出。
2. 任务 B:CI 结论写进 PR;若补了 workflow,贴一次绿色运行记录。
3. 任务 C:点测单逐项结果(执行人、时间、每项 pass/fail)附在 PR 或运维记录。
4. 任务 D:所选方案 + 理由 + 对应测试(方案 1/2 需要新增至少一条"policy 落后触发重算/过期"的断言)。
5. `tsc --noEmit` exit 0;前端 `rectification-*.test.ts` ≥738 且 fail=0`skill-registry.test.ts` 16/16。
+218
View File
@@ -0,0 +1,218 @@
# 任务书 · 第二轮:真实成本与交付物(2026-08-31)
基线:`origin/staging` @ `3a4396a4`
**下面所有行号只是线索,请按选择器/函数名定位。**
## 为什么要做
前两份任务书(`TASK-billing-pricing-20260830.md``TASK-rectification-convergence-20260830.md`)已完成大部分实现。按代码逐条核对后,剩余缺口集中在两类:
**A 类 · 卡在缺真实数据,不是实现问题。** 计费任务 0 的聚合端点已就绪,但 staging 没有跑出真实单位成本,于是会员参数不敢动、缓存降幅无法验证、定价测算页只能显示估算。上一轮拒绝凭估算改参数是对的,**本轮负责把这个输入补上**。
**B 类 · 交付物本身还是空的。** 校正任务 7(逐条回答 → 区间收窄归因表)是 P0,且不依赖任何外部数据,但排在 8/9/10 后面一起没做。用户为一次校正付的钱,买的就是这张表。
还有一个上一轮遗留的阻塞项:**上下文缓存目前只对 Anthropic 生效**,若生产 provider 不是 Anthropic,则「随便聊」会员档的毛利模型不成立。
### 已完成,不要重做
计费任务 0/1/2/3/6,校正任务 0/1/2/3/6。核对依据见两份 PROGRESS 末尾的「外部核对(2026-08-31)」。
---
## 硬红线
1. **任务 0 是门控。** 会员参数、售价、公平使用上限的任何调整,必须在任务 0 产出真实单位成本之后。**不得凭估算改数**——上一轮正确地拒绝了这件事,本轮不得倒退。
2. **不得用生产用户数据做压测。** 任务 0 的数据采集用真实调用,但必须是可控账号,且不得为了凑数据而批量刷接口造成成本与容量浪费。
3. **归因表的收窄贡献必须来自服务端打分,不得由模型叙述生成。** 一张看起来专业、实则模型编的归因表比没有更糟。
4. **零贡献证据必须显式标注**,不得静默丢弃。这是诚实性的一部分。
5. **禁止答案回流**(沿用上一轮红线 3):任何以已知答案调出来的规则不得进入生产打分链路。
6. **上游同步必须过污染审查**(沿用上一轮红线 4):不得同步带 `pl9_1993``observed_*_case_only``target_minute` 标记的打分逻辑。
7. **诚实性不可让渡**:不得放宽 `confirmation-gate.ts` 的 blocker,不得降低 sealed holdout 门槛。
8. **不得修改既有测试断言** —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
9. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`
10. **数据库测试必须真跑。** `npm run test:db` 需要 Docker。没有 Docker 就不要推 —— 写进 `BLOCKED.md` 并停下。
11. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:**诚实性不回退 > 数据不损坏 > 功能与测试不回归 > 可验证的改进 > 成本 > 代码整洁**。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/round2-cost-delivery-20260831 \
../.worktrees/round2-cost-delivery-20260831 origin/staging
```
`pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索同类记录。
**先读这些:**
- 两份 PROGRESS 末尾的「外部核对(2026-08-31)」——本轮范围的依据
- `frontend/src/app/api/admin/usage/aggregate/route.ts` —— 任务 0 的现成入口
- `frontend/src/lib/agent-generation-settings.ts` —— `cachedSystemMessage()` 的 provider 分支
- `frontend/src/lib/rectification-agentic/v9/evidence-model.ts``core/rectification-decision.ts` —— 任务 2 要接归因的地方
- `references/upstream/1993-session-correction-rerun.txt` —— 归因表的目标形态就在这份记录里
---
## 任务 0P0,门控)· 在 staging 采到真实单位成本
### 事实
计费任务 0 已交付聚合端点,但 `report.full` 从未有过真实行(上一轮才接上计费),`chat.standard``rectification` 也缺分叉后的新数据。没有这批数据,任务 3(会员参数)与任务 1 的降幅验证都无法进行。
### 要做什么
1. 把当前 staging 部署起来,确认 `/api/health` 的 SHA 与 `origin/staging` 一致,并在 PROGRESS 记录。
2. 用可控账号跑出每类功能的真实调用,**每类至少 5 次、覆盖典型与偏长两种形态**:
- `chat.standard`:含多领域计算的完整咨询
- `rectification`:至少一个跑到证据停止或预算耗尽的完整 case
- `report.full`:至少一份分章节完整生成
3. 用聚合端点导出 per-feature 的 `runs / p50 / p95 / max``cost_microusd``input_tokens``output_tokens``duration_ms`,以及 `metadata.cache` 的命中情况。
4. 结果写进 PROGRESS,形成一张表:**功能 → p50 成本 → p95 成本 → 缓存命中率**。
### 验收
- 三类功能各有不少于 5 条真实 `usage_ledger` 记录。
- PROGRESS 里有上述表格,数值可复核到 request_id(脱敏,只列聚合值)。
- 明确记录当前生产/staging 实际使用的 provider 与 model_id —— **任务 1 依赖这个结论**
### 停止条件
若 staging 无法部署或无法产生真实调用,写进 `BLOCKED.md` 并停下。**任务 2 不受影响,可并行开工**;任务 1、3 必须等本任务。
---
## 任务 1P0,依赖任务 0 的 provider 结论)· 让上下文缓存对生产 provider 真正生效
### 事实
`frontend/src/lib/agent-generation-settings.ts`
```ts
/** Adds only Anthropic's message-level cache marker; other providers keep current behavior. */
export function cachedSystemMessage(content: string, model?: unknown) {
if (modelProviderId(model) !== "anthropic") return null;
...
}
```
用量解析覆盖全 provider,但缓存标记只对 Anthropic 下发。同文件顶部注释与 `api/daily-starlanguage/route.ts:105``deepseek-v4-flash`)都指向 DeepSeek 系。
会员「随便聊」档的毛利模型完全依赖这项降本,因此这是上线阻塞项。
### 要做什么
1. 按任务 0 确认的生产 provider,查清其上下文缓存契约——**以 provider 官方文档为准,不要凭记忆写**。要弄清:怎么声明、命中如何计价、最小可缓存长度、前缀稳定性要求。
2. 为该 provider 实现缓存声明,保证 system + skill 前缀落在缓存边界内。校正的 system message 内嵌绑定 Skill 全文(`v9/agent-run.ts``bootstrap`),是最值得缓存的稳定前缀。
3. 不支持缓存的 provider 保持现有行为,返回 `null`,不报错、不改变请求形状。
4. **降幅必须实测**:用任务 0 的同一批 case 形态再跑一轮,对比 p50 `cost_microusd`。没有前后对比就不许在 PROGRESS 声称降本。
### 验收
- 生产 provider 的请求能在账本 `metadata.cache` 看到非零命中。
- 对话与校正的 p50 `cost_microusd` 相对任务 0 基线有可测量下降,降幅写进 PROGRESS。
- 非目标 provider 行为不变,既有测试全绿。
---
## 任务 2(P0,无依赖,可立即开工)· 逐条回答 → 区间收窄归因表
> 这是 `TASK-rectification-convergence-20260830.md` 的任务 7,原文保留有效,本节为可独立执行的展开。
### 事实
`references/upstream/1993-session-correction-rerun.txt` 里,整场真实会话最有价值的产物是用户主动索要的那张表:
| 你的回答 | 压缩后的分钟区间 | 作用 |
| --- | --- | --- |
| 2017 入职,岗位内容不合 | 14:48–14:50 | 很强的收窄点 |
| 2023 末起量,一改风格就稳 | 14:48–14:50 | 最接近最终分钟的一条 |
本仓没有这个概念:`v9/evidence-model.ts``core/*.ts` 中不存在 `narrow` / `contribution` / `attribution` 语义,只有「这条证据支持哪个候选」,没有「这条回答把区间从 X 压到了 Y」。
同一份记录也证明了产品价值的真实边界:用户的 8 条生平锚点把窗口从 30 分钟压到 3 分钟,**收窄是真的**。归因表就是把这件事变得可见。
### 要做什么
1. 每次证据写入后记录**区间快照**:写入前宽度、写入后宽度、该条证据的归属贡献。**持久化**,不靠事后重算(重算会因候选集变化而不可复现)。沿用既有 receipt / SQL 函数模式,状态留在 Postgres。
2. 报告(校正任务 3 的产物)新增归因表:用户原话(走 `display_date_label` 口径)→ 收窄前后区间 → 一句作用说明。
3. 收窄贡献由服务端打分产出,**模型只做措辞**(红线 3)。
4. 无贡献的证据也要列出并标注「未产生收窄」(红线 4)。
5. 归因表要能回答用户那句真实提问:**「这个结论有多少来自我的回答?」** ——若最终区间主要由声明窗口或先验决定,必须说出来。
### 验收
- 报告含归因表,条目数等于已确认证据数。
- 每条的收窄前后宽度与该轮实际候选集一致,可复核。
- 零贡献证据被显式标注,不被静默丢弃。
- 归因数据来自持久化快照,重开页面结果一致。
---
## 任务 3(P1,依赖任务 0 与任务 1)· 会员参数与售价重设
> 这是 `TASK-billing-pricing-20260830.md` 的任务 5,上一轮正确地保留未改,本轮在有数据后执行。
### 事实
`product_entitlements` 的公平使用参数仍为种子值(`20260806020000_billing_products_subscriptions.sql:146-155`):月卡 `billingLimit 2000`、年卡 `24000`、Pro 月卡 `5000`、Pro 年卡 `60000`。上一轮已完成语义澄清与文案约束,只差参数本身。
### 要做什么
1. 用任务 0 / 任务 1 的实测 p50、p95 成本重设 `minuteLimit` / `dayLimit` / `billingLimit`。**按 p95 定,不按均值**——公平使用要挡的是尾部。
2. `billingLimit` 的定位是**熔断**,不是对用户承诺的配额;「随便聊」靠 `minuteLimit` + `dayLimit` 保护真人容量。语义已在上一轮写入 admin 文案,本轮只改数。
3. 按产品已定规则,移除 standard 月卡/年卡的 `rectification``report.full` 权益(改为单独计费)。**移除前必须核查存量订阅依赖**,有在期会员依赖这些配额时给出兼容处理,不得让权益凭空消失。
4. 售价(`billing_products.price_cents`)改动需产品确认后再执行,本任务只准备迁移,不擅自调价。
5. 全部走幂等迁移或既有 `admin_save_product_draft` 审计路径,**不得手改生产库**。
### 验收
- 迁移幂等,`npm run db:migrate:check` 在隔离 Docker PostgreSQL 里 apply / check / reapply 三次退出码均为 0。
- 新参数下,按 p95 成本估算的会员毛利写进 PROGRESS。
- 存量订阅的兼容路径有明确记录。
- 定价测算页读到新参数后,输出表与 PROGRESS 的数字一致。
---
## 任务 4(P1)· 「为什么是这个结论」的质疑通道
> `TASK-rectification-convergence-20260830.md` 的任务 8,原文有效。
真实会话里用户问「你是信息记忆联想了吗?还是靠印度占星专业技术推理出来的?」——这一问是全部真相的来源。本仓没有任何「为什么」入口(`v9/choice-card.ts``why` 是每个选项的说明,不是结论来源)。
内容全部来自服务端已有的 `claimCards` / `evidenceRefs` / `confirmation-gate` blocker,**不得由模型即兴生成**。与任务 2 的归因表共用同一份服务端数据,不要建第二套。
---
## 任务 5(P1)· 中途改窗口与保留证据重跑
> `TASK-rectification-convergence-20260830.md` 的任务 9,原文有效。
要点重申:同一 case 内重跑**不重复扣费**(沿用 `rectification:case:<caseId>` 幂等键);重跑必须留痕;新窗口与既有证据冲突时如实呈现,不得静默丢弃。
---
## 任务 6(P1)· 首轮批量出题
> `TASK-rectification-convergence-20260830.md` 的任务 10,原文有效。
`src/mastra/agentic-rectification.ts` 中「一次一问」仍在。上游手册的节奏是第 1 轮 3–5 道 A/B/C/D 一起出。批量轮在轮次预算里**记为一轮**。
---
## 建议执行顺序
1. **任务 2** —— 无依赖,交付物核心,立即开工
2. **任务 0** —— 与任务 2 并行,它是任务 1、3 的输入
3. **任务 1** —— 拿到 provider 结论后立即做,它是「随便聊」上线的阻塞项
4. **任务 3** —— 有成本数据后执行
5. 任务 4 / 5 / 6 —— 依次
## 交付
- PROGRESS 写进 `PROGRESS-round2-cost-delivery-20260831.md`,任务 0 的成本表与任务 1 的降幅对比单列。
- 每个任务一个提交。
- 推 staging 前:`./node_modules/.bin/tsc --noEmit` 通过、`npm run lint` 无新增 error、`npm run test:db` 在 Docker 下 fail=0、`npm run build` 通过。
- 全量 `./node_modules/.bin/tsx --test tests/*.test.ts` 的失败清单与基线逐条比对,确认无新增。
+5 -1
View File
@@ -54,9 +54,13 @@
}
],
"varga_none_style_option": {
"label": "都不是这些特质",
"label": "都不太像我",
"answer_class": "no"
},
"varga_unsure_style_option": {
"label": "一时说不好",
"answer_class": "unsure"
},
"unsure_style_option": {
"label": "这段记不清楚",
"answer_class": "unsure"
+561
View File
@@ -6557,3 +6557,564 @@
- 复发自:无
- 修复版本:待发布
## BUG-429 | 同一账号资料变化时星盘库被两个 effect 各写一遍 localStorage
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 主对话页星盘库 hydration 与本人记录 upsert
- 用户现象:进入对话或改一次出生资料时,主线程同步 `JSON.stringify` 并写 `localStorage` 两次;账号切换时两条路径靠 ref 提前 return 分工,没有显式分支。
- 触发条件:`accountId``profile` 引用变化。
- 根因:两个 `useEffect` 依赖数组都是 `[accountId, profile]`,一个负责首次本地+云端,一个无条件 upsert 本人。行为能跑通,但每次资料变化都重复序列化。
- 修复:合并为一个 effect,按 `clear` / `hydrate-then-persist` / `persist-self` 显式分支。云端失败仍保留本地库;本人记录仍 upsert;切账号仍清空。
- 验证:`frontend/tests/chart-library-session.test.ts` 覆盖清空、切账号 hydration、同账号 persist-self、云端只保留 other、云端失败保留本地。既有 `chart-library-other-profile` 源码锁未改。
- 防复发:同一份资料不得用两个同依赖 effect 靠 ref 提前 return 分工;分支名必须写在代码里。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-430 | 字体栈声明了 Inter 却从未加载,Windows/Linux 静默落到系统黑体
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:全站 `--font-body`、后台 antd `fontFamily`
- 用户现象:设计稿写了 Inter,实际请求里没有 woff2;非苹果系统落到 Segoe UI / 微软雅黑。
- 触发条件:打开任意带根布局的页面。
- 根因:仓库 0 个字体文件、0 处 `@font-face`、0 处 `next/font`CSS 与 admin token 却把 Inter 写在 StyreneB 之后。
- 修复:根布局用 `next/font/google` 加载 Inter`display: "swap"``--font-inter`)。`--font-body` 与 admin `fontFamily` 改为 `var(--font-inter, Inter)`。Tiempos / 宋体栈未改。构建产物与 `/` HTML 可见 woff2 preload。
- 验证:`frontend/tests/site-style-isolation-contract.test.ts``next build``/` HTML 含 Inter woff2 preload。
- 防复发:字体栈里出现的西文家族必须有 `next/font``@font-face`;缺授权的展示字体(Tiempos)不得用同一套办法偷偷补上。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-431 | 自托管 PostgreSQL 缺库时仍对用户写“Supabase 尚未配置”
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`GET /api/account`、会员余额、登录失败提示、会话/报告/星盘库等 503
- 用户现象:运行时已是 Better Auth + 本地 PostgreSQL,缺业务库 URL 或未设 `AUTH_PROVIDER=self-hosted` 时,会员页“当前余额”和首页仍显示“Supabase 尚未配置”。
- 触发条件:未注入 `APP_DATABASE_URL` / `SERVICE_DATABASE_URL`,或 `AUTH_PROVIDER` 未设为 `self-hosted` 因而落到旧适配器路径。
- 根因:BUG-010 已禁止浏览器直连 Supabase,但 API 与 `friendlyError` 仍把配置失败翻译成 Supabase 文案和 `SUPABASE_NOT_CONFIGURED`。产品已不用 Supabase。
- 修复:对外统一为“数据库尚未配置” / `DATABASE_NOT_CONFIGURED`,以及“请先配置数据库环境变量。”。内部类名仍叫 `SupabaseConfigurationError`,避免本轮改适配器标识。
- 验证:`frontend/tests/self-hosted-error-copy.test.ts` 扫描 `frontend/src` 禁止上述旧文案,并锁定错误码与内部消息。
- 防复发:用户可见中文不得再出现“Supabase 尚未配置”;缺库 503 必须说数据库,不得说已退役的托管服务。
- 相关记录:BUG-010
- 复发自:BUG-010
- 修复版本:待发布
## BUG-432 | 生时校正区分题出题层静默丢弃探针后无卡无记账死锁
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`POST /api/rectification/agent` 消息分支、`decideFromDossier``buildMethodFollowupPlan`、口述采集 focus
- 用户现象:点选两道区分题后,助手口述家人采集问但没有选项卡。用户用自由文本回答后,服务端回没有出路的终结话术;`current_question``choice_card` 均为 nullcase 停在 `collecting_evidence`;该回合 receipt 的 phases / tools / tool_activities 全空,用户这句话里的新证据没有入账本。
- 触发条件:训练证据已齐、已答 dasha 边界探针、剩余探针含无年份 varga 与缺 `style_options``varga_style`;方法覆盖未齐时出题层回落到口述采集;随后用户在无 active choice focus 时发自由文本。
- 根因:`inspectDiscriminatorProbes``withCompletedContrastOptions` 把缺 `style_options``varga_style` 降级后仍视为可渲染,并选出无年份 varga 探针进入 `ask_candidate_discriminator``renderableContrastProbe` 仍走未降级的 `completeStyleOptions`,把同一探针静默丢弃。无年份桶只在覆盖齐时使用,于是回落到 `source=method_coverage` 的家人采集且不带 `choice_frame`。路由只信决策层:`persistServerOwnedFocus` skipped、`openQuestionFromPersistedFocus` 为 null 后直接返回终结话术,既不落证据也不跑 Agent。口述采集题不落 focus,刷新后问题丢失;卡片持久化失败时返回无正文下一步。
- 修复:出题层与决策层共用 `withCompletedContrastOptions`;丢弃探针写入 `dropped_probes`。仅当出题层给出 `intent=distinguish_candidates` 且带 `choice_frame` 的 followup 时才进入 `ask_candidate_discriminator`,否则落到采集。消息分支拿不到可渲染卡且仍有 followup 时落入 `runV9AgentTurn`;仅当 `next_followup` 为 null 时才收口,话术给出继续采集或按区间看盘。口述采集持久化不带 choice 的 collect focus;卡片持久化失败时落到口述采集下一步。冲突探针写回时保留 `style_options`。收尾加固:口述采集投影带 `kind=collect_spoken``current_probe` / `inference.next_probe` 只对可渲染选择题放行;降级口述采集剥掉区分探针身份(`semantic_key` / 年份 / `probe_id`),正文与 schema prompt 同一次生成;卡片失败后的假分支删除,focus 持久化失败按 `safeErrorCode` / caseId 打脱敏日志;`decideFromDossier` 把可选 `birthDate` 传给门控 plan,门控通过时 `decision.probe` 取 plan 真正选中的探针。点选路径 `persistApplied` 先加载一次 compute,把同一 `birthDate` 喂给 `decideAfterInferenceChange` 与后续出题 plan。Skill 版本保持 `10.0.13`。未放宽 confirmation gate,未增加「已确认唯一分钟」路径。
- 验证:`frontend/tests/rectification-discriminator-followup-consistency.test.ts` 锁定同一探针池上决策层与出题层一致、`decision.probe``plan.next_followup.semantic_key` 一致、缺 `style_options` 的 D9/D10 风格题在方法覆盖未齐时仍出区分卡、无星座时钟键 fail-closed 到采集、低龄年份探针传入 `birthDate` 时门控与真实 plan 结论一致、消息路径不再返回死路话术。`rectification-answer-choice.test.ts` 锁定口述采集后仍有 collect focus 与 `current_question`collect 时 `current_probe` 为空、选择题时非空,降级采集不含区分题身份,点选路径 `persistApplied``decideAfterInferenceChange` 之前加载 compute 并传入同一 `birthDate``rectification-server-focus.test.ts` 锁定 collect schema 不是未渲染区分卡,降级采集的 questionId / schema 不含区分题键且同一探针随后可出卡。`rectification-turn-intent-classifier.test.ts` 按新收口话术改断言。`npx tsc --noEmit``npx tsx --test tests/rectification-*.test.ts tests/birth-time-rectification-contract.test.ts tests/agentic-rectification-*.test.ts``npx tsx --test tests/*.test.ts`
- 防复发:可渲染判定必须共用选项补全,不得一边降级接受一边静默丢弃。无卡时不得把用户消息当终结话术丢掉。有合法 choice schema 的 active focus 仍走轻量分类 + `applyRectificationChoice`。不得引入语义正则、A/B/C/D 位置推断或静态选项 fallback。口述采集题不得占用区分探针身份。无选择卡的问题必须由 Agent 正文问出来,`current_probe` 只对可渲染选择题放行。
- 相关记录:BUG-404、BUG-407、BUG-410、BUG-411
- 复发自:BUG-404
- 修复版本:待发布
## BUG-433 | 根错误页和 404 在摘掉 globals 后变成裸 HTML
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`src/app/error.tsx``src/app/not-found.tsx`;任意未匹配路由与根渲染崩溃
- 用户现象:访问不存在的路径或触发根错误边界时,页面不居中、无配色、无间距,按钮塌成无样式文字。
- 触发条件:根布局不再 import `globals.css` 之后,这两页仍用 Tailwind 工具类和 `Button`
- 根因:它们与 admin 共享根布局段,不能 import `site-styles`;工具类定义只在站点 CSS chunk 里。上一轮把“必须另开布局链”写成唯一出路,漏了仓库里 `global-error.tsx` 已有的内联样式模式。
- 修复:照 `global-error.tsx` 改为模块常量 + 内联 `style`fallback 色值与 `globals.css` token 一致。错误页“重试”用原生 button,“返回对话”用 `next/link`。404 仍保留既有源码锁 `render={<Link href="/" />}`,用 `cloneElement` 套内联样式,不引入 `Button`
- 验证:`frontend/tests/site-style-isolation-contract.test.ts` 新增断言(既有断言未改);`frontend/tests/root-error-boundaries-contract.test.ts` 保持绿灯;`next build``_not-found.html` body 无 `min-h-svh` 等工具类;admin 路由 CSS 仍不含 `.message-list`
- 防复发:根布局段页面(`error.tsx` / `not-found.tsx` / `forbidden.tsx` / `global-error.tsx`)只能用内联样式,不得依赖工具类或 `globals.css` / `site-styles`
- 相关记录:BUG-219、BUG-430
- 复发自:无
- 修复版本:待发布
## BUG-434 | 入门付费墙的关闭按钮掉到标题下面,弹窗头部从未有过样式
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 入门问题完成后弹出的「解锁完整咨询」弹窗,移动端最明显
- 用户现象:标题「解锁完整咨询」独占一行,关闭「×」以灰色圆角块的形式落在标题下方靠左,和礼物图标那段挤在一起;头部与正文之间没有间距。
- 触发条件:打开该弹窗即出现,与账号状态无关。
- 根因:`onboarding-redeem-paywall.tsx` 写的是 `<div className="dialog-header">`,而 `dialog-header` 这个类在全仓 CSS 里**没有任何定义**。同类弹窗(`account-dialog-overlay.tsx``membership/page.tsx`)用的都是 `<header className="account-modal-header">``display:flex; justify-content:space-between; padding-bottom`)。付费墙自 `a17ff258` 引入起就拼错,未被任何测试或样式覆盖,因此一直是无样式块级 div,标题与按钮竖排。
- 修复:改为 `<header className="account-modal-header">`,与另外两处弹窗完全一致。未新增任何 CSS,未改文案、`aria-label="关闭"``aria-modal`、焦点陷阱与 44px 触达。
- 验证:`frontend/tests/membership-page.test.ts` 新增两条 —— 一条锁付费墙必须复用 `account-modal-header` 且不得再出现 `dialog-header`;一条**通用守卫**,抽出该组件所有静态 `className` 逐个断言在 `globals.css` 里有对应选择器。反向验证:把 `dialog-header` 放回去,两条同时变红;还原后 38 pass / 0 fail。
- 防复发:弹窗头部一律用 `account-modal-header`,不得为单个弹窗另造头部类名。新增组件若引入新类名,必须同时在 `globals.css` 落规则 —— 上面那条通用守卫会挡住这类拼写漂移。
- 相关记录:BUG-433
- 复发自:无
- 修复版本:待发布
## BUG-435 | 删除会话确认弹窗的两个按钮没有任何样式,"确认删除"也不是红色
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 侧边栏删除会话时弹出的「删除聊天记录?」确认框
- 用户现象:「取消」和「确认删除」渲染成浏览器原生灰色按钮 —— 没有圆角、没有 44px 高度、没有内边距,破坏性操作也没有任何红色提示。
- 触发条件:打开该确认框即出现。
- 根因:`page.tsx:4000` 的「取消」完全没写 class`page.tsx:4001` 的「确认删除」写的是 `danger-button`,而这个类在全仓 CSS 里没有定义。全局只给 `button` 设了 `font: inherit; color: inherit`,没有任何兜底规则,所以两个按钮都是裸原生按钮。同一文件 60 行之前的退出登录确认框用的是正确写法 `button-secondary` + `button-primary danger-primary`
- 修复:两个按钮改成与退出登录框完全一致的 `button-secondary` / `button-primary danger-primary`。未新增 CSS,未改文案与 `role="alertdialog"`。星盘库「删除」上同样无定义的 `danger-button` 修饰符一并去掉(它本就没生效,渲染不变)。
- 验证:`./node_modules/.bin/next build` 后用类名扫描脚本核对,`danger-button` 已从"无规则类名"清单消失;`tsc --noEmit` exit 0、`eslint` 0 error、全量测试无新增失败。
- 防复发:破坏性确认框一律复用 `button-secondary` + `button-primary danger-primary`,不得临时造修饰符。新类名必须同时在 `globals.css` 落规则 —— 参见 BUG-434 在 `membership-page.test.ts` 里加的通用守卫。
- 相关记录:BUG-434
- 复发自:无
- 修复版本:待发布
## BUG-436 | 主对话消息间距规则从 8-19 起失效,消息比设计稿挤
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 主对话(非空、非生时校正会话)里相邻消息之间的竖向间距
- 用户现象:消息之间只剩气泡自身的内边距,比设计稿定的 `--space-8`(移动端 `--space-6`)窄。
- 触发条件:任何有历史消息的普通咨询会话。
- 根因:`globals.css``.conversation:not(.is-empty):not(.is-rectification) .message + .message { margin-top: var(--space-8) }` 写于 `200c39d2`2026-07-22),当时消息是 `.message-list` 的直接兄弟。`7d667fec`(2026-08-19)给每条消息套了一层 `<div className="message-entry">`,而这个类在 CSS 里没有任何定义,两条 `.message` 从此不再相邻,选择器永久不匹配。同一规则组里 `.message` 又被覆盖成 `padding: 0`,于是间距归零。`3198fb6b` 把同一层包装搬进 `ChatTranscript`,未改变这一点。
- 修复:选择器改为 `.message-entry + .message-entry`,桌面 `--space-8`、移动 `--space-6` 数值不变。`.message-list` 里除 `.message-entry` 外只有可选的 `.error-message`,不会误伤。引导语消息在 `.welcome` 内且只在 `.is-empty` 时渲染,本就被选择器排除。
- 验证:`frontend/tests/class-name-definition-contract.test.ts`(本次新增的通用守卫)反向验证 —— 把选择器改回 `.message + .message``message-entry` 立即被判为无规则类名,测试变红;改回则绿。
- 防复发:给消息列表加包装层时必须同步迁移依赖相邻兄弟的选择器。新类名漏定义由上述守卫兜底;该守卫会剥掉 CSS 注释再判定,避免"只在注释里出现过"被当成已定义。
- 相关记录:BUG-434、BUG-435
- 复发自:无
- 修复版本:待发布
## BUG-437 | 区分卡因过期候选集前缀被二次加前缀,界面无卡且 Agent 只承接不提问
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`probeFromEngine``candidate_split_hash` 归一化、`projectRectificationChoiceCard` 身份校验、`scoreAndPersistCurrentEvidence` 组包的 `candidateSetVersion`、GET `choice_card`
- 用户现象:`current_question` 是合法选择题(`kind=choice``focus_id``probe_id` 齐全、题干已生成),`choice_card` 却是 null。Agent 按提示第 4 条只承接不复述,正文只剩「请你凭印象选一个」,界面没有卡片,用户无法继续。`dropped_probes` 为空,看不出被抑制的原因。
- 触发条件:某一轮打分改变了候选分钟列表(例如 05:05 换成 05:00),此前铸出的 varga 对比探针仍带旧候选集前缀,随后任意一次读路径重建对比包。
- 根因:`scoreAndPersistCurrentEvidence` 组对比包时 `candidateSetVersion` 取上一轮 `inference_state.candidate_set_id``candidateTimes` 取本轮 `score.candidates`,于是新写入的探针 hash 带着旧集合前缀,而同一份 state 的 `candidate_set_id` 按新候选生成。之后 `probeFromEngine``hash.includes(candidateSetVersion)` 判断是否已有前缀,旧前缀不含新集合 id,于是再加一层前缀得到双前缀 hash。落 focus 时 `stampChoiceSchemaWithProbe` 写的是 state 里探针的单前缀原值,`projectRectificationChoiceCard``candidate_split_hash` 全等校验因此永远不成立,直接 return null,且不记入任何丢弃账本。
- 修复:新增 `splitHashForCandidateSet`hash 为 `<候选集 id>:<语义键>` 形态时按当前候选集重铸,已带当前前缀时保持不变,不透明引擎 hash 只加一次前缀,杜绝前缀叠加。新增 `isSameCandidateSplit`,两侧同为 `<任意候选集 id>:<同一语义键>` 时视为同一 split(语义键本身已编码候选分组),`projectRectificationChoiceCard` 改用它做身份校验,语义键不同或不透明 hash 不同仍然 fail closed。组包 `candidateSetVersion` 改为按本轮 `candidateRange``score.candidates` 计算的 `candidateSetId`,与写入 state 的候选集同源。Skill 版本保持 `10.0.13`,未放宽 confirmation gate。
- 验证:`frontend/tests/rectification-choice-card.test.ts` 锁定「探针 hash 带旧候选集前缀时 GET 仍返回卡片」(去掉修复即失败)与「持久化 split 属于另一探针时仍隐藏」;`rectification-candidate-contrast-packet.test.ts` 锁定前缀重铸幂等、不透明 hash 只加一次、split 身份忽略前缀但不忽略探针、组包按本轮候选集作用域。`npx tsc --noEmit``npx tsx --test tests/rectification-*.test.ts tests/birth-time-rectification-contract.test.ts tests/agentic-rectification-*.test.ts` 658 项 0 失败;全量 `npx tsx --test tests/*.test.ts` 失败集合与改动前一致(23 项数据库/部署环境类)。
- 防复发:探针 `candidate_split_hash` 只能有一层候选集前缀,任何读路径不得对已带前缀的 hash 再加前缀。打分写入的对比包必须与本轮候选集同源,不得沿用上一轮 `candidate_set_id`。卡片身份校验不得因候选集前缀差异静默吞掉合法卡片。
- 相关记录:BUG-410、BUG-411、BUG-432
- 复发自:无
- 修复版本:待发布
## BUG-438 | 移动端侧边抽屉里「新建对话」「我的报告」居中悬空,与其余左对齐项不齐
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 移动端(<768px)侧边抽屉顶部两个导航项
- 用户现象:品牌行、星盘列表、收藏对话、历史对话全部左对齐,只有「新建对话」和「我的报告」的图标+文字组居中飘在抽屉正中,视觉上完全不成列。
- 触发条件:在移动端打开侧边抽屉即出现。
- 根因:`.new-chat` / `.report-nav-button` 的基础样式是 `justify-content: center`(为桌面折叠图标栏准备),左对齐靠顶层的 `[data-state="expanded"] … { justify-content: flex-start }` 翻回来。但 `data-state` 取的是 `open ? "expanded" : "collapsed"``open` 是**桌面**折叠状态;移动抽屉由独立的 `openMobile` 控制。组件侧 `isCollapsedDesktop = state === "collapsed" && !isMobile`,移动端恒为 false,所以文字标签照常渲染;CSS 侧却因 `data-state="collapsed"` 匹配不上覆盖规则,按钮保持居中。JS 与 CSS 对"展开"的定义不一致。品牌行没被带歪,是因为 `[data-state="collapsed"] .brand-row` 那整套图标栏规则写在 `@media (min-width: 768px)` 里,移动端本就不生效。
- 修复:把默认翻过来 —— 基础样式改为 `justify-content: flex-start`(移动抽屉与桌面展开态都正确),删掉依赖 `data-state` 的顶层覆盖,居中改为 `@media (min-width: 768px)` 内的 `[data-state="collapsed"]` 规则,与既有的 `.brand-row``[data-sidebar="menu-button"]``.profile-trigger` 等图标栏规则并列。全仓仅此一处顶层 `[data-state=…]` 选择器,其余均已按视口收口。
- 验证:`frontend/tests/sidebar-contract.test.ts` 新增一条 —— 锁基础层左对齐、禁止再出现 `[data-state="expanded"]` 版覆盖、并要求居中规则位于 768px media 内。反向验证三次(基础样式改回 center / 覆盖加回来 / 居中规则移出 media),每次都变红。
- 既有断言改动(唯一一处):`frontend/tests/personal-report-entry.test.ts` 原先断言 `.report-nav-button` 必须 `justify-content: center`,那正是本条缺陷本身,改为 `flex-start` 并注明原因。该断言其余部分(`display: flex``align-items: center`)未动。
- 防复发:`data-state` 只描述桌面折叠态,不得用它决定移动抽屉的布局;折叠图标栏的样式一律写进 `@media (min-width: 768px)`,不得作为基础层默认值再由覆盖翻回。
- 相关记录:BUG-434、BUG-436
- 复发自:无
- 修复版本:待发布
## BUG-439 | 回答里的围栏代码块没有任何样式,长代码行撑破消息栏
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`/` 主对话中 Agent 回答里的 ``` 围栏代码块
- 用户现象:代码块没有容器,行内代码的小胶囊底色套在整块代码上;一行长代码不换行也不滚动,直接溢出消息栏,移动端会把版面顶横。
- 触发条件:回答里出现围栏代码块。
- 根因:`globals.css` 只有 `.message-markdown code`(行内胶囊),**没有任何 `pre` 规则**。react-markdown 输出的裸 `<pre>` 保留浏览器默认的 `white-space: pre` 且无 `overflow`,内容宽度不受 `.message-content``max-width` 约束。`.message-content` 上的 `min-width: 0` 只能让 flex 收缩,挡不住块内溢出。
- 修复:新增 `MarkdownCodeBlock` 组件接管 `pre`,输出「语言标签 + 复制按钮」的头条和可横向滚动的代码区;`.markdown-code pre > code``overflow-x: auto` 并重置行内胶囊底色,容器 `overflow: hidden` 收住圆角。未引入语法高亮依赖。
- 验证:`frontend/tests/markdown-code-block-contract.test.ts``overflow-x: auto``white-space: pre` 必须同时存在、胶囊底色必须重置、`pre` 必须走该组件、复制按钮有 aria-label、剪贴板被拒时不得误显示「已复制」、卸载时清定时器。
- 防复发:给 markdown 新增块级元素时必须同时给出容器与溢出策略;行内与块级代码的样式不得共用一条规则。
- 相关记录:BUG-434
- 复发自:无
- 修复版本:待发布
## BUG-440 | 生时校正采集阶段否定后无下一问,对话空转
- 状态:resolved
- 首次发现:2026-08-29
- 最近更新:2026-08-29
- 影响面:`buildMethodFollowupPlan` 方法覆盖口述题、`classifyRectificationTurnIntent` 采集焦点、`applyCollectFocusDenial` / `persistNextInterviewIfIdle`、POST `/api/rectification/agent` 自由文本路径、GET `current_question`
- 用户现象:点完若干区分卡后,助手口述问家里有没有结婚、添丁或住院这类记得住时间的事。用户回「没有」。助手只确认「记下了」,随后 `current_question``choice_card` 均为 null`interview.type = ask_fact_collection`,对话停住,无法继续。
- 触发条件:方法覆盖未齐(尤其 relatives),剩余可渲染探针都是无年份分盘题;用户对家人口述采集题给出明确否定,且该轮走自由文本而不是点选。
- 根因:三段同时成立才会空转。(1) 「没有」既不是证据也不是计分答案,`answered_probes` 与 revision 不变;若焦点以 `resolved` 关闭,`declined_skipped_topics` 只聚合 `status in ('declined','skipped')``buildMethodFollowupPlan` 又只认 `target_domain`,覆盖度不前进。(2) 无年份分盘题被 `coverageComplete` 挡在 dated 排序之后,计划回落到同领域口述采集题;家人覆盖要求「有家人证据或有 declined 记录」,家里确实没发生过事的用户两条都不满足,而 `varga.d12` 本就是这道家人题、`no` 分支可以计分淘汰候选,却被当成不计分采集题问出去并丢失。(3) 点选路径有 `persistNextInterviewAfterChoice` 保证下一问落库;自由文本路径跑完 `runV9AgentTurn` 后没有同等兜底。Agent 不重复刚被否掉的问题(这点是对的),于是只确认不提问,`current_question` 变成 null。
- 修复:P0:即将返回某领域 `intent=collect_method_evidence` + `source=method_coverage` 口述题时,若 `rankRenderableDiscriminators` 的 yearless 桶里有同领域可渲染探针,改出该 distinguish 卡(`followupFromRanked`,按既有 `rankDiscriminatorScore` 取最高)。未放宽 coverageComplete 对 yearless 的总排序,避免回归 BUG-405/407。无年份 varga 探针允许铸出 choice_frame,不得向题干借用账本年份。P1:采集焦点自由文本接入同一套轻量分类器;明确否定时服务端把焦点 resolve 成 `declined`(不是 `resolved`),并按点选路径持久化下一问。同步修正 `rectification-resolve-focus` 工具描述。未引入语义正则、关键词表或 A/B/C/D 位置推断。`focusStatusForAnswer``no → declined` 语义未改。P2`runV9AgentTurn` 成功后若无 open focus 且 plan 仍有 followup,按点选路径补落下一问。收尾:(a) 分类器新增可选 `has_new_dated_event`,缺失按 false;选择题与采集题快路径在该字段为 true 时先确定性地应用答案(计分 / declined),不落确定性正文、不 return,继续 `runV9AgentTurn` 记同一句里的新带时间事件;为 false 时行为与原先逐字一致。应用答案时 `deferFollowup`,让 Agent 看到没有 active focus,不得再 resolve。(b) `persistNextInterviewIfIdle` 预检改为先 `decideFromDossier`,用同一个 `decision.sessionOutcome` 建 plan,并复用该 decision 做 `publicNextAction`,不再写死 `collect_evidence`。Skill 版本保持 10.0.13,未放宽 confirmation gate。
- 验证:`frontend/tests/rectification-collect-stall.test.ts` 锁 D12 卡、答 C declined 计分、occupation 否定推进 horary、自由文本后 `current_question` 非 null、有年份区分题仍优先。新增:缺失 `has_new_dated_event` 按 false;采集/选择题快路径在该字段为 true 时应用答案后进入 Agent 且不落确定性正文;采集否定 + deferFollowup 不抢先落下一问;选择题 deferFollowup 仍计分且不落下一问/正文;`persistNextInterviewIfIdle` 同一 dossier 只调用一次 `decideFromDossier`,预检 sessionOutcome 与决策层相同。`npx tsc --noEmit` 通过;eslint 改动文件 0 error。指定套件 `tests/rectification-*.test.ts tests/birth-time-rectification-contract.test.ts tests/agentic-rectification-*.test.ts` 672 项、0 失败。全量 `tests/*.test.ts` 2266 项、0 失败。失败集合未扩大到产品逻辑。
- 防复发:同一领域的问题若存在可渲染计分卡,不得改用不计分口述题。采集题的明确否定必须产生 declined 记录,不得写成 resolved。出题层还有 followup 时 `current_question` 不得为 null。不得为了救这条空转而取消 coverageComplete 对 yearless 的总限制。确定性快路径不得吞掉同一句话里的新带时间事件。同一次调用里出题层与决策层必须用同一个 sessionOutcome。
- 相关记录:BUG-404、BUG-405、BUG-407、BUG-411、BUG-432、BUG-437
- 复发自:无
- 修复版本:待发布
## BUG-441 | 口述采集题已落库但界面看不到,助手只说看下面这一问
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:GET `current_question``openQuestionFromPersistedFocus``agentVisibleLatestProjection``stableFollowupQuestionId`、POST `/api/rectification/agent` 出卡判定与空正文兜底、Agent 系统提示第 4 条。可见性策略已被 BUG-449 取代。
- 用户现象:助手正文只写「接下来看下面这一问。」,界面没有任何问题。GET 里 `choice_card` 为 null`current_question` 已是合法口述采集题(`kind=collect_spoken`、prompt 非空),问题只存在于数据库和 API。
- 触发条件:方法覆盖回落到不带 choice_frame 的口述采集焦点;Agent 按选择卡承接措辞说话;前端只解析 `choice_card`
- 根因:三段同时成立。(1) 前端从不渲染 `current_question``applyCaseSnapshot` 只解析 `latest_result` / `choice_card` / `case``kind=collect_spoken` 在界面上没有承载位置。(2) 面向 Agent 的两套投影自相矛盾:`projectTurnDecision` 给出 `current_question.kind=collect_spoken` 与 prompt;记完证据后的 `agentVisibleLatestProjection` 取自 `openQuestionFromPersistedFocus`,后者对 collect focus 一律返回 null。Agent 收到打架信号,退回「卡片会显示」的措辞。(3) `stableFollowupQuestionId` 在 followup 既无 semantic_key 也无 probe_year 时退到 `${method_id}:${ask_theme}``persistPlanFocus` 传入的 active_focus 计划正好两者都没有,questionId 退化成 `active_focus:active_focus`
- 修复:P1/P2 仍在:`openQuestionFromPersistedFocus` 对 collect focus 返回带 `kind=collect_spoken` 的同形结果;`choiceReady` / 出卡判定只对 `kind=choice` 为真;`current_probe``inference.next_probe` 仍只对可渲染选择题放行;collect 在无 semantic_key / probe_year 时用 `collect:<domain>:<intent>``active_focus:active_focus` 不得与新 id 互相 already_open。P0 独立提示条已撤回:前端不再解析或渲染 `current_question`。~~口述采集题改由 Agent 用自己的话在正文里问出来~~(该可见性策略已被 BUG-449 取代:题干改由服务器按焦点生命周期确定性接在正文之后,模型不得自行写题干)。「请点选」「看下面这一问」仍只允许在确实有选择卡时使用。服务端 collect focus 继续持久化,GET 仍返回 `current_question`。Skill 保持 10.0.13,未放宽 confirmation gate。
- 验证:`frontend/tests/rectification-spoken-collect.test.ts` 锁:GET `collect_spoken` 不再渲染独立块、`.rectification-spoken-collect-prompt` 不存在、选择卡分支仍在。collect openQuestion 不为卡片、probe 在 collect 下为空、active_focus 派生 questionId 带 domain。可见性回归见 BUG-449。BUG-440 collect-stall 全绿,D12 同领域计分卡仍优先于家人口述题。
- 防复发:服务端问题的可见性由正文与确定性兜底保证,不得为无选项的问题新造视觉容器,更不得借用选择卡的样式类。同一焦点在所有面向 Agent 的投影里形态必须一致。「请点选」类措辞只允许在有选择卡时使用。不得用正文文本判断助手有没有问出来。collect questionId 不得退化为 `active_focus:active_focus`。口述采集题的可见性策略见 BUG-449,不得再改回「指望模型问」。
- 相关记录:BUG-404、BUG-411、BUG-432、BUG-437、BUG-440、BUG-449
- 复发自:BUG-432(无选择卡的问题曾要求由 Agent 正文问出,前端仍不渲染 `current_question`
- 修复版本:待发布
## BUG-442 | 职业采集关不上、无年份卡出不来、覆盖门压掉区间出口
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`applyOccupationCollectLedgerNorm` / `occupationCovered``buildMethodFollowupPlan` 无年份分盘卡、`decideRectification` 覆盖门、POST `/api/rectification/agent` 区间出口、`houseTableAlignedToMinute` / `publicChartForMinute`
- 用户现象:账本已有足够可计分事件和多个领域,助手仍反复追问职业口述题;候选几乎均匀,`margin_percent` 停在门槛以下,entropy 不降;`can_offer_range` / `can_adopt` / `propose_allowed` / `selection_allowed` 全为 false,用户从头到尾没被告知可以按当前区间收口。对外代表分钟与宫位表也可能不是同一时刻。
- 触发条件:职业采集焦点下用户给出职业性质描述,模型把证据记成 `domain=career` 的入职/换工作;训练事件已达标,剩余高信息量探针全是无年份分盘题;引擎回执已 `accept_allowed` / `propose_allowed`,决策层仍因 occupation 覆盖 flag 落 `collect()`
- 根因:四段同时成立。(1) 覆盖判定 `isOccupationNote` 要求 `domain=occupation`,职业口述回答被记成 career 域,occupation 方法层永不关闭,`next_followup` 恒为职业口述采集题。(2) 无年份分盘卡的出题条件绑在 `coverageComplete` 上,覆盖不齐则 d24/d7/d4/d5 一道都出不来,margin 无法被这些探针拉开。(3) `decideRectification` 撞上 `coverageBlocks` 直接 `collect()`,把 `canOfferRange` 压成 false;同一份引擎回执已经放行 accept/propose,决策层因覆盖 flag 单方面压掉区间出口。(4) 对外投影的 `representative_time` 取推理层代表分钟,`house_table` / `natal_recast` 却可能是另一分钟算出的盘面。
- 修复:P0occupation 采集焦点下,服务端把该轮职业性质描述归一成 `domain=occupation` / `eventKind=occupation_note`(无日期允许,occupation_note 不参与计分)。覆盖端:已有确认 career 证据且 occupation 采集焦点已按 questionId / target_domain 关闭时也算覆盖,不读正文、不用关键词。P1:训练门开时允许出无年份分盘卡,不再要求覆盖齐;训练门未开仍不出,避免回到 BUG-405/407。同领域可渲染计分卡仍优先于不计分口述题(BUG-440)。P2:训练门已开且出题层给不出带 choice_frame 的可渲染区分卡时,允许 `canOfferRange` 并播报「可以先按当前区间看盘,也可以再补一件记得时间的经历」;引擎 accept/propose 已放行而覆盖 flag 要压掉时至少放行 `canOfferRange``canAdopt` 与 confirmation gate 维持 fail-closed。P3:对外 `representative_time``house_table``natal_recast` 必须是同一分钟;对不上则取 `house_tables_by_time` 对应表,否则标成不可用。Skill 保持 10.0.13。未引入语义正则、关键词表、A/B/C/D 位置推断或轮次状态机。
- 验证:`frontend/tests/rectification-occupation-coverage-exit.test.ts` 用本例 15 件可计分、4 域账本锁:occupation 采集回答落 occupation_notecareer 证据 + 已关闭 occupation 焦点覆盖成立;career 单独仍不覆盖;训练门开且覆盖未齐能出无年份分盘卡,训练门关仍不出;训练门开且无可渲染区分卡时 `canOfferRange` 为真、`canAdopt` / `canConfirmExactMinute` 为假;路由播报二选一;代表分钟与 `house_table` / `natal_recast` 同源,构造不一致回执不得直接发出。collect focus 仍不是选择卡。BUG-440 collect-stall、BUG-437 hash、D12 同领域卡优先保持绿。
- 防复发:覆盖判定不得依赖模型选对 domain。同一个覆盖 flag 不得同时锁死出题与出口。训练门已开且无可渲染区分卡时必须给用户出路。对外代表分钟与盘面必须同源。不得用正文文本判断覆盖或意图。
- 相关记录:BUG-404、BUG-407、BUG-410、BUG-432、BUG-437、BUG-440、BUG-441
- 复发自:BUG-440(覆盖未齐时的收口分支存在,但 occupation 记账域错误导致永远走不到;无年份卡仍被同一覆盖门锁死)
- 修复版本:待发布
## BUG-443 | D9/D10 风格卡用了类型表原文、题干贴了年份、题干贴着选项
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`varga-type-tables``d9StyleLabel` / `d10StyleLabel``completeStyleOptions` 风格题固定项、`choice-card` 题干、`birth-time-choice.css` legend、`.rectification-choice-why`
- 用户现象:D9/D10 风格点选卡选项是「和谐、美感、合作」「照顾、情感」这类类型表词条,三个抽象名词并列、维度不统一,无法对号入座。题干出现「2024–2026 年这段里,做事风格更接近其中一种?」;风格题没有「这段」可指。题干与第一个选项几乎没有间隔。
- 触发条件:候选在 D9 或 D10 上升上分开,出 `choice_kind=varga_style` 点选卡。
- 根因:三段同时成立。(1) `d9StyleLabel` / `d10StyleLabel` 直接输出 `D9_TYPE_TABLE[sign].trait` / `D10_TYPE_TABLE[sign].style`,解读用的占星类型表字段被当成用户选项。(2) `choice-card.ts``periodFor``varga_style``lifePeriodLabel(evidence, domain)``eventQuestionPrompt` 再把时间段贴到长期风格题干上;风格描述的是平时怎么做 / 怎么相处,不随年份变化。(3) `.birth-time-choice-question` 用 grid `gap` 分隔子项,但题干是 `<legend>`legend 不参与 fieldset 的 grid 布局,gap 对题干与选项不生效。
- 修复:类型表为 D9/D10 每行新增面向用户的 `userChoice`,不改 `trait` / `occupation` / `style` / `spouse` / `marriage`。标签函数改输出 `userChoice``answer_class` 与 sign 的映射不变。风格题 `no` 改为「都不太像我」,unsure 单独为「一时说不好」;existence / event_quality 固定选项文案不变。`varga_style` 不再返回时间段,题干写成长期表述。legend 与 `.rectification-choice-why``--space-3` 下外边距,不把 legend 改成 `p`、不新增视觉容器。Skill 保持 10.0.13。未改计分、探针身份或 hash。
- 验证:`frontend/tests/rectification-varga-style-copy.test.ts` 锁:12×2 `userChoice` 非空且不等于 `trait`/`style`sign → `answer_class` 仍按组序;风格卡题干无年份/「这段」;existence / event_quality 仍锁年份且固定选项不变;四选项 `isRenderableStyleOptions`legend / why 源码具有非零下边距。修复前:标签等于类型表原文、题干带 `lifePeriodLabel` 年份、「这段记不清楚」用在风格题、legend 仅 `margin-bottom: var(--space-1)`。合同金标 `contracts/probe-question-v1.json` 同步。
- 防复发:面向用户的选项文案不得直接复用占星类型表字段。长期风格题不得锁定时间段。卡片内题干与选项必须有独立于 grid gap 的间距。existence / event_quality 的年份月份锁定(BUG-408、BUG-412)不得被风格题改动带回。
- 相关记录:BUG-408、BUG-412
- 复发自:无
- 修复版本:待发布
## BUG-444 | 无年份分盘对比题重新出成无星座依据的点选卡
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`isRenderableProbe` / `inspectDiscriminatorProbes` / `rankRenderableDiscriminators` / `buildChoiceFrame` / `shouldAttachChoiceFrame`、无年份 remaining-layer 分盘对比题
- 用户现象:家里有没有结婚添丁住院、有没有搬家、学业压力、收入变化、生病受伤这类题可以在题干没有任何时间范围的情况下出成四选一点选卡;选项仍写「明确发生且时间吻合」「这段记不清楚」。答了以后按分组顺序灌进后验,候选被无依据地拉开或压掉。
- 触发条件:`REMAINING_LAYERS` 产出 `year: 0` 的分盘对比探针;训练门已开;出题层把无年份 `varga.*` 一律当成可渲染计分卡。
- 根因:三段同时成立。(1) 为满足 BUG-440「同领域可渲染计分卡优先于不计分口述采集题」,把 `choice-card` 的无年份豁免从性格倾向题放宽到所有 `semantic_key``varga.` 开头的探针,重新打开了 BUG-414 关掉的门。(2) 存在类和质量类固定选项预设了题干里没有的时间范围(「时间吻合」「这段」)。(3) 非 `varga_style` 的分盘对比题 `remainingStyleOptions` 不绑定星座,`remainingOutcomes` 按分组顺序映射 yes / weak_yes / no,属于无依据计分。
- 修复:没有具体年份或月份时,只有 `choice_kind === varga_style` 且计分选项带 `sign` 才允许铸卡。同一条判定走共享的 `isRenderableProbe`,同时作用于 `inspectDiscriminatorProbes``rankRenderableDiscriminators`。被排除的探针进入 `dropped_probes`,理由 `yearless_ungrounded_contrast`。题干无时间范围时选项不得含「时间吻合」「这段」。`oos_blind` 且无年份的 followup 显式不得挂 `choice_frame`。覆盖门与区间出口保持 BUG-442:训练门已开且没有可渲染区分卡时仍可 `canOfferRange`。计划回落到带年份的采集题。未改 confirmation gate、answer_class 映射、探针身份或 hash。Skill 保持 10.0.13。
- 验证:`frontend/tests/rectification-yearless-ungrounded.test.ts` 逐层锁无年份 d4/d7/d12/d2/d11/d30existence)与 d24/d5event_quality)不出卡;带 sign 的无年份 d9/d10 仍出卡,缺 sign 的 varga_style 不出卡;带年份的 dasha_boundary / age_band / known_event_quality / reserved_event 仍出卡;被排除探针出现在 `dropped_probes`;决策层与出题层同一探针池结论一致;oos_blind 无年份无卡。修复前:上述无年份 existence/quality 层会铸卡,决策可能落 `ask_candidate_discriminator`。BUG-440 collect-stall、BUG-437 hash、BUG-442 occupation 覆盖与区间出口、BUG-414 无年份家人卡、BUG-443 风格文案保持绿。
- 防复发:只有选项绑定星座的性格倾向题可以没有时间范围;其余分盘对比题必须有具体年份或月份;无时间范围的卡不得有预设时间的选项;被规则排除的探针必须记入 `dropped_probes`;不得为了「有题可问」重新放宽本条。判定必须看 `kind``sign` 字段,不得按层名白名单,不得对题干做语义分析。
- 相关记录:BUG-432、BUG-440
- 复发自:BUG-414
- 修复版本:待发布
## BUG-445 | 深色主题次要字色与行动色在卡片底上对比度不足 4.5:1
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:深色主题 token、围栏代码块语言标签、根边界页行动色
- 用户现象:深色模式下卡片底上的次要说明文字和黏土橙行动色发灰发糊,代码块语言标签是其中一处已上线的实例。
- 触发条件:系统偏好或账户菜单选择深色;任何把 `ink-tertiary``action` 画在 `canvas-muted` 上的界面。
- 根因:对比度合同只验了字色对阅读面 `--color-canvas`,漏掉了卡片底 `--color-canvas-muted`。实测 `--color-ink-tertiary` `#928e84``#30302d` 为 4.05:1`--color-action` `#d4785a` 为 4.18:1,均低于 WCAG AA 4.5:1。
- 修复:两个深色块把 `--color-ink-tertiary` 改为 `#9c988e``--color-action` / `--color-focus` / `--report-accent` 改为 `#d78064`(色相 15°、饱和度不变,只提亮明度)。四个根边界页内联 token 与 `DESIGN.md` 对照表同步,清掉 `#d4785a`
- 验证:`frontend/tests/dark-theme-contract.test.ts` 把对比度断言扩成 8 种字色 × 4 种底色,失败信息打印具体配对和比值;反向把 tertiary 改回 `#928e84` 后该测试变红。
- 防复发:新增字色或底色 token 时必须进入这张 32 组矩阵,不得只验阅读面;不得靠放宽 4.5 阈值让测试通过。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-446 | 回答操作等控件热区小于文档承诺的 44px
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:回答下方赞/踩/复制/重新生成、侧栏星盘 chip、登录页链接按钮、模型选择项、生时跳过、报告中心章节按钮
- 用户现象:回答下方四个图标按钮只有 26×26px 且没有 padding,很难点准;若干列表项和链接也低于 44px。
- 触发条件:任意一条助手回答下方的操作行;侧栏星盘 chip;登录页链接;onboarding 下拉;报告中心章节操作。
- 根因:`DESIGN.md` 写了 44px,样式却把高频图标做成 26px/`padding: 0`chip 32px,若干文字按钮 40px。
- 修复:视觉尺寸不动。图标行与 chip、登录链接用透明伪元素扩大热区。选择项、跳过、报告章节按钮把 `min-height` 提到 44px。回答操作四个按钮中心距只有 27px、下方 follow-up 只有 8px 边距,44×44 会互相吃点击,热区停在不重叠的 27×34;chip 与登录链接因 8px 间距停在 40px。见 `BLOCKED.md`
- 验证:`frontend/tests/touch-target-contract.test.ts` 锁 26px 视觉、伪元素尺寸、以及三处 `min-height >= 44`。改前改后截图对比视觉尺寸。
- 防复发:新增可点元素必须满足 44px 命中区;视觉小于 44px 时用伪元素补,且扩大后的热区不得与相邻可点元素重叠。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-447 | 字数上限只有 maxLength,到顶后界面毫无反应
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:主对话输入框、姓名输入、生时引导经历框、选择题补充说明
- 用户现象:打到字数上限后键盘像失灵,没有提示说明已经到顶。
- 触发条件:主输入写满 500 字(入门称呼为 80)、姓名 80、引导经历 500、补充说明 240。
- 根因:四处只有 `maxLength`,没有接近上限时的可见反馈,也没有 `aria-describedby`
- 修复:剩余字数进入阈值(500→50,短输入按 10% 且不低于 8)才挂载计数;平时不显示。计数用 `ink-tertiary`,到顶转 `danger``aria-live="polite"``aria-atomic="true"`,可见数字不进 live 文本,避免每字播报。主输入计数放进已有 `.composer-footer`,不新造一层把输入框顶高。
- 验证:`frontend/tests/character-remaining-contract.test.ts` 锁阈值、两句 live 文案、四处 `aria-describedby` 接线。
- 防复发:带 `maxLength` 的产品输入必须在阈值内给出计数,并只在阈值内挂载 live region。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-448 | 生时引导框用 ⌘/Ctrl+Enter 发送且没有任何说明
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`birth-time-guide-turn` 经历输入框
- 用户现象:主对话和生时校正对话都是 Enter 发送,到引导框按 Enter 只会换行,界面没有任何键位说明。
- 触发条件:生时引导流程里在「说说这段经历」多行框按 Enter。
- 根因:该框绑定了修饰键发送,且未处理 `isComposing`;另外两处已经是 Enter 发送并忽略组字中的 Enter。
- 修复:统一为 Enter 发送、Shift+Enter 换行,并加上 `!event.nativeEvent.isComposing`。保留「整理为经历草稿」按钮。在既有 `#birth-time-guide-hint` 补一句键位说明。选择统一发送键而不是只补提示,是因为用户已在另外两处养成 Enter 习惯,漏掉 `isComposing` 会让中文选词误发送。
- 验证:`frontend/tests/composer-ime-contract.test.ts` 锁三处 `onKeyDown` 都判断 `isComposing`,引导框不再看 `metaKey`/`ctrlKey`
- 防复发:产品内多行发送框必须同一套 Enter / Shift+Enter / `isComposing` 合同;新的输入框不得只改发送键不处理组字。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-449 | 口述采集题已落库但自由文本后助手只说记下了,用户看不到下一问
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:POST `/api/rectification/agent` 自由文本路径、`persistNextInterviewIfIdle``persistCollectSpokenAssistantIfNew``runV9AgentTurn` 口述题可见性、Agent 系统提示第 4 条
- 用户现象:答完一件事后助手只说「记下了:…」,之后没有任何问题也没有卡片,但 GET 里 `current_question` 已是合法 `collect_spoken` 题。
- 触发条件:自由文本记下带年份经历(本例唯一证据为 2022 搬家、`precision=year`);轮内 `rectification-record-evidence-batch``autoRescoreAfterEvidenceChange``persistPlanFocus` 新建 `collect:relationship:collect_method_evidence`;助手正文非空。
- 根因:五段同时成立。(1) `persistNextInterviewAfterChoice` 对 collect 焦点返回 `hostNarration = spokenFollowupForUser(followup)`;点选路径把它写进正文,所以点选没坏。(2) `persistNextInterviewIfIdle` 调用同一个函数,却丢弃 `hostNarration`,只返回 `{persisted, choiceReady}`。自由文本路径因此没有题干输出。(3) 它还在开头 `if (dossier.conversationSummary.activeFocus) return` 提前返回;本 case 的焦点是轮内 `persistPlanFocus` 建的,所以这条兜底连跑都没跑。(4) route 唯一的兜底 `persistEmptyCollectSpokenAssistant` 只在 `!result.answerText.trim()` 时触发;正文非空一律不补。(5) 选择卡不受影响(卡片由 `current_question` 渲染);区分题有 `discriminatorInvariant` fail-closed。collect 两样都没有。BUG-441 把可见性改成「指望模型问」后,非空正文不再由服务器保证。
- 修复:P0:在 `runV9AgentTurn` 已有的首份 dossier 上快照 `activeFocus.id`;轮后若 `projectCurrentQuestion``collect_spoken` 且 focus id 与轮前不同(含轮前为 null),服务器补题干。同一 id 不补。choice 永不补。P1:题干复用 `spokenFollowupForUser` / schema `prompt`;非空正文作为独立一段接在同一条助手消息后并 `answer.delta`;空正文整条消息就是题干。补出文本过 `INTERNAL_TOKEN_RE`,不得含「请点选」。agent-run 与 route 共用 helper + 本轮 `collectSpokenEmitted`,同一轮最多一次。P2`persistNextInterviewIfIdle` 返回 `hostNarration`,交给 P1activeFocus 提前返回保留,但不吞掉 P0。P3:撤回「口述采集题必须由你用自己的话问出来」;有持久化当前问题时题干一律不由模型写。Skill 保持 10.0.13。未放宽 confirmation gate / coverageComplete,未回退 b43808b0 无年份收窄。未用正文文本判断是否已问,未新造视觉容器。
- 验证:`frontend/tests/rectification-spoken-collect.test.ts` 锁:2022 relocation 自由文本后正文含感情采集题干;同一 focus id 下一轮不重复补;choice 正文无题干;空正文整条即题干;in-turn / idle 共用同一句题干且每轮一次;无内部 token、无「请点选」。改写「非空不补」与 route `if (!answerText.trim())` 旧锁。`assert.doesNotMatch(/answerText\.(?:includes|match|search)\(/)` 仍绿。BUG-440 / BUG-441 / BUG-442 / b43808b0 无年份收窄回归保持绿。修复前:`persistEmptyCollectSpokenAssistant({ answerText: "记下了:2022年搬家。" })` 返回 `null`
- 防复发:口述采集题的可见性由服务器确定性输出保证,不得依赖模型是否照做,也不得用正文文本反推;点选路径与自由文本路径必须共用同一句题干来源(`spokenFollowupForUser` / schema prompt)。不得为口述题新造视觉容器或复用选择卡样式。
- 相关记录:BUG-404、BUG-432、BUG-440、BUG-441
- 复发自:BUG-441
- 修复版本:待发布
## BUG-450 | 点选题答完后的非收敛区间提议卡死
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`offerRangeWithoutAdopt``persistNextInterviewIfIdle``projectRectificationChoiceCard``buildCandidateContrastPacket`、出题层 `rankRenderableDiscriminators``record-evidence-batch`、POST `/api/rectification/agent` 消息快路径
- 用户现象:点选题答完后,助手说「可以先按当前区间看盘,也可以再补一件记得时间的经历」;界面既没有可采用的时间卡,也没有下一道口述题。用户随后用真实带时间经历回答时,旧焦点仍指向已经回答的采集题,对话到此停止。
- 触发条件:训练门已开、occupation 等方法覆盖未齐、`datedMethodCollectOpen` 为假、引擎已放行 accept/propose、`probe === null``canOfferRange=true``canAdopt` / `selectionAllowed` / `proposeAllowed` 全为假;随后 `collect_spoken` 焦点收到已成功入账的真实事件,且本轮工具调用可能先执行 `record-evidence-batch`
- 根因:五段同时成立。(A) 探针耗尽是假耗尽:带年份探针的分支只把已淘汰候选切出,当前活跃候选在所有 `answer_class` 分支中待遇相同,按活跃集合重算信息量为零;旧出题层沿用全窗口信息量,不重算,也不把该探针写入 `dropped_probes`,于是以 `probe=null` 静默失效。(B) `probe=null` 且方法覆盖未齐时,`rectification-decision.ts``offerRangeWithoutAdopt` 仍把 `canOfferRange` 设为真,但保持 `canAdopt` / `selectionAllowed` / `proposeAllowed` 为假并关闭 focus;这把本应继续补覆盖的状态送进没有承载的区间出口。(C) `provisional_range``deferAdoption` 原本连 `intent=collect_method_evidence` 的覆盖追问一起吞掉,把它从 `next_followup` 挪到 `deferred_followup`;消费端不读后者,因覆盖不足进入的状态反而结构性阻止补覆盖,形成自锁。(D) 用户用真实事件回答口述采集焦点并成功落库后,服务端没有按 active focus 确定性执行 `resolved`;焦点继续存在,`persistNextInterviewIfIdle` 因 active focus 提前返回,旧题仍出现在 `current_question`。这与「确实没有」必须走 `declined` 的语义不同。(E) 本轮工具顺序可能是 `record-evidence-batch` 先于 `rectification-read-case`,模型未拿到服务端 focusId 时仍能记证据;旧实现把工具顺序当成硬前置,既没有先补读 dossier,也没有保证后续焦点关闭和下一问落库。
- 修复:P0`provisional_range` 只对 `intent=distinguish_candidates` 延后 followup`collect_method_evidence` 覆盖题继续从 `next_followup` 返回并正常落焦点。`adopt_representative` / `validated_range` / `exact_minute_confirmed` / `provisional_range_user_stopped` / `completed_with_range` 的既有收口行为不变。P1:对比包和 `rankRenderableDiscriminators` 均按当前活跃候选重算探针信息量;零区分力探针显式写入 `dropped_probes.reason=no_split_among_active`,不放宽 `MIN_BOUNDARY_DAYS`、distinguish 合同或确认门。P2`record-evidence-batch` 成功且当前 active focus 是 `collect_spoken` 时,服务端按 dossier 中的 focus 自行 `resolve(status=resolved, evidenceId=acceptedEvidenceId)`,不依赖模型传 `focusId`;否定回答仍走 `declined`。关闭后继续走下一问持久化。P3`offerRangeWithoutAdopt` 使用统一的 `nonConvergingRangeNarration` 输出可信区间、代表分钟及「代表分钟不是已确认的唯一出生分钟」口径,并在同一轮持久化至少一个可执行的 `collect_spoken` focus;不为无选项问题新造视觉容器,也不为看盘按钮放宽 `canAdopt` / `confirmationAllowed`。P4:若本轮尚未成功执行 `rectification-read-case``record-evidence-batch` 先自行读取一次 dossier 再落库,不因模型工具顺序错误打断用户。
- 验证:修复前基线(`79f65ac8`)实际回归输出:record-before-read 时第一条 RPC 为 `insert_agentic_rectification_tool_receipt` 而非 dossier read`provisional_range.next_followup``undefined` 而不是 `collect_method_evidence`。修复后 `tests/rectification-eight-method.test.ts` 为 62/62;该套件同时锁定首条 RPC、accepted evidence 使用其 evidence ID 关闭 spoken collect focus、resolve 发生在 record 之后且不再返回旧题;`tests/rectification-range-offer-deadend.test.ts` 覆盖活跃候选零信息量 drop、仍有真实 split 时继续出区分卡、数字区间与代表分钟叙述、口述 focus 落库、userStopped 行为;`tests/rectification-spoken-collect.test.ts` 与 BUG-440 / BUG-442 / b43808b0 无年份收窄回归保持绿。`npx tsc --noEmit` 通过;改动文件 ESLint 0 error6 条既有 unused-vars warning);focused rectification/contract suite 727/727;全量 `tests/*.test.ts` 2339/2339`/opt/anaconda3/bin/python3.12 -m pytest tests/test_rectification_event_probes.py tests/test_candidate_discriminator_contract.py` 35/35。
- 防复发:任何因缺某项覆盖而进入的状态,不得阻止获取该项覆盖;探针信息量必须针对当前活跃候选计算,失去区分力的探针必须显式 drop;口述采集焦点被真实证据回答后必须由服务端确定性关闭,不依赖模型传 `focusId``resolved``declined` 语义不得互换;助手正文承诺的每个出口,同一轮必须有对应承载,区间提议必须带数值与代表分钟口径;`record-evidence-batch` 不得因未先 read-case 而丢弃用户证据。不得用 `answerText` 文本判断问题是否已经问出,不得引入语义正则、关键词表或 A/B/C/D 位置推断,不得回退无年份 `varga_style` 收窄或放宽 confirmation gate。
- 相关记录:BUG-405、BUG-407、BUG-432、BUG-440、BUG-441、BUG-442、BUG-449
- 复发自:BUG-432(出题层/决策层/可见承载再次分裂)、BUG-440(覆盖不足状态再次空转)、BUG-441/BUG-449(口述焦点可见性与服务端兜底边界未覆盖本路径)
- 修复版本:`fix(rectification): prevent collect focus dead-end after choice answers`Skill 保持 `10.0.13`
## BUG-451 | 个人报告整份调用失败掩盖单章失败与截断边界
- 状态:mitigated
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:个人报告 worker、`createPersonalReportAgent``/reports/[reportId]``personal_report_jobs`
- 用户现象:一次模型输出承载整份报告;任一主题写坏或最终 JSON 解析失败,整份报告显示 `report_schema_invalid`,用户看不到已成功生成的主题。生成日志也无法区分模型停止、输出截断和校验失败。
- 触发条件:完整报告包含多个 write 主题,单次 writer 输出超过 provider 默认预算或任一主题未通过绑定/最终 schema 校验。
- 根因:报告 writer 只做整份报告的一次结构化调用加一次修复重试,没有章节级持久化、重试预算或 `finishReason`/token telemetry`report_document` 也没有可安全表达半成品的中间语义。
- 修复:先上线并采集 `finishReason``inputTokens``outputTokens`;随后改为串行 plan → section → summary → assemble。每章按 `evidenceRefs` 过滤 bundle 并独立落库/重试,耗尽后生成明确 blocked disclosure;摘要最后只接收章节标题与 `claimStatus`;仅最终 assemble 写 `report_document`,并把 job progress phase/percent 返给等待页。
- 验证:任务 0 真实 staging 观测为 1 次报告失败、`finishReason=length` 占比 0%、`outputTokens` p50/p95 为 3069/3069,失败归因为 report-level parse 而非截断;分章节单元测试、续做/blocked 测试、真实 Docker 数据库 RLS/权限测试均通过;TypeScript、ESLint0 error)通过。
- 防复发:模型调用必须记录 allowlist 指标字段且不得记录 prompt、模型原文或用户资料;章节中间结果只能写 `personal_report_sections`;章节生成保持串行;摘要不得接收正文全文;blocked 章节必须显式渲染,不能静默丢失;最终成品仍须通过服务端 canonical parse。
- 相关记录:BUG-352
- 复发自:无
- 修复版本:待 staging 部署;本地提交待生成
## BUG-452 | 采集焦点落库失败后静默结束
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`persistNextInterviewAfterChoice``persistFocusAfterChoice``persistExhaustionCollect`、POST `/api/rectification/agent` 非终态轮末出口
- 用户现象:答完家人采集题后助手只说「记下了,这方面先跳过。」,没有下一问也没有时间卡。
- 触发条件:上一采集焦点刚被关闭,下一条 `collect_method_evidence` 焦点落库返回 `duplicate_focus``skipped`;该轮同时没有可采用的候选承载。
- 根因:采集焦点落库失败时 `persistNextInterviewAfterChoice` 返回 `hostNarration=null`,随后被 denial 兜底文案「记下了,这方面先跳过。」吞掉;轮末也没有结构化不变量检查 `current_question` 或真实可采用承载,最终形成非终态但无可继续交互入口的静默死路。
- 修复:采集焦点首次落库失败后重读 dossier 并重试一次;若已存在同 `questionId` 的 active focus,则按 `already_open` 复用。重试仍失败时复用 `persistExhaustionCollect` 口径,正文写出可信区间、代表分钟和口述下一问,并尝试落一个可回答的采集焦点。自由文本轮末新增结构化非终态出口检查:若未 accepted、未 confirmed、未 user-stopped,且既无 `current_question` 也无真实可采用承载,则确定性补口述采集焦点、把题干写入正文,并记录不含用户内容的 `rectification_nonterminal_exit_repaired` 日志。删除会成为终局的 denial 单句兜底。
- 验证:修复前新增 fixture 证明 `duplicate_focus``skipped` 均会得到 `hostNarration=null``current_question=null`;修复后两条路径均有问题正文并持久化或复用非空焦点。新增非终态出口不变量回归,锁定服务端补出 `collect_spoken` 焦点和题干。复刻五证据、五条已答探针、活跃候选 `05:00/05:07/04:53`、family declined 的 case,锁定 `career.2023.dasha_activation``no_split_among_active` drop、`nextAction=offer_provisional_range``canAdopt=false`,轮末 `current_question.question_id=collect:occupation:collect_method_evidence` 且正文含「你长期做什么工作?」。`assert.doesNotMatch(afterRun, /answerText\.(?:includes|match|search)\(/)` 保持通过 本地 `npx tsc --noEmit`、改动文件 ESLint、目标四组回归 `122/122` 通过;focused 全套 `730/731`,唯一 Docker/PostgreSQL migration fixture 失败单独重跑 `1/1` 通过;全量基线 `2342/2347`,五项失败均为并行数据库容器/migration 环境波动或本机缺少 PyYAML,未扩大到产品逻辑。
- 防复发:非终态轮次结束时必须有 `current_question` 或真实可采用承载;任何采集焦点落库失败都不得静默降级为一句确认话。该不变量只看焦点与 decision/承载字段,不得用正文文本判断助手是否问出问题。不得为无选项题新造视觉容器或复用选择卡样式;不得引入语义正则、关键词表或 A/B/C/D 位置推断;`resolved``declined` 语义不得互换。
- 相关记录:BUG-441、BUG-449、BUG-450
- 复发自:BUG-450
- 修复版本:`fix(rectification): prevent silent collect focus stalls`Skill 保持 `10.0.13`
## BUG-453 | 可采用区间已由服务端放行但前端不渲染时间卡
- 状态:resolved
- 首次发现:2026-08-30
- 最近更新:2026-08-30
- 影响面:`rectification-agentic-chat.tsx` 时间卡渲染门控、`choice-action` 点选快路径、Case GET 默认投影
- 用户现象:答完最后一道选择题后助手只说「已记录你的选择,并更新了候选比较。」;界面没有时间卡也没有下一问,流程停住。
- 触发条件:服务端已给出 `can_adopt=true``selection_allowed=true``precision_stage=ready_to_adopt`,但当前轮是确定性点选路径,没有产生 `rectification-offer-candidates` 模型工具 receipt;前端仍以历史 `offeredSelectionOnce` 作为时间卡前置条件。
- 根因:服务端允许采用,前端却把承载绑定到模型是否调用某个工具;确定性点选路径不会产生该 receipt step,因此 `selectionAllowed=true` 仍无法渲染时间卡。点选快路径原正文也只保留确认句,没有区间、代表分钟和采用提示。
- 修复:P0:时间卡改为只依赖服务端 `selectionAllowed=true``canAdopt=true`,保留最新已结算助手消息、忙碌/只读/重生成状态和选择卡互斥门;不再依赖 `offeredSelectionOnce`。P1:点选进入 `offer_provisional_range + can_adopt=true` 时由服务端确定性写出可信区间、代表分钟、代表分钟免责声明和「可以从下面的时间里选一个采用」;该正文同时落入确定性 turn。P2:非终态轮末只看 `current_question` 或决策 `canAdopt`,缺失时确定性补口述采集焦点并记录结构化日志。P3:Case GET 默认只保留活跃候选/代表分钟对应宫位表与当前焦点 probe;`detail=full` 保留完整 receipt`decision``gates``inference_state` 不被瘦身修改。模型侧继续使用既有的精简 projection。
- 验证:新增/更新前端门控与 `canAdopt` camel/snake 解析回归;保留 duplicate focus、skipped focus、非终态出口和五证据 case 回归;P1 断言确定性落库正文包含区间、代表分钟、免责声明和采用提示;P3 断言默认投影保留决策字段与完整 `inference_state`,并移除非当前 probe 与非活跃宫位表。提交前运行 TypeScript、改动文件 ESLint、rectification/agentic 回归及全量测试;真实线上模型 74 秒路径耗时未在本轮复测。
- 防复发:UI 承载只看服务端状态,不得绑定模型工具 receiptready-to-adopt 的确定性正文必须写区间、代表分钟和采用提示。非终态轮次必须有 `current_question` 或真实可采用承载;所有承载判断只看焦点与 decision 字段,不得用正文文本判断。不得为无选项问题新造视觉容器或复用选择卡样式,不得引入语义正则、关键词表或 A/B/C/D 位置推断;不得改变 confirmation gate、`resolved`/`declined` 语义或 Skill `10.0.13`
- 相关记录:BUG-441、BUG-449、BUG-450、BUG-452
- 复发自:BUG-450
- 修复版本:待发布
## BUG-454 | 新增后台页面未同步 capability audit 路由契约
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:`backend-quality-gate.yml` 的 Python quality gate、`_capability_audit()` 应用路由扫描
- 用户现象:推送 `53450f5bf362c85d8b3bfbae7f2ab724e30f3208``staging` 后,quality gate run `2217``validate` 失败,部署未继续。
- 触发条件:新增 `frontend/src/app/admin/feature-pricing/page.tsx``frontend/src/app/admin/pricing-simulator/page.tsx` 后,运行 `tests/test_api_server_security.py::test_capability_audit_scans_registry_and_local_sources`
- 根因:`_scan_app_routes()` 按文件系统扫描真实 `page.*` 路由,而测试在 `tests/test_api_server_security.py` 中维护旧的硬编码完整列表,未加入两个新路由。
- 修复:在路由快照断言中加入 `admin/feature-pricing``admin/pricing-simulator`,保持测试与真实页面树的排序结果一致。未修改 workflow 配置或路由扫描逻辑。
- 验证:针对性 capability audit 测试通过;随后执行 quality gate 关注的 Python 测试集合,确认原失败断言已消除。
- 防复发:新增或删除前端 `page.*` 路由时,必须同步 capability audit 的路由契约测试;quality gate 失败时先查看失败测试,不把下游 `publish` 的附带状态当成根因。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-455 | 定价配置异常被通用运行失败吞掉
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:POST `/api/rectification/agent`、POST `/api/consult`、POST `/api/reports``FeaturePricingError` 错误映射
- 用户现象:环境没有发布对应 feature/model tier 定价时,生时校正显示通用“暂时不可用,请稍后再试”,咨询和报告也只落入通用 503;文案暗示重试,但实际需要运维发布定价配置。
- 触发条件:`resolve_feature_pricing` 返回 `feature_pricing_missing``feature_pricing_model_unavailable` 或其他 fail-closed 定价错误。
- 根因:三个付费入口没有保留 `FeaturePricingError.code`;生时校正把 reserve 异常统一改成 `billing_unavailable`,随后又未在外层显式映射而落到 `run_failed`consult/reports 则由通用 catch 吞掉。
- 修复:三个入口均只把 `FeaturePricingError.code` 写入脱敏服务端日志;生时校正保留内部 `feature_pricing_*` reason,并统一映射为公开 `billing_unavailable`;咨询和报告统一返回 `pricing_configuration_unavailable`。用户文案明确“计费配置不可用/尚未完成,请联系支持”,不返回内部错误原文。
- 验证:新增三入口路由契约,断言内部 code 被保留、公开错误不落入 `run_failed`、响应不包含 `error.message`TypeScript、ESLint、billing/consult/report 聚焦回归通过。rectification 聚焦套件 `705/705`;全量 `2368/2369`,唯一失败为默认 `python3` 缺少 PyYAML,指定已有 PyYAML 的解释器后原失败文件 `39/39` 通过。真实 staging 定价行与日志因本地无环境凭据未验证。
- 防复发:付费入口必须把配置缺失与瞬时运行错误分开;内部日志记录稳定 code,公开响应只使用安全错误码和可执行文案。部署完成不等于定价可用,仍须通过 admin flow 发布当前环境实际 model tier 的定价。
- 相关记录:无
- 复发自:无
- 修复版本:待发布
## BUG-456 | opening 成功后未持久化当前问题槽
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:POST `/api/rectification/agent` opening 成功路径、`persistNextInterviewIfIdle``ensureNonTerminalTurnExit`
- 用户现象:开场回复正文看起来提出了问题,但服务端 `current_question` 仍为空,页面显示“当前没有可回答的问题,正在等待服务端更新”。
- 触发条件:新 Case 执行 `action=opening` 且 Agent 没有自行调用可持久化 focus 的工具。
- 根因:成功轮后的问题槽持久化与非终态出口检查都被限制在 `action === "message"`;opening 只保存散文回复,没有建立服务端拥有的可回答问题槽。
- 修复:成功的 `opening``message` 一起执行 `persistNextInterviewIfIdle`;随后执行 `ensureNonTerminalTurnExit`,在仍无问题且没有已确认、用户停止或真实可采用承载时确定性补出问题。`read_only` 保持无副作用,不纳入该分支。
- 验证:回归锁定 opening/message 共用问题槽持久化与非终态出口路径,并以真实 `persistNextInterviewIfIdle` fixture 断言投影后的 `current_question` 非空;TypeScript、ESLint 与 rectification 聚焦套件 `705/705` 通过。未进行真实 staging opening smoke。
- 防复发:任何会启动或推进 Case 的成功非终态轮都必须以服务端状态结束:存在 `current_question` 或真实可采用承载;不得以模型正文是否包含问句代替结构化状态。
- 相关记录:BUG-452、BUG-453
- 复发自:BUG-452(非终态出口只覆盖 message,未覆盖 opening
- 修复版本:待发布(Skill 保持 `10.0.13`
## BUG-457 | 功能定价草稿保存成功但列表为空且菜单入口被隐藏
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:`/admin/feature-pricing``/admin/pricing-simulator``GET /api/admin/feature-pricing`、后台功能定价可读性
- 用户现象:管理员保存定价草稿后接口没有报错,但列表仍为空;左侧菜单找不到“功能定价”;功能和模型档位只显示内部英文键,非研发人员无法判断含义。
- 触发条件:以 `admin_runtime` 读取启用 RLS 的 `public.feature_pricing`;或由 Refine 根据 `resourcePermissions` 生成后台菜单;或打开功能定价、定价测算列表。
- 根因:建表迁移只向 `admin_runtime` 授予了 `SELECT`,没有为启用 RLS 的表创建读取策略,因此安全定义者函数可以保存草稿,但后台直查被 RLS 静默过滤为 0 行;同时 `feature-pricing``pricing-simulator` 未加入菜单权限映射;界面直接渲染稳定内部键值。
- 修复:新增向前迁移,为 `admin_runtime` 添加 `feature_pricing` 只读 RLS 策略;补齐两个 Refine 资源的权限映射;统一提供中文标签并保留括号内原始键值,供功能定价和定价测算共同使用,同时把草稿、已发布、已停用状态本地化。
- 验证:契约测试锁定菜单映射、中文标签/原始值以及只读 RLS 策略;本地 PostgreSQL fixture 插入 `rectification/standard` 草稿后,以 `admin_runtime` 成功读回该行;TypeScript、ESLint 与聚焦测试通过。
- 防复发:任何后台运行角色直查启用 RLS 的新表时,表权限和对应只读 policy 必须成对交付并用该真实角色查询验证;新增 Refine resource 必须同步 `resourcePermissions`;后台稳定键值必须显示可读标签且不得改变提交值。
- 相关记录:BUG-454、BUG-455
- 复发自:无
- 修复版本:待发布
## BUG-458 | 订阅续费测试依赖固定时钟导致 staging quality gate 过期失败
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:`frontend/tests/database-billing-admin.test.ts`、Gitea `backend-quality-gate` staging validate job
- 用户现象:功能定价修复推送后 workflow run `2232``validate` 失败,后续 `publish`、迁移和部署未执行;前端全量测试 `2370/2371` 通过,仅数据库计费契约失败。
- 触发条件:CI 在 `2026-08-31 10:00 UTC` 之后执行订阅续费测试;测试把上一份月订阅结束时间和下一份订阅期望开始时间固定为该时刻。
- 根因:测试期望没有遵守 `settle_order` 的真实契约 `greatest(v_now, max(existing_subscription.ends_at))`。当数据库当前时间晚于写死的旧订阅结束时间时,新订阅会正确地从当前时间开始,固定字符串断言却仍期待过去时间。
- 修复:将已有月订阅结束时间设为相对当前时间的一天后,并直接断言续费订阅的 `starts_at` 等于上一份订阅的 `ends_at``ends_at` 等于 `starts_at + interval '1 month'`;不修改正确的计费函数。
- 验证:`database-billing-admin.test.ts` PostgreSQL 聚焦测试 `1/1` 通过;使用已安装 PyYAML 的 Python 解释器运行前端完整测试 `2371/2371` 通过;远端 workflow 结果另行记录。
- 防复发:涉及 `clock_timestamp()``now()` 或续费基准的契约测试不得把当前日期附近的绝对时间作为期望;应断言记录间关系和周期不变量。
- 相关记录:BUG-455、BUG-457
- 复发自:无
- 修复版本:`test(billing): make renewal timing deterministic`
## BUG-459 | Skill 升版身份同步不完整:SHA 目标目录易错且身份分散在五处
- 状态:resolved
- 首次发现:2026-08-31
- 最近更新:2026-08-31
- 影响面:`skills/skill-package-registry.json``skills/jyotish-birth-time-rectification/**``RECTIFICATION_SKILL_VERSION``frontend/tests/skill-registry.test.ts``openRectificationCase`
- 用户现象:升版后新建 Case 直接失败(`skill_registry_version_mismatch`),或 `skill-registry.test.ts` 在发布门禁处失败。
- 触发条件:升级 rectification Skill 版本时只改了部分身份位点;或对 Skill 根目录而非 `versions/<version>/` 计算 SHA256。
- 根因:Skill 身份分散在五处——版本目录 `SKILL.md` frontmatter、根目录 `SKILL.md` 副本、registry 条目的 `version`/`sha256`/`packagePath``RECTIFICATION_SKILL_VERSION` 常量、测试内硬编码 SHA。registry 的 `packagePath` 指向 `versions/<version>/`,但根目录存在同名 `SKILL.md` 副本,容易让人误以为 SHA 应对根目录计算;根目录含 `versions/` 整棵子树,对它计算会把全部历史版本吃进哈希,**且能正常返回一个值、不会立刻报错**。`openRectificationCase``v9/case-service.ts`)校验 registry active 版本必须等于 `RECTIFICATION_SKILL_VERSION`,任一处不同步即导致新建 Case 整体失败。
- 修复:升版按固定顺序执行——先建 `versions/<new>/` 并改完全部字节(含 frontmatter 与根目录副本),**最后**统一对 `versions/<new>/` 调用 `computeSkillPackageSha256()`,再回填 registry 与测试。10.0.13 → 10.0.14 按此流程完成。
- 验证:对 `versions/10.0.14/` 实算 SHA 与 registry、`skill-registry.test.ts` 三处一致;registry active 条目唯一;根目录与版本目录 `SKILL.md` 字节一致;`rectification-*``skill-registry` 套件 730 项 0 失败;`tsc --noEmit` 通过。未进行真实 staging smoke。
- 防复发:SHA256 只能对 registry `packagePath` 指向的目录计算,绝不对 Skill 根目录计算;改字节与算 SHA 不得交错,必须先定稿再统一重算;升版必须在同一变更内同步全部五处身份位点;`skill-registry.test.ts` 是发布门禁,不得跳过。存量 Case 绑定各自的 `skill_version``resolveExact``deprecated` 放行,因此升版不需要数据迁移,但旧 Case 不会获得新版 prompt 约束。
- 相关记录:BUG-456
- 复发自:无(提交 `797a423a` 曾出现同类的"改了 Skill 字节但未刷新 registry hash",当时未单独立项)
- 修复版本:Skill `10.0.14`
## BUG-460 | 非终态轮缺少统一出口闸导致 answer_choice 与消息早退进入无问题死路
- 状态:resolved
- 首次发现:2026-09-01
- 最近更新:2026-09-01
- 影响面:POST `/api/rectification/agent``opening``message``answer_choice``stop_and_review` 推进轮,`decideRectification` 的 probe 耗尽转向,以及服务端问题在 turn 历史中的可见性
- 用户现象:真实校正会话已有多条证据、多个领域并答完多道区分题后,仍保持正确的 `can_adopt=false`,但 `current_question=null`,界面永久显示等待服务端更新。
- 触发条件:最后一步走 `answer_choice`,或 `message` 命中任一 HTTP 200 确定性早退;同时普通 discriminator 已耗尽、候选分离不足,原决策链直接返回区间而未继续尝试 holdout、带日期收集或 Nakshatra 边界题。
- 根因:BUG-456 只在当时命中的 opening/message 分支附近补了兜底调用,没有建立所有推进轮共享的后置条件;后来新增或既有的 structured choice 与多个消息早退仍可绕过。选择题历史另以「接下来请点选下面这一问。」代替服务端 focus prompt,导致回看时看不到真实问题。
- 修复:所有成功的推进轮统一经过 awaited 公共出口闸;immediate 路径必须等 focus 持久化完成后才返回 HTTP 200,stream 路径必须在 `done` 前完成。`read_only` 显式无副作用。闸门最终重读 Case 并验证 `current_question || canAdopt || 已终态`,无法建立后置条件时 fail-closed。probe 耗尽按普通 discriminator → holdout → dated collect → Nakshatra 边界题 → 明确区间出口转向,所有路径继续使用既有 delivery capability,事故 Case 的 `canAdopt=false` 不变。服务端 focus prompt 直接写入 turn 历史,不让模型复述。
- 验证:任务 0 三条不变量覆盖四种推进 action 与 answer_choice 响应可见时序;上一轮五条收紧不变量继续通过;rectification、skill registry 与 TypeScript 最终数字见本次交付报告。未进行真实 staging smoke。
- 防复发:新增 action 必须先加入穷举的 execution-kind 映射,否则 TypeScript/结构契约失败;所有 HTTP 200 推进出口只能在公共闸门之后可见,所有流式成功出口只能在公共闸门之后发送 `done`。不得用正文、问号或 A/B/C/D 文本推断当前问题,也不得通过放宽采用门槛消除无下一步状态。
- 相关记录:BUG-459
- 复发自:BUG-456
- 修复版本:待发布
## BUG-461 | 口述采集卡重复助手正文,选择题卡题干与气泡不对齐
- 状态:resolved
- 首次发现:2026-09-01
- 最近更新:2026-09-01
- 影响面:`rectification-agentic-chat` 当前问题槽、`RectificationChoiceCard` legend、`.rectification-question-slot` 对齐
- 用户现象:口述采集时卡片把助手已经说过的题干再显示一遍,并多出灰色提示「请在下方输入框回答…」。选择题卡在未点选时与助手气泡左缘错位,点选后回看才对齐。
- 触发条件:当前问题是 `collect_spoken`;或当前问题是选择题且卡片仍在问题槽、尚未变成消息附件。
- 根因:BUG-449/460 已把服务端题干写入助手正文,BUG-441/452 也禁止为无选项问题新造视觉容器,但后续测试把口述采集卡和可见 legend 锁成了必有。问题槽是消息列表的兄弟节点,没有 `--assistant-content-inset`;已回答卡片在 `.rectification-message-wrap` 内,所以只有回看才对齐。
- 修复:口述采集不再渲染第二块视觉卡,题干只留在正文,`collectSpokenPrompt` 只驱动输入框占位。选择题 legend 改为 `sr-only`。问题槽与助手内容使用同一 inset,且不给槽内选择题再套一层 inset。
- 验证:`rectification-spoken-collect``rectification-agentic-entry``rectification-varga-style-copy` 改为锁新行为;口述采集不得再出现 `__spoken` / `__prompt` / `aria-describedby`。未做登录后的真实页面点选。
- 防复发:不得为 `collect_spoken` 新造视觉容器或复用选择卡样式;选择题可见题干不得与助手正文重复;问题槽与已回答卡片必须共用 `--assistant-content-inset`。不得用正文文本判断助手有没有问出来。
- 相关记录:BUG-441、BUG-449、BUG-452、BUG-460
- 复发自:BUG-441
- 修复版本:待发布
## BUG-462 | 区间出口采集撞上开场题号后,成功计费轮被误报 run_failed
- 状态:resolved
- 首次发现:2026-09-01
- 最近更新:2026-09-01
- 影响面:`exhaustionSpokenCollectFollowup``persistCollectFocus`、POST `/api/rectification/agent` 流式成功出口
- 用户现象:助手已经记下带年份事件并完成计费后,界面同时出现「回答未完成,已保留现有内容;本次不会扣点。」和「生时校正暂时不可用,请稍后再试。」
- 触发条件:非收敛区间出口后需要再问口述采集,且开场通用采集题号 `collect:unknown:collect_method_evidence` 已作为历史行存在;或职业/健康压力等域不在 SQL `target_domain` 白名单里。
- 根因:耗尽采集只轮转家庭/升学/财务后就退回 `domain: null`,题号与开场题相同。`unique (case_id, question_id)` 含已结束行,落库报 `focus_idempotency_conflict`。出口闸因此认为没有下一步并抛 `agentic_rectification_nonterminal_exit_missing`。该异常发生在 `billing.settled``run.completed` 之后,被映射成 `run_failed`;前端只要看到 `error` 就把本轮标成失败并提示不扣点。
- 修复:耗尽采集在家庭/升学/财务之后问职业,再问健康压力/搬家/事业/感情,最后用 `domain: "other"`。职业落库映射为 `other`,健康压力映射为 `health`。同一题号冲突时改写 `questionId:next`。流式 `result.ok` 后出口闸失败只记日志,仍发送 `done`,不再发 `run_failed`
- 验证:`rectification-range-offer-deadend` 锁职业与 `other` 回退;`rectification-server-focus` 锁域映射与 `:next` 重试;`rectification-spoken-collect` 锁成功出口不 `send error``rectification-collect-stall` 冲突路径改为两次落库。未做登录后的真实页面复现。
- 防复发:耗尽回退不得再用 `collect:unknown:*`。SQL 不允许的域必须在落库前映射。流式成功轮在计费完成后不得把出口闸失败发成用户可见的 `run_failed`。出口闸仍须在 `send done` 之前 awaited。不得放宽 `canAdopt`
- 相关记录:BUG-450、BUG-452、BUG-460
- 复发自:BUG-460
- 修复版本:待发布
## BUG-463 | 采用门把分离与 holdout 绑死导致引擎可出牌时用户无法采用
- 状态:resolved
- 首次发现:2026-09-01
- 最近更新:2026-09-01
- 影响面:`deliveryCapability` / `decideRectification`、GET case overlay、`persistNextInterviewIfIdle`、候选卡采用 RPC 投影
- 用户现象:多条可评分证据、多领域、区分题已答完,引擎 receipt 已允许 acceptance/selection/propose,界面仍是 `can_adopt=false`、没有时间卡、不能保存,并继续轮询财务/职业等采集题。
- 触发条件:方法覆盖(含职业确认或拒答关闭)已满足,候选领先不足 `MIN_SEPARATION_LEAD`,或 holdout 未通过/不可用;引擎四门自洽且 `confirmation_allowed=false`
- 根因:策略死结,不是实现回归。引擎 receipt 与 Skill `10.0.14` 都把分离/holdout 留在唯一分钟确认门,交付物是代表分钟加可信区间。TypeScript `deliveryCapability` 却把这两项写进 `canAdopt`,overlay 再与引擎取交集,于是引擎的“可以”永远被 TS 盖成“不行”。上一轮 `TASK-rectification-nonterminal-exit-20260901.md` 与 BUG-460/462 的防复发写了“不得放宽 `canAdopt`”;该红线已由产品负责人于 2026-09-01 显式推翻,本条记录授权变更。
- 修复:`locallySelectable` 只保留候选存在、方法覆盖完成、以及证据下限 `keep_collecting`。分离充分、holdout passed 与引擎 `confirmation_allowed` 只进入 `canConfirmExactMinute`。覆盖完成后 idle 持久化不再追问,GET overlay 放出候选卡;采用 RPC 仍读引擎 snapshot 的 `selection_allowed`,写入 `accepted` 不写 `confirmed`
- 验证:事故形状 1a–4 与交付 B1–B3;授权组断言(分离不足/holdout/并列/`offer_provisional_range` 不采用)改为 provisional 采用且确认门仍关;证据下限、引擎 ceiling、确认门、非终态出口闸与 `read_only` 保持全绿。`rectification-*.test.ts``skill-registry.test.ts` 最终数字见本次交付;`./node_modules/.bin/tsc --noEmit` 必须通过。未进行真实 staging smoke。
- 防复发:`canAdopt` / `selectionAllowed` / `proposeAllowed` / `canConfirmExactMinute` 只在 `deliveryCapability` 计算并经 `...capability` 展开,出口不得覆写。不得把分离或 holdout 重新绑回采用门。不得放宽确认门、表达边界、3/2 证据下限或引擎 ceiling fail-closed。不得 bump skill 版本或改 Python 引擎来绕过本条。
- 相关记录:BUG-456、BUG-460、BUG-462
- 复发自:无(授权策略变更,不是同一实现回归)
- 修复版本:待发布
+142 -13
View File
@@ -16,12 +16,13 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet
| Page floor | `--color-canvas-soft` | `#f3f2ee` | App background and quiet secondary bands |
| Warm surface | `--color-canvas-muted` | `#ebe9e3` | Cards, user messages, table headings |
| Strong neutral | `--color-canvas-strong` | `#e1ded6` | Pressed and emphasized neutral surfaces |
| Sidebar glass | `--color-sidebar` | `rgba(235, 233, 227, .86)` | Desktop and mobile navigation |
| Sidebar glass | `--color-sidebar` | `rgba(235, 233, 227, .86)` | Desktop navigation only |
| Sidebar solid | `--color-sidebar-solid` | `#ebe9e3` | Mobile drawer, and any reduced-transparency surface |
| Selected surface | `--color-selected` | `rgba(255, 255, 255, .62)` | Current navigation and raised light rows |
| Ink | `--color-ink` | `#1d1d1f` | Headlines and primary text |
| Body | `--color-ink-strong` | `#32322f` | Strong body copy |
| Secondary text | `--color-ink-secondary` | `#676762` | Supporting copy and labels |
| Tertiary text | `--color-ink-tertiary` | `#8a8983` | Hints and metadata |
| Secondary text | `--color-ink-secondary` | `#5f5f59` | Supporting copy and labels, section headings |
| Tertiary text | `--color-ink-tertiary` | `#6a6963` | Hints, metadata, empty-state copy |
| Primary action | `--color-action` | `#85432f` | High-signal links, rings, and compact actions |
| Primary soft | `--color-action-soft` | `#f4e8e2` | Editorial emphasis without a dark block |
| Primary active | `--color-action-hover` | `#6f3627` | Hover and pressed action |
@@ -34,15 +35,109 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet
| Error | `--color-danger` | `#9a2f2f` | Errors and destructive actions |
| Accessible focus | `--color-focus` | `#85432f` | Keyboard focus and input focus |
The personal report carries a second, narrower palette for its printed-paper
surface. It is deliberately separate — the report is a document, not app chrome —
and nothing outside `.personal-report-*` may use it.
| Role | Token | Value | Usage |
|---|---|---:|---|
| Report paper | `--report-paper` | `#f8f5ee` | The report sheet itself |
| Report rule | `--report-rule` | `#c9c2b7` | Rules and dividers inside the sheet |
| Report accent | `--report-accent` | `#85432f` | Report headings, same hue as the action color |
Rules: neutrals stay warm; the action color is scarce; roughly ninety percent of the interface remains light. Dark ink is punctuation, never a page-scale surface. No raw color may appear in UI styles outside these tokens and their documented alpha mixes.
### Where the action color appears
Scarce is a budget, not zero. The sidebar spends it in exactly three places, and
a fourth needs a reason:
1. `新建对话` — the one high-signal action in the nav, as tinted text and icon on
a transparent row (`--color-action`, deepening to `--color-action-hover`).
Never a filled block: a full-width terracotta surface would break the
ninety-percent-light rule.
2. The 2px bar on the active session (`--sidebar-ring`).
3. The profile initial's circle (`--color-action`), which only renders when the
account has no uploaded avatar.
2 and 3 both disappear on a new account with an avatar — no sessions, no
initial — which is how the sidebar ended up with no accent pixel at all. Item 1
is what guarantees the nav still has a visual anchor in that state. `我的报告`
and every section label stay neutral on purpose; giving them color too would
flatten the hierarchy the accent exists to create.
The glass surface assumes a light backdrop. The mobile drawer floats above the
scrim instead, so the translucency darkened it to `#E3E1DC` and flattened the
warmth; the drawer takes `--color-sidebar-solid` and drops `backdrop-filter`.
Hierarchy inside the nav comes from ink rank, not hue: section headings sit on
`--color-ink-secondary`, their body and empty-state copy on
`--color-ink-tertiary`. Both used to share tertiary, which is why the nav read
as one flat grey. Empty sections name the next step in that same neutral copy
rather than adding a second tinted call to action.
### Dark theme
The dark theme is a role-preserving restatement of the palette, not an inversion.
Every themeable token is redefined; `frontend/tests/dark-theme-contract.test.ts`
fails if one is missed.
Three states, the standard pattern: with no attribute the page follows the OS
through `prefers-color-scheme`; `data-theme="dark"` or `data-theme="light"` on the
root element pins a choice and wins in both directions. Plain CSS cannot share a
declaration list, so the dark palette is written twice — once inside the media
query, once under the attribute — and a contract test asserts the two copies stay
identical.
| Role | Light | Dark | Why it is not an inversion |
|---|---|---|---|
| Page floor | `#f3f2ee` | `#1a1a19` | On light, raised surfaces step *down* in lightness; on dark they step *up*. The floor is the darkest surface in dark mode, the second-lightest in light mode. |
| Reading surface | `#fbfaf7` | `#262624` | Warm, never neutral black. A cool grey would change the product's identity rather than its brightness. |
| Warm card | `#ebe9e3` | `#30302d` | One step above the reading surface in both themes, by opposite directions. |
| Primary action | `#85432f` | `#d78064` | The clay hue is kept and lifted. `#85432f` is 2.1:1 on `#262624` — unreadable. |
| Action hover | `#6f3627` | `#e59273` | Hover darkens on light and lightens on dark. |
| Dark punctuation | `#1d1d1f` | `#f2f0ea` | A dark block on a light page becomes a light block on a dark page; the token name describes the light-theme value, the role is "highest-contrast block". |
| Semantic hues | `#9a2f2f` / `#28633e` / `#b07b22` | `#e08573` / `#74b189` / `#d8a758` | Lifted for contrast; their `-muted` pairs stop being pale washes and become dark tints of the same hue. |
| Scrim | `rgba(29,29,31,.34)` | `rgba(0,0,0,.58)` | A scrim has to darken a surface that is already dark. |
| Shadow | low alpha | ~5× alpha | Without a light ground to fall on, a soft shadow registers as nothing. |
Contrast is asserted, not assumed: eight inks (ink, ink-strong, ink-secondary,
ink-tertiary, action, danger, success, warning) must all clear 4.5:1 against
four dark surfaces (canvas-soft, canvas, canvas-muted, sidebar-solid). The four
neutral surfaces must increase in lightness in floor → canvas → muted → strong
order. Adding a text or surface token means adding it to that 32-pair matrix;
checking only the reading surface is not enough.
**Surfaces that do not follow the theme, on purpose:**
- The payment QR keeps literal white in both themes. Scanners need the light
modules to actually be light.
- `@media print` keeps white paper.
- The admin is antd + Refine and stays light. Re-skinning `admin.css` alone would
give a dark shell around light antd components, which reads as broken; doing it
properly means switching antd to `theme.darkAlgorithm` as well, and that is a
separate change.
**The control** is a radio group inside the account menu (avatar → 外观), so it
keeps menu semantics: arrow keys reach it and the current choice is announced.
Three options, matching the three states — 浅色, 深色, 跟随系统. Picking one does
not close the menu, so the change is visible where it was made.
"跟随系统" **removes** `data-theme` rather than writing a third value; the media
query has nothing to match otherwise. The choice is stored under `jyotisha-theme`
and re-applied by a synchronous script at the top of `<head>` — it must not be
deferred, or every load flashes the other theme before hydration. Blocked storage
degrades to following the OS. The preference is per-device, not per-account, and
is read through an external store so a change in one tab reaches the others.
## 3. Typography
### Font stacks
- Display: `"Tiempos Headline", "Songti SC", "STSong", "Noto Serif CJK SC", Georgia, serif`. The licensed Copernicus/Tiempos files are unavailable; the Chinese Song serif stack is the declared production substitute.
- Body/UI: `StyreneB, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`.
- Code/data: `"JetBrains Mono", "SFMono-Regular", Consolas, monospace`.
- Body/UI: `StyreneB, var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/google` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter. Tiempos Headline remains unloaded; the Songti stack is still the declared production substitute.
- Code/data: `"JetBrains Mono", "SFMono-Regular", Consolas, monospace`, exposed as `--font-mono`.
- Root boundary pages (`error.tsx`, `not-found.tsx`, `forbidden.tsx`, `global-error.tsx`) sit in the shared root layout segment and must not import `globals.css` — importing it would drag the chat stylesheet onto every admin route. They therefore cannot read `--font-mono` or any token, and inline their own values: a system stack for body text and `ui-monospace, SFMono-Regular, Menlo, monospace` for code. Keep those literals in step with the stacks above by hand.
### Scale
@@ -68,8 +163,9 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3:
- Chat reading width: 760px for the welcome/composer and 900px for long answers.
- Admin content width: 1200px, centered.
- Desktop shell: 288px sidebar plus flexible reading panel.
- Breakpoints: mobile below 768px, tablet 7681023px, desktop 1024px and above.
- All full-height surfaces use `100dvh`. Touch targets are at least 44px.
- Layout breakpoints: mobile below 768px, tablet 7681023px, desktop 1024px and above. These three decide the shell — sidebar mode, grid columns, drawer versus rail.
- Component breakpoints are separate and narrower, each owned by one surface rather than the shell: 900px and 480px (redemption and profile forms), 760px and 720px (personal report and report centre), 640px (rectification candidates), 639px (membership header), 620px (onboarding cards), 430px (toasts and the conversational rectification panel). Do not repurpose one for another surface, and do not assume testing the three layout tiers covers them.
- All full-height surfaces use `100dvh`. Touch targets are at least 44px. Where adjacent controls sit closer than 44px, the hit expands to the largest size that does not overlap; those exceptions are locked by `frontend/tests/touch-target-contract.test.ts`.
## 5. Components
@@ -218,18 +314,36 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3:
### Admin panel and data table
- **Structure:** editorial header, cream form panels, and a light data table separated by warm hairlines.
- **States:** loading, empty, populated, generated codes, error, copy success.
- **Responsive:** form collapses to two columns then one; table scrolls horizontally.
The admin is **not** built from this design system. It runs on antd + Refine, and
`src/app/admin/admin.css` only re-skins antd to sit near the product palette
(warm floor, hairline borders, the same body font). Admin routes deliberately do
not load `globals.css` — that isolation keeps the 33 KB chat stylesheet off every
admin page — so nothing here inherits the tokens above.
- **Structure:** antd `Layout` with sider, header and content, wrapped in
`.admin-app-shell`; tables, cards, tags and statistics are antd components.
- **Own classes:** only `.admin-app-shell`, `.admin-loading` and `.admin-text-list`.
Anything else must be an `.ant-*` override inside `admin.css`.
- **Colors:** `admin.css` writes literal hex because it cannot see the tokens. Those
literals must stay in sync with the palette above by hand; this is the one
sanctioned exception to the no-raw-color rule.
- **Content width:** 1200px centred, set by antd layout, not by `.admin-scroll`.
- **Loading:** antd `<Spin>`, outside the waiting vocabulary in section 9.
## 6. Motion & Interaction
`--ease-out` is `cubic-bezier(.22, 1, .36, 1)` and is the only easing curve in the
system.
| Type | Duration | Easing | Usage |
|---|---:|---|---|
| Micro | 120ms | ease-out | Button and row feedback |
| Standard | 180ms | `cubic-bezier(.22, 1, .36, 1)` | Sheet and message entry |
| Micro | 120ms | ease-out / `--ease-out` | Button, row, popover and tooltip feedback |
| Standard | 180ms | `--ease-out` | Dialogs, onboarding cards, auth steps |
| Message | 160ms | `--ease-out` | A message row entering the transcript |
| Sheet | 280ms | `--ease-out` | Rectification sheet rising from the bottom |
| Spatial | Instant | None | Sidebar state changes and mobile drawer |
| Emphasis | 360ms | `cubic-bezier(.16, 1, .3, 1)` | Loading mark only |
Looping animation is covered separately in section 9.
Only `transform`, `opacity`, and color/filter transitions animate. Reduced-motion disables non-essential animation. Motion communicates state; decorative looping is limited to an active loading state.
@@ -250,3 +364,18 @@ case validation are complete, low and medium results never apply a minute, and h
require explicit confirmation of the matching representative time. Journey telemetry is limited to
metric name, phase, and optional confidence; it never records messages, dates, coordinates, case IDs,
or user IDs.
## 9. 等待与加载
等待按语义只分三类。同类必须共用同一套实现;不要为新入口再写一套旋转或呼吸。
| 类别 | 语义 | 组件 / 样式 | 时长 | `prefers-reduced-motion: reduce` |
| --- | --- | --- | ---: | --- |
| 整页 / 整块阻塞 | 载入账户、正在准备问题、生时评估浮层 | `AppLoadingIndicator`(轨道环 `app-loading-orbit` | 1.4s linear | 全局 `*` 规则把循环收成一帧;不要改它的 DOM |
| 行内 / 局部等待 | 出生地解析、咨询时间线 live 步、个人报告列表与详情 | `InlineSpinner``inline-spin` | 0.8s linear | `animation: none`,收成静止圆点,不要半圈圆弧 |
| 流式生成中 | 引导语打字、Agent 活动字 | `onboarding-caret` / `agent-activity-shimmer` | 700ms steps / 1.6s linear | 保持现有全局降级 |
今日星语首次拉取是行内等待,但不用 spinner、也不用透明度呼吸:卡片用静态占位文案(`aria-busy` 仍保留)。轨道环消失后不得再换一套动效继续等。
Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
+2 -1
View File
@@ -13,7 +13,8 @@
"db:migrate": "node scripts/db-migrate.mjs",
"db:migrate:check": "node scripts/db-migrate.mjs --check",
"lint": "eslint",
"data:china": "node scripts/pull-china-locations.mjs"
"data:china": "node scripts/pull-china-locations.mjs",
"data:world": "node scripts/pull-global-locations.mjs"
},
"dependencies": {
"@ant-design/cssinjs": "^1.24.0",
@@ -0,0 +1 @@
{"code":"AD","name":"Andorra","nativeName":"Andorra","regions":[{"code":"AD-07","name":"Andorra la Vella","nativeName":"Andorra la Vella","latitude":42.5063174,"longitude":1.5218355,"timezone":"Europe/Andorra","cities":[{"code":"1","name":"Andorra la Vella","latitude":42.50779,"longitude":1.52109,"timezone":"Europe/Andorra"}]},{"code":"AD-02","name":"Canillo","nativeName":"Canillo","latitude":42.5868092,"longitude":1.6574431,"timezone":"Europe/Andorra","cities":[{"code":"3","name":"Canillo","latitude":42.5676,"longitude":1.59756,"timezone":"Europe/Andorra"},{"code":"4","name":"El Tarter","latitude":42.57952,"longitude":1.65362,"timezone":"Europe/Andorra"}]},{"code":"AD-03","name":"Encamp","nativeName":"Encamp","latitude":42.5359764,"longitude":1.5836773,"timezone":"Europe/Andorra","cities":[{"code":"5","name":"Encamp","latitude":42.53474,"longitude":1.58014,"timezone":"Europe/Andorra"},{"code":"7","name":"Pas de la Casa","latitude":42.54277,"longitude":1.73361,"timezone":"Europe/Andorra"}]},{"code":"AD-08","name":"Escaldes-Engordany","nativeName":"Escaldes-Engordany","latitude":42.486196,"longitude":1.6037711,"timezone":"Europe/Andorra","cities":[{"code":"10","name":"les Escaldes","latitude":42.50729,"longitude":1.53414,"timezone":"Europe/Andorra"}]},{"code":"AD-04","name":"La Massana","nativeName":"La Massana","latitude":42.545625,"longitude":1.5147392,"timezone":"Europe/Andorra","cities":[{"code":"2","name":"Arinsal","latitude":42.57205,"longitude":1.48453,"timezone":"Europe/Andorra"},{"code":"9","name":"la Massana","latitude":42.54499,"longitude":1.51483,"timezone":"Europe/Andorra"}]},{"code":"AD-05","name":"Ordino","nativeName":"Ordino","latitude":42.5994433,"longitude":1.5402327,"timezone":"Europe/Andorra","cities":[{"code":"6","name":"Ordino","latitude":42.55623,"longitude":1.53319,"timezone":"Europe/Andorra"}]},{"code":"AD-06","name":"Sant Julià de Lòria","nativeName":"Sant Julià de Lòria","latitude":42.4668541,"longitude":1.4923277,"timezone":"Europe/Andorra","cities":[{"code":"8","name":"Sant Julià de Lòria","latitude":42.46372,"longitude":1.49129,"timezone":"Europe/Andorra"}]}]}
@@ -0,0 +1 @@
{"code":"AE","name":"United Arab Emirates","nativeName":"دولة الإمارات العربية المتحدة","regions":[{"code":"AE-AZ","name":"Abu Dhabi","nativeName":"أبو ظبي","latitude":24.453884,"longitude":54.3773438,"timezone":"Asia/Dubai","cities":[{"code":"12","name":"Abu Dhabi","latitude":24.41361,"longitude":54.43295,"timezone":"Asia/Dubai"},{"code":"16","name":"Al Ain City","latitude":24.19167,"longitude":55.76056,"timezone":"Asia/Dubai"},{"code":"19","name":"Al Dhafra","latitude":23.65745,"longitude":53.72225,"timezone":"Asia/Dubai"},{"code":"26","name":"Bani Yas City","latitude":24.30978,"longitude":54.62944,"timezone":"Asia/Dubai"},{"code":"11","name":"Ghayathi","latitude":23.9045342,"longitude":52.5871026,"timezone":"Asia/Dubai"},{"code":"34","name":"Khalifa City","latitude":24.42588,"longitude":54.605,"timezone":"Asia/Dubai"},{"code":"17","name":"Liwa Oasis","latitude":22.8697255,"longitude":53.2408608,"timezone":"Asia/Dubai"},{"code":"41","name":"Mussafah","latitude":24.35893,"longitude":54.48267,"timezone":"Asia/Dubai"},{"code":"42","name":"Muzayri","latitude":23.14355,"longitude":53.7881,"timezone":"Asia/Dubai"},{"code":"25","name":"Ruwais","latitude":24.11028,"longitude":52.73056,"timezone":"Asia/Dubai"},{"code":"49","name":"Zayed City","latitude":23.65416,"longitude":53.70522,"timezone":"Asia/Dubai"}]},{"code":"AE-AJ","name":"Ajman","nativeName":"عجمان","latitude":25.3936557,"longitude":55.4451432,"timezone":"Asia/Dubai","cities":[{"code":"14","name":"Ajman","latitude":25.40328,"longitude":55.52341,"timezone":"Asia/Dubai"},{"code":"37","name":"Manama","latitude":25.32568,"longitude":56.00259,"timezone":"Asia/Dubai"},{"code":"38","name":"Masfut","latitude":24.83982,"longitude":56.05158,"timezone":"Asia/Dubai"}]},{"code":"AE-DU","name":"Dubai","nativeName":"دبي","latitude":25.2647227,"longitude":55.2924146,"timezone":"Asia/Dubai","cities":[{"code":"32","name":"Dubai","latitude":25.0657,"longitude":55.17128,"timezone":"Asia/Dubai"}]},{"code":"AE-FU","name":"Fujairah","nativeName":"فجيرة","latitude":25.1244604,"longitude":56.3355085,"timezone":"Asia/Dubai","cities":[{"code":"28","name":"Dibba Al Fujairah","latitude":25.5858,"longitude":56.24792,"timezone":"Asia/Dubai"},{"code":"30","name":"Dibba Al-Fujairah","latitude":25.59246,"longitude":56.26176,"timezone":"Asia/Dubai"},{"code":"31","name":"Dibba Al-Hisn","latitude":25.61955,"longitude":56.27291,"timezone":"Asia/Dubai"},{"code":"20","name":"Fujairah","latitude":25.11641,"longitude":56.34141,"timezone":"Asia/Dubai"}]},{"code":"AE-RK","name":"Ras Al Khaimah","nativeName":"رأس الخيمة","latitude":25.7386349,"longitude":55.9249224,"timezone":"Asia/Dubai","cities":[{"code":"43","name":"Ras Al Khaimah","latitude":25.46116,"longitude":56.04058,"timezone":"Asia/Dubai"}]},{"code":"AE-SH","name":"Sharjah","nativeName":"الشارقة","latitude":25.3461498,"longitude":55.4210633,"timezone":"Asia/Dubai","cities":[{"code":"18","name":"Al Batayih","latitude":25.22317,"longitude":55.74272,"timezone":"Asia/Dubai"},{"code":"13","name":"Al Dhaid","latitude":25.28812,"longitude":55.88157,"timezone":"Asia/Dubai"},{"code":"22","name":"Al Hamriyah","latitude":25.46121,"longitude":55.54813,"timezone":"Asia/Dubai"},{"code":"23","name":"Al Madam","latitude":24.95536,"longitude":55.7682,"timezone":"Asia/Dubai"},{"code":"33","name":"Kalba","latitude":24.99816,"longitude":56.27207,"timezone":"Asia/Dubai"},{"code":"36","name":"Khor Fakkan","latitude":25.33966,"longitude":56.3028,"timezone":"Asia/Dubai"},{"code":"39","name":"Milehah","latitude":25.10097,"longitude":55.91282,"timezone":"Asia/Dubai"},{"code":"40","name":"Murbaḩ","latitude":25.27623,"longitude":56.36256,"timezone":"Asia/Dubai"},{"code":"46","name":"Sharjah","latitude":25.33737,"longitude":55.41206,"timezone":"Asia/Dubai"}]},{"code":"AE-UQ","name":"Umm Al Quwain","nativeName":"أم القوي","latitude":25.6112488,"longitude":55.6174631,"timezone":"Asia/Dubai","cities":[{"code":"47","name":"Umm AL Quwain","latitude":25.49326,"longitude":55.7352,"timezone":"Asia/Dubai"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"AG","name":"Antigua and Barbuda","nativeName":"Antigua and Barbuda","regions":[{"code":"AG-10","name":"Barbuda","nativeName":"Barbuda","latitude":17.6219203,"longitude":-61.7839588,"timezone":"America/Antigua","cities":[{"code":"144","name":"Codrington","latitude":17.63333,"longitude":-61.83333,"timezone":"America/Antigua"}]},{"code":"AG-11","name":"Redonda","nativeName":"Round","latitude":16.938416,"longitude":-62.3455148,"timezone":"America/Antigua","cities":[]},{"code":"AG-03","name":"Saint George","nativeName":"Saint George","latitude":17.1218022,"longitude":-61.78123,"timezone":"America/Antigua","cities":[{"code":"148","name":"Piggotts","latitude":17.11667,"longitude":-61.8,"timezone":"America/Antigua"}]},{"code":"AG-04","name":"Saint John","nativeName":"Saint John","latitude":17.1151502,"longitude":-61.8583488,"timezone":"America/Antigua","cities":[{"code":"149","name":"Potters Village","latitude":17.11337,"longitude":-61.81962,"timezone":"America/Antigua"},{"code":"150","name":"Saint Johns","latitude":17.12096,"longitude":-61.84329,"timezone":"America/Antigua"}]},{"code":"AG-05","name":"Saint Mary","nativeName":"Saint Mary","latitude":17.054815,"longitude":-61.8602368,"timezone":"America/Antigua","cities":[{"code":"143","name":"Bolands","latitude":17.06565,"longitude":-61.87466,"timezone":"America/Antigua"}]},{"code":"AG-06","name":"Saint Paul","nativeName":"Saint Paul","latitude":17.0325831,"longitude":-61.7706567,"timezone":"America/Antigua","cities":[{"code":"145","name":"Falmouth","latitude":17.02741,"longitude":-61.78136,"timezone":"America/Antigua"},{"code":"146","name":"Liberta","latitude":17.04141,"longitude":-61.79052,"timezone":"America/Antigua"}]},{"code":"AG-07","name":"Saint Peter","nativeName":"Saint Peter","latitude":17.1070239,"longitude":-61.7502143,"timezone":"America/Antigua","cities":[{"code":"142","name":"All Saints","latitude":17.06671,"longitude":-61.79303,"timezone":"America/Antigua"},{"code":"147","name":"Parham","latitude":17.09682,"longitude":-61.77046,"timezone":"America/Antigua"}]},{"code":"AG-08","name":"Saint Philip","nativeName":"Saint Philip","latitude":17.066357,"longitude":-61.7061957,"timezone":"America/Antigua","cities":[]}]}
@@ -0,0 +1 @@
{"code":"AI","name":"Anguilla","nativeName":"Anguilla","regions":[{"code":"AI-01","name":"Blowing Point","nativeName":"Blowing Point","latitude":18.1765553,"longitude":-63.1021296,"timezone":"America/Anguilla","cities":[]},{"code":"AI-02","name":"East End","nativeName":"East End","latitude":18.2342629,"longitude":-62.997374,"timezone":"America/Anguilla","cities":[]},{"code":"AI-03","name":"George Hill","nativeName":"George Hill","latitude":18.2036599,"longitude":-63.0648112,"timezone":"America/Anguilla","cities":[]},{"code":"AI-04","name":"Island Harbour","nativeName":"Island Harbour","latitude":18.2556888,"longitude":-63.0012814,"timezone":"America/Anguilla","cities":[]},{"code":"AI-05","name":"North Hill","nativeName":"North Hill","latitude":18.2050863,"longitude":-63.0858457,"timezone":"America/Anguilla","cities":[]},{"code":"AI-06","name":"North Side","nativeName":"North Side","latitude":18.2300544,"longitude":-63.051889,"timezone":"America/Anguilla","cities":[]},{"code":"AI-07","name":"Sandy Ground","nativeName":"Sandy Ground","latitude":18.2018551,"longitude":-63.0973471,"timezone":"America/Anguilla","cities":[]},{"code":"AI-08","name":"Sandy Hill","nativeName":"Sandy Hill","latitude":18.218921,"longitude":-63.0129916,"timezone":"America/Anguilla","cities":[]},{"code":"AI-09","name":"South Hill","nativeName":"South Hill","latitude":18.1892475,"longitude":-63.0976367,"timezone":"America/Anguilla","cities":[]},{"code":"AI-10","name":"Stoney Ground","nativeName":"Stoney Ground","latitude":18.2179997,"longitude":-63.0400727,"timezone":"America/Anguilla","cities":[]},{"code":"AI-11","name":"The Farrington","nativeName":"The Farrington","latitude":18.2134701,"longitude":-63.0272162,"timezone":"America/Anguilla","cities":[]},{"code":"AI-12","name":"The Quarter","nativeName":"The Quarter","latitude":18.2145334,"longitude":-63.0564489,"timezone":"America/Anguilla","cities":[]},{"code":"AI-13","name":"The Valley","nativeName":"The Valley","latitude":18.2145861,"longitude":-63.0517759,"timezone":"America/Anguilla","cities":[]},{"code":"AI-14","name":"West End","nativeName":"West End","latitude":18.1721666,"longitude":-63.1567037,"timezone":"America/Anguilla","cities":[]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"AS","name":"American Samoa","nativeName":"American Samoa","regions":[{"code":"AS-02","name":"Eastern","nativeName":"Eastern","latitude":-14.2782402,"longitude":-170.6243227,"timezone":"Pacific/Pago_Pago","cities":[{"code":"154920","name":"Ituʻau","latitude":-14.3061643,"longitude":-170.8069144,"timezone":"Pacific/Pago_Pago"},{"code":"154921","name":"Maʻoputasi","latitude":-14.2521685,"longitude":-170.7418767,"timezone":"Pacific/Pago_Pago"},{"code":"154924","name":"Saʻole","latitude":-14.299957,"longitude":-170.6176337,"timezone":"Pacific/Pago_Pago"},{"code":"154923","name":"Sua","latitude":-14.2771088,"longitude":-170.7144263,"timezone":"Pacific/Pago_Pago"},{"code":"154922","name":"Vaifanua","latitude":-14.2557804,"longitude":-170.7630181,"timezone":"Pacific/Pago_Pago"}]},{"code":"AS-03","name":"Manuʻa","nativeName":"Injury","latitude":-14.214605,"longitude":-169.533465,"timezone":"Pacific/Pago_Pago","cities":[{"code":"154928","name":"Faleasao","latitude":-14.2038549,"longitude":-169.5490737,"timezone":"Pacific/Pago_Pago"},{"code":"154929","name":"Fitiuta","latitude":-14.2038634,"longitude":-169.5169044,"timezone":"Pacific/Pago_Pago"},{"code":"154925","name":"Ofu","latitude":-14.1713595,"longitude":-169.7193912,"timezone":"Pacific/Pago_Pago"},{"code":"154926","name":"Olosega","latitude":-14.1742227,"longitude":-169.6398124,"timezone":"Pacific/Pago_Pago"},{"code":"154927","name":"Tau","latitude":-14.2421501,"longitude":-169.5510165,"timezone":"Pacific/Pago_Pago"}]},{"code":"AS-05","name":"Rose","nativeName":"Rose","latitude":-14.5424865,"longitude":-168.1637445,"timezone":"Pacific/Pago_Pago","cities":[]},{"code":"AS-04","name":"Swains","nativeName":"Swains","latitude":-11.052904,"longitude":-171.0767349,"timezone":"Pacific/Pago_Pago","cities":[]},{"code":"AS-01","name":"Western","nativeName":"Western","latitude":-14.3323075,"longitude":-170.7961334,"timezone":"Pacific/Pago_Pago","cities":[{"code":"154916","name":"Lealataua","latitude":-14.3226152,"longitude":-170.9084993,"timezone":"Pacific/Pago_Pago"},{"code":"154917","name":"Leasina","latitude":-14.2958164,"longitude":-170.8045932,"timezone":"Pacific/Pago_Pago"},{"code":"154918","name":"Tualatai","latitude":-14.3792354,"longitude":-170.8103312,"timezone":"Pacific/Pago_Pago"},{"code":"154919","name":"Tualauta","latitude":-14.3587689,"longitude":-170.7558332,"timezone":"Pacific/Pago_Pago"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"AW","name":"Aruba","nativeName":"Aruba","regions":[{"code":"AW-01","name":"Noord","nativeName":"Noord","latitude":12.5636805,"longitude":-70.0329869,"timezone":"America/Aruba","cities":[]},{"code":"AW-09","name":"Oranjestad","nativeName":"Oranjestad","latitude":12.5201024,"longitude":-70.0371329,"timezone":"America/Aruba","cities":[{"code":"160400","name":"Alto Vista","latitude":12.5705,"longitude":-70.01912,"timezone":"America/Aruba"},{"code":"160398","name":"Moco","latitude":12.55134,"longitude":-70.00843,"timezone":"America/Aruba"},{"code":"160395","name":"Noord","latitude":12.56596,"longitude":-70.03198,"timezone":"America/Aruba"},{"code":"160396","name":"Oranjestad","latitude":12.52398,"longitude":-70.02703,"timezone":"America/Aruba"},{"code":"160399","name":"Palm Beach","latitude":12.56847,"longitude":-70.03887,"timezone":"America/Aruba"},{"code":"160397","name":"Paradera","latitude":12.5351,"longitude":-70.00688,"timezone":"America/Aruba"},{"code":"160394","name":"Pos Chiquito","latitude":12.46497,"longitude":-69.96233,"timezone":"America/Aruba"},{"code":"160401","name":"San Nicolas","latitude":12.43624,"longitude":-69.90713,"timezone":"America/Aruba"},{"code":"160404","name":"Santa Cruz","latitude":12.50953,"longitude":-69.98094,"timezone":"America/Aruba"},{"code":"160405","name":"Savaneta","latitude":12.45026,"longitude":-69.93811,"timezone":"America/Aruba"},{"code":"160402","name":"Tanki Leendert","latitude":12.53914,"longitude":-70.02004,"timezone":"America/Aruba"},{"code":"160403","name":"Washington","latitude":12.55837,"longitude":-70.03816,"timezone":"America/Aruba"}]},{"code":"AW-03","name":"Oranjestad East","nativeName":"Oranjestad East","latitude":12.5022696,"longitude":-70.0149586,"timezone":"America/Aruba","cities":[]},{"code":"AW-02","name":"Oranjestad West","nativeName":"Oranjestad West","latitude":12.5361486,"longitude":-70.0430597,"timezone":"America/Aruba","cities":[]},{"code":"AW-04","name":"Paradera","nativeName":"Paradera","latitude":12.5362871,"longitude":-70.0148511,"timezone":"America/Aruba","cities":[]},{"code":"AW-05","name":"San Nicolaas Noord","nativeName":"San Nicolaas Noord","latitude":12.4642717,"longitude":-69.9434939,"timezone":"America/Aruba","cities":[]},{"code":"AW-06","name":"San Nicolaas Zuid","nativeName":"San Nicolaas Zuid","latitude":12.4278558,"longitude":-69.9381079,"timezone":"America/Aruba","cities":[]},{"code":"AW-07","name":"Santa Cruz","nativeName":"Santa Cruz","latitude":12.5112836,"longitude":-69.9876966,"timezone":"America/Aruba","cities":[]},{"code":"AW-08","name":"Savaneta","nativeName":"Savaneta","latitude":12.4526385,"longitude":-69.9496521,"timezone":"America/Aruba","cities":[]}]}
@@ -0,0 +1 @@
{"code":"AX","name":"Aland Islands","nativeName":"Åland","regions":[{"code":"AX-12","name":"Brändö","nativeName":"Brändö","latitude":60.4135873,"longitude":21.0440498,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-08","name":"Eckerö","nativeName":"Eckerö","latitude":60.2231814,"longitude":19.5389379,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-03","name":"Finström","nativeName":"Finström","latitude":60.2310519,"longitude":19.9867259,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-09","name":"Föglö","nativeName":"Föglö","latitude":60.0107325,"longitude":20.4246997,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-10","name":"Geta","nativeName":"Geta","latitude":60.3740157,"longitude":19.8498128,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-06","name":"Hammarland","nativeName":"Hammarland","latitude":60.2196758,"longitude":19.7378481,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-02","name":"Jomala","nativeName":"Jomala","latitude":60.1522947,"longitude":19.9489144,"timezone":"Europe/Mariehamn","cities":[{"code":"160431","name":"Bomarsund","latitude":60.21234,"longitude":20.24023,"timezone":"Europe/Mariehamn"},{"code":"160427","name":"Finström","latitude":60.26667,"longitude":19.93333,"timezone":"Europe/Mariehamn"},{"code":"160432","name":"Godby","latitude":60.22995,"longitude":19.9881,"timezone":"Europe/Mariehamn"},{"code":"160428","name":"Hammarland","latitude":60.21667,"longitude":19.75,"timezone":"Europe/Mariehamn"},{"code":"160429","name":"Jomala","latitude":60.14902,"longitude":19.951,"timezone":"Europe/Mariehamn"},{"code":"160433","name":"Lemland","latitude":60.07178,"longitude":20.08472,"timezone":"Europe/Mariehamn"},{"code":"160434","name":"Saltvik","latitude":60.28333,"longitude":20.05,"timezone":"Europe/Mariehamn"},{"code":"160435","name":"Sund","latitude":60.25,"longitude":20.11667,"timezone":"Europe/Mariehamn"},{"code":"160430","name":"Tjudö","latitude":60.33333,"longitude":19.95,"timezone":"Europe/Mariehamn"},{"code":"160426","name":"Yomala","latitude":60.16689,"longitude":19.93341,"timezone":"Europe/Mariehamn"},{"code":"160436","name":"Ytterby","latitude":60.16667,"longitude":20.01667,"timezone":"Europe/Mariehamn"}]},{"code":"AX-15","name":"Kökar","nativeName":"Kökar","latitude":59.9211709,"longitude":20.9103514,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-14","name":"Kumlinge","nativeName":"Kumlinge","latitude":60.2587693,"longitude":20.7782292,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-04","name":"Lemland","nativeName":"Lemland","latitude":60.0690169,"longitude":20.0860081,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-13","name":"Lumparland","nativeName":"Lumparland","latitude":60.1168385,"longitude":20.2610583,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-01","name":"Mariehamn","nativeName":"Mariehamn","latitude":60.102423,"longitude":19.94126,"timezone":"Europe/Mariehamn","cities":[{"code":"160425","name":"Mariehamn","latitude":60.09726,"longitude":19.93481,"timezone":"Europe/Mariehamn"}]},{"code":"AX-05","name":"Saltvik","nativeName":"Saltvik","latitude":60.2755756,"longitude":20.0612065,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-16","name":"Sottunga","nativeName":"Sot","latitude":60.1307977,"longitude":20.6459821,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-07","name":"Sund","nativeName":"Hälsosam","latitude":60.2530332,"longitude":20.1198637,"timezone":"Europe/Mariehamn","cities":[]},{"code":"AX-11","name":"Vårdö","nativeName":"Vårdö","latitude":60.2421692,"longitude":20.3744261,"timezone":"Europe/Mariehamn","cities":[]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"BB","name":"Barbados","nativeName":"Barbados","regions":[{"code":"BB-01","name":"Christ Church","nativeName":"Christ Church","latitude":13.082618,"longitude":-59.5374451,"timezone":"America/Barbados","cities":[{"code":"8452","name":"Oistins","latitude":13.07067,"longitude":-59.54637,"timezone":"America/Barbados"}]},{"code":"BB-02","name":"Saint Andrew","nativeName":"Saint Andrew","latitude":13.2432904,"longitude":-59.57317,"timezone":"America/Barbados","cities":[{"code":"8450","name":"Greenland","latitude":13.25808,"longitude":-59.57763,"timezone":"America/Barbados"}]},{"code":"BB-03","name":"Saint George","nativeName":"Saint George","latitude":13.1384022,"longitude":-59.5443721,"timezone":"America/Barbados","cities":[]},{"code":"BB-04","name":"Saint James","nativeName":"Saint James","latitude":13.1863793,"longitude":-59.625192,"timezone":"America/Barbados","cities":[{"code":"8451","name":"Holetown","latitude":13.18672,"longitude":-59.63808,"timezone":"America/Barbados"}]},{"code":"BB-05","name":"Saint John","nativeName":"Saint John","latitude":13.1899463,"longitude":-59.5592151,"timezone":"America/Barbados","cities":[]},{"code":"BB-06","name":"Saint Joseph","nativeName":"Saint Joseph","latitude":13.1899463,"longitude":-59.5592151,"timezone":"America/Barbados","cities":[{"code":"8447","name":"Bathsheba","latitude":13.21133,"longitude":-59.52596,"timezone":"America/Barbados"}]},{"code":"BB-07","name":"Saint Lucy","nativeName":"Saint Lucy","latitude":13.3024068,"longitude":-59.6129445,"timezone":"America/Barbados","cities":[]},{"code":"BB-08","name":"Saint Michael","nativeName":"Saint Michael","latitude":13.1182985,"longitude":-59.6015783,"timezone":"America/Barbados","cities":[{"code":"8448","name":"Bridgetown","latitude":13.10732,"longitude":-59.62021,"timezone":"America/Barbados"}]},{"code":"BB-09","name":"Saint Peter","nativeName":"Saint Peter","latitude":13.2619779,"longitude":-59.6207444,"timezone":"America/Barbados","cities":[{"code":"8453","name":"Speightstown","latitude":13.25072,"longitude":-59.64396,"timezone":"America/Barbados"}]},{"code":"BB-10","name":"Saint Philip","nativeName":"Saint Philip","latitude":13.129539,"longitude":-59.4674102,"timezone":"America/Barbados","cities":[{"code":"8449","name":"Crane","latitude":13.10487,"longitude":-59.44861,"timezone":"America/Barbados"}]},{"code":"BB-11","name":"Saint Thomas","nativeName":"Saint Thomas","latitude":13.177202,"longitude":-59.5853833,"timezone":"America/Barbados","cities":[]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"BH","name":"Bahrain","nativeName":"‏البحرين","regions":[{"code":"BH-13","name":"Capital","nativeName":"عاصمة","latitude":26.4243233,"longitude":50.4747251,"timezone":"Asia/Bahrain","cities":[{"code":"9757","name":"Jidd Ḩafş","latitude":26.21861,"longitude":50.54778,"timezone":"Asia/Bahrain"},{"code":"9760","name":"Manama","latitude":26.22787,"longitude":50.58565,"timezone":"Asia/Bahrain"},{"code":"9761","name":"Sitrah","latitude":26.15472,"longitude":50.62056,"timezone":"Asia/Bahrain"}]},{"code":"BH-15","name":"Muharraq","nativeName":"محارق","latitude":26.2465946,"longitude":50.6164127,"timezone":"Asia/Bahrain","cities":[{"code":"9754","name":"Al Ḩadd","latitude":26.24556,"longitude":50.65417,"timezone":"Asia/Bahrain"},{"code":"9753","name":"Al Muharraq","latitude":26.25722,"longitude":50.61194,"timezone":"Asia/Bahrain"}]},{"code":"BH-17","name":"Northern","nativeName":"شمالي","latitude":26.2778477,"longitude":50.4181123,"timezone":"Asia/Bahrain","cities":[{"code":"9758","name":"Madīnat Ḩamad","latitude":26.11528,"longitude":50.50694,"timezone":"Asia/Bahrain"}]},{"code":"BH-14","name":"Southern","nativeName":"الجنوب","latitude":25.8400069,"longitude":50.5902693,"timezone":"Asia/Bahrain","cities":[{"code":"9755","name":"Ar Rifā‘","latitude":26.13,"longitude":50.555,"timezone":"Asia/Bahrain"},{"code":"9756","name":"Dār Kulayb","latitude":26.06861,"longitude":50.50389,"timezone":"Asia/Bahrain"},{"code":"9759","name":"Madīnat ‘Īsá","latitude":26.17361,"longitude":50.54778,"timezone":"Asia/Bahrain"}]}]}
@@ -0,0 +1 @@
{"code":"BI","name":"Burundi","nativeName":"Burundi","regions":[{"code":"BI-BB","name":"Bubanza","nativeName":"Bubanza","latitude":-3.1112787,"longitude":29.3828358,"timezone":"Africa/Bujumbura","cities":[{"code":"9762","name":"Bubanza","latitude":-3.0804,"longitude":29.391,"timezone":"Africa/Bujumbura"}]},{"code":"BI-BM","name":"Bujumbura Mairie","nativeName":"Bujumbura Mairie","latitude":-3.3493959,"longitude":29.3631843,"timezone":"Africa/Bujumbura","cities":[{"code":"9763","name":"Bujumbura","latitude":-3.38193,"longitude":29.36142,"timezone":"Africa/Bujumbura"}]},{"code":"BI-BL","name":"Bujumbura Rural","nativeName":"Rural de Bujumbura","latitude":-3.4759817,"longitude":29.4535702,"timezone":"Africa/Bujumbura","cities":[]},{"code":"BI-BR","name":"Bururi","nativeName":"Bururi","latitude":-3.8581592,"longitude":29.584379,"timezone":"Africa/Bujumbura","cities":[{"code":"9764","name":"Bururi","latitude":-3.94877,"longitude":29.62438,"timezone":"Africa/Bujumbura"}]},{"code":"BI-CA","name":"Cankuzo","nativeName":"Faire du chauffeur","latitude":-3.1450876,"longitude":30.58987,"timezone":"Africa/Bujumbura","cities":[{"code":"9765","name":"Cankuzo","latitude":-3.2186,"longitude":30.5528,"timezone":"Africa/Bujumbura"}]},{"code":"BI-CI","name":"Cibitoke","nativeName":"Cibitoke","latitude":-2.8477341,"longitude":29.261662,"timezone":"Africa/Bujumbura","cities":[{"code":"9766","name":"Cibitoke","latitude":-2.8869,"longitude":29.1248,"timezone":"Africa/Bujumbura"}]},{"code":"BI-GI","name":"Gitega","nativeName":"Gitega","latitude":-3.5382336,"longitude":29.910701,"timezone":"Africa/Bujumbura","cities":[{"code":"9767","name":"Gitega","latitude":-3.42708,"longitude":29.92463,"timezone":"Africa/Bujumbura"}]},{"code":"BI-KR","name":"Karuzi","nativeName":"Karuzi","latitude":-3.1255377,"longitude":30.0978347,"timezone":"Africa/Bujumbura","cities":[{"code":"9768","name":"Karuzi","latitude":-3.10139,"longitude":30.16278,"timezone":"Africa/Bujumbura"}]},{"code":"BI-KY","name":"Kayanza","nativeName":"Kayanza","latitude":-3.0214816,"longitude":29.6458034,"timezone":"Africa/Bujumbura","cities":[{"code":"9769","name":"Kayanza","latitude":-2.9221,"longitude":29.6293,"timezone":"Africa/Bujumbura"}]},{"code":"BI-KI","name":"Kirundo","nativeName":"Kirundo","latitude":-2.5853872,"longitude":30.0933449,"timezone":"Africa/Bujumbura","cities":[{"code":"9770","name":"Kirundo","latitude":-2.5845,"longitude":30.0959,"timezone":"Africa/Bujumbura"}]},{"code":"BI-MA","name":"Makamba","nativeName":"Makamba","latitude":-4.1978315,"longitude":29.8073996,"timezone":"Africa/Bujumbura","cities":[{"code":"9771","name":"Makamba","latitude":-4.1348,"longitude":29.804,"timezone":"Africa/Bujumbura"}]},{"code":"BI-MU","name":"Muramvya","nativeName":"Muramvya","latitude":-3.2751181,"longitude":29.6307855,"timezone":"Africa/Bujumbura","cities":[{"code":"9772","name":"Muramvya","latitude":-3.2682,"longitude":29.6079,"timezone":"Africa/Bujumbura"}]},{"code":"BI-MY","name":"Muyinga","nativeName":"Muyinga","latitude":-2.7335985,"longitude":30.3171441,"timezone":"Africa/Bujumbura","cities":[{"code":"9773","name":"Muyinga","latitude":-2.8451,"longitude":30.3414,"timezone":"Africa/Bujumbura"}]},{"code":"BI-MW","name":"Mwaro","nativeName":"Mwaro","latitude":-3.4650043,"longitude":29.6708508,"timezone":"Africa/Bujumbura","cities":[{"code":"9774","name":"Mwaro","latitude":-3.51128,"longitude":29.70334,"timezone":"Africa/Bujumbura"}]},{"code":"BI-NG","name":"Ngozi","nativeName":"Ngozi","latitude":-2.8776895,"longitude":29.8994986,"timezone":"Africa/Bujumbura","cities":[{"code":"9775","name":"Ngozi","latitude":-2.9075,"longitude":29.8306,"timezone":"Africa/Bujumbura"}]},{"code":"BI-RM","name":"Rumonge","nativeName":"Rumange","latitude":-3.9754049,"longitude":29.4388014,"timezone":"Africa/Bujumbura","cities":[{"code":"9776","name":"Rumonge","latitude":-3.9736,"longitude":29.4386,"timezone":"Africa/Bujumbura"}]},{"code":"BI-RT","name":"Rutana","nativeName":"Rutana","latitude":-3.8618714,"longitude":30.0676979,"timezone":"Africa/Bujumbura","cities":[{"code":"9777","name":"Rutana","latitude":-3.9279,"longitude":29.992,"timezone":"Africa/Bujumbura"}]},{"code":"BI-RY","name":"Ruyigi","nativeName":"Ruyigi","latitude":-3.4733471,"longitude":30.2501278,"timezone":"Africa/Bujumbura","cities":[{"code":"9778","name":"Ruyigi","latitude":-3.47639,"longitude":30.24861,"timezone":"Africa/Bujumbura"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"BL","name":"Saint-Barthelemy","nativeName":"Saint-Barthélemy","regions":[{"code":"FR-BL","name":"Saint-Barthélemy","nativeName":"Saint-Barthélemy","latitude":17.9,"longitude":-62.83333333,"timezone":"America/St_Barthelemy","cities":[{"code":"161653","name":"Gustavia","latitude":17.89790833,"longitude":-62.85055556,"timezone":"America/St_Barthelemy"}]}]}
@@ -0,0 +1 @@
{"code":"BM","name":"Bermuda","nativeName":"Bermuda","regions":[{"code":"BM-DEV","name":"Devonshire","nativeName":"Devonshire","latitude":32.3038062,"longitude":-64.7606954,"timezone":"Atlantic/Bermuda","cities":[{"code":"153518","name":"Devonshire","latitude":32.293,"longitude":-64.8,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-HA","name":"Hamilton","nativeName":"Hamilton","latitude":32.3379906,"longitude":-64.7357556,"timezone":"Atlantic/Bermuda","cities":[{"code":"153515","name":"Hamilton","latitude":32.294,"longitude":-64.783,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-PAG","name":"Paget","nativeName":"Paget","latitude":32.281074,"longitude":-64.7784787,"timezone":"Atlantic/Bermuda","cities":[{"code":"153516","name":"Paget","latitude":32.278,"longitude":-64.769,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-PEM","name":"Pembroke","nativeName":"Pembroke","latitude":32.3007672,"longitude":-64.796263,"timezone":"Atlantic/Bermuda","cities":[]},{"code":"BM-SGE","name":"Saint George's","nativeName":"Saint George's","latitude":32.3722689,"longitude":-64.698502,"timezone":"Atlantic/Bermuda","cities":[{"code":"153514","name":"St. George's","latitude":32.381,"longitude":-64.674,"timezone":"Atlantic/Bermuda"},{"code":"153517","name":"Tuckers Town","latitude":32.3328,"longitude":-64.6881,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-SAN","name":"Sandys","nativeName":"Sandys","latitude":32.2999528,"longitude":-64.8674103,"timezone":"Atlantic/Bermuda","cities":[{"code":"153513","name":"Somerset","latitude":32.282,"longitude":-64.844,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-SMI","name":"Smith's","nativeName":"Smith's","latitude":32.3133966,"longitude":-64.7310588,"timezone":"Atlantic/Bermuda","cities":[{"code":"153512","name":"Flatts","latitude":32.328,"longitude":-64.736,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-SOU","name":"Southampton","nativeName":"Southampton","latitude":32.2517959,"longitude":-64.8496989,"timezone":"Atlantic/Bermuda","cities":[{"code":"153511","name":"Southampton","latitude":32.249,"longitude":-64.861,"timezone":"Atlantic/Bermuda"}]},{"code":"BM-WAR","name":"Warwick","nativeName":"Warwick","latitude":32.2661534,"longitude":-64.8081198,"timezone":"Atlantic/Bermuda","cities":[{"code":"153510","name":"Warwick","latitude":32.267,"longitude":-64.806,"timezone":"Atlantic/Bermuda"}]}]}
@@ -0,0 +1 @@
{"code":"BN","name":"Brunei","nativeName":"Negara Brunei Darussalam","regions":[{"code":"BN-BE","name":"Belait","nativeName":"Belait","latitude":4.4349938,"longitude":114.4946524,"timezone":"Asia/Brunei","cities":[{"code":"9820","name":"Kuala Belait","latitude":4.58361,"longitude":114.2312,"timezone":"Asia/Brunei"},{"code":"9823","name":"Seria","latitude":4.60637,"longitude":114.32476,"timezone":"Asia/Brunei"}]},{"code":"BN-BM","name":"Brunei-Muara","nativeName":"Brunei-Muara","latitude":4.9752098,"longitude":114.9367663,"timezone":"Asia/Brunei","cities":[{"code":"9816","name":"Bandar Seri Begawan","latitude":4.89035,"longitude":114.94006,"timezone":"Asia/Brunei"},{"code":"9818","name":"Berakas A","latitude":4.97032,"longitude":114.92989,"timezone":"Asia/Brunei"},{"code":"9819","name":"Kapok","latitude":5.02447,"longitude":115.04664,"timezone":"Asia/Brunei"},{"code":"9821","name":"Mentiri","latitude":4.97058,"longitude":115.02078,"timezone":"Asia/Brunei"},{"code":"9822","name":"Serasa","latitude":5.01718,"longitude":115.05841,"timezone":"Asia/Brunei"}]},{"code":"BN-TE","name":"Temburong","nativeName":"Temburong","latitude":4.629184,"longitude":115.1634408,"timezone":"Asia/Brunei","cities":[{"code":"9817","name":"Bangar","latitude":4.70861,"longitude":115.07167,"timezone":"Asia/Brunei"}]},{"code":"BN-TU","name":"Tutong","nativeName":"Tutong","latitude":4.682681,"longitude":114.6562706,"timezone":"Asia/Brunei","cities":[{"code":"9824","name":"Tutong","latitude":4.80278,"longitude":114.64917,"timezone":"Asia/Brunei"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"BQ","name":"Bonaire, Sint Eustatius and Saba","nativeName":"Caribisch Nederland","regions":[{"code":"BQ-BQ1","name":"Bonaire","nativeName":"Bonaire","latitude":12.2018902,"longitude":-68.2623822,"timezone":"America/Curacao","cities":[{"code":"150058","name":"Boven Bolivia","latitude":12.1819,"longitude":-68.2181,"timezone":"America/Kralendijk"},{"code":"150059","name":"Dorp Tera Kora","latitude":12.13333,"longitude":-68.26667,"timezone":"America/Kralendijk"},{"code":"150060","name":"Oranjestad","latitude":17.48333333,"longitude":-62.98333333,"timezone":"America/Kralendijk"},{"code":"150061","name":"Rincon","latitude":12.23833333,"longitude":-68.3325,"timezone":"America/Kralendijk"},{"code":"150062","name":"St. Johns","latitude":17.6210075,"longitude":-63.2431683,"timezone":"America/Kralendijk"},{"code":"150063","name":"The Bottom","latitude":17.62611111,"longitude":-63.24916667,"timezone":"America/Kralendijk"},{"code":"150064","name":"Windwardside","latitude":17.62864,"longitude":-63.23136,"timezone":"America/Kralendijk"},{"code":"150065","name":"Zion's Hill","latitude":17.643,"longitude":-63.22649,"timezone":"America/Kralendijk"}]},{"code":"BQ-BQ2","name":"Saba","nativeName":"Saba","latitude":17.6354642,"longitude":-63.2326763,"timezone":"America/Curacao","cities":[]},{"code":"BQ-BQ3","name":"Sint Eustatius","nativeName":"Sint Eustatius","latitude":17.4890306,"longitude":-62.973555,"timezone":"America/Curacao","cities":[]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"BZ","name":"Belize","nativeName":"Belize","regions":[{"code":"BZ-BZ","name":"Belize","nativeName":"Belize","latitude":17.5002768,"longitude":-88.1988737,"timezone":"America/Belize","cities":[{"code":"16133","name":"Belize City","latitude":17.49952,"longitude":-88.19756,"timezone":"America/Belize"},{"code":"16143","name":"San Pedro","latitude":17.91598,"longitude":-87.9659,"timezone":"America/Belize"}]},{"code":"BZ-CY","name":"Cayo","nativeName":"Cayo","latitude":17.1582776,"longitude":-89.0681672,"timezone":"America/Belize","cities":[{"code":"16134","name":"Belmopan","latitude":17.25,"longitude":-88.76667,"timezone":"America/Belize"},{"code":"16135","name":"Benque Viejo el Carmen","latitude":17.075,"longitude":-89.13917,"timezone":"America/Belize"},{"code":"16142","name":"San Ignacio","latitude":17.1588,"longitude":-89.0696,"timezone":"America/Belize"},{"code":"16145","name":"Valley of Peace","latitude":17.33472,"longitude":-88.83472,"timezone":"America/Belize"}]},{"code":"BZ-CZL","name":"Corozal","nativeName":"Corozal","latitude":18.2276324,"longitude":-88.2921257,"timezone":"America/Belize","cities":[{"code":"16136","name":"Corozal","latitude":18.39375,"longitude":-88.38849,"timezone":"America/Belize"}]},{"code":"BZ-OW","name":"Orange Walk","nativeName":"Orange Walk","latitude":17.7831833,"longitude":-88.8620283,"timezone":"America/Belize","cities":[{"code":"16138","name":"Hopelchén","latitude":17.8,"longitude":-89.1,"timezone":"America/Belize"},{"code":"16139","name":"Orange Walk","latitude":18.08124,"longitude":-88.56328,"timezone":"America/Belize"},{"code":"16144","name":"Shipyard","latitude":17.89382,"longitude":-88.65452,"timezone":"America/Belize"}]},{"code":"BZ-SC","name":"Stann Creek","nativeName":"Stann Creek","latitude":16.9666599,"longitude":-88.2247368,"timezone":"America/Belize","cities":[{"code":"16137","name":"Dangriga","latitude":16.9697,"longitude":-88.23313,"timezone":"America/Belize"},{"code":"16140","name":"Placencia","latitude":16.51419,"longitude":-88.36647,"timezone":"America/Belize"}]},{"code":"BZ-TOL","name":"Toledo","nativeName":"Toledo","latitude":16.1345572,"longitude":-88.8219041,"timezone":"America/Belize","cities":[{"code":"16141","name":"Punta Gorda","latitude":16.09835,"longitude":-88.8097,"timezone":"America/Belize"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"CG","name":"Congo","nativeName":"République du Congo","regions":[{"code":"CG-11","name":"Bouenza","nativeName":"Bouenza","latitude":-4.2868653,"longitude":13.3972129,"timezone":"Africa/Brazzaville","cities":[{"code":"17345","name":"Kayes","latitude":-4.20493,"longitude":13.28608,"timezone":"Africa/Brazzaville"},{"code":"17348","name":"Madingou","latitude":-4.15361,"longitude":13.55,"timezone":"Africa/Brazzaville"}]},{"code":"CG-BZV","name":"Brazzaville","nativeName":"Brazzaville","latitude":-4.2694407,"longitude":15.2712256,"timezone":"Africa/Brazzaville","cities":[{"code":"17339","name":"Brazzaville","latitude":-4.26613,"longitude":15.28318,"timezone":"Africa/Brazzaville"}]},{"code":"CG-8","name":"Cuvette","nativeName":"Cuvette","latitude":-0.4936855,"longitude":16.1602013,"timezone":"Africa/Brazzaville","cities":[{"code":"17349","name":"Makoua","latitude":0.00694,"longitude":15.63333,"timezone":"Africa/Brazzaville"},{"code":"17352","name":"Owando","latitude":-0.48193,"longitude":15.89988,"timezone":"Africa/Brazzaville"}]},{"code":"CG-15","name":"Cuvette-Ouest","nativeName":"Cuvette-Ouest","latitude":0.280252,"longitude":14.5292599,"timezone":"Africa/Brazzaville","cities":[{"code":"17342","name":"Ewo","latitude":-0.8725,"longitude":14.82056,"timezone":"Africa/Brazzaville"}]},{"code":"CG-5","name":"Kouilou","nativeName":"Kouilou","latitude":-4.1960964,"longitude":11.9104388,"timezone":"Africa/Brazzaville","cities":[]},{"code":"CG-2","name":"Lékoumou","nativeName":"Chambre à coucher","latitude":-3.2315605,"longitude":13.5211721,"timezone":"Africa/Brazzaville","cities":[{"code":"17354","name":"Sibiti","latitude":-3.68192,"longitude":13.34985,"timezone":"Africa/Brazzaville"}]},{"code":"CG-7","name":"Likouala","nativeName":"Likouala","latitude":1.6788828,"longitude":17.4747723,"timezone":"Africa/Brazzaville","cities":[{"code":"17344","name":"Impfondo","latitude":1.61804,"longitude":18.05981,"timezone":"Africa/Brazzaville"}]},{"code":"CG-9","name":"Niari","nativeName":"Quasi-partage","latitude":-2.9077055,"longitude":12.3298313,"timezone":"Africa/Brazzaville","cities":[{"code":"17341","name":"Dolisie","latitude":-4.19834,"longitude":12.66664,"timezone":"Africa/Brazzaville"},{"code":"17350","name":"Mossendjo","latitude":-2.94968,"longitude":12.70423,"timezone":"Africa/Brazzaville"}]},{"code":"CG-14","name":"Plateaux","nativeName":"Plateaux","latitude":-2.1709644,"longitude":15.2286129,"timezone":"Africa/Brazzaville","cities":[{"code":"17340","name":"Djambala","latitude":-2.54472,"longitude":14.75333,"timezone":"Africa/Brazzaville"},{"code":"17343","name":"Gamboma","latitude":-1.87639,"longitude":15.86444,"timezone":"Africa/Brazzaville"}]},{"code":"CG-16","name":"Pointe-Noire","nativeName":"Pointe-Noire","latitude":-4.7975373,"longitude":11.8503297,"timezone":"Africa/Brazzaville","cities":[{"code":"17347","name":"Loandjili","latitude":-4.75611,"longitude":11.85778,"timezone":"Africa/Brazzaville"},{"code":"17353","name":"Pointe-Noire","latitude":-4.77609,"longitude":11.86352,"timezone":"Africa/Brazzaville"}]},{"code":"CG-12","name":"Pool","nativeName":"Pool","latitude":-3.7298545,"longitude":15.053872,"timezone":"Africa/Brazzaville","cities":[{"code":"17346","name":"Kinkala","latitude":-4.36139,"longitude":14.76444,"timezone":"Africa/Brazzaville"}]},{"code":"CG-13","name":"Sangha","nativeName":"Sangha","latitude":1.4284362,"longitude":15.4222522,"timezone":"Africa/Brazzaville","cities":[{"code":"17351","name":"Ouésso","latitude":1.61361,"longitude":16.05167,"timezone":"Africa/Brazzaville"},{"code":"17355","name":"Sémbé","latitude":1.64806,"longitude":14.58056,"timezone":"Africa/Brazzaville"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"DJ","name":"Djibouti","nativeName":"Djibouti","regions":[{"code":"DJ-AS","name":"Ali Sabieh","nativeName":"Autrement l'a trouvé","latitude":11.2133809,"longitude":42.8699249,"timezone":"Africa/Djibouti","cities":[{"code":"30561","name":"Ali Sabieh","latitude":11.15583,"longitude":42.7125,"timezone":"Africa/Djibouti"},{"code":"30567","name":"Goubétto","latitude":11.42389,"longitude":43.00028,"timezone":"Africa/Djibouti"},{"code":"30569","name":"Holhol","latitude":11.31028,"longitude":42.92944,"timezone":"Africa/Djibouti"}]},{"code":"DJ-AR","name":"Arta","nativeName":"Art","latitude":11.5219459,"longitude":42.8393629,"timezone":"Africa/Djibouti","cities":[{"code":"30563","name":"Arta","latitude":11.52639,"longitude":42.85194,"timezone":"Africa/Djibouti"}]},{"code":"DJ-DI","name":"Dikhil","nativeName":"Dikhil","latitude":11.1054336,"longitude":42.3704744,"timezone":"Africa/Djibouti","cities":[{"code":"30564","name":"Dikhil","latitude":11.10454,"longitude":42.36971,"timezone":"Africa/Djibouti"},{"code":"30568","name":"Gâlâfi","latitude":11.71583,"longitude":41.83611,"timezone":"Africa/Djibouti"}]},{"code":"DJ-DJ","name":"Djibouti","nativeName":"Djibouti","latitude":11.8145966,"longitude":42.8453061,"timezone":"Africa/Djibouti","cities":[{"code":"30565","name":"Djibouti","latitude":11.58901,"longitude":43.14503,"timezone":"Africa/Djibouti"},{"code":"30570","name":"Loyada","latitude":11.46111,"longitude":43.25278,"timezone":"Africa/Djibouti"}]},{"code":"DJ-OB","name":"Obock","nativeName":"Obosek","latitude":12.2111051,"longitude":43.1685809,"timezone":"Africa/Djibouti","cities":[{"code":"30562","name":"Alaïli Ḏaḏḏa","latitude":12.42167,"longitude":42.89556,"timezone":"Africa/Djibouti"},{"code":"30571","name":"Obock","latitude":11.96693,"longitude":43.28835,"timezone":"Africa/Djibouti"}]},{"code":"DJ-TA","name":"Tadjourah","nativeName":"Tadamand","latitude":12.0358121,"longitude":42.5174505,"timezone":"Africa/Djibouti","cities":[{"code":"30566","name":"Dorra","latitude":12.15028,"longitude":42.47624,"timezone":"Africa/Djibouti"},{"code":"30572","name":"Tadjourah","latitude":11.78778,"longitude":42.88222,"timezone":"Africa/Djibouti"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"DM","name":"Dominica","nativeName":"Dominica","regions":[{"code":"DM-02","name":"Saint Andrew","nativeName":"Saint Andrew","latitude":15.4238539,"longitude":-61.3416513,"timezone":"America/Dominica","cities":[{"code":"31003","name":"Calibishie","latitude":15.59297,"longitude":-61.34901,"timezone":"America/Dominica"},{"code":"31008","name":"Marigot","latitude":15.53886,"longitude":-61.28375,"timezone":"America/Dominica"},{"code":"31017","name":"Wesley","latitude":15.56667,"longitude":-61.31667,"timezone":"America/Dominica"},{"code":"31018","name":"Woodford Hill","latitude":15.58093,"longitude":-61.33149,"timezone":"America/Dominica"}]},{"code":"DM-03","name":"Saint David","nativeName":"Saint David","latitude":15.4238539,"longitude":-61.3416513,"timezone":"America/Dominica","cities":[{"code":"31004","name":"Castle Bruce","latitude":15.44397,"longitude":-61.25723,"timezone":"America/Dominica"},{"code":"31012","name":"Rosalie","latitude":15.36667,"longitude":-61.26667,"timezone":"America/Dominica"}]},{"code":"DM-04","name":"Saint George","nativeName":"Saint George","latitude":15.3058849,"longitude":-61.3477286,"timezone":"America/Dominica","cities":[{"code":"31013","name":"Roseau","latitude":15.30174,"longitude":-61.38808,"timezone":"America/Dominica"}]},{"code":"DM-05","name":"Saint John","nativeName":"Saint John","latitude":15.5739158,"longitude":-61.4369757,"timezone":"America/Dominica","cities":[{"code":"31011","name":"Portsmouth","latitude":15.58333,"longitude":-61.46667,"timezone":"America/Dominica"}]},{"code":"DM-06","name":"Saint Joseph","nativeName":"Saint Joseph","latitude":15.4063155,"longitude":-61.4239093,"timezone":"America/Dominica","cities":[{"code":"31014","name":"Saint Joseph","latitude":15.4,"longitude":-61.43333,"timezone":"America/Dominica"},{"code":"31015","name":"Salisbury","latitude":15.43689,"longitude":-61.43637,"timezone":"America/Dominica"}]},{"code":"DM-07","name":"Saint Luke","nativeName":"Saint Luke","latitude":15.2526483,"longitude":-61.3606848,"timezone":"America/Dominica","cities":[{"code":"31009","name":"Pointe Michel","latitude":15.25976,"longitude":-61.37452,"timezone":"America/Dominica"}]},{"code":"DM-08","name":"Saint Mark","nativeName":"Saint Mark","latitude":15.2285265,"longitude":-61.3500041,"timezone":"America/Dominica","cities":[{"code":"31016","name":"Soufrière","latitude":15.23374,"longitude":-61.35881,"timezone":"America/Dominica"}]},{"code":"DM-09","name":"Saint Patrick","nativeName":"Saint Patrick","latitude":15.2770426,"longitude":-61.2964775,"timezone":"America/Dominica","cities":[{"code":"31002","name":"Berekua","latitude":15.23333,"longitude":-61.31667,"timezone":"America/Dominica"},{"code":"31006","name":"La Plaine","latitude":15.32768,"longitude":-61.24753,"timezone":"America/Dominica"}]},{"code":"DM-10","name":"Saint Paul","nativeName":"Saint Paul","latitude":15.3630184,"longitude":-61.3613416,"timezone":"America/Dominica","cities":[{"code":"31007","name":"Mahaut","latitude":15.36357,"longitude":-61.39701,"timezone":"America/Dominica"},{"code":"31010","name":"Pont Cassé","latitude":15.36667,"longitude":-61.35,"timezone":"America/Dominica"}]},{"code":"DM-11","name":"Saint Peter","nativeName":"Saint Peter","latitude":15.4997591,"longitude":-61.4403471,"timezone":"America/Dominica","cities":[{"code":"31005","name":"Colihaut","latitude":15.48478,"longitude":-61.46215,"timezone":"America/Dominica"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"ER","name":"Eritrea","nativeName":"ኤርትራ","regions":[{"code":"ER-AN","name":"Anseba","nativeName":"ኣንሰባ","latitude":16.5000101,"longitude":37.5000048,"timezone":"Africa/Asmara","cities":[{"code":"31888","name":"Keren","latitude":15.77792,"longitude":38.45107,"timezone":"Africa/Asmara"}]},{"code":"ER-DU","name":"Debub","nativeName":"ዲቡብ .","latitude":14.9099667,"longitude":38.8150692,"timezone":"Africa/Asmara","cities":[{"code":"31881","name":"Adi Keyh","latitude":14.84444,"longitude":39.37722,"timezone":"Africa/Asmara"},{"code":"31886","name":"Dekemhāre","latitude":15.07,"longitude":39.0475,"timezone":"Africa/Asmara"},{"code":"31890","name":"Mendefera","latitude":14.88722,"longitude":38.81528,"timezone":"Africa/Asmara"}]},{"code":"ER-GB","name":"Gash-Barka","nativeName":"ጋሽ-እንቋዕ ደሓን መጻእኩም","latitude":15.25,"longitude":37.5,"timezone":"Africa/Asmara","cities":[{"code":"31882","name":"Akordat","latitude":15.54798,"longitude":37.88291,"timezone":"Africa/Asmara"},{"code":"31885","name":"Barentu","latitude":15.10582,"longitude":37.59067,"timezone":"Africa/Asmara"},{"code":"31891","name":"Teseney","latitude":15.11,"longitude":36.6575,"timezone":"Africa/Asmara"}]},{"code":"ER-MA","name":"Maekel","nativeName":"ጉሓት","latitude":15.333333,"longitude":38.916667,"timezone":"Africa/Asmara","cities":[{"code":"31883","name":"Asmara","latitude":15.33805,"longitude":38.93184,"timezone":"Africa/Asmara"}]},{"code":"ER-SK","name":"Northern Red Sea","nativeName":"ሰሜናዊ ቀይሕ ባሕሪ","latitude":16,"longitude":39,"timezone":"Africa/Asmara","cities":[{"code":"31889","name":"Massawa","latitude":15.60811,"longitude":39.47455,"timezone":"Africa/Asmara"}]},{"code":"ER-DK","name":"Southern Red Sea","nativeName":"ደቡባዊ ቀይሕ ባሕሪ","latitude":13.75,"longitude":41.5,"timezone":"Africa/Asmara","cities":[{"code":"31884","name":"Assab","latitude":13.00917,"longitude":42.73944,"timezone":"Africa/Asmara"},{"code":"31887","name":"Edd","latitude":13.93088,"longitude":41.6938,"timezone":"Africa/Asmara"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"FJ","name":"Fiji Islands","nativeName":"Fiji","regions":[{"code":"FJ-01","name":"Ba","nativeName":"Ba","latitude":36.0613893,"longitude":-95.8005872,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-02","name":"Bua","nativeName":"Bua","latitude":43.0964584,"longitude":-89.50088,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-03","name":"Cakaudrove","nativeName":"Cakaudrove","latitude":-16.8133648,"longitude":179.2868682,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-C","name":"Central","nativeName":"Central","latitude":34.0440066,"longitude":-118.2472738,"timezone":"Pacific/Fiji","cities":[{"code":"39145","name":"Naitasiri","latitude":-17.83333,"longitude":178.25,"timezone":"Pacific/Fiji"},{"code":"39146","name":"Namosi","latitude":-18.05,"longitude":178.13333,"timezone":"Pacific/Fiji"},{"code":"39149","name":"Rewa","latitude":-18.08333,"longitude":178.33333,"timezone":"Pacific/Fiji"},{"code":"39150","name":"Serua","latitude":-18.16667,"longitude":178,"timezone":"Pacific/Fiji"},{"code":"39151","name":"Suva","latitude":-18.14161,"longitude":178.44149,"timezone":"Pacific/Fiji"},{"code":"39152","name":"Tailevu","latitude":-17.83333,"longitude":178.5,"timezone":"Pacific/Fiji"}]},{"code":"FJ-E","name":"Eastern","nativeName":"Eastern","latitude":32.8094305,"longitude":-117.1289937,"timezone":"Pacific/Fiji","cities":[{"code":"39137","name":"Kadavu","latitude":-18.99331,"longitude":178.22021,"timezone":"Pacific/Fiji"},{"code":"39139","name":"Lau","latitude":-18.20488,"longitude":-178.79251,"timezone":"Pacific/Fiji"},{"code":"39141","name":"Levuka","latitude":-18.06667,"longitude":179.31667,"timezone":"Pacific/Fiji"},{"code":"39142","name":"Lomaiviti","latitude":-17.66667,"longitude":178.8,"timezone":"Pacific/Fiji"}]},{"code":"FJ-04","name":"Kadavu","nativeName":"Kadavu","latitude":-19.0127122,"longitude":178.1876676,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-05","name":"Lau","nativeName":"Lau","latitude":31.6687015,"longitude":-106.3955763,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-06","name":"Lomaiviti","nativeName":"Lomaiviti","latitude":-17.709,"longitude":179.091,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-07","name":"Macuata","nativeName":"Mute","latitude":-16.4864922,"longitude":179.2847251,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-08","name":"Nadroga-Navosa","nativeName":"Grahoga-navos","latitude":-17.9865278,"longitude":177.658113,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-09","name":"Naitasiri","nativeName":"I have it","latitude":-17.8975754,"longitude":178.2071598,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-10","name":"Namosi","nativeName":"Nomis","latitude":-18.0864176,"longitude":178.1291387,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-N","name":"Northern","nativeName":"Northern","latitude":-16.8133648,"longitude":179.2868682,"timezone":"Pacific/Fiji","cities":[{"code":"39135","name":"Bua","latitude":-16.83333,"longitude":178.75,"timezone":"Pacific/Fiji"},{"code":"39136","name":"Cakaudrove","latitude":-16.66667,"longitude":179.41667,"timezone":"Pacific/Fiji"},{"code":"39138","name":"Labasa","latitude":-16.4332,"longitude":179.36451,"timezone":"Pacific/Fiji"},{"code":"39143","name":"Macuata","latitude":-16.5,"longitude":179.25,"timezone":"Pacific/Fiji"}]},{"code":"FJ-11","name":"Ra","nativeName":"Ra","latitude":37.1003153,"longitude":-95.6744246,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-12","name":"Rewa","nativeName":"Rewa","latitude":34.7923517,"longitude":-82.3609264,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-R","name":"Rotuma","nativeName":"Rotum","latitude":-12.5025069,"longitude":177.0724164,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-13","name":"Serua","nativeName":"Serua","latitude":-18.1804749,"longitude":178.050979,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-14","name":"Tailevu","nativeName":"Tailev","latitude":-17.8269111,"longitude":178.293248,"timezone":"Pacific/Fiji","cities":[]},{"code":"FJ-W","name":"Western","nativeName":"Western","latitude":42.9662198,"longitude":-78.7021134,"timezone":"Pacific/Fiji","cities":[{"code":"39133","name":"Ba","latitude":-17.5343,"longitude":177.67407,"timezone":"Pacific/Fiji"},{"code":"39134","name":"Ba Province","latitude":-17.66667,"longitude":177.66667,"timezone":"Pacific/Fiji"},{"code":"39140","name":"Lautoka","latitude":-17.61686,"longitude":177.45049,"timezone":"Pacific/Fiji"},{"code":"39144","name":"Nadi","latitude":-17.80309,"longitude":177.41617,"timezone":"Pacific/Fiji"},{"code":"39147","name":"Nandronga and Navosa","latitude":-18,"longitude":177.66667,"timezone":"Pacific/Fiji"},{"code":"39148","name":"Ra","latitude":-17.5,"longitude":178.16667,"timezone":"Pacific/Fiji"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"FO","name":"Faroe Islands","nativeName":"Føroyar","regions":[{"code":"FO-EY","name":"Eysturoy","nativeName":"Eysturoy","latitude":62.1988704,"longitude":-6.8178451,"timezone":"Atlantic/Faroe","cities":[{"code":"153758","name":"Eiði","latitude":62.2789,"longitude":-7.0123,"timezone":"Atlantic/Faroe"},{"code":"153748","name":"Eystur","latitude":62.1567,"longitude":-6.8233,"timezone":"Atlantic/Faroe"},{"code":"153752","name":"Fuglafjørður","latitude":62.2101,"longitude":-6.8123,"timezone":"Atlantic/Faroe"},{"code":"153754","name":"Nes","latitude":62.0987,"longitude":-6.7654,"timezone":"Atlantic/Faroe"},{"code":"153747","name":"Runavík","latitude":62.1323,"longitude":-6.789,"timezone":"Atlantic/Faroe"},{"code":"153757","name":"Sjóvar","latitude":62.1234,"longitude":-6.8765,"timezone":"Atlantic/Faroe"}]},{"code":"FO-NO","name":"Northern Isles","nativeName":"Northern Isles","latitude":62.2805689,"longitude":6.7017061,"timezone":"Atlantic/Faroe","cities":[{"code":"153763","name":"Hvannasund","latitude":62.3456,"longitude":-6.789,"timezone":"Atlantic/Faroe"},{"code":"153746","name":"Klaksvík","latitude":62.221,"longitude":-6.58,"timezone":"Atlantic/Faroe"},{"code":"153767","name":"Kunoy","latitude":62.3456,"longitude":-6.8901,"timezone":"Atlantic/Faroe"}]},{"code":"FO-SA","name":"Sandoy","nativeName":"Sandoy","latitude":61.8333877,"longitude":-6.7239899,"timezone":"Atlantic/Faroe","cities":[{"code":"153769","name":"Húsavík","latitude":61.8096495,"longitude":-6.6794956,"timezone":"Atlantic/Faroe"},{"code":"153761","name":"Sandur","latitude":61.8348155,"longitude":-6.8175826,"timezone":"Atlantic/Faroe"},{"code":"153768","name":"Skálavík","latitude":61.8302676,"longitude":-6.664032,"timezone":"Atlantic/Faroe"},{"code":"153762","name":"Skopun","latitude":62.0123,"longitude":-6.9876,"timezone":"Atlantic/Faroe"},{"code":"153772","name":"Skúvoy","latitude":61.7664432,"longitude":-6.826962,"timezone":"Atlantic/Faroe"}]},{"code":"FO-ST","name":"Streymoy","nativeName":"Streymoy","latitude":62.1237192,"longitude":-7.0158828,"timezone":"Atlantic/Faroe","cities":[{"code":"153760","name":"Kvívík","latitude":62.1456,"longitude":-7.2098,"timezone":"Atlantic/Faroe"},{"code":"153750","name":"Sunda","latitude":62.1345,"longitude":-7.0987,"timezone":"Atlantic/Faroe"},{"code":"153745","name":"Tórshavn","latitude":62,"longitude":-7,"timezone":"Atlantic/Faroe"},{"code":"153755","name":"Vestmanna","latitude":62.1567,"longitude":-7.2345,"timezone":"Atlantic/Faroe"}]},{"code":"FO-SU","name":"Suðuroy","nativeName":"Suðuroy","latitude":61.5210651,"longitude":-6.8141866,"timezone":"Atlantic/Faroe","cities":[{"code":"153771","name":"Fámjin","latitude":61.5246866,"longitude":-6.8792705,"timezone":"Atlantic/Faroe"},{"code":"153770","name":"Hov","latitude":61.5069357,"longitude":-6.7557422,"timezone":"Atlantic/Faroe"},{"code":"153759","name":"Hvalba","latitude":61.5987,"longitude":-6.9876,"timezone":"Atlantic/Faroe"},{"code":"153766","name":"Porkeri","latitude":61.5432,"longitude":-6.9876,"timezone":"Atlantic/Faroe"},{"code":"153764","name":"Sumba","latitude":61.4033267,"longitude":-6.7117361,"timezone":"Atlantic/Faroe"},{"code":"153751","name":"Tvøroyri","latitude":61.5623,"longitude":-6.778,"timezone":"Atlantic/Faroe"},{"code":"153753","name":"Vágur","latitude":61.4789,"longitude":-6.8101,"timezone":"Atlantic/Faroe"}]},{"code":"FO-VA","name":"Vágar","nativeName":"Vágar","latitude":62.0898985,"longitude":-7.2345841,"timezone":"Atlantic/Faroe","cities":[{"code":"153756","name":"Sørvágur","latitude":62.0654,"longitude":-7.3787,"timezone":"Atlantic/Faroe"},{"code":"153749","name":"Vágar","latitude":62.0667,"longitude":-7.1,"timezone":"Atlantic/Faroe"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"GA","name":"Gabon","nativeName":"Gabon","regions":[{"code":"GA-1","name":"Estuaire","nativeName":"Estuaire","latitude":0.4302639,"longitude":9.4655189,"timezone":"Africa/Libreville","cities":[{"code":"48131","name":"Cocobeach","latitude":1.00019,"longitude":9.58229,"timezone":"Africa/Libreville"},{"code":"48138","name":"Libreville","latitude":0.39241,"longitude":9.45356,"timezone":"Africa/Libreville"},{"code":"48150","name":"Ntoum","latitude":0.39051,"longitude":9.76096,"timezone":"Africa/Libreville"}]},{"code":"GA-2","name":"Haut-Ogooué","nativeName":"Haut-Ogooué","latitude":-1.231544,"longitude":13.8309713,"timezone":"Africa/Libreville","cities":[{"code":"48133","name":"Franceville","latitude":-1.63333,"longitude":13.58357,"timezone":"Africa/Libreville"},{"code":"48139","name":"Lékoni","latitude":-1.58431,"longitude":14.25905,"timezone":"Africa/Libreville"},{"code":"48145","name":"Moanda","latitude":-1.56652,"longitude":13.1987,"timezone":"Africa/Libreville"},{"code":"48147","name":"Mounana","latitude":-1.4085,"longitude":13.15857,"timezone":"Africa/Libreville"},{"code":"48151","name":"Okondja","latitude":-0.65487,"longitude":13.67533,"timezone":"Africa/Libreville"}]},{"code":"GA-3","name":"Moyen-Ogooué","nativeName":"Moyen-Ogooué","latitude":-0.5003495,"longitude":10.4991063,"timezone":"Africa/Libreville","cities":[{"code":"48136","name":"Lambaréné","latitude":-0.7001,"longitude":10.24055,"timezone":"Africa/Libreville"},{"code":"48149","name":"Ndjolé","latitude":-0.17827,"longitude":10.76488,"timezone":"Africa/Libreville"}]},{"code":"GA-4","name":"Ngounié","nativeName":"Ngounié","latitude":-1.5910727,"longitude":10.978503,"timezone":"Africa/Libreville","cities":[{"code":"48132","name":"Fougamou","latitude":-1.21544,"longitude":10.58378,"timezone":"Africa/Libreville"},{"code":"48142","name":"Mbigou","latitude":-1.90046,"longitude":11.906,"timezone":"Africa/Libreville"},{"code":"48143","name":"Mimongo","latitude":-1.61952,"longitude":11.60675,"timezone":"Africa/Libreville"},{"code":"48146","name":"Mouila","latitude":-1.86846,"longitude":11.05594,"timezone":"Africa/Libreville"},{"code":"48148","name":"Ndendé","latitude":-2.40077,"longitude":11.35813,"timezone":"Africa/Libreville"}]},{"code":"GA-5","name":"Nyanga","nativeName":"Lune","latitude":-3.0080415,"longitude":10.9964746,"timezone":"Africa/Libreville","cities":[{"code":"48141","name":"Mayumba","latitude":-3.43198,"longitude":10.6554,"timezone":"Africa/Libreville"},{"code":"48155","name":"Tchibanga","latitude":-2.93323,"longitude":10.98178,"timezone":"Africa/Libreville"}]},{"code":"GA-6","name":"Ogooué-Ivindo","nativeName":"Ogooué-Ivindo","latitude":0.60082,"longitude":12.6887911,"timezone":"Africa/Libreville","cities":[{"code":"48130","name":"Booué","latitude":-0.09207,"longitude":11.93846,"timezone":"Africa/Libreville"},{"code":"48140","name":"Makokou","latitude":0.57381,"longitude":12.86419,"timezone":"Africa/Libreville"},{"code":"48156","name":"Zadie","latitude":0.92582,"longitude":13.90813,"timezone":"Africa/Libreville"}]},{"code":"GA-7","name":"Ogooué-Lolo","nativeName":"Ogooué-lolo","latitude":-1.0002904,"longitude":12.4952424,"timezone":"Africa/Libreville","cities":[{"code":"48135","name":"Koulamoutou","latitude":-1.13667,"longitude":12.46399,"timezone":"Africa/Libreville"},{"code":"48137","name":"Lastoursville","latitude":-0.81742,"longitude":12.70818,"timezone":"Africa/Libreville"}]},{"code":"GA-8","name":"Ogooué-Maritime","nativeName":"Ogooué-Maritime","latitude":-1.4958157,"longitude":9.5060896,"timezone":"Africa/Libreville","cities":[{"code":"48134","name":"Gamba","latitude":-2.65,"longitude":10,"timezone":"Africa/Libreville"},{"code":"48152","name":"Omboué","latitude":-1.57464,"longitude":9.26184,"timezone":"Africa/Libreville"},{"code":"48154","name":"Port-Gentil","latitude":-0.71933,"longitude":8.78151,"timezone":"Africa/Libreville"}]},{"code":"GA-9","name":"Woleu-Ntem","nativeName":"Woleu-Ntem","latitude":1.2950125,"longitude":11.8218827,"timezone":"Africa/Libreville","cities":[{"code":"48129","name":"Bitam","latitude":2.07597,"longitude":11.50065,"timezone":"Africa/Libreville"},{"code":"48144","name":"Mitzic","latitude":0.78205,"longitude":11.54904,"timezone":"Africa/Libreville"},{"code":"48153","name":"Oyem","latitude":1.5995,"longitude":11.57933,"timezone":"Africa/Libreville"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"GD","name":"Grenada","nativeName":"Grenada","regions":[{"code":"GD-10","name":"Carriacou","nativeName":"Carriacou","latitude":12.4829985,"longitude":-61.4448436,"timezone":"America/Grenada","cities":[{"code":"52030","name":"Hillsborough","latitude":12.48292,"longitude":-61.45597,"timezone":"America/Grenada"}]},{"code":"GD-01","name":"Saint Andrew","nativeName":"Saint Andrew","latitude":12.1111935,"longitude":-61.6865971,"timezone":"America/Grenada","cities":[{"code":"52029","name":"Grenville","latitude":12.12278,"longitude":-61.62498,"timezone":"America/Grenada"}]},{"code":"GD-02","name":"Saint David","nativeName":"Saint David","latitude":12.0428069,"longitude":-61.6778988,"timezone":"America/Grenada","cities":[{"code":"52031","name":"Saint Davids","latitude":12.04903,"longitude":-61.66875,"timezone":"America/Grenada"}]},{"code":"GD-03","name":"Saint George","nativeName":"Saint George","latitude":12.0535331,"longitude":-61.751805,"timezone":"America/Grenada","cities":[{"code":"52032","name":"Saint George's","latitude":12.05288,"longitude":-61.75226,"timezone":"America/Grenada"}]},{"code":"GD-04","name":"Saint John","nativeName":"Saint John","latitude":12.1444029,"longitude":-61.7121841,"timezone":"America/Grenada","cities":[{"code":"52028","name":"Gouyave","latitude":12.16462,"longitude":-61.72965,"timezone":"America/Grenada"}]},{"code":"GD-05","name":"Saint Mark","nativeName":"Saint Mark","latitude":12.1886437,"longitude":-61.6954355,"timezone":"America/Grenada","cities":[{"code":"52034","name":"Victoria","latitude":12.19021,"longitude":-61.70677,"timezone":"America/Grenada"}]},{"code":"GD-06","name":"Saint Patrick","nativeName":"Saint Patrick","latitude":12.2102986,"longitude":-61.6350539,"timezone":"America/Grenada","cities":[{"code":"52033","name":"Sauteurs","latitude":12.21833,"longitude":-61.63917,"timezone":"America/Grenada"}]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"GF","name":"French Guiana","nativeName":"Guyane française","regions":[{"code":"FR-973","name":"Guyane","nativeName":"Guyane","latitude":4,"longitude":-53,"timezone":"America/Cayenne","cities":[{"code":"162109","name":"Apatou","latitude":5.15666667,"longitude":-54.34305556,"timezone":"America/Cayenne"},{"code":"162110","name":"Awala-Yalimapo","latitude":5.73611111,"longitude":-53.90805556,"timezone":"America/Cayenne"},{"code":"162111","name":"Camopi","latitude":3.16694444,"longitude":-52.33111111,"timezone":"America/Cayenne"},{"code":"162112","name":"Cayenne","latitude":4.93861111,"longitude":-52.335,"timezone":"America/Cayenne"},{"code":"162113","name":"Grand-Santi","latitude":4.27333333,"longitude":-54.38166667,"timezone":"America/Cayenne"},{"code":"162114","name":"Iracoubo","latitude":5.48,"longitude":-53.20555556,"timezone":"America/Cayenne"},{"code":"162115","name":"Kourou","latitude":5.15833333,"longitude":-52.64277778,"timezone":"America/Cayenne"},{"code":"162116","name":"Macouria","latitude":5.01361111,"longitude":-52.47388889,"timezone":"America/Cayenne"},{"code":"162117","name":"Mana","latitude":5.66722222,"longitude":-53.77694444,"timezone":"America/Cayenne"},{"code":"162118","name":"Maripasoula","latitude":3.64111111,"longitude":-54.02777778,"timezone":"America/Cayenne"},{"code":"162119","name":"Matoury","latitude":4.85055556,"longitude":-52.33111111,"timezone":"America/Cayenne"},{"code":"162120","name":"Montsinéry-Tonnegrande","latitude":4.89277778,"longitude":-52.49277778,"timezone":"America/Cayenne"},{"code":"162121","name":"Ouanary","latitude":4.20916667,"longitude":-51.67166667,"timezone":"America/Cayenne"},{"code":"162122","name":"Papaichton","latitude":3.80972222,"longitude":-54.14888889,"timezone":"America/Cayenne"},{"code":"162123","name":"Régina","latitude":4.31361111,"longitude":-52.12916667,"timezone":"America/Cayenne"},{"code":"162124","name":"Remire-Montjoly","latitude":4.905,"longitude":-52.27638889,"timezone":"America/Cayenne"},{"code":"162125","name":"Roura","latitude":4.72833333,"longitude":-52.32416667,"timezone":"America/Cayenne"},{"code":"162126","name":"Saint-Élie","latitude":4.82555556,"longitude":-53.28805556,"timezone":"America/Cayenne"},{"code":"162127","name":"Saint-Georges","latitude":3.88944444,"longitude":-51.80111111,"timezone":"America/Cayenne"},{"code":"162128","name":"Saint-Laurent-du-Maroni","latitude":5.50388889,"longitude":-54.02888889,"timezone":"America/Cayenne"},{"code":"162129","name":"Saül","latitude":3.62277778,"longitude":-53.20833333,"timezone":"America/Cayenne"},{"code":"162130","name":"Sinnamary","latitude":5.3775,"longitude":-52.95861111,"timezone":"America/Cayenne"}]}]}
@@ -0,0 +1 @@
{"code":"GG","name":"Guernsey","nativeName":"Guernsey","regions":[{"code":"GG-04","name":"Alderney","nativeName":"Alderney","latitude":49.7168911,"longitude":-2.1971694,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-01","name":"Castel","nativeName":"Castel","latitude":49.4677402,"longitude":-2.5913459,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-02","name":"Forest","nativeName":"Forest","latitude":49.4312917,"longitude":-2.5927968,"timezone":"Europe/Guernsey","cities":[{"code":"160552","name":"Forest Parish","latitude":49.42983,"longitude":-2.60168,"timezone":"Europe/Guernsey"}]},{"code":"GG-07","name":"Sark","nativeName":"Sark","latitude":49.4283373,"longitude":-2.3599394,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-03","name":"St Andrew","nativeName":"St Andrew","latitude":49.4499006,"longitude":-2.5688225,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-05","name":"St Martin","nativeName":"St Martin","latitude":49.4373488,"longitude":-2.555524,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-06","name":"St Peter Port","nativeName":"St Peter Port","latitude":49.4568142,"longitude":-2.5389979,"timezone":"Europe/Guernsey","cities":[{"code":"160553","name":"Saint Peter Port","latitude":49.45981,"longitude":-2.53527,"timezone":"Europe/Guernsey"}]},{"code":"GG-08","name":"St Pierre du Bois","nativeName":"St Pierre du Bois","latitude":49.4395995,"longitude":-2.6408963,"timezone":"Europe/Guernsey","cities":[{"code":"160555","name":"Saint Pierre du Bois Parish","latitude":49.45399,"longitude":-2.63808,"timezone":"Europe/Guernsey"},{"code":"160554","name":"Vale Parish","latitude":49.4944,"longitude":-2.5179,"timezone":"Europe/Guernsey"}]},{"code":"GG-09","name":"St Sampson","nativeName":"St Sampson","latitude":49.4817273,"longitude":-2.5388395,"timezone":"Europe/Guernsey","cities":[{"code":"160556","name":"Saint Sampson","latitude":49.48389,"longitude":-2.52333,"timezone":"Europe/Guernsey"}]},{"code":"GG-10","name":"St Saviour","nativeName":"St Saviour","latitude":49.4491648,"longitude":-2.6213557,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-11","name":"Torteval","nativeName":"Torteval","latitude":49.4302321,"longitude":-2.6638621,"timezone":"Europe/Guernsey","cities":[]},{"code":"GG-12","name":"Vale","nativeName":"Vale","latitude":49.495456,"longitude":-2.5225087,"timezone":"Europe/Guernsey","cities":[]}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"GL","name":"Greenland","nativeName":"Kalaallit Nunaat","regions":[{"code":"GL-AV","name":"Avannaata","nativeName":"Avannaata","latitude":76.5049265,"longitude":-56.2911976,"timezone":"America/Nuuk","cities":[{"code":"154930","name":"Ilulissat","latitude":69.2279488,"longitude":-51.1280539,"timezone":"America/Nuuk"},{"code":"154931","name":"Qaanaaq","latitude":77.4674009,"longitude":-69.2483711,"timezone":"America/Danmarkshavn"},{"code":"154933","name":"Upernavik","latitude":72.7868603,"longitude":-56.1623671,"timezone":"America/Nuuk"},{"code":"154932","name":"Uummannaq","latitude":70.6786897,"longitude":-52.1497824,"timezone":"America/Nuuk"}]},{"code":"GL-KU","name":"Kujalleq","nativeName":"Kujalleq","latitude":61,"longitude":-45,"timezone":"America/Nuuk","cities":[{"code":"154934","name":"Nanortalik","latitude":60.1430825,"longitude":-45.2562905,"timezone":"America/Nuuk"},{"code":"154935","name":"Narsaq","latitude":60.9114078,"longitude":-46.0685708,"timezone":"America/Nuuk"},{"code":"154936","name":"Qaqortoq","latitude":60.719596,"longitude":-46.0548163,"timezone":"America/Nuuk"}]},{"code":"GL-QT","name":"Qeqertalik","nativeName":"Qeqertalik","latitude":68.8249553,"longitude":-54.7622472,"timezone":"America/Danmarkshavn","cities":[{"code":"154937","name":"Aasiaat","latitude":68.707628,"longitude":-52.8995136,"timezone":"America/Nuuk"},{"code":"154938","name":"Kangaatsiaq","latitude":68.3081845,"longitude":-53.4768533,"timezone":"America/Nuuk"},{"code":"154939","name":"Qasigiannguit","latitude":68.8198632,"longitude":-51.2091637,"timezone":"America/Nuuk"},{"code":"154940","name":"Qeqertarsuaq","latitude":69.2443037,"longitude":-53.5644006,"timezone":"America/Nuuk"}]},{"code":"GL-QE","name":"Qeqqata","nativeName":"Qeqqata","latitude":66.5,"longitude":-48,"timezone":"America/Nuuk","cities":[{"code":"154941","name":"Maniitsoq","latitude":65.4065799,"longitude":-52.9422573,"timezone":"America/Nuuk"},{"code":"154942","name":"Sisimiut","latitude":66.9394574,"longitude":-53.7456766,"timezone":"America/Nuuk"}]},{"code":"GL-SM","name":"Sermersooq","nativeName":"Sermersooq","latitude":66,"longitude":-40,"timezone":"America/Nuuk","cities":[{"code":"154943","name":"Ammassalik","latitude":65.6145267,"longitude":-37.6501466,"timezone":"America/Nuuk"},{"code":"154944","name":"Ittoqqortoormiit","latitude":70.4863421,"longitude":-21.979866,"timezone":"America/Nuuk"},{"code":"154945","name":"Ivittuut","latitude":61.2065184,"longitude":-48.1819851,"timezone":"America/Nuuk"},{"code":"154946","name":"Nuuk","latitude":64.1791009,"longitude":-51.7480091,"timezone":"America/Nuuk"},{"code":"154947","name":"Paamiut","latitude":62.0025962,"longitude":-49.7051959,"timezone":"America/Nuuk"}]}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"code":"GP","name":"Guadeloupe","nativeName":"Guadeloupe","regions":[{"code":"GP-01","name":"Basse-Terre","nativeName":"Basse-Terre","latitude":16.0000778,"longitude":-61.7333373,"timezone":"America/Guadeloupe","cities":[{"code":"154948","name":"Baie-Mahault","latitude":16.2497607,"longitude":-61.6775072,"timezone":"America/Guadeloupe"},{"code":"154949","name":"Baillif","latitude":16.0497331,"longitude":-61.7570591,"timezone":"America/Guadeloupe"},{"code":"154950","name":"Basse-Terre","latitude":15.9991631,"longitude":-61.749981,"timezone":"America/Guadeloupe"},{"code":"154951","name":"Bouillante","latitude":16.1386937,"longitude":-61.7989897,"timezone":"America/Guadeloupe"},{"code":"154952","name":"Capesterre-Belle-Eau","latitude":16.0485402,"longitude":-61.7021045,"timezone":"America/Guadeloupe"},{"code":"154953","name":"Deshaies","latitude":16.3193605,"longitude":-61.8242578,"timezone":"America/Guadeloupe"},{"code":"154954","name":"Gourbeyre","latitude":16.0056595,"longitude":-61.7302387,"timezone":"America/Guadeloupe"},{"code":"154955","name":"Goyave","latitude":16.1231077,"longitude":-61.6974021,"timezone":"America/Guadeloupe"},{"code":"154956","name":"Lamentin","latitude":16.2443132,"longitude":-61.7519194,"timezone":"America/Guadeloupe"},{"code":"154957","name":"Petit-Bourg","latitude":16.1694881,"longitude":-61.7404331,"timezone":"America/Guadeloupe"},{"code":"154958","name":"Pointe-Noire","latitude":16.2270277,"longitude":-61.8063122,"timezone":"America/Guadeloupe"},{"code":"154959","name":"Saint-Claude","latitude":16.0405744,"longitude":-61.7364873,"timezone":"America/Guadeloupe"},{"code":"154960","name":"Sainte-Rose","latitude":16.2937252,"longitude":-61.7659576,"timezone":"America/Guadeloupe"},{"code":"154961","name":"Terre-de-Bas","latitude":15.8554196,"longitude":-61.653292,"timezone":"America/Guadeloupe"},{"code":"154962","name":"Terre-de-Haut","latitude":15.8580376,"longitude":-61.6096098,"timezone":"America/Guadeloupe"},{"code":"154963","name":"Trois-Rivières","latitude":15.9931362,"longitude":-61.6905296,"timezone":"America/Guadeloupe"},{"code":"154964","name":"Vieux-Fort","latitude":15.9607038,"longitude":-61.7135177,"timezone":"America/Guadeloupe"},{"code":"154965","name":"Vieux-Habitants","latitude":16.0846618,"longitude":-61.7993923,"timezone":"America/Guadeloupe"}]},{"code":"GP-02","name":"Pointe-à-Pitre","nativeName":"Pointe-à-Pitre","latitude":16.2408636,"longitude":-61.5334077,"timezone":"America/Guadeloupe","cities":[{"code":"154967","name":"Anse-Bertrand","latitude":16.4668181,"longitude":-61.5521768,"timezone":"America/Guadeloupe"},{"code":"154968","name":"Capesterre-de-Marie-Galante","latitude":15.9176399,"longitude":-61.2707786,"timezone":"America/Guadeloupe"},{"code":"154971","name":"Grand-Bourg","latitude":15.908538,"longitude":-61.3340618,"timezone":"America/Guadeloupe"},{"code":"154969","name":"La Désirade","latitude":16.2584545,"longitude":-61.1490279,"timezone":"America/Guadeloupe"},{"code":"154970","name":"Le Gosier","latitude":16.2272625,"longitude":-61.5573449,"timezone":"America/Guadeloupe"},{"code":"154973","name":"Le Moule","latitude":16.3353687,"longitude":-61.4424293,"timezone":"America/Guadeloupe"},{"code":"154966","name":"Les Abymes","latitude":16.2677383,"longitude":-61.5790727,"timezone":"America/Guadeloupe"},{"code":"154972","name":"Morne-à-l'Eau","latitude":16.3211061,"longitude":-61.6635689,"timezone":"America/Guadeloupe"},{"code":"154974","name":"Petit-Canal","latitude":16.392131,"longitude":-61.5371312,"timezone":"America/Guadeloupe"},{"code":"154975","name":"Pointe-à-Pitre","latitude":16.2331304,"longitude":-61.5623485,"timezone":"America/Guadeloupe"},{"code":"154976","name":"Port-Louis","latitude":16.4201282,"longitude":-61.5703989,"timezone":"America/Guadeloupe"},{"code":"154977","name":"Saint-François","latitude":16.2783495,"longitude":-61.3362498,"timezone":"America/Guadeloupe"},{"code":"154978","name":"Saint-Louis","latitude":15.9653589,"longitude":-61.355625,"timezone":"America/Guadeloupe"},{"code":"154979","name":"Sainte-Anne","latitude":16.2564613,"longitude":-61.4720288,"timezone":"America/Guadeloupe"}]}]}

Some files were not shown because too many files have changed in this diff Show More