diff --git a/TASK-rectification-convergence-20260830.md b/TASK-rectification-convergence-20260830.md index 06b78eab..f788213b 100644 --- a/TASK-rectification-convergence-20260830.md +++ b/TASK-rectification-convergence-20260830.md @@ -146,32 +146,55 @@ git worktree add -b codex/rectification-convergence-20260830 \ --- -## 任务 0(P0,门控,不写代码)· 拿到上游访谈手册与一份真实会话 +## 任务 0(已完成 2026-08-31)· 上游访谈手册与阈值文档 -### 事实 +三份文件已获取,归档于 `references/upstream/`(来源:上游维护者本地 skill 包 `~/.workbuddy/skills/jyotish-birth-time-rectification/`,获取日期 2026-08-31): -调研发现一个**仓库外**的独立 skill 包:`~/.workbuddy/skills/jyotish-birth-time-rectification/`,含 `references/interview_playbook.md`(访谈手册)与 `references/evidence_thresholds.md`。它不在 `yinduzhanxing` 仓库里,只有 `yinduzhanxing/tests/test_birth_time_rectification_skill_contract.py:7` 引用了这个路径。 +- `references/upstream/interview_playbook.md` +- `references/upstream/evidence_thresholds.md` +- `references/upstream/test_birth_time_rectification_skill_contract.py` -产品侧「本地 Agent 能收敛」的观察,如果另有机制,最可能就在这两份文件里。 +### 结论:上游的「收敛」= 从不试图确认分钟 -### 要做什么 +**1. 停止规则与本仓方向相反。** -1. 向上游维护者索取 `interview_playbook.md` 与 `evidence_thresholds.md`,放进 `references/upstream/` 并在 PROGRESS 记录来源与获取日期。 -2. 索取**一次真实本地校正会话的完整记录**,要能回答三个问题: - - 一共问了几轮? - - 最后交付的是一个分钟,还是一个区间?区间多宽? - - `confidence` / `can_apply` 是什么值? -3. 把答案写进 PROGRESS,并据此确认或推翻这条判断:**「上游的收敛 = 问完固定题数后给出候选区间并停止」。** +`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 +``` -- 两份文件在仓库里,或明确记录「无法获取」及原因。 -- 会话记录的三个问题有答案。 -- PROGRESS 给出结论:上游的终止机制是什么,Jyotisha 要复制的具体是哪一条。 +即:**证据不足 → 停下来,交付当前区间。** -### 如果拿不到 +本仓 `frontend/src/lib/birth-time-evidence.ts:188-189` 把 `minConfirmationEvents(4)` / `minConfirmationDomains(3)` 当作**确认门**,达不到 → **继续问**。 -**不阻塞任务 1–3。** 任务 1 的修法不依赖这份手册——轮次预算该接还是要接。但拿不到这件事要写进 `BLOCKED.md`,因为它是「本地能收敛」这条观察唯一可能的另一种解释。 +同一类阈值,上游当「可以开始交付的地板」,本仓当「可以下结论的天花板」。前者三轮内必达,后者按 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 轮 3–5 道 A/B/C/D 选择题;第 2 轮只要最强 domain 的带日期事件;第 3 轮只问能区分 top 候选的 1–3 个事件。手册明写 "Never collect a long autobiography before the first route decision." + +**4. 终止话术是写死的。** + +> 当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。 + +### 对已完成任务的影响 + +任务 1 实现的 `budgetExhausted()`(轮次 8 / 答题 10 / 平台期 2)方向正确但只覆盖了一半——上游是**证据状态型**停止且三轮即止。差额由**任务 6** 补齐。 --- @@ -318,6 +341,42 @@ do_not_apply_as_birth_time_truth --- +## 任务 6(P0)· 接过上游的终止语义 + +### 事实 + +见任务 0 结论。任务 1 已接入轮次型预算,但上游的终止是**证据状态型**的,且 `exhausted` 在上游是正常终态而非兜底态。 + +### 要做什么 + +1. **增加证据型停止规则**,与既有轮次预算并列(任一命中即停并交付): + - 带日期事件 < 3 + - 覆盖 domain < 2 + - 并列第一(已有 `separation` 可判) + - 用户不确定度过高,无法可靠映射 support/conflict + + 阈值取 `references/upstream/evidence_thresholds.md` 的 Minimum Standalone Gate(3 事件 / 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 的四条属性测试全绿。 + +--- + ## 交付 - PROGRESS 写进 `PROGRESS-rectification-convergence-20260830.md`,任务 0 的会话记录结论单列。 diff --git a/references/upstream/evidence_thresholds.md b/references/upstream/evidence_thresholds.md new file mode 100644 index 00000000..b7aa559c --- /dev/null +++ b/references/upstream/evidence_thresholds.md @@ -0,0 +1,41 @@ +# Evidence Thresholds + +## Labels + +- `blocked`: required input, route, or event evidence is missing. +- `user_history_verification_required`: dates are too sparse or imprecise. +- `parameter_sensitive`: small changes in the candidate grid materially alter the lead. +- `manual_pattern_consensus`: standalone conversation evidence points to one interval, but no external calculator/audit was executed. +- `single_adapter_support`: one user-supplied adapter supports the leading interval. +- `multi_adapter_consensus`: multiple executed adapters support the same interval; this is still not birth-time truth. + +## Standalone Cap + +In `standalone_core`, the highest allowed label is `manual_pattern_consensus`. +Never label `高置信候选` unless the current user actually provided and executed additional audit adapters. + +## Minimum Standalone Gate + +To move beyond `blocked`, require all of the following: + +- one valid route: `range`, `block`, or `unknown` +- at least three dated events +- at least two domains +- no unresolved tie for first place +- the explicit boundary `不等于唯一真实出生分钟` + +Use `medium` only when there are at least five dated events across at least two domains and the lead interval is clearly ahead. +Use `low` when the evidence points somewhere but remains shallow or fragile. + +## Optional Enhanced Gate + +Use `高置信候选` only when all are visible and actually executed in the current user's environment: + +- at least five dated events across at least two domains +- a clear leading interval without a material tie +- adapter status table with raw-result references +- if used, separate visibility for `Vimshottari + Narayana` +- if used, separate visibility for `Functional Benefic/Malefic`, relevant vargas, MEVG, Real Case Calibration, and holdout +- no decisive conflict from any executed adapter + +Any blocked, conflicting, or missing raw-evidence item caps the output at `manual_pattern_consensus`, `medium`, `low`, `parameter_sensitive`, or `blocked` as appropriate. `高置信候选` **不等于唯一真实出生分钟**. diff --git a/references/upstream/interview_playbook.md b/references/upstream/interview_playbook.md new file mode 100644 index 00000000..6b641cb2 --- /dev/null +++ b/references/upstream/interview_playbook.md @@ -0,0 +1,70 @@ +# Interview Playbook + +## Intake + +Collect birth date, birthplace, timezone, known time/range, source of the time, confidence in that source, and the user's priority domain. Ask permission before collecting dated life events. + +## Routes + +### `range` + +For a remembered time or bounded interval: + +1. Restate the interval in clock time. +2. Split it into `early / middle / late`. +3. Ask 3-5 A/B/C/D questions across education, relocation, relationship, career, and recent learning/expression. +4. Request dated events only in the strongest 2-3 domains. + +### `block` + +For `morning`, `afternoon`, `evening`, family recollections, or zodiac-hour style descriptions: + +1. Convert the label into a stated coarse interval. +2. Split that interval into `early / middle / late`. +3. Ask the same domain questions. +4. Request events from the leading domains only. + +### `unknown` + +For no usable time memory: + +1. State that the first valid result can only be a broad block. +2. Compare `morning / afternoon / evening / night` through high-signal domains. +3. Do not refine the winning block until enough dated evidence exists. +4. If one broad block leads, begin the next round by splitting only that block. + +## Event Ledger + +Use one row per event with: + +- `id` +- `domain` +- `date` +- `precision` (`day`, `month`, `year`) +- `reliability` (`high`, `medium`, `low`) +- `user wording` +- `supports` (candidate labels helped by this event) +- `conflicts` (candidate labels weakened by this event) + +Confirm date precision instead of inventing a day. + +## Question Rhythm + +- First round: 3-5 high-information A/B/C/D questions. +- Second round: ask only for dated events in the strongest domains. +- Third round: ask only the next 1-3 events that could separate the top candidates. +- Never collect a long autobiography before the first route decision. + +## 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 +- an optional adapter was promised but cannot be executed and the user wants an audited result + +## Final Wording + +State: "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。" diff --git a/references/upstream/test_birth_time_rectification_skill_contract.py b/references/upstream/test_birth_time_rectification_skill_contract.py new file mode 100644 index 00000000..7644d3b6 --- /dev/null +++ b/references/upstream/test_birth_time_rectification_skill_contract.py @@ -0,0 +1,271 @@ +import json +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SKILL = REPO_ROOT / "skills" / "jyotish-birth-time-rectification" +if not SKILL.is_dir(): + SKILL = Path("/Users/wuyongnaren/.workbuddy/skills/jyotish-birth-time-rectification") + + +def test_rectification_skill_resources_and_portable_contract_exist() -> None: + skill_text = (SKILL / "SKILL.md").read_text(encoding="utf-8") + for relative_path in ( + "references/main_repository_router.md", + "references/interview_playbook.md", + "references/evidence_thresholds.md", + "references/api_reference.md", + "references/manual_test_prompts.md", + "references/testing_playbook.md", + "references/adapter_integration_guide.md", + "references/public_distribution_checklist.md", + "assets/rectification_session_template.md", + "scripts/example.py", + ): + assert (SKILL / relative_path).is_file(), relative_path + assert relative_path in skill_text + + for required_term in ( + "standalone_core", + "adapter_enhanced", + "main_repository_enhanced", + "range / block / unknown", + "Vimshottari + Narayana", + "Functional Benefic/Malefic", + "MEVG", + "Real Case Calibration", + "候选时间段", + "临时代表时间", + "唯一真实出生分钟", + "高置信候选", + "Require no fixed repository path", + "manual_test_prompts.md", + "testing_playbook.md", + "adapter_integration_guide.md", + "public_distribution_checklist.md", + ): + assert required_term in skill_text + + assert "/Users/wuyongnaren/Documents/印度占星" not in skill_text + + +def test_router_and_thresholds_define_portable_modes_and_caps() -> None: + router = (SKILL / "references/main_repository_router.md").read_text(encoding="utf-8") + for required_term in ( + "standalone_core", + "adapter_enhanced", + "main_repository_enhanced", + "supports", + "conflicts", + "executed", + "blocked", + "not_applicable", + ): + assert required_term in router + assert "/Users/wuyongnaren/Documents/印度占星" not in router + assert ".venv" not in router + + thresholds = (SKILL / "references/evidence_thresholds.md").read_text(encoding="utf-8") + assert "manual_pattern_consensus" in thresholds + assert "高置信候选" in thresholds + assert "不等于唯一真实出生分钟" in thresholds + assert "standalone_core" in thresholds + + +def test_template_and_api_reference_preserve_audit_and_portability() -> None: + template = (SKILL / "assets/rectification_session_template.md").read_text(encoding="utf-8") + for heading in ( + "Runtime mode", + "supports", + "conflicts", + "Functional Benefic/Malefic (optional)", + "External evidence / MEVG / Real Case (optional)", + ): + assert heading in template + + api_reference = (SKILL / "references/api_reference.md").read_text(encoding="utf-8") + for term in ( + "route", + "known_time", + "known_block", + "adapters", + "repository_root", + "main_repository_enhanced", + "candidate_range_not_birth_time_truth", + ): + assert term in api_reference + assert "/Users/wuyongnaren/Documents/印度占星" not in api_reference + + +def test_scaffold_runs_standalone_and_returns_candidate_only_receipt() -> None: + request = { + "route": "range", + "known_time": {"start": "14:00", "end": "16:00"}, + "events": [ + { + "id": "career-1", + "domain": "career", + "date": "2019-07", + "precision": "month", + "reliability": "high", + "supports": ["middle", "late"], + "conflicts": ["early"], + "note": "工作结构变化后责任显著增加", + }, + { + "id": "relationship-1", + "domain": "relationship", + "date": "2016-10-03", + "precision": "day", + "reliability": "high", + "supports": ["middle"], + "conflicts": ["early"], + "note": "关系进入与承诺加强", + }, + { + "id": "relocation-1", + "domain": "relocation", + "date": "2014", + "precision": "year", + "reliability": "medium", + "supports": ["late"], + "conflicts": [], + "note": "离乡发展", + }, + ], + } + result = subprocess.run( + [sys.executable, str(SKILL / "scripts/example.py")], + input=json.dumps(request), + text=True, + capture_output=True, + check=True, + ) + receipt = json.loads(result.stdout) + assert receipt["claim_status"] == "candidate_range_not_birth_time_truth" + assert receipt["summary"]["claim_status"] == "candidate_range_not_birth_time_truth" + assert receipt["runtime_mode"] == "standalone_core" + assert receipt["summary"]["confidence_label"] in {"medium", "manual_pattern_consensus"} + assert receipt["candidate_result"]["candidate_range"]["representative_time"] + + +def test_scaffold_can_report_adapter_enhanced_without_fixed_repo_path() -> None: + request = { + "route": "unknown", + "events": [ + { + "id": "career-1", + "domain": "career", + "date": "2020-05", + "precision": "month", + "reliability": "high", + "supports": ["afternoon"], + "conflicts": ["night"], + "note": "职业方向明显切换", + }, + { + "id": "relationship-1", + "domain": "relationship", + "date": "2018-09", + "precision": "month", + "reliability": "high", + "supports": ["afternoon"], + "conflicts": ["morning"], + "note": "稳定关系建立", + }, + { + "id": "education-1", + "domain": "education", + "date": "2012", + "precision": "year", + "reliability": "medium", + "supports": ["afternoon"], + "conflicts": [], + "note": "学习方向变化", + }, + ], + "adapters": [ + { + "name": "user_vimshottari_engine", + "type": "calculator", + "location": "./tools/rectify.py", + "status": "executed", + "summary": "Vimshottari and Narayana both prefer afternoon", + "raw_result_ref": "outputs/rectify-afternoon.json", + } + ], + } + result = subprocess.run( + [sys.executable, str(SKILL / "scripts/example.py")], + input=json.dumps(request), + text=True, + capture_output=True, + check=True, + ) + receipt = json.loads(result.stdout) + assert receipt["runtime_mode"] == "adapter_enhanced" + assert receipt["summary"]["confidence_label"] == "single_adapter_support" + assert receipt["audit_appendix"]["adapter_statuses"][0]["status"] == "executed" + + +def test_scaffold_main_repository_enhanced_can_execute_with_minimal_valid_payload() -> None: + request = { + "runtime_mode": "main_repository_enhanced", + "repository_root": "/Users/wuyongnaren/Documents/印度占星", + "request": { + "birth_date": "1993-04-17", + "start_time": "14:29", + "end_time": "14:31", + "lat": 36.683333, + "lon": 114.35, + "tz": 8, + "events": [ + {"id": "edu-1", "domain": "education", "date": "2011-09", "precision": "month"}, + {"id": "career-1", "domain": "career", "date": "2019-07-01", "precision": "day"}, + {"id": "rel-1", "domain": "relationship", "date": "2021", "precision": "year"} + ] + } + } + result = subprocess.run( + [sys.executable, str(SKILL / "scripts/example.py")], + input=json.dumps(request), + text=True, + capture_output=True, + check=True, + ) + receipt = json.loads(result.stdout) + assert receipt["runtime_mode"] == "main_repository_enhanced" + assert receipt["claim_status"] == "candidate_range_not_birth_time_truth" + assert receipt["summary"]["claim_status"] == "candidate_range_not_birth_time_truth" + + +def test_scaffold_main_repository_enhanced_returns_structured_block_when_fields_missing() -> None: + request = { + "runtime_mode": "main_repository_enhanced", + "repository_root": "/Users/wuyongnaren/Documents/印度占星", + "request": { + "events": [ + { + "date": "2020-05", + "category": "career", + "description": "职业方向明显切换", + "confidence": "high" + } + ] + } + } + result = subprocess.run( + [sys.executable, str(SKILL / "scripts/example.py")], + input=json.dumps(request), + text=True, + capture_output=True, + check=True, + ) + receipt = json.loads(result.stdout) + assert receipt["runtime_mode"] == "main_repository_enhanced" + assert receipt["claim_status"] == "candidate_range_not_birth_time_truth" + assert receipt["candidate_result"]["status"] == "blocked" + assert receipt["candidate_result"]["reason"] == "missing_required_fields" + assert "birth_date" in receipt["candidate_result"]["missing_fields"]