diff --git a/.build-and-verify/config.json b/.build-and-verify/config.json index 7e978e2..9644b77 100644 --- a/.build-and-verify/config.json +++ b/.build-and-verify/config.json @@ -417,6 +417,26 @@ ], "checkParallel": true, "timeoutSeconds": 300 + }, + { + "id": "verify.worktree-setup", + "command": "set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1&& .\\.venv\\Scripts\\python.exe -m pytest tests\\test_setup_worktree.py", + "paths": [ + ".gitignore", + ".build-and-verify/config.json", + "scripts/setup-worktree.ps1", + "tests/test_setup_worktree.py", + "requirements*.txt" + ], + "inputs": [ + ".gitignore", + ".build-and-verify/config.json", + "scripts/setup-worktree.ps1", + "tests/test_setup_worktree.py", + "requirements.txt", + "requirements-dev.txt" + ], + "checkParallel": true } ] } diff --git a/.gitignore b/.gitignore index 5fe00e3..41b2209 100644 --- a/.gitignore +++ b/.gitignore @@ -72,7 +72,7 @@ $RECYCLE.BIN/ # ======================== .codex/environments/* !.codex/environments/environment.toml -.claude/worktrees/ +.worktrees/ # ======================== # Logs diff --git a/myspec/changes/2026-07-31-share-worktree-dependencies/issues/01-share-root-python-environment.md b/myspec/changes/2026-07-31-share-worktree-dependencies/issues/01-share-root-python-environment.md new file mode 100644 index 0000000..8a69d15 --- /dev/null +++ b/myspec/changes/2026-07-31-share-worktree-dependencies/issues/01-share-root-python-environment.md @@ -0,0 +1,32 @@ +# 01 共享根目录 Python 环境 + +- 状态:ready-for-agent +- 前置票据:无 + +## 用户结果 + +开发者只需在主仓库根目录安装一次约 1.4 GB 的 Python(脚本语言)环境;随后初始化根目录 `.worktrees/` 中的任意工作树,即可通过目录链接运行完整验证,不再重复下载或复制依赖。 + +## 实现范围 + +- 将工作树约定从 `.claude/worktrees/` 统一为根目录 `.worktrees/`。 +- 更新 `scripts/setup-worktree.ps1`: + - 主仓库继续创建和更新根目录 `.venv`; + - 工作树只创建指向主仓库根目录 `.venv` 的目录链接; + - 共享环境缺失或依赖清单不一致时明确停止并给出恢复位置。 +- 更新现有用户入口测试和必要说明。 + +## 验收标准 + +- [ ] 新检出的主仓库可通过 `scripts/setup-worktree.ps1` 创建并准备根目录 `.venv`。 +- [ ] 新工作树位于仓库根目录 `.worktrees/`。 +- [ ] 新工作树运行同一脚本后,`.venv` 是指向主仓库根目录环境的目录链接。 +- [ ] 工作树不会执行 `pip install`,也不会创建独立大型虚拟环境。 +- [ ] 共享环境不存在时,脚本停止并提示在主仓库根目录初始化。 +- [ ] 依赖清单与主仓库不一致时,脚本停止且不修改共享环境。 +- [ ] Build and Verify(构建与验证)通过共享链接完成真实主流程。 + +## TDD(测试驱动开发)证据 + +- [ ] 修改实现前,公开入口检查因缺少工作树链接行为而失败。 +- [ ] 同一检查在最小实现后通过。 diff --git a/myspec/changes/2026-07-31-share-worktree-dependencies/spec.md b/myspec/changes/2026-07-31-share-worktree-dependencies/spec.md new file mode 100644 index 0000000..7266e5d --- /dev/null +++ b/myspec/changes/2026-07-31-share-worktree-dependencies/spec.md @@ -0,0 +1,32 @@ +# 共享工作树 Python 环境 + +## 背景 + +Quant Research Lab(量化实验室)的 Python(脚本语言)虚拟环境位于主仓库根目录 `.venv`,当前约占 1.4 GB。工作树不应重复创建和安装该环境。工作树目录当前由 Claude(代码代理)放在 `.claude/worktrees/`,需要与技能仓库统一为仓库根目录 `.worktrees/`。 + +## 用户结果 + +开发者在主仓库根目录准备一次 `.venv` 后,可以在根目录 `.worktrees/` 创建和初始化任意工作树;工作树通过目录链接直接使用共享环境,无需再次下载或复制大型依赖。 + +## 需求 + +1. 仓库的工作树统一创建在根目录 `.worktrees/`,不再使用 `.claude/worktrees/`。 +2. 主仓库根目录 `.venv` 是唯一 Python(脚本语言)依赖实体。 +3. `scripts/setup-worktree.ps1` 在主仓库根目录运行时继续负责创建或更新根目录 `.venv`。 +4. 该脚本在链接工作树内运行时,只把工作树的 `.venv` 链接到主仓库根目录 `.venv`。 +5. 共享环境不存在时,工作树初始化必须停止,并提示先在主仓库根目录初始化;不得跨工作树修改主仓库环境。 +6. 工作树与主仓库的依赖清单不一致时必须停止,不得静默复用或修改共享环境。 +7. Build and Verify(构建与验证)仍只负责构建和验证,不负责安装依赖。 + +## 验证边界 + +- 从公开入口 `scripts/setup-worktree.ps1` 验证主仓库初始化、工作树链接、缺失环境停止和依赖清单不一致停止。 +- 从 Build and Verify(构建与验证)入口验证通过共享链接完成真实主流程。 +- 先保留因目标行为缺失而失败的检查,再实现使同一检查通过。 + +## 非目标 + +- 不复制或迁移当前正在使用的工作树。 +- 不引入新的 Python(脚本语言)依赖管理工具。 +- 不让工作树自动更新主仓库共享环境。 +- 不为不同工作树创建独立大型虚拟环境。 diff --git a/myspec/specs/joinquant-archive-sync/spec.md b/myspec/specs/joinquant-archive-sync/spec.md index c6bac65..8cb7a9b 100644 --- a/myspec/specs/joinquant-archive-sync/spec.md +++ b/myspec/specs/joinquant-archive-sync/spec.md @@ -1,8 +1,11 @@ # joinquant-archive-sync Specification ## Purpose + TBD - created by archiving change add-joinquant-archive-sync. Update Purpose after archive. + ## Requirements + ### Requirement: 页面对象必须映射到稳定仓库目录 系统 MUST(必须)以 `joinquant/strategies//` 表示一个聚宽策略详情页,并分别以 `backtests//` 和 `simulations//` 表示该策略的回测和模拟交易对象。目录主键 MUST 使用可复核的本地稳定键;会随页面刷新变化的远端传输标识 MUST 作为清单别名保存而不能成为唯一主键。聚宽页面和官方 `get_backtest` 接口没有独立“构建”对象,不得杜撰 `builds/` 或 `sync-build` 协议。 @@ -13,7 +16,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 远端传输标识变化 - **WHEN** 同一页面对象的远端传输标识变化但稳定页面身份和代码摘要未变化 - **THEN** 系统更新别名而不创建重复策略、回测或模拟交易目录 - ### Requirement: 历史运行同步必须由明确目标驱动 系统 MUST 只同步调用者明确指定的历史回测记录。有效目标 SHALL(应当)为策略页面内可复核的序号或详情链接;缺失目标、含义不明确的目标或 `latest`(最新)选择器 MUST 被拒绝。多个目标只有逐项明确列出后才能同步。补录小于当前最大页面序号的历史回测时,系统 MUST 保持 `latest_backtest_id`(最新回测编号)单调不减。 @@ -32,7 +34,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 补录较早历史回测 - **WHEN** 策略索引的最新回测编号为 115,调用者显式同步页面序号 88 - **THEN** 系统归档页面序号 88,同时保持最新回测编号为 115 - ### Requirement: 代码和运行上下文必须完整保存 系统 MUST 保存策略当前默认代码、每个已同步回测的完整代码,以及每个已同步模拟交易的当前代码和完整代码变更历史。回测目录 MUST 包含代码、参数、数据和报告;模拟交易目录 MUST 包含来源回测、代码、参数、快照、代码变更记录、数据和报告。 @@ -47,7 +48,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 模拟交易发生代码变更 - **WHEN** 模拟交易的代码历史新增记录 - **THEN** 系统按远端顺序增量保存所有新增版本及摘要,并保留每条历史记录的 `sourceBacktestId`、操作时间、历史顺序到完整代码文件的可离线复核映射;此前版本不得重复下载或丢失 - ### Requirement: 结构化数据必须按运行状态校验 系统 MUST 对已完成回测保存并校验结果、资金、持仓、订单、自定义记录、风险和分期风险数据。对于失败或取消运行,系统 MUST 根据远端状态验证允许为空的数据集,而不能伪造空表为成功结果。分页 MUST 持续到接口提供明确终止条件。每个运行 MUST 保存 Research 单次原始返回包,并把其 SHA256 绑定到所有结构化数据集;模拟交易增量 MUST 保留不可变来源链,严格验证 MUST 对照原始返回、压缩 JSON 和 Parquet 内容,要求累计行有来源覆盖并从文件复算数据流摘要。活动模拟交易的 `results`(结果)请求重叠点 MUST 来自已验证来源链中的稳定前序时间;清单最高时间行从后续源响应消失时,系统 MUST 使用该前序重叠或安全全量刷新证明连续,同时保持累计事实和数据流高水位单调不回退。 @@ -70,7 +70,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 活动结果临时游标行消失 - **WHEN** 已归档 `results` 同时包含稳定前序行和更晚的临时行,后续 Research 响应不再返回该临时最高时间但包含新增结果 - **THEN** 系统只在已验证来源链提供稳定前序重叠或安全全量刷新能够证明连续时合并提交,保留全部既有已验证事实和新来源事实且不产生业务键重复;否则失败且不推进游标 - ### Requirement: 每种预期数据都必须有独立完整性状态 每个归档对象的 `manifest.json` MUST 列出全部预期数据集,并为每个数据集记录来源、行数或字节数、时间范围、分页证据、文件 SHA256 和 `complete`、`capped_free`、`missing_at_source`、`unsupported_api_version` 或 `failed` 状态。对象只有在必需核心数据均为 `complete` 且其余预期数据均具有可接受的显式状态时才能通过完整性门禁。具有稳定采集围栏但未通过门禁的对象 MUST 使用 `gate.status=fail` 原子保留已验证文件、失败数据集状态和失败证据。部分归档 MUST NOT 通过默认查询或导出路径访问,也 MUST NOT 报告为完整。 @@ -97,7 +96,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 校验或查询部分归档 - **WHEN** 调用方校验或查询 `gate.status=fail` 的对象 - **THEN** 校验过程验证清单结构和引用文件摘要并明确返回 `partial`,默认查询和导出路径则拒绝该对象 - ### Requirement: 日志必须按价值、来源和费用分别处理 系统 MUST 把归因日志作为独立核心数据集,默认完整同步并严格校验,同时完整同步失败运行的错误日志。历史回测 MUST 只读取目标源码明确指定的单一日志,校验 Token(标识)、连续序号、唯一运行边界,并把 `run_end` 的最终资产与该回测 Research(研究环境)最终资金记录关联;关联不一致时不得认定归因日志属于目标回测。模拟交易 MUST 保存完整代码历史及其源码映射,但只能读取和归档模拟交易启动生命周期实际初始化的单一归因路径;后续代码历史中仅出现、却没有生命周期所有权的其他路径不得读取或保存到该模拟交易目录。活动运行不得包含 `run_end`,已结束运行必须且只能包含一个末尾 `run_end`。归属不唯一、身份冲突、断序、运行边界或最终资产关联不符都 MUST 阻断对象完整性门禁。源代码未实现归因写入器时 MUST 以启动代码摘要和判断证据标记 `missing_at_source`,不得伪造补全。普通控制台日志和性能响应 MUST 默认获取全部免费内容;只有后续内容存在但无法免费取得或无法证明分页结束时才能标记 `capped_free`。 @@ -136,7 +134,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 日志响应不是合法 JSON - **WHEN** 日志或性能响应因未转义代码文本而无法按标准 JSON 解析 - **THEN** 系统先原样保存响应,再用容错解析恢复可识别记录,并在清单中记录解析错误和恢复数量 - ### Requirement: 积分日志必须显式选择和确认 系统 MUST 只在调用者明确指定运行和日志类型后请求积分日志,并 MUST 在下载前展示聚宽返回的范围和积分价格。未获得当次明确确认时,系统不得发起消耗积分的导出。 @@ -147,7 +144,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 已确认指定积分导出 - **WHEN** 调用者确认了指定运行、日志类型、范围和聚宽返回的积分价格 - **THEN** 系统明确披露聚宽按完整日志固定计费且不提供远端分段下载;只有调用者确认完整日志范围、当次积分价格和本地保留范围后才下载一次完整日志,在本地只保留指定行范围,并把完整付费源文件、费用确认、选择范围和文件摘要写入清单 - ### Requirement: 活动模拟交易必须由显式手动命令同步 系统 MUST 只在调用者显式运行 `sync-active-simulations --repository <仓库路径>` 时扫描和增量同步全部活动模拟交易。该入口 MUST 复用既有采集、归档清单和完整性门禁;它 MUST NOT 创建或调用 Windows 任务计划程序、专用 worktree(工作树)、自动分支、Git(版本管理)提交或 PR Flow(拉取请求流程)。 @@ -158,7 +154,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 手动同步遇到对象失败 - **WHEN** 任一活动模拟交易在手动同步中未通过采集或完整性门禁 - **THEN** 系统返回失败对象和原因,不创建 Git 提交或 PR,并保留既有已验证归档 - ### Requirement: 浏览器认证和下载不得暴露明文凭证 生产同步 MUST 使用仓库现有 Playwright(浏览器自动化)及仓库外专用持久浏览器目录复用登录状态。认证流程 MAY(可以)只在内存中读取聚宽域名的会话 Cookie(浏览器凭证),并使用 Windows DPAPI(数据保护接口)在仓库外保存只允许当前 Windows 用户解密的密文。系统 MUST NOT(不得)保存用户名、密码、明文 Cookie、`storage_state`(浏览器状态导出)或非聚宽域名 Cookie,也不得打印或提交任何凭证与浏览器配置;同步流程 MUST 不依赖 Codex 会话。大文件 MUST 通过浏览器下载事件进入临时目录而不能通过 Agent 对话传输。 @@ -181,7 +176,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 首次实施验证真实下载能力 - **WHEN** 开始实现生产浏览器和 Research API(研究接口)适配器 - **THEN** 系统先用一个明确指定且存在归因写入器的真实回测验证登录、结构化导出、官方文件下载、完整归因日志和本地落盘;自动下载失败时必须验证人工导入同一暂存协议,两条路径均失败则停止后续实施并报告阻断 - ### Requirement: 归档必须紧凑并可直接查询 系统 MUST 将原始证据保存为 gzip(压缩)JSON/JSONL,将结构化事实保存为 Parquet + Zstd(列式压缩格式),并按需生成读取 Parquet 的 DuckDB(嵌入式分析数据库)视图。压缩原始数据、JSONL 日志和 Parquet MUST 使用 Git LFS(大文件存储),代码、清单和小型文本继续使用普通 Git。系统 MUST 只在调用者指定数据范围时物化 Vibe-Trading 可读取的 CSV,不保存重复 DuckDB 数据库副本。 @@ -192,7 +186,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 为 Vibe-Trading 导出数据 - **WHEN** 调用者指定策略、运行、数据集、字段和时间范围 - **THEN** 系统只生成该范围的 CSV,并记录来源文件摘要和过滤条件 - ### Requirement: 仓库 Skill 必须提供统一同步入口 仓库 MUST 只在 `.agents/skills/joinquant-archive-sync/` 保存一份真实 Skill(技能)目录,并在其中自包含 `SKILL.md`、Python CLI(命令行接口)、运行依赖和参考资料。`.claude/skills/joinquant-archive-sync` MUST 使用同仓库相对 SymbolicLink(符号链接)指向该目录;不得创建 Plugin(插件)、marketplace(市场)、第二份同步脚本或缓存更新层。认证、抓取、解析、落盘、完整性校验和查询逻辑 MUST 只实现一次,Codex、Claude 和人工命令 MUST 调用该同一入口;系统 MUST NOT 提供 Windows 任务计划程序调用入口。 @@ -203,14 +196,12 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: Codex 和 Claude 发现同一 Skill - **WHEN** 分别从 Codex 和 Claude 项目入口加载 `joinquant-archive-sync` - **THEN** 两端解析到同一仓库目录,且 `SKILL.md` 与执行脚本的 SHA256 完全一致 - ### Requirement: 正式运行必须保持在聚宽云端 系统 MUST 只进行策略编写辅助、资料同步、本地归档、查询和复盘,不得在本地把任何结果声明为正式回测或模拟交易结果。 #### Scenario: 本地查询归档数据 - **WHEN** Vibe-Trading 或 Agent 查询本地 Parquet 或 CSV - **THEN** 输出明确保留聚宽运行身份和来源链接,不把本地计算替代为正式云端裁决 - ### Requirement: 端到端回归必须覆盖手动同步入口 实现完成前 MUST 从 Codex、Claude 的仓库 Skill 或手动同步入口执行 `self-test`(自检)端到端回归。`self-test` MUST 在进程内生成小型证据并复用生产同步核心,覆盖明确目标选择、完整性门禁、临时归档、重复同步、查询和 CSV(逗号分隔值)输出;DuckDB(嵌入式分析数据库)使用内存数据库,归档只写系统临时目录,不得访问网络或加载历史归档。单元测试组合不得替代该主流程回归。真实聚宽能力只由首次实施 PoC(概念验证)验证,不进入常规端到端回归。 @@ -225,7 +216,6 @@ TBD - created by archiving change add-joinquant-archive-sync. Update Purpose aft #### Scenario: 手动模拟交易入口回归 - **WHEN** 验收从 `sync-active-simulations` 入口使用受控外部边界执行活动模拟交易同步 - **THEN** 入口验证同一 `.venv`、CLI 路径和完整性门禁,且不访问真实聚宽、创建 Git 提交、PR、专用工作树或 Windows 任务 - ### Requirement: 官方回测摘要必须保留来源和用途边界 系统 MUST 将 `data/official-summary.csv` 作为聚宽回测详情页官方导出源文件的唯一合法路径,`reports/` MUST 只存放人工分析报告。所有回测清单的 `official_summary` 数据集 MUST 明确记录版本化证据,包括详情页下载来源、编码、表头、行数和关联的 Research(研究环境)数据集。系统 MUST 迁移既有官方摘要和清单引用,不得保留 `reports/` 下的旧位置、双路径或旧路径兼容读取。文档 MUST 区分可近似对齐、仅可交叉校验和不可由官方摘要推导的字段,并说明日常分析应读取的权威明细数据集。 diff --git a/myspec/specs/joinquant-docs-sync/spec.md b/myspec/specs/joinquant-docs-sync/spec.md index ae2f848..71df27b 100644 --- a/myspec/specs/joinquant-docs-sync/spec.md +++ b/myspec/specs/joinquant-docs-sync/spec.md @@ -1,8 +1,11 @@ # joinquant-docs-sync Specification ## Purpose + TBD - created by archiving change create-joinquant-docs-sync-skill. Update Purpose after archive. + ## Requirements + ### Requirement: 同步必须由显式官方来源清单驱动 系统 MUST(必须)只读取来源清单中明确列出的聚宽官方公开文档,并为每个来源定义唯一 `source_id`、规范化 URL(网页地址)、目标文件名、正文选择器和最小内容长度。系统 MUST NOT(不得)登录聚宽、读取凭证或访问非清单来源。 @@ -13,14 +16,12 @@ TBD - created by archiving change create-joinquant-docs-sync-skill. Update Purpo #### Scenario: 来源清单存在冲突 - **WHEN** 两个来源具有相同标识、URL(网页地址)或目标文件名 - **THEN** 系统在启动浏览器或写入文件前拒绝执行并报告冲突项 - ### Requirement: 预览不得改变目标目录 系统 MUST 提供 `preview`(预览)命令,渲染并转换全部来源,输出文档和 API(接口)的新增、修改与删除摘要,但不得创建、修改或删除目标目录中的文件。 #### Scenario: 预览检测到远端变化 - **WHEN** 远端规范化内容与本地清单摘要不同 - **THEN** 系统返回变化文件和 API(接口)稳定键,且目标目录内容与摘要保持不变 - ### Requirement: 同步必须验证后再写入 系统 MUST 在写入前验证 HTTP(网页请求)结果、正文选择器、最小长度、错误页特征、Markdown(文档)代码围栏和 API(接口)索引冲突。任一来源失败时 MUST 保留上次完整版本;内容摘要未变化时 MUST 不重写文件。 @@ -35,7 +36,6 @@ TBD - created by archiving change create-joinquant-docs-sync-skill. Update Purpo #### Scenario: 重复同步相同内容 - **WHEN** 所有规范化内容与已记录 SHA-256(完整性摘要)一致 - **THEN** 系统返回零变化并保持现有文件修改时间不变 - ### Requirement: API 索引必须可离线复核 系统 MUST 从规范化文档识别函数、数据表和因子条目,并为每个条目记录 `source_id`、类型、名称、稳定键、标题和签名或表名证据。稳定键 MUST 使用 `source_id:kind:name`,字段行不得被误识别为独立 API(接口)。 @@ -46,21 +46,18 @@ TBD - created by archiving change create-joinquant-docs-sync-skill. Update Purpo #### Scenario: 文档包含数据表和普通字段 - **WHEN** 一个章节包含完整限定表名及其字段表 - **THEN** 系统只为完整数据表生成 API(接口)条目,不为字段名生成独立条目 - ### Requirement: 校验必须从文件重算证据 系统 MUST 提供 `verify`(校验)命令,从目标文件重新计算 SHA-256(完整性摘要)、文档数量和 API(接口)索引稳定键,并与清单比较;不得只信任清单声明。 #### Scenario: 已同步文件被修改 - **WHEN** 文档、API(接口)索引或清单引用文件的内容发生变化 - **THEN** 校验失败并指出不匹配文件,不更新清单 - ### Requirement: 仓库 Skill 必须提供统一发现入口 仓库 MUST 在 `.agents/skills/joinquant-docs-sync/` 保存唯一真实 Skill(技能)目录,并让 `.claude/skills/joinquant-docs-sync` 通过同仓库相对 SymbolicLink(符号链接)指向该目录。Skill(技能)所有动作 MUST 路由到同一 CLI(命令行接口)。 #### Scenario: Codex 和 Claude 发现技能 - **WHEN** Codex(代码代理)和 Claude(代码代理)分别加载 `joinquant-docs-sync` - **THEN** 两端解析到同一 `SKILL.md` 和 `jq_docs_sync.py` 文件摘要 - ### Requirement: 端到端回归必须覆盖发布入口 实现完成前 MUST 从仓库 Skill(技能)公开命令运行离线 `self-test`(自检),在临时目录复用生产转换、索引、写入和校验核心,覆盖预览、首次同步、重复同步和篡改检测。单元测试组合不得替代该主流程回归。 diff --git a/myspec/specs/local-quant-research-runtime/spec.md b/myspec/specs/local-quant-research-runtime/spec.md index 530484f..930b2ed 100644 --- a/myspec/specs/local-quant-research-runtime/spec.md +++ b/myspec/specs/local-quant-research-runtime/spec.md @@ -1,8 +1,11 @@ # local-quant-research-runtime Specification ## Purpose + TBD - created by archiving change refactor-local-research-three-layer-architecture. Update Purpose after archive. + ## Requirements + ### Requirement: 本地研究必须使用三层单向架构 系统 MUST 将本地研究组织为 Strategy Module、Skill 通用能力层和 vectorbt 执行底层,并且依赖只能从 Strategy Module 经共享 contracts 进入 Skill 能力和执行底层。具体策略不得导入 vectorbt 上下文、订单枚举、记录结构或原始 Portfolio;共享能力不得导入具体策略的私有实现。 @@ -13,7 +16,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 第二个策略复用共享能力 - **WHEN** 仓库提供一个实现同一 Strategy Module Interface 的最小测试策略 - **THEN** 共享 CLI、日常超时检查、结果包和停止状态无需修改即可运行该策略 - ### Requirement: vectorbt 必须成为唯一账户账本 系统 MUST 使用 vectorbt `Portfolio.from_order_func()` 处理即时和延迟场景的订单接受、拒绝、部分成交、费用、共享现金、持仓和组合估值。策略只保存决策所需的单位、止损、冻结计划、原因和归因轨迹,不得维护第二套成交、现金、持仓、费用或净值账本。 @@ -32,7 +34,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 复用 vectorbt 持仓与交易访问器 - **WHEN** 共享账本需要 trades、positions 或 returns 视图 - **THEN** 它直接惰性缓存 vectorbt 的现成访问器结果,并默认保留所需记录;没有等价性和性能证据时不得关闭记录后自行重建 - ### Requirement: 每个策略必须只有一个公开 Strategy Module 每个本地研究策略 MUST 暴露一个版本化公开 Strategy Module,负责配置校验、输入准备、订单程序和策略结果扩展。策略 MAY 使用私有实现文件,但外部调用和测试不得把这些私有文件当作稳定 Interface。 @@ -51,14 +52,12 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 子进程加载策略 - **WHEN** 全新 `_execute` 子进程加载唯一 Strategy Module - **THEN** 它把冻结策略根放到 `sys.path` 首位并使用标准 `importlib.import_module()`,不建立 UUID 私有命名空间、全局导入锁或手工模块缓存生命周期 - ### Requirement: 仓库内策略加载不得模拟敌对插件沙箱 Strategy Module MUST 是仓库内受版本管理和代码审查的可信代码。共享运行 MUST 使用受限源码路径、冻结输入、清理环境、全新子进程和超时限制执行策略;它 MUST NOT 安装 Python audit hook(审计钩子)或自行模拟操作系统沙箱。本 capability(能力)不得声称支持第三方不可信策略。 #### Scenario: 执行仓库内策略 - **WHEN** 固定 `_execute` 子进程加载已通过源码边界校验的 Strategy Module - **THEN** 它直接通过标准导入执行,不复制或注入 `adapter_guard`,并继续受输入冻结、环境清理和超时限制 - ### Requirement: 所有项目必须通过固定共享入口运行 系统 MUST 从项目 `.venv` 调用共享本地研究 CLI。项目配置 MUST 声明仓库内 strategy root、module 和 symbol,不得声明策略专属 project entry、任意命令、系统 Python 或隐式依赖安装。 @@ -69,7 +68,6 @@ Strategy Module MUST 是仓库内受版本管理和代码审查的可信代码 #### Scenario: 配置尝试执行任意命令 - **WHEN** 配置包含旧 `command`、策略专属 `project_entry`、仓库外模块路径或安装命令 - **THEN** 系统在启动项目进程前拒绝配置并返回 `evidence_insufficient` - ### Requirement: 单场景停止状态必须保持固定 共享运行 MUST 每次只接受一个场景,并且只返回 `complete`、`evidence_insufficient` 或 `failed`。完整运行 MUST 返回 `next_action=return_to_caller`;Skill 不得循环多个场景、解释策略字段或自动给出研究推荐。 @@ -80,7 +78,6 @@ Strategy Module MUST 是仓库内受版本管理和代码审查的可信代码 #### Scenario: 执行或证据失败 - **WHEN** vectorbt 执行异常、输出缺失、摘要冲突、临时清理失败或性能超限 - **THEN** 系统返回 `failed` 并保留紧凑失败证据,不发布完整运行 - ### Requirement: 重构必须保持结果一致并提供性能观测 系统 MUST 在真实规模、扩展资产和延迟场景校验重构前后结果。Schema、行数、成交、费用、现金、持仓、净值和逻辑摘要 MUST 完全一致;时间、内存和同逻辑核心/扩展 Parquet 数据载荷体积 MUST 在验证报告中列出,由用户人工确认,不设置自动相对门禁。日常 cold/warm 执行 MUST 继续满足各 180 秒超时。 diff --git a/myspec/specs/local-quant-research-workflow/spec.md b/myspec/specs/local-quant-research-workflow/spec.md index 03fb437..9c2e67b 100644 --- a/myspec/specs/local-quant-research-workflow/spec.md +++ b/myspec/specs/local-quant-research-workflow/spec.md @@ -1,8 +1,11 @@ # local-quant-research-workflow Specification ## Purpose + TBD - created by archiving change build-turtle-etf-local-research-workflow. Update Purpose after archive. + ## Requirements + ### Requirement: Skill 编排与共用脚本分层 系统 SHALL(必须)由 `run-local-quant-research` Skill(技能)只负责用户意图、流程顺序、输入输出、停止状态和安全边界;配置契约、运行身份、共享行情中心、项目安全调用和证据收口 SHALL(必须)由 `scripts/research/` 下与具体策略解耦的共用脚本实现。 @@ -21,7 +24,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 通用层不解释策略语义 - **WHEN** 使用非海龟策略模块运行同一流程 - **THEN** Skill、共用脚本和共享行情中心无需海龟资产、参数、信号、风险或报告规则即可完成运行 - ### Requirement: 共享日线行情中心 系统 SHALL(必须)在仓库已忽略的 `.local/market-data/` 提供与任何策略解耦的共享行情中心;完整行情值不得写入公开仓库。首版只实现日线行情,但 SHALL(必须)通过来源、标的类型、频率和显式字段能力允许以后追加其他标的。 @@ -48,7 +50,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 首版范围外的数据类型 - **WHEN** 调用者要求分钟线、基本面、财务或因子数据 - **THEN** 系统明确报告首版不支持,不把未实现的数据类型伪装成日线行情 - ### Requirement: 快照身份与完整性门禁 系统 SHALL(必须)验证快照引用的来源、标的类型、频率、数据截止日、价格口径、字段、证券清单、批次、导出代码摘要和文件 SHA256(文件摘要),不得用隐式默认值补齐缺失身份。 @@ -59,7 +60,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 快照不可追溯或已被修改 - **WHEN** 任一身份字段缺失或任一文件摘要不匹配 - **THEN** 系统拒绝执行研究入口;身份或来源本来就缺失时输出 `evidence_insufficient`,既有文件被篡改或内容不一致时输出 `failed` - ### Requirement: 权威 Parquet 与可重建 DuckDB 视图 系统 SHALL(必须)把每个批次的 `market-data.parquet` 作为本地唯一行情事实源;聚宽导出的 CSV(逗号分隔文件)只允许存在于传输和导入暂存阶段。DuckDB(嵌入式分析数据库)只从权威 Parquet 建立可重建的内存查询视图,不得保存持久数据库副本或第二份权威行情。`batch_id` SHALL(必须)绑定规范化逻辑内容与来源契约,Parquet 字节摘要 SHALL(必须)单独用于文件完整性验证,避免编码器版本变化静默改变逻辑身份。 @@ -94,7 +94,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 禁止持久 DuckDB 副本 - **WHEN** 查询或研究运行结束 - **THEN** `.local/market-data/` 中不存在作为长期事实源的 `.duckdb` 文件,后续查询可仅凭快照清单和权威 Parquet 重建 - ### Requirement: 唯一三态收口 每次运行 SHALL(必须)且只能以 `complete`、`evidence_insufficient` 或 `failed` 之一收口;流程状态不得与项目研究建议混为一谈。 @@ -109,7 +108,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 完整成功 - **WHEN** 输入门禁、项目流程、声明输出、摘要校验和证据固化全部通过 - **THEN** 系统输出 `complete` - ### Requirement: 不可变且原子固化的研究证据 系统 SHALL(必须)以快照摘要、生产配置摘要、规范化单场景配置摘要、自动发现的策略/共享运行时代码摘要和执行后端身份生成 `run_id`,先在暂存位置生成产物,全部校验通过后一次性固化包含输入、状态、结果包路径和输出摘要的不可变证据;不同场景配置不得复用同一 `run_id`。 @@ -136,7 +134,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 失败后重试 - **WHEN** 调用者重试一个 `failed` 或 `evidence_insufficient` 运行 - **THEN** 系统保留原尝试证据并创建新的尝试记录;只有新尝试全部通过才可固化为 `complete` - ### Requirement: 共享 vectorbt 后端与策略模块分层 通用本地研究流程 SHALL(必须)把 vectorbt(向量化回测框架)`Portfolio.from_order_func()` 作为即时和后续执行的唯一账户账本。共享 runtime 负责成交、费用、现金、持仓和净值;策略模块只提供准备、订单程序、后续计划和版本化结果扩展,不维护第二套账户事实。 @@ -155,7 +152,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 更换后端不改变通用分析契约 - **WHEN** 项目从旧逐日实现迁移到 vectorbt 后端 - **THEN** 共享 writer 输出与聚宽现有归档同名同义的 `results`、`balances`、`positions`、`orders` 四类共同执行事实和独立 `local-research-package/2` 清单;既有聚宽归档无需改动,分析层无需解释 vectorbt 对象 - ### Requirement: 每次 Skill 调用只交付一个场景结果 本地研究流程 SHALL(必须)每次只接受一个策略项目、一个快照和一个场景配置,只编排一次项目执行、单份兼容结果校验、运行身份和证据收口,不接收候选数组、不循环多个场景,也不调用或包含策略分析 Skill(技能)。`scripts/research/local_quant_research/` 和策略项目不得导入绩效、归因、稳健性、压力、证据矩阵、报告或推荐算法。 @@ -188,7 +184,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda - **WHEN** 两个调用场景的证券集合不同 - **THEN** 每个调用只接收与自身证券集合完全一致的不可变快照 - **AND** 快照共享相同不可变批次、截止日、字段和价格口径,重叠证券行情及公司行动摘要必须完全一致 - ### Requirement: 仓库运行与能力复用边界 系统 MUST(必须)使用项目 `.venv`(虚拟环境)运行本地 Python(编程语言)入口,并复用既有聚宽认证和归档能力,不得保存或打印账号、密码、Token(访问令牌)或 Cookie(浏览器凭证)。 @@ -199,7 +194,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 运行环境或依赖缺失 - **WHEN** 项目 `.venv` 不存在或明确必需依赖不可用 - **THEN** 系统报告具体缺项并停止,不回退到系统 Python 或静默安装依赖 - ### Requirement: Skill 结构和通用性验证 实现 SHALL(必须)以 `.agents/skills/run-local-quant-research/` 中的元数据、`.claude/skills/` 兼容链接、仓库布局测试、契约测试、确定性回归、共享行情链路回归和用户入口 E2E(端到端)回归验证 `run-local-quant-research` Skill(技能)。组合式 E2E(端到端)夹具 SHALL(必须)先通过共享行情能力完成 CSV(逗号分隔文件)暂存导入、Parquet(列式文件)固化和不可变快照创建,再通过 Skill 文档公开的 `run` 入口执行本地研究。仓库 `verify --full`(全量验证)MUST 保留全部测试,只在本机进程及其子进程内执行,不得联网调用外部系统;其性能预算为 60 秒,超预算 MUST(必须)记录性能报告和告警,但不得覆盖各检查决定的功能通过状态。 diff --git a/myspec/specs/local-research-archive-promotion/spec.md b/myspec/specs/local-research-archive-promotion/spec.md index c365739..3acf6aa 100644 --- a/myspec/specs/local-research-archive-promotion/spec.md +++ b/myspec/specs/local-research-archive-promotion/spec.md @@ -1,8 +1,11 @@ # local-research-archive-promotion Specification ## Purpose + TBD - created by archiving change refactor-local-research-three-layer-architecture. Update Purpose after archive. + ## Requirements + ### Requirement: 完成本地研究必须能够显式晋升为策略档案 共享 CLI MUST 提供独立晋升动作,只接受已通过 `complete` 门禁且清单验证成功的 `.local/quant-research///`。目标 MUST 为 `joinquant/strategies//research/archives//`,并且 `analysis_id` 必须由调用者显式提供。 @@ -13,7 +16,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 尝试晋升失败运行 - **WHEN** 源运行缺少 `complete` 状态、必需文件或有效摘要 - **THEN** 系统拒绝晋升,不创建目标档案且不修改源运行 - ### Requirement: 策略档案必须对分析自包含 每个档案 MUST 包含不可变清单、完整策略源码、运行配置、代码身份、四张核心事实表、全部声明策略扩展、性能证据、环境证据、行情快照身份和机械执行报告。调用者 MUST 能在不访问 `.local` 的情况下查询结果、比较参数、检查策略代码和重新生成机械执行报告。 @@ -28,7 +30,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 机械执行报告保持事实边界 - **WHEN** 共享流程为完成包生成 `report/` 内容 - **THEN** 报告只包含可从包内复核的运行身份、参数、数据范围、成交与持仓统计、净值摘要、性能和完整性事实,不生成策略推荐、稳健性结论或实盘准入判断 - ### Requirement: 晋升不得重新计算研究结果 晋升 MUST 只复制并校验完成运行中已经固化的字节。它不得加载 Strategy Module、调用 vectorbt、重新生成核心事实或扩展、重新序列化 Parquet、重新计算报告指标,且不得计入回测冷启动或预热耗时。 @@ -39,7 +40,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 共享行情保持单份 - **WHEN** 晋升一个使用共享行情快照的运行 - **THEN** 档案只复制快照身份、来源、范围、字段、价格口径和摘要,不复制共享 market-data.parquet - ### Requirement: 晋升必须不可变、幂等且冲突安全 晋升 MUST 先在目标同级暂存目录复制全部文件并复核摘要,再以原子目录替换发布。相同 analysis_id 和相同内容 MUST 返回复用;相同 analysis_id 但内容不同 MUST 失败且不得覆盖、合并或生成隐式后缀。 @@ -60,7 +60,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 源树包含不支持的文件对象 - **WHEN** 晋升前扫描发现符号链接、目录连接、硬链接或非普通文件 - **THEN** 系统在复制前拒绝晋升且不创建完成目标 - ### Requirement: 本地研究档案必须与聚宽正式运行隔离 本地研究档案 MUST 只写入 `research/archives/`,其清单和报告 MUST 明确标记本地探索性 vectorbt 来源。正式聚宽回测和模拟交易 MUST 继续分别写入 `backtests/` 和 `simulations/`,分析层不得因目录位于同一策略下而混淆运行身份。 diff --git a/myspec/specs/local-research-result-package/spec.md b/myspec/specs/local-research-result-package/spec.md index 193239d..56d0dea 100644 --- a/myspec/specs/local-research-result-package/spec.md +++ b/myspec/specs/local-research-result-package/spec.md @@ -1,8 +1,11 @@ # local-research-result-package Specification ## Purpose + TBD - created by archiving change refactor-local-research-three-layer-architecture. Update Purpose after archive. + ## Requirements + ### Requirement: 共享结果包必须定义后端中立的核心事实 系统 MUST 用版本化 Schema 定义结果、资金、持仓和订单四张核心事实表,并允许策略通过版本化扩展增加归因等证据。核心 writer 不得导入具体策略动作码,策略扩展不得改变核心表的字段含义。 @@ -13,7 +16,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 最小策略不提供归因 - **WHEN** 第二个测试策略只提供核心执行账本而没有策略扩展 - **THEN** 共享 writer 仍生成合法完整结果包,并明确声明没有对应扩展 - ### Requirement: 策略扩展必须使用有边界的 Arrow 契约 每个 `ResultExtension.table` MUST 只包含扁平 `string`、`bool`、`int64` 或 `float64` 列。浮点缺失值 MUST 使用 Arrow null,不得使用 NaN。dictionary、list、struct、map、union、run-end encoded 及其他类型 MUST 在冷/热比较前以 `result_contract_failed` 拒绝;共享层不得实现递归 Arrow 类型解释器或任意类型逻辑哈希。 @@ -24,7 +26,6 @@ TBD - created by archiving change refactor-local-research-three-layer-architectu #### Scenario: 拒绝越界扩展 - **WHEN** 扩展包含 NaN、嵌套、字典编码或其他未允许类型 - **THEN** 系统在冷/热确定性比较和 Parquet 写入前返回 `failed` 与 `result_contract_failed` - ### Requirement: 账本视图和事实数据只能物化一次 ExecutionLedger MUST 对 orders、assets、cash 和 value 使用只读惰性缓存;共享 writer MUST 直接消费这些视图并只执行一次公共事实转换、一次 Parquet 固化和一次回读。内部 writer MUST 复用这次回读事实完成 Schema、唯一键、跨表勾稽、报告和最终清单;公开 validator MUST 只从磁盘读取并供复用、晋升和外部查询。运行路径不得通过 JSON、字典或 Arrow 往返重新构造账本,不得在多层重复复制完整矩阵,也不得通过 `preloaded_*` 参数或 provisional/final 两套完整包维护第二条校验路径。 @@ -35,7 +36,6 @@ ExecutionLedger MUST 对 orders、assets、cash 和 value 使用只读惰性缓 #### Scenario: 策略不需要某类轨迹 - **WHEN** Strategy Module 未声明某个策略审计字段 - **THEN** 执行底层不为该字段分配行数乘证券数的稠密矩阵 - ### Requirement: 结果包必须可验证并原子发布 系统 MUST 为每个数据集记录路径、状态、行数、时间范围、Schema 版本和 SHA256,并在暂存目录完成写入、回读、跨表校验和核心事实摘要后原子发布。核心事实确定性使用 NumPy 数组摘要;扩展确定性使用精确 Schema 与 `Table.equals(check_metadata=True)`;文件完整性使用最终 Parquet 字节的 SHA256。任何校验或发布失败 MUST 清理暂存目录且不得留下可被识别为完整的结果。 @@ -46,7 +46,6 @@ ExecutionLedger MUST 对 orders、assets、cash 和 value 使用只读惰性缓 #### Scenario: 回读事实不一致 - **WHEN** Parquet 回读后的核心事实摘要、扩展表或文件摘要与内存事实和最终清单不同 - **THEN** 系统返回 `failed`,删除暂存结果且不覆盖既有完整运行 - ### Requirement: 共享分析必须读取本地和聚宽结果 analysis_data MUST 通过后端中立清单读取新的本地结果包和既有聚宽归档,并为相同概念提供一致查询视图。任何来源差异、缺失数据集和公式版本 MUST 保留显式证据,不得伪造成相同来源或静默补全。 @@ -57,7 +56,6 @@ analysis_data MUST 通过后端中立清单读取新的本地结果包和既有 #### Scenario: 使用 vectorbt 统计交叉校验 - **WHEN** 分析流程调用 vectorbt returns 或 stats 复核收益指标 - **THEN** 结果被标记为交叉校验,不静默替换现有 Alpha、Information Ratio、CVaR 或其他公式版本 - ### Requirement: 结果包必须记录诚实的日常性能证据 结果包内 `performance.json` MUST 记录从 writer 启动到最终 evidence/report/manifest 写入前的 `prefinalization_seconds` 及各阶段耗时,不得宣称包含尚未发生的自身写入或父进程发布。日常 cold/warm 检查 MUST 使用 writer 返回时的完整耗时,覆盖策略执行、核心事实、策略扩展、Parquet 写入、回读校验、摘要和最终元数据写入。系统不得为发布性能比较恢复 provisional/final 双包、第二次元数据写入或旁路清单;发布级性能差异只在外部验证报告中列示并由用户人工确认。 diff --git a/myspec/specs/standard-strategy-analysis-data/spec.md b/myspec/specs/standard-strategy-analysis-data/spec.md index b7c4d24..7c5944f 100644 --- a/myspec/specs/standard-strategy-analysis-data/spec.md +++ b/myspec/specs/standard-strategy-analysis-data/spec.md @@ -3,7 +3,9 @@ ## Purpose 定义本地研究、远端归档或其他执行流程向量化分析交接结果时使用的最小标准结果包契约,使分析只依赖包内身份、冻结参数、共同事实和完整性证据,而不依赖生产流程或目录结构。 + ## Requirements + ### Requirement: 标准结果包是唯一分析交接对象 任何生产流程 MAY(可以)生成标准结果包,结果包 MAY(可以)存放在任意可读位置。量化分析 MUST NOT(不得)要求了解生产流程、归档目录结构、执行引擎或来源类型,也不得直接把生产流程的临时运行对象当作分析输入。 @@ -12,7 +14,6 @@ - **WHEN** 不同生产流程生成内容和身份相同的标准结果包 - **THEN** 分析读取器以相同方式打开并计算,不进入生产者专用分支 - ### Requirement: 结果包必须自包含并可校验 完整结果包 MUST(必须)包含:自证策略和场景身份的 `manifest.json`(清单)、内容摘要、冻结的 `config/scenario.json`(场景配置)、完成门禁,以及摘要绑定的核心数据文件。读取器 MUST(必须)拒绝未知契约、未完成门禁、摘要漂移、越界路径和身份冲突。 @@ -21,7 +22,6 @@ - **WHEN** 完整结果包在不改变任何包内字节的情况下移动 - **THEN** 包内身份和内容摘要保持有效 - ### Requirement: 共同事实保持最小且稳定 结果包 MUST(必须)提供 `results`(收益)、`balances`(资产)、`positions`(持仓)和 `orders`(订单)四类共同事实。累计策略收益在查询时派生单日收益;合法空持仓和空订单必须可验证。官方风险、基准、交易明细或策略专用事件 MUST NOT(不得)为了填满通用表而伪造。 @@ -30,7 +30,6 @@ - **WHEN** 生产流程没有官方风险或成本执行证据 - **THEN** 结果包或分析能力将其标记为 `missing_at_source`(来源缺失),共同分析继续运行 - ### Requirement: 策略专用证据使用命名扩展 结果包 MAY(可以)包含任意名称的扩展。每个扩展 MUST(必须)声明版本、字段、唯一键、文件摘要、行数和时间范围。通用读取器按字段能力识别扩展,不把某个策略的字段强加给其他策略。 @@ -39,7 +38,6 @@ - **WHEN** 扩展含有效的 `time`、`event_id` 和 `event_type` - **THEN** 读取器暴露归因能力,并保留 `security`(证券)、`reason_code`(原因代码)、风险字段和 `details_json`(详细数据)等已存在字段 - ### Requirement: 分析计划和基准与结果包分离 分析计划定义要比较的场景和分析参数;独立基准清单定义基准收益与口径。分析计划及其声明的基线配置组成可移动的计划包,基线配置 MUST(必须)相对计划文件解析。二者 MUST(必须)作为显式输入单独校验,不写入或改写结果包。分析身份 MUST(必须)由结果包内容摘要、计划摘要、基线配置摘要、基准摘要和公式版本共同派生,不包含路径或生产者信息;本地交付目录不得参与分析身份。 diff --git a/myspec/specs/standard-strategy-analysis-workflow/spec.md b/myspec/specs/standard-strategy-analysis-workflow/spec.md index 5ef3d65..8b7aa50 100644 --- a/myspec/specs/standard-strategy-analysis-workflow/spec.md +++ b/myspec/specs/standard-strategy-analysis-workflow/spec.md @@ -3,7 +3,9 @@ ## Purpose 定义量化分析如何以生产者和存储位置无关的方式,只读消费显式标准结果包、分析计划与独立基准,并产出可重复、可追溯的收益、风险、归因和稳健性结论。 + ## Requirements + ### Requirement: 分析输入必须与结果包位置和生产者解耦 系统 MUST(必须)只要求调用方显式提供一个或多个标准结果包、一个分析计划和一个独立基准清单。结果包的生产者、父目录、文件名和传入路径 MUST NOT(不得)参与场景判断、计算分支或分析身份。读取器 MUST(必须)直接按标准结果包契约校验共同事实,不得经由生产者类型、执行引擎或遗留来源清单分支。 @@ -22,7 +24,6 @@ - **WHEN** 调用方未提供结果包、分析计划或基准清单 - **THEN** 系统停止并报告缺失项,不扫描目录或猜测 `latest`(最新)对象 - ### Requirement: 结果包必须自证身份和完整性 系统 MUST(必须)从包内清单读取策略、场景和内容摘要,从冻结场景配置读取参数,并验证完成门禁和文件摘要。包内参数 MUST(必须)与分析计划中同一场景的展开参数完全一致;结果包的场景标识集合 MUST(必须)与分析计划的展开场景集合完全一致。重复场景、重复内容、身份不一致、缺少计划场景或额外场景 MUST(必须)被拒绝。 @@ -36,7 +37,6 @@ - **WHEN** 分析计划包含调用方未提供结果包的场景 - **THEN** 系统在生成交付前停止,并报告缺少和额外场景 - ### Requirement: 共同分析和扩展能力必须由数据契约驱动 系统 MUST(必须)使用 `results`(收益)、`balances`(资产)、`positions`(持仓)和 `orders`(订单)共同事实运行统一计算。扩展能力 MUST(必须)按声明字段识别;归因扩展的最小字段为 `time`(时间)、`event_id`(事件标识)和 `event_type`(事件类型),不得依赖扩展名称、执行引擎或生产者类型。 @@ -50,7 +50,6 @@ - **WHEN** 结果包缺少某项分析所需扩展 - **THEN** 系统将该项标记为 `evidence_insufficient`(证据不足),不阻断无依赖的共同分析 - ### Requirement: 来源原生亏损事件必须可审计 系统 MUST(必须)按来源原生 `valuation.security_daily_pnl`(估值事件单标的日盈亏)从小到大保留全部亏损事件,并在 Markdown(标记文档)报告中展示损失最大的前十条及事件总数。系统 MUST(必须)标识来源明确声明的退出事件,按亏损日期展示来源单标的盈亏合计、组合日盈亏和差额,并以 `0.02` 为勾稽容差。入场、事件前止损线、前一交易日信号、实际成交价或止损失败损失缺少语义匹配的来源字段时,系统 MUST(必须)逐字段标记 `evidence_insufficient`(证据不足),不得从活跃持仓或订单重建完整交易。 @@ -64,7 +63,6 @@ - **WHEN** 亏损事件缺少入场、事件前止损线、前一交易日信号、实际成交价或语义匹配的止损失败损失 - **THEN** 报告逐字段显示证据不足,不从共同持仓或订单事实推断 - ### Requirement: 成本执行压力必须复用标准场景契约 佣金、单边滑点或额外交易日延迟压力 MUST(必须)作为分析计划中 `dimension=cost_execution`(成本执行)的普通场景声明。冻结场景配置 MUST(必须)是压力参数的唯一事实;分析不得维护第二份成本定义、从基线结果推导延迟成交或重新运行研究。 @@ -78,7 +76,6 @@ - **WHEN** 计划声明的成本执行场景缺少结果包,或冻结配置与计划展开参数不一致 - **THEN** 系统按标准场景输入规则停止,不启动上游研究或在分析端补算 - ### Requirement: 硬门禁与诊断项必须分离 基线、参数场景和成本执行场景 MUST(必须)同时满足计划声明的 CAGR、最大回撤和 Calmar 门槛。滚动窗口硬门禁 MUST(必须)检查正 CAGR 窗口占比和任一窗口最大回撤,不增加 Calmar 门槛。固定时期、资产删除、Bootstrap(区块自助抽样)、历史压力、持仓冲击和 CVaR(条件风险价值) MUST(必须)作为诊断项交付,其失败或证据不足不得单独否决进入聚宽确认的建议。 @@ -92,7 +89,6 @@ - **WHEN** 基线、参数场景、成本执行场景或滚动窗口硬门禁失败或证据不足 - **THEN** 系统建议修订后再进入聚宽确认 - ### Requirement: 分析交付必须确定、可追溯且只读 系统 MUST(必须)把结果包内容摘要、分析计划摘要、基准摘要、脚本版本和证据矩阵写入确定性 JSON(结构化数据)及 Markdown(标记文档)报告。分析前后输入身份不一致时 MUST(必须)停止。分析 MUST NOT(不得)修改结果包、启动上游流程、联网或读取凭证。 diff --git a/myspec/specs/turtle-etf-local-research/spec.md b/myspec/specs/turtle-etf-local-research/spec.md index 5032870..24228de 100644 --- a/myspec/specs/turtle-etf-local-research/spec.md +++ b/myspec/specs/turtle-etf-local-research/spec.md @@ -1,8 +1,11 @@ # turtle-etf-local-research Specification ## Purpose + TBD - created by archiving change build-turtle-etf-local-research-workflow. Update Purpose after archive. + ## Requirements + ### Requirement: 海龟项目使用固定 11 ETF 基线 海龟项目 SHALL(必须)只从项目配置读取证券和分组。当前基线 SHALL 固定为 11 只 ETF 和 6 个资产组,不按动量、信号时间或代码顺序筛选突破标的。 @@ -11,7 +14,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda - **WHEN** 项目加载 `baseline.json` - **THEN** 证券必须为 `510300.XSHG`、`512100.XSHG`、`512480.XSHG`、`159819.XSHE`、`516160.XSHG`、`513100.XSHG`、`513180.XSHG`、`515180.XSHG`、`516080.XSHG`、`518880.XSHG`、`511010.XSHG` - **AND** 分组必须与项目配置逐项一致,17 ETF 扩展不得进入当前基线 - ### Requirement: 行情使用未复权事实和连续经济执行口径 项目 SHALL 使用共享 Parquet(列式文件)快照中的原始未复权 OHLC、`pre_close`、`factor`、停牌和涨跌停事实,并在内存中使用应用日可见原始行情派生连续经济价格。成交额和流动性 SHALL NOT(不得)进入海龟策略输入或订单规则。 @@ -29,7 +31,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 无策略层流动性规则 - **WHEN** 成交额很低、缺失或超过任意假设比例 - **THEN** 海龟策略不得缩小、拒绝或延迟入场、加仓、退出和止损;不存在单笔成交额 1% 规则 - ### Requirement: 信号固定为 55/20/20 海龟规则 项目 SHALL 使用此前 55 日最高价突破入场、此前 20 日最低价退出和 20 日递推 N。所有信号 SHALL 在收盘检查并错位到下一交易日开盘执行。 @@ -45,7 +46,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 无前视成交 - **WHEN** T 日收盘形成入场、加仓、退出或止损信号 - **THEN** 订单只能使用 T 日及以前信息,并在 T+1 日开盘执行;基线不增加额外延迟 - ### Requirement: 每个逻辑单位冻结自己的 N 风险 项目 SHALL 为每只 ETF 保存最多 4 个逻辑单位。单位候选基础数量 MUST(必须)为 `floor_to_100_shares(signal_equity × 1% / signal_n)`,且只有真实买入成交才建立单位。 @@ -57,7 +57,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 候选未成交 - **WHEN** 停牌、涨停、现金缩放、整手取整或官方拒单使候选没有净新增成交 - **THEN** 项目不得建立单位、推进加仓档位或更新共同止损,并在后续交易日重新检查 - ### Requirement: 加仓使用固定 0.5N 档位 项目 SHALL 以首次实际成交价和首次信号日 N 冻结 `0.5N`、`1.0N`、`1.5N` 三个后续档位。每只 ETF 每日最多新增一个单位,最多 4 个单位。 @@ -69,7 +68,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 四单位后停止加仓 - **WHEN** 单标的已有 4 个逻辑单位 - **THEN** 项目不得再生成加仓候选,但继续每日检查止损和 20 日趋势退出 - ### Requirement: 保护性止损按逐单位冻结 N 只上移 每个真实成交的入场或加仓单位 SHALL 生成 `actual_fill_price - 2 × frozen_signal_n` 候选止损,共同止损 SHALL 取历史候选止损最大值。 @@ -85,7 +83,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 保护性完整退出 - **WHEN** 收盘价小于或等于共同止损 - **THEN** 项目在下一交易日开盘尝试完整退出;只有全部实际持仓退出后才清除单位、档位和止损状态 - ### Requirement: 风险预算使用 4/6/12 N 单位 项目 SHALL 使用单标的最多 4 个逻辑单位、资产组最多 6 个有效单位、组合最多 12 个有效单位。资产组比例 SHALL 先计算,组合比例 SHALL 再对组缩放后的有效单位计算。 @@ -101,7 +98,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 输入顺序不影响结果 - **WHEN** 同一单位簿以不同证券列顺序输入 - **THEN** 还原证券顺序后的组比例、组合比例、现金比例和目标数量必须一致 - ### Requirement: 组合事件执行全量仓位再分配 有效入场、有效加仓、保护性止损或趋势退出 SHALL 触发一次全组合目标重算。无事件日 SHALL NOT 调仓。 @@ -118,7 +114,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda - **WHEN** 4/6/12 缩放后的目标需要超过可用现金 - **THEN** 项目将预计卖出净收入、买入价、滑点和每笔佣金纳入计算,在 `[0,1]` 上求全部可调整目标共同的最大可行现金比例,按 100 股向下取整 - **AND** 不得使用余额补仓、最大余数分配、融资或按代码顺序买到现金耗尽 - ### Requirement: 动作顺序和状态隔离固定 同一组合事件 SHALL 按“完整退出、再分配卖出、入场或加仓、再分配买入”执行。再分配动作 SHALL 只改变实际持仓。 @@ -134,7 +129,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 同标的退出覆盖买入 - **WHEN** 同一 ETF 同日同时满足退出和入场或加仓条件 - **THEN** 只保留完整退出,取消该 ETF 买入候选 - ### Requirement: 市场不可交易时失败安全 项目 SHALL 根据执行日开盘价、停牌、涨停和跌停决定订单可交易性,状态 SHALL 只按真实成交更新。 @@ -150,7 +144,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 官方拒单 - **WHEN** vectorbt 官方订单结果为拒绝或零成交 - **THEN** 项目记录拒单原因,单位、档位和止损不得推进 - ### Requirement: 使用 vectorbt 官方生命周期作为唯一生产本地执行路径 项目 SHALL 使用 `Portfolio.from_order_func()`、一个共享现金组和 Numba 回调执行基线。项目 SHALL NOT 另建订单、现金、持仓和权益引擎,也 SHALL NOT 保留旧执行兼容层。 @@ -162,7 +155,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 额外延迟保持冻结动作 - **WHEN** 研究配置显式增加一个或多个交易日延迟 - **THEN** 项目冻结原动作、目标、原因和 N,延迟日只按开盘、费用、现金、持仓和可交易性机械执行;再分配动作仍不得改变海龟单位状态 - ### Requirement: 旧交易控制从生产路径物理删除 项目 SHALL NOT 在配置、输入、回调、结果适配或分析订单路径中保留资金仓位上限、计划风险上限、交易用协方差、最低对齐样本、目标波动率、强制波动率减仓或旧新增订单分配。 @@ -170,7 +162,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 旧字段被拒绝 - **WHEN** 风险配置包含任一旧字段 - **THEN** 项目必须以明确错误拒绝,不得用极大值、`null`、默认值或回退路径继续 - ### Requirement: 结果包保持标准四表并增加海龟归因 项目 SHALL 输出聚宽口径兼容的 `results`、`balances`、`positions`、`orders` 四类共同事实,海龟专用信息 SHALL 只写入归因扩展。 @@ -182,7 +173,6 @@ TBD - created by archiving change build-turtle-etf-local-research-workflow. Upda #### Scenario: 聚宽结果无需海龟扩展 - **WHEN** 独立策略分析读取没有海龟归因的现有聚宽结果 - **THEN** 通用收益和实际暴露分析仍可运行,海龟单位指标返回缺失或零,不要求聚宽结果改动 - ### Requirement: 单场景性能与完整入口验收 项目 SHALL 从公开本地研究入口完成一个场景,冷启动和预热 SHALL 分别计时并都不超过 180 秒,规范化结果摘要 SHALL 一致。 diff --git a/myspec/specs/worktree-python-environment/spec.md b/myspec/specs/worktree-python-environment/spec.md new file mode 100644 index 0000000..168e995 --- /dev/null +++ b/myspec/specs/worktree-python-environment/spec.md @@ -0,0 +1,37 @@ +# Worktree Python Environment + +## Purpose + +让开发者在仓库根目录集中维护一次 Python 环境,并由根目录 `.worktrees/` 中的工作树安全复用。 + +## Requirements + +### Requirement: 工作树共享主仓库 Python 环境 + +仓库 SHALL 将主仓库根目录 `.venv` 作为唯一 Python 依赖实体,并让根目录 `.worktrees/` 中的工作树通过目录链接复用该环境,而不复制或安装独立依赖。 + +#### Scenario: 初始化主仓库环境 + +- **WHEN** 开发者在主仓库根目录运行 `scripts/setup-worktree.ps1` +- **THEN** 脚本创建或更新根目录 `.venv` 并记录其对应的依赖清单 + +#### Scenario: 初始化工作树环境 + +- **WHEN** 开发者在依赖清单一致的链接工作树中运行 `scripts/setup-worktree.ps1` +- **THEN** 脚本将工作树 `.venv` 链接到主仓库根目录 `.venv`,且不安装或修改共享环境中的依赖 +### Requirement: 工作树拒绝不安全的共享环境 + +工作树初始化 MUST 在共享环境缺失、陈旧或依赖清单与主仓库不一致时停止,并提示开发者先在主仓库根目录准备环境。 + +#### Scenario: 共享环境不可复用 + +- **WHEN** 工作树运行 `scripts/setup-worktree.ps1`,且共享环境缺失、未对应当前依赖清单或工作树清单与主仓库不同 +- **THEN** 脚本以失败状态停止,不创建工作树环境且不修改主仓库环境 +### Requirement: 共享环境支持仓库验证入口 + +Build and Verify SHALL 仅通过工作树的共享 `.venv` 链接执行构建和验证,不负责安装依赖。 + +#### Scenario: 从工作树执行验证 + +- **WHEN** 工作树已链接主仓库共享环境并运行 Build and Verify +- **THEN** 验证通过该链接完成真实检查流程,且不会安装依赖 diff --git a/scripts/setup-worktree.ps1 b/scripts/setup-worktree.ps1 index 74aefd2..77ab656 100644 --- a/scripts/setup-worktree.ps1 +++ b/scripts/setup-worktree.ps1 @@ -4,19 +4,60 @@ param() $ErrorActionPreference = 'Stop' $projectRoot = Split-Path -Parent $PSScriptRoot $python = Join-Path $projectRoot '.venv\Scripts\python.exe' +$manifests = 'requirements.txt', 'requirements-dev.txt' + +function Get-DependencyFingerprint([string]$root) { + foreach ($manifest in $manifests) { + "$(Get-FileHash (Join-Path $root $manifest) | Select-Object -ExpandProperty Hash) $manifest" + } +} Push-Location $projectRoot try { + $gitDir = [IO.Path]::GetFullPath((git rev-parse --git-dir), $projectRoot) + $commonGitDir = [IO.Path]::GetFullPath((git rev-parse --git-common-dir), $projectRoot) + if ($gitDir -ne $commonGitDir) { + $mainRoot = Split-Path -Parent $commonGitDir + $sharedVenv = Join-Path $mainRoot '.venv' + if (-not (Test-Path (Join-Path $sharedVenv 'Scripts\python.exe'))) { + Write-Error 'Shared Python environment is missing; run scripts/setup-worktree.ps1 in the main repository first.' + } + foreach ($manifest in $manifests) { + if ((Get-FileHash (Join-Path $projectRoot $manifest)).Hash -ne (Get-FileHash (Join-Path $mainRoot $manifest)).Hash) { + Write-Error "Worktree and main repository dependency manifests differ: $manifest" + } + } + $fingerprintPath = Join-Path $sharedVenv '.requirements.sha256' + if (-not (Test-Path $fingerprintPath) -or (Compare-Object (Get-Content $fingerprintPath) (Get-DependencyFingerprint $mainRoot))) { + Write-Error 'Shared Python environment is stale; run scripts/setup-worktree.ps1 in the main repository first.' + } + $worktreeVenv = Join-Path $projectRoot '.venv' + if (Test-Path $worktreeVenv) { + $existingVenv = Get-Item $worktreeVenv -Force + if ($existingVenv.LinkType -eq 'Junction' -and [IO.Path]::GetFullPath([string]$existingVenv.Target) -eq [IO.Path]::GetFullPath($sharedVenv)) { + exit 0 + } + Write-Error 'Worktree .venv exists but is not linked to the main repository environment.' + } + New-Item -ItemType Junction -Path $worktreeVenv -Target $sharedVenv | Out-Null + exit 0 + } + if (-not (Test-Path $python)) { py -3.12 -m venv .venv if ($LASTEXITCODE) { exit $LASTEXITCODE } } + Remove-Item (Join-Path $projectRoot '.venv\.requirements.sha256') -Force -ErrorAction SilentlyContinue + & $python -m pip install --upgrade pip if ($LASTEXITCODE) { exit $LASTEXITCODE } & $python -m pip install -r requirements.txt -r requirements-dev.txt - exit $LASTEXITCODE + if ($LASTEXITCODE) { exit $LASTEXITCODE } + + Get-DependencyFingerprint $projectRoot | Set-Content (Join-Path $projectRoot '.venv\.requirements.sha256') -Encoding ascii + exit 0 } finally { Pop-Location diff --git a/tests/test_setup_worktree.py b/tests/test_setup_worktree.py new file mode 100644 index 0000000..b2cb2da --- /dev/null +++ b/tests/test_setup_worktree.py @@ -0,0 +1,213 @@ +import hashlib +import json +import os +import shutil +import subprocess +from pathlib import Path + + +REPO_ROOT = Path(__file__).parents[1] +SCRIPT = REPO_ROOT / "scripts" / "setup-worktree.ps1" + + +def run(*args, cwd, env=None): + return subprocess.run(args, cwd=cwd, env=env, text=True, encoding="utf-8", errors="replace", capture_output=True) + + +def make_repo(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + (repo / "scripts").mkdir() + shutil.copy2(SCRIPT, repo / "scripts" / SCRIPT.name) + (repo / "requirements.txt").write_text("base\n", encoding="utf-8") + (repo / "requirements-dev.txt").write_text("dev\n", encoding="utf-8") + run("git", "init", cwd=repo) + run("git", "config", "user.email", "test@example.com", cwd=repo) + run("git", "config", "user.name", "Test", cwd=repo) + run("git", "add", ".", cwd=repo) + run("git", "commit", "-m", "fixture", cwd=repo) + return repo + + +def write_fingerprint(repo): + lines = [] + for name in ("requirements.txt", "requirements-dev.txt"): + digest = hashlib.sha256((repo / name).read_bytes()).hexdigest().upper() + lines.append(f"{digest} {name}") + (repo / ".venv" / ".requirements.sha256").write_text("\n".join(lines) + "\n", encoding="ascii") + + +def prepare_shared_venv(repo): + shared_venv = repo / ".venv" + shared_python = shared_venv / "Scripts" / "python.exe" + shared_python.parent.mkdir(parents=True) + shared_python.write_text("fixture", encoding="utf-8") + write_fingerprint(repo) + return shared_venv + + +def test_repository_uses_root_worktrees_directory(): + ignored_paths = (REPO_ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() + + assert ".worktrees/" in ignored_paths + assert ".claude/worktrees/" not in ignored_paths + + +def test_build_and_verify_runs_worktree_setup_checks(): + config = json.loads((REPO_ROOT / ".build-and-verify" / "config.json").read_text(encoding="utf-8")) + + check = next(item for item in config["verify"]["checks"] if item["id"] == "verify.worktree-setup") + assert "tests\\test_setup_worktree.py" in check["command"] + assert set(check["paths"]) >= { + ".gitignore", + "scripts/setup-worktree.ps1", + "tests/test_setup_worktree.py", + "requirements*.txt", + } + + +def test_main_repository_initializes_and_updates_venv(tmp_path): + repo = make_repo(tmp_path) + (repo / "requirements.txt").write_text("", encoding="utf-8") + (repo / "requirements-dev.txt").write_text("", encoding="utf-8") + env = os.environ | {"PIP_NO_INDEX": "1"} + + first = run("pwsh", "-NoProfile", "-File", str(repo / "scripts" / SCRIPT.name), cwd=repo, env=env) + second = run("pwsh", "-NoProfile", "-File", str(repo / "scripts" / SCRIPT.name), cwd=repo, env=env) + + assert first.returncode == 0, first.stderr + assert second.returncode == 0, second.stderr + assert (repo / ".venv" / "Scripts" / "python.exe").is_file() + fingerprint = (repo / ".venv" / ".requirements.sha256").read_text(encoding="utf-8") + assert "requirements.txt" in fingerprint + assert "requirements-dev.txt" in fingerprint + + +def test_failed_main_repository_update_invalidates_fingerprint(tmp_path): + repo = make_repo(tmp_path) + (repo / "requirements.txt").write_text("package-that-cannot-exist==0\n", encoding="utf-8") + result = run("py", "-3.12", "-m", "venv", ".venv", cwd=repo) + assert result.returncode == 0, result.stderr + write_fingerprint(repo) + + result = run( + "pwsh", + "-NoProfile", + "-File", + str(repo / "scripts" / SCRIPT.name), + cwd=repo, + env=os.environ | {"PIP_NO_INDEX": "1"}, + ) + + assert result.returncode != 0 + assert not (repo / ".venv" / ".requirements.sha256").exists() + + +def test_build_and_verify_uses_shared_venv_from_worktree(tmp_path): + repo = make_repo(tmp_path) + runtime = repo / ".build-and-verify" / "runtime" + shutil.copytree(REPO_ROOT / ".build-and-verify" / "runtime", runtime) + config = { + "version": 1, + "build": {"checks": []}, + "verify": { + "checks": [ + { + "id": "verify.shared-venv-smoke", + "command": ".\\.venv\\Scripts\\python.exe -c \"from pathlib import Path; Path('bav-marker').write_text('ok')\"", + "paths": ["probe.txt"], + "inputs": ["probe.txt"], + } + ] + }, + } + (repo / ".build-and-verify" / "config.json").write_text(json.dumps(config), encoding="utf-8") + run("git", "add", ".build-and-verify", cwd=repo) + result = run("git", "commit", "-m", "add build verification", cwd=repo) + assert result.returncode == 0, result.stderr + result = run("py", "-3.12", "-m", "venv", ".venv", cwd=repo) + assert result.returncode == 0, result.stderr + write_fingerprint(repo) + worktree = tmp_path / "worktree" + result = run("git", "worktree", "add", str(worktree), cwd=repo) + assert result.returncode == 0, result.stderr + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree) + assert result.returncode == 0, result.stderr + (worktree / "probe.txt").write_text("changed", encoding="utf-8") + + result = run( + str(worktree / ".venv" / "Scripts" / "python.exe"), + str(worktree / ".build-and-verify" / "runtime" / "build_and_verify.py"), + "verify", + "--project", + ".", + cwd=worktree, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert (worktree / "bav-marker").read_text(encoding="utf-8") == "ok" + + +def test_linked_worktree_reuses_main_repository_venv(tmp_path): + repo = make_repo(tmp_path) + shared_venv = prepare_shared_venv(repo) + (shared_venv / "shared-marker").write_text("shared", encoding="utf-8") + worktree = tmp_path / "worktree" + result = run("git", "worktree", "add", str(worktree), cwd=repo) + assert result.returncode == 0, result.stderr + + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + (fake_bin / "py.cmd").write_text("@exit /b 99\n", encoding="ascii") + env = os.environ | {"PATH": f"{fake_bin}{os.pathsep}{os.environ['PATH']}"} + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree, env=env) + + assert result.returncode == 0, result.stderr + assert (worktree / ".venv" / "shared-marker").read_text(encoding="utf-8") == "shared" + + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree, env=env) + assert result.returncode == 0, result.stderr + + +def test_linked_worktree_stops_when_shared_venv_is_missing(tmp_path): + repo = make_repo(tmp_path) + worktree = tmp_path / "worktree" + result = run("git", "worktree", "add", str(worktree), cwd=repo) + assert result.returncode == 0, result.stderr + + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree) + + assert result.returncode != 0 + assert "run scripts/setup-worktree.ps1 in the main repository first" in (result.stdout + result.stderr) + assert not (worktree / ".venv").exists() + + +def test_linked_worktree_stops_when_shared_venv_is_stale(tmp_path): + repo = make_repo(tmp_path) + prepare_shared_venv(repo) + worktree = tmp_path / "worktree" + result = run("git", "worktree", "add", str(worktree), cwd=repo) + assert result.returncode == 0, result.stderr + for root in (repo, worktree): + (root / "requirements.txt").write_text("updated\n", encoding="utf-8") + + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree) + + assert result.returncode != 0 + assert "Shared Python environment is stale" in (result.stdout + result.stderr) + assert not (worktree / ".venv").exists() + + +def test_linked_worktree_stops_when_dependency_manifests_differ(tmp_path): + repo = make_repo(tmp_path) + prepare_shared_venv(repo) + worktree = tmp_path / "worktree" + result = run("git", "worktree", "add", str(worktree), cwd=repo) + assert result.returncode == 0, result.stderr + (worktree / "requirements.txt").write_text("different\n", encoding="utf-8") + + result = run("pwsh", "-NoProfile", "-File", str(worktree / "scripts" / SCRIPT.name), cwd=worktree) + + assert result.returncode != 0 + assert "dependency manifests differ" in (result.stdout + result.stderr) + assert not (worktree / ".venv").exists() diff --git a/tests/test_skill_layout.py b/tests/test_skill_layout.py index 48c5658..244e929 100644 --- a/tests/test_skill_layout.py +++ b/tests/test_skill_layout.py @@ -250,7 +250,7 @@ def test_build_and_verify_covers_local_quant_research_without_local_data( assert config["verify"]["maxParallel"] == 10 assert config["verify"]["fullBudgetSeconds"] == 60 - assert len(checks) == 17 + assert len(checks) == 18 assert [ check["id"] for check in config["verify"]["checks"][:10] ] == [