W02 安装工程、启动骨架与验证环境:pyproject/uv 安装工程、启动装配骨架、conftest 用例身份与 OS 离线隔离。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
This commit is contained in:
parent
50a5522014
commit
b86516505f
11
.agent/rules/任务与证据.md
Normal file
11
.agent/rules/任务与证据.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 任务与证据规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/接口契约/任务工具与事件.md`、`模块设计/S02-任务运行.md`;机械门禁:`tests/集成/test_取消恢复与未知调用.py` 等(随 W05/W06/W08 落地)。
|
||||||
|
|
||||||
|
1. **持久化记账**:任务、步骤、尝试、检查点与有序事件全部落库;禁止以主会话内存当账本。
|
||||||
|
2. **冻结输入**:任务创建即冻结来源范围(作品、叙事截止、用途、结构版本)与角色/模型/工具策略副本;执行中不得扩大。
|
||||||
|
3. **租约与迟到拒绝**:完成写入校验尝试与租约身份;旧持有者输出不能更新当前候选。
|
||||||
|
4. **两种用途不混**:`run_purpose`(production/evaluation/maintenance)与 `content_purpose`(planning/generation/detection/extraction)分别绑定;评测受隔离库权限约束。
|
||||||
|
5. **未知调用先对账**:已发出而结果不明的模型调用必须先对账;确定可靠保存才复用,重发是新预算尝试。
|
||||||
|
6. **终态明确**:流结束无完成依据、截断、错误、未知用量分别表示;文本片段不是完成事件。
|
||||||
|
7. **原文保护**:临时原文先租约后字节,普通清理不得删除持久证据与受控归档;`migrating` 状态不误删。
|
||||||
9
.agent/rules/元数据消费.md
Normal file
9
.agent/rules/元数据消费.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 元数据消费规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/数据模型/元数据与结构演进.md`、`接口契约/元数据投影与版本.md`;机械门禁:`tests/架构/test_元数据不能跨模块写值.py`(随 S04 实现落地)。
|
||||||
|
|
||||||
|
1. **实例归属业务**:元数据模块拥有模具(类型、字段、结构版本),业务模块拥有实例值;禁止经元数据接口写业务实例。
|
||||||
|
2. **稳定 ID**:类型与字段用稳定 ID 登记;禁止传任意表名列名绕过登记。
|
||||||
|
3. **只增不改**:作品级结构扩展只追加;已发布字段定义不改语义,退出用弃用标记。
|
||||||
|
4. **投影按用途**:表单、提示词、导出各按字段用途投影;收紧投影不删除已存值,未知字段不静默丢弃。
|
||||||
|
5. **结构切换并发保护**:结构升级与内容写入并发时以版本约束拒绝冲突;历史值按其当时结构解释。
|
||||||
9
.agent/rules/审校与修订.md
Normal file
9
.agent/rules/审校与修订.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 审校与修订规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/模块设计/B06-审校修订.md`、`接口契约/正文结构与中文选区.md`;机械门禁:`tests/单元/test_正文格式与选段.py` 等(随 W10/W22/W23 落地)。
|
||||||
|
|
||||||
|
1. **选区语义**:修改项按段落 ID + 段内码点区间(左闭右开)定位;重复片段不得按首次命中替换;应用前重验原文与哈希。
|
||||||
|
2. **不擅动正文形态**:全角标点、异体字、组合字符与有意空白不做自动规范化;规范化必须是显式编辑操作。
|
||||||
|
3. **AI 保护区只限自动任务**:保护区不禁止作者本人编辑自己的正文。
|
||||||
|
4. **受控修订**:修订包带文档基线、目的与允许范围;有界返修不无限循环,等待作者有明确状态。
|
||||||
|
5. **派生失败单独可见**:正文成功后的派生(事实、索引、摘要)失败不回滚正文,也不被静默吞掉。
|
||||||
10
.agent/rules/文档与资源生成.md
Normal file
10
.agent/rules/文档与资源生成.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 文档与资源生成规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/文件设计/工程配置与部署.md`;机械门禁:`工具/维护索引.py --检查`、`工具/构建资源包.py`。
|
||||||
|
|
||||||
|
1. **生成物只由登记输入产生**:`src/muse/资源/运行资源.zip` 与 `清单.json` 只从 `工具/资源登记.json` 打包;`docs/接口契约/生成/*` 只从 `工具/生成输入.json` 生成。改生成物先改登记。
|
||||||
|
2. **生成物不手改**:`src/muse/资源/`、生成 Schema 与 OpenAPI 是产物;直接编辑会被下次生成覆盖并视为违规。
|
||||||
|
3. **索引一致**:所有 `目录.md` 表内相对链接必须可达;技能分组索引与磁盘技能一一对应;`make 检查` 全绿才算文档变更完成。
|
||||||
|
4. **资源带发布身份**:资源包必须有 `发布身份` 与逐文件 sha256;生产加载只认安装包内清单,不读未审工作树。
|
||||||
|
5. **声明即验证**:写入 README/Makefile/工作流的命令必须先在本地真实执行通过;未验证的命令不得声明。
|
||||||
|
6. **单一权威**:正文性定义只在所属权威文档修改;索引、投影和引用不复制第二份定义。
|
||||||
13
.agent/rules/模块依赖.md
Normal file
13
.agent/rules/模块依赖.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 模块依赖规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/总体架构.md`;机械门禁:`tests/架构/test_模块依赖.py` + `make 检查`。
|
||||||
|
|
||||||
|
1. **业务只经公共接口**:跨模块调用只走目标模块 `接口.py` 暴露的用例;禁止导入其他模块的 `存储.py`、`模型.py` 内部符号。
|
||||||
|
2. **共享基础零反向依赖**:`src/muse/共享/`、`配置.py`、`资源加载.py` 禁止导入业务模块。`启动.py` 是组合入口,可以选择并绑定具体实现,但不承载业务规则。
|
||||||
|
3. **元数据不写业务值**:S04 元数据只描述、校验和投影结构;业务实例值由拥有它的业务模块写入。
|
||||||
|
4. **技能不携带实现**:`.agent/skills/` 只保留方法与操作说明;禁止在技能目录放可执行脚本或 `sys.path` 注入。
|
||||||
|
5. **接入不直写表**:HTTP/CLI/主会话接入层禁止手写 SQL;持久化一律走业务模块存储接口。
|
||||||
|
6. **宿主不裁决正式内容**:模型宿主(Pi/直接调用)只产候选与证据;作者确认与正式提交不在宿主层发生。
|
||||||
|
7. **薄入口**:`python -m muse` 与 CLI 分发入口只做参数解析与转交,不创建连接或写业务逻辑。具名适配器可管理已装配连接的事务范围以调用 S04 等事务参与者;连接凭据、用途和构造仍由基础设施统一处理,禁止手写 SQL。
|
||||||
|
|
||||||
|
违反任一条:先修依赖方向,再继续;不以"临时导入"放行。
|
||||||
9
.agent/rules/正式变更.md
Normal file
9
.agent/rules/正式变更.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 正式变更规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/接口契约/命令与作者决策.md`、`模块设计/S01-正式变更.md`;机械门禁:`tests/架构/test_正式写入入口.py`(随 S01 实现落地)。
|
||||||
|
|
||||||
|
1. **唯一写入口**:正式内容(正文、事实、规划、设定、决策)只能经 S01 正式变更模块提交;禁止第二条写路径、禁止裸连写库。
|
||||||
|
2. **作者确认前置**:候选必须经作者明确确认才能转正;模型与工具没有作者确认身份(`调用身份.作者` 为空的调用禁止写正式内容)。
|
||||||
|
3. **携带基线**:每个提交携带正文、候选、结构和来源版本;旧版本请求必须被拒绝且不改动作者新稿。
|
||||||
|
4. **原子与幂等**:多参与者提交要么全部生效要么全部回滚;相同 `command_id` + 相同请求幂等重放,携带不同请求必须拒绝。
|
||||||
|
5. **失败可见**:提交失败保留作者本地缓冲;服务器回执是已保存的唯一依据,不能用 UI 状态代替。
|
||||||
12
.agent/rules/测试隔离.md
Normal file
12
.agent/rules/测试隔离.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 测试隔离规则
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/验证设计/测试用例重写.md`、`改造计划/执行与验收.md`;机械门禁:`tests/conftest.py`(OS 进程隔离 + 用例选择 + 全跳过守卫)、`工具/维护索引.py`。
|
||||||
|
|
||||||
|
1. **默认离线**:默认测试进程由 macOS sandbox 或 Linux seccomp 禁止外部连接,覆盖收集阶段、数据库驱动及其子进程;防护不可用时失败,不退回无防护执行。显式外部用例使用 `--外部环境` 与环境标记选择。
|
||||||
|
2. **显式外部环境**:需要隔离 PostgreSQL 的用例标记 `数据库` 并使用 `隔离数据库URL` 夹具;缺 `MUSE_TEST_DATABASE_URL` 时直接失败,不跳过或回退旧库。
|
||||||
|
实际Pi循环另标记`宿主`,通过`make 宿主测试`显式提供固定Node与Pi包路径;普通数据库测试剔除此标记。宿主验证使用合成提供方,不等于真实模型验证。
|
||||||
|
3. **用例身份**:完整 `case_id` 与清单中的实际文件、符号及参数行绑定,不靠名称后缀匹配;`pytest --case TC-…` 选择用例,`--case 'TC-…[参数ID]'` 选择参数行。同符号承接多个 ID 时必须分别绑定无交叠参数。未知 ID、目标缺失与冒用均失败;Junit 留存完整 ID 和参数 ID。
|
||||||
|
4. **可判定性**:用例体内必须有 assert/raise/fail/skip/xfail;纯 print 用例被索引检查在执行前拦截。
|
||||||
|
5. **空收集不通过**:零收集(退出码 5)、全跳过(守卫改写退出码 5)、超时与失败都如实呈现;不得以返回码 0 掩盖。
|
||||||
|
6. **替身显式标记**:协议替身、模型替身、宿主替身必须在用例名或文档串标明;替身通过不宣称真实链路通过。
|
||||||
|
7. **新旧收集分离**:新用例只放 `tests/单元|契约|集成|架构/`;旧测试树由 `.venv-旧` 与旧 harness 运行,两套互不接管。
|
||||||
7
.agent/skills/操作/目录.md
Normal file
7
.agent/skills/操作/目录.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||||
|
|------|----------|----------|----------|----------|
|
||||||
|
| 维护元数据结构 | [SKILL.md](维护元数据结构/SKILL.md) | 查看库内固定结构,或在明确的维护配置中发布内置种子;不替作者启用版本。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||||
|
| 查看与恢复任务 | [SKILL.md](查看与恢复任务/SKILL.md) | 查询持久任务与事件,按作者请求暂停、取消或经登记检查恢复任务。 | 需要该项操作时 | 只使用已实现的具名入口 |
|
||||||
|
| 决定正文候选去留 | [SKILL.md](决定正文候选去留/SKILL.md) | 比较、修改候选并执行作者明确决定 | 使用新版候选入口时 | 使用被审版本,不自动替作者采纳 |
|
||||||
|
| 完善故事基础设定 | [SKILL.md](完善故事基础设定/SKILL.md) | 私人探索保存与最终格式检查 | 使用新版探索入口 | 不自动进入正式规划 |
|
||||||
|
| 确认规划候选 | [SKILL.md](确认规划候选/SKILL.md) | 读取规划候选并执行作者明确决定 | 使用新版规划入口 | 使用被审版本,回查正式回执 |
|
||||||
9
.agent/约束/作者主权.md
Normal file
9
.agent/约束/作者主权.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 作者主权约束
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/接口契约/命令与作者决策.md`;属不可逾越边界。
|
||||||
|
|
||||||
|
1. **先审后入**:AI 产出的一切正式内容变更先成候选,作者明确确认后才入正典;不存在自动确认。
|
||||||
|
2. **作者身份不可伪造**:写入请求的作者身份来自会话与作者动作;模型工具调用没有作者身份,不能代行确认。
|
||||||
|
3. **决策可追溯**:每次采纳、拒绝、修改都记录作者、理由与所见版本;撤销与回退走同一审计链。
|
||||||
|
4. **作者新稿不受迟到响应影响**:切章、后发请求与旧版本写入不得覆盖作者当前缓冲。
|
||||||
|
5. **私人探索边界**:探索稿生命周期独立,不因任何自动化流程被提升为正式内容或被删除。
|
||||||
9
.agent/约束/凭据与外部调用.md
Normal file
9
.agent/约束/凭据与外部调用.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 凭据与外部调用约束
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/文件设计/后端-基础设施.md`(凭据读取);属不可逾越边界。
|
||||||
|
|
||||||
|
1. **值不入库不入文**:凭据值不写入源码、文档、测试、日志、错误消息与批次证据;只记获取位置(环境变量名或受控存储路径)。
|
||||||
|
2. **受控读取**:运行时凭据经 `src/muse/基础设施/凭据读取.py` 按引用解析;缺引用即失败,不猜测。
|
||||||
|
3. **生产不读测试配置**:生产装配不加载 `配置/测试.example.toml` 派生文件;评测与维护用途使用隔离凭据。
|
||||||
|
4. **调用留痕**:真实模型调用绑定任务与尝试,保存输入输出证据(脱敏后);凭据在持久化与日志前脱敏。
|
||||||
|
5. **离线默认禁外呼**:测试与确定性脚本默认禁止网络、数据库与模型调用;需要时显式标记并使用隔离环境。
|
||||||
10
.agent/约束/数据与读取范围.md
Normal file
10
.agent/约束/数据与读取范围.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 数据与读取范围约束
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/数据模型/存储与对象关系.md`、`改造计划/切换与回退.md`;属不可逾越边界。
|
||||||
|
|
||||||
|
1. **正式权威**:正式内容以 PostgreSQL 目标库为唯一权威;SQLite 只是运行态账本,冲突以 PG 为准。
|
||||||
|
2. **无默认回退**:缺数据库配置即启动失败;禁止回退连接旧正式库(`100.64.0.8:5433/muse-example`)或任何隐式默认库。新代码不得内置旧库连接串。
|
||||||
|
3. **旧库冻结**:改造期内旧正式库与 `data/muse.db` 对新实现只读参照;写入与结构变更走 W29/W30 切换流程。
|
||||||
|
4. **来源范围冻结**:模型与工具只能读取任务冻结的来源范围(作品、叙事截止、用途、结构版本);不得扩大 `work_id`、`as_of` 或 `purpose`。
|
||||||
|
5. **评测隔离**:生产角色无权读取 oracle 与匿名映射,评测角色不得写业务对象。角色任务仍由冻结范围限制读取;评测编排可读取其获准答案,不将答案交给写手或盲评员。数据库角色与任务范围分别承担各自边界。
|
||||||
|
6. **参考原文授权**:`data/sources/` 原文只能按授权、租约与归档合同访问;普通清理不得删除持久证据与归档。
|
||||||
9
.agent/规范/中文正文与界面.md
Normal file
9
.agent/规范/中文正文与界面.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 中文正文与界面规范
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/接口契约/正文结构与中文选区.md`;机械门禁:`tests/单元/test_正文格式与选段.py`(随 W10 落地)+ `tests/夹具/中文正文.json`。
|
||||||
|
|
||||||
|
1. **UTF-8 与码点**:正文传输统一 UTF-8;位置按 Unicode 码点左闭右开计数;前端集中转换 UTF-16 偏移。
|
||||||
|
2. **不规范化**:全角标点、异体字、组合字符、有意空白与连续空行原样保留;文件名可做 NFC 检查,正文规范化必须是显式编辑操作。
|
||||||
|
3. **输入法组合期不提交**:半成文字不进入保存与候选链路。
|
||||||
|
4. **界面中文**:作者可见的界面、错误与说明使用简体中文白话;数量与状态来自数据,不写死。
|
||||||
|
5. **正文协议权威**:编辑器 HTML 只是显示适配;结构身份、哈希与位置语义以正文协议为准。
|
||||||
11
.agent/规范/命名.md
Normal file
11
.agent/规范/命名.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 命名规范
|
||||||
|
|
||||||
|
> 权威合同:`docs/系统架构/新版设计/项目目录与文件职责.md`(命名与职责节);机械门禁:`make 检查`(索引一致性)。
|
||||||
|
|
||||||
|
1. **中文优先**:自有业务目录、文件、模块、类、函数与测试名使用中文;技术约定名称(`pyproject.toml`、`uv.lock`、`Makefile`、`conftest.py`、`node_modules` 等)保留英文。
|
||||||
|
2. **展示名与稳定身份分离**:对外展示用中文名;稳定身份(case_id、类型 ID、字段 ID、事件类型)用既定英文/十六进制形式,不为改名破坏身份。
|
||||||
|
3. **模块文件三件套**:业务模块公开面 = `接口.py`;内部 = `模型.py` + `存储.py`;不新增平行命名。
|
||||||
|
4. **测试符号**:`test_<行为名>__<case6>`;参数行保留稳定行 ID。
|
||||||
|
5. **工具脚本**:`工具/<动词+对象>.py`;登记文件与生成物同名配对(如 `资源登记.json` → `src/muse/资源/`)。
|
||||||
|
6. **一个概念一个名**:术语见 `.agent/规范/术语.md`;不为风格引入第二名称。
|
||||||
|
7. **React 识别前缀**:组件使用 `Ui` 加中文业务名,Hook 使用 `useMuse` 加中文业务名,保留框架和静态检查所需的英文识别前缀;其余自有业务名仍使用中文。
|
||||||
8
.agent/规范/术语.md
Normal file
8
.agent/规范/术语.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 术语规范(执行提示)
|
||||||
|
|
||||||
|
> 权威定义:`docs/系统架构/新版设计/领域模型/术语表.md`。本文件只保留执行提示,不维护第二套定义。
|
||||||
|
|
||||||
|
1. **使用前先对表**:为业务概念命名或改写文档时,先查术语表;表内已有名称直接用,不造同义词。
|
||||||
|
2. **避免使用列**:术语表每条的 _避免使用_(如把参考书称当前作品、把规划称历史事实、把候选称定稿)在代码、文档与提示词中同样适用。
|
||||||
|
3. **新旧词不混**:旧实现的英文目录名与旧模块名不进入新代码;引用旧路径时明确标注"旧"。
|
||||||
|
4. **读者是作者**:面向作者的说明用白话直陈;"Shadow/Canonical"等内部词在界面与文档中用"候选/正式"。
|
||||||
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{ts,tsx,js,jsx,json,yaml,yml,toml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
20
.gitattributes
vendored
Normal file
20
.gitattributes
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 文本换行与差异属性:统一 LF,中文文件名可读
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.py text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.sql text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
|
||||||
|
# 二进制资源
|
||||||
|
*.png binary
|
||||||
|
*.zip binary
|
||||||
|
*.db binary
|
||||||
|
|
||||||
|
# 语言学上敏感的合并行为:清单与锁文件以本侧为准时人工处理
|
||||||
|
uv.lock -merge
|
||||||
39
.github/workflows/构建.yml
vendored
Normal file
39
.github/workflows/构建.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: 构建
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: 构建
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.6.4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: web/pnpm-lock.yaml
|
||||||
|
- name: 安装前端依赖
|
||||||
|
run: make 前端安装
|
||||||
|
- name: 安装依赖
|
||||||
|
run: make 安装
|
||||||
|
- name: 生成与构建
|
||||||
|
run: make 构建
|
||||||
|
- name: 干净环境安装验证
|
||||||
|
run: |
|
||||||
|
uv venv /tmp/muse-装验 --python 3.12
|
||||||
|
uv pip install --python /tmp/muse-装验/bin/python dist/*.whl
|
||||||
|
cd / && /tmp/muse-装验/bin/python -c "import muse; from muse.资源加载 import 核对资源; 核对资源()"
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: 发布包
|
||||||
|
path: dist/*
|
||||||
60
.github/workflows/检查.yml
vendored
Normal file
60
.github/workflows/检查.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
name: 检查
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, feat/**]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
name: 检查
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14
|
||||||
|
env:
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
POSTGRES_DB: muse_acceptance
|
||||||
|
ports: ["5432:5432"]
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d muse_acceptance"
|
||||||
|
--health-interval 5s --health-timeout 5s --health-retries 10
|
||||||
|
env:
|
||||||
|
MUSE_TEST_DATABASE_URL: postgresql://postgres@127.0.0.1:5432/muse_acceptance
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.6.4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24.15.0"
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: web/pnpm-lock.yaml
|
||||||
|
- name: 安装前端依赖
|
||||||
|
run: make 前端安装
|
||||||
|
- name: 安装依赖
|
||||||
|
run: make 安装
|
||||||
|
- name: 格式与类型检查
|
||||||
|
run: |
|
||||||
|
make 格式
|
||||||
|
make 类型
|
||||||
|
- name: 索引一致性
|
||||||
|
run: uv run python 工具/维护索引.py --检查
|
||||||
|
- name: 前端检查与交互测试
|
||||||
|
run: |
|
||||||
|
make 前端检查
|
||||||
|
make 前端测试
|
||||||
|
- name: 离线测试
|
||||||
|
run: make 测试
|
||||||
|
- name: 初始化隔离用途角色
|
||||||
|
run: psql "$MUSE_TEST_DATABASE_URL" -v ON_ERROR_STOP=1 -f 数据库/初始化/用途角色.sql
|
||||||
|
- name: 隔离数据库测试
|
||||||
|
run: make 数据库测试
|
||||||
|
- name: 固定Pi宿主整链
|
||||||
|
run: |
|
||||||
|
npm install --prefix "$RUNNER_TEMP/muse-pi" --no-save --ignore-scripts @earendil-works/pi-coding-agent@0.84.4
|
||||||
|
MUSE_PI_NODE="$(command -v node)" MUSE_PI_PACKAGE="$RUNNER_TEMP/muse-pi/node_modules/@earendil-works/pi-coding-agent" make 宿主测试
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
73
Makefile
Normal file
73
Makefile
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Muse 安装、生成、检查、测试和构建入口。
|
||||||
|
# 每个声明命令都必须真实可执行(执行与验收.md:声明命令前验证它真实可执行)。
|
||||||
|
|
||||||
|
.PHONY: 安装 格式 格式写入 类型 检查 测试 数据库测试 宿主测试 前端安装 前端检查 前端测试 生成 构建 资源核对
|
||||||
|
|
||||||
|
# 安装:建立并锁定 Python 依赖环境
|
||||||
|
安装:
|
||||||
|
uv sync --group dev
|
||||||
|
|
||||||
|
前端安装:
|
||||||
|
pnpm --dir web install --frozen-lockfile
|
||||||
|
|
||||||
|
前端检查:
|
||||||
|
pnpm --dir web run lint
|
||||||
|
pnpm --dir web run typecheck
|
||||||
|
pnpm --dir web exec tsc --noEmit --module esnext --moduleResolution bundler --target es2022 --skipLibCheck --typeRoots node_modules/@types --types node ../src/muse/基础设施/宿主/Pi工具桥.ts
|
||||||
|
|
||||||
|
前端测试:
|
||||||
|
pnpm --dir web run test
|
||||||
|
|
||||||
|
# 格式:静态检查(ruff lint + 格式校验;旧测试树由旧环境维护,不进入范围)
|
||||||
|
新测试目录 := $(shell for d in tests/单元 tests/契约 tests/集成 tests/架构; do [ -d "$$d" ] && printf '%s ' "$$d"; done)
|
||||||
|
格式:
|
||||||
|
uv run ruff check src $(新测试目录) tests/conftest.py 工具
|
||||||
|
uv run ruff format --check src $(新测试目录) tests/conftest.py 工具
|
||||||
|
|
||||||
|
# 格式写入:按项目规则实际格式化
|
||||||
|
格式写入:
|
||||||
|
uv run ruff check --fix src $(新测试目录) tests/conftest.py 工具
|
||||||
|
uv run ruff format src $(新测试目录) tests/conftest.py 工具
|
||||||
|
|
||||||
|
# 类型:pyright 类型检查
|
||||||
|
类型:
|
||||||
|
uv run pyright
|
||||||
|
|
||||||
|
# 检查:格式 + 类型 + 索引核对的统一入口
|
||||||
|
检查: 格式 类型
|
||||||
|
uv run python 工具/维护索引.py --检查
|
||||||
|
|
||||||
|
# 测试:默认离线(按标记剔除外部环境用例;未标记用例在 socket 防护下运行)
|
||||||
|
# 用法:make 测试 全部离线用例
|
||||||
|
# make 测试 用例=TC-a5c7d4adafe3 按稳定用例 ID 选择
|
||||||
|
测试:
|
||||||
|
uv run pytest -m "not 数据库 and not 网络 and not 真实模型 and not 浏览器 and not 宿主" $(if $(用例),--case $(用例),)
|
||||||
|
|
||||||
|
# 数据库测试:需要 MUSE_TEST_DATABASE_URL;缺失即失败,不静默跳过
|
||||||
|
数据库测试:
|
||||||
|
@test -n "$$MUSE_TEST_DATABASE_URL" || { echo "缺少 MUSE_TEST_DATABASE_URL(隔离库连接串);拒绝静默跳过" >&2; exit 1; }
|
||||||
|
uv run pytest --外部环境 -m "数据库 and not 宿主"
|
||||||
|
|
||||||
|
宿主测试:
|
||||||
|
@test -n "$$MUSE_TEST_DATABASE_URL" -a -n "$$MUSE_PI_NODE" -a -n "$$MUSE_PI_PACKAGE" || { echo "缺少隔离库或固定Pi/Node路径"; exit 1; }
|
||||||
|
uv run pytest --外部环境 -m 宿主
|
||||||
|
|
||||||
|
# 迁移:对配置指向的目标库执行显式迁移(维护命令,不在启动时自动执行)
|
||||||
|
迁移:
|
||||||
|
uv run python -m muse 迁移 $(配置)
|
||||||
|
|
||||||
|
# 生成:资源包、接口合同与索引(生成物只由登记输入产生)
|
||||||
|
生成:
|
||||||
|
uv run python 工具/生成接口.py
|
||||||
|
cd web && pnpm exec openapi-typescript ../docs/接口契约/生成/openapi.generated.json -o src/接口/生成/类型.ts
|
||||||
|
pnpm --dir web run build
|
||||||
|
uv run python 工具/构建资源包.py
|
||||||
|
uv run python 工具/维护索引.py --写入
|
||||||
|
|
||||||
|
# 构建:先保证生成物最新,再做可安装包
|
||||||
|
构建: 生成
|
||||||
|
uv build
|
||||||
|
|
||||||
|
# 资源核对:安装包内资源与清单哈希一致
|
||||||
|
资源核对:
|
||||||
|
uv run python -c "from muse.资源加载 import 核对资源; m = 核对资源(); print('资源清单发布身份:', m['发布身份'], '条目数:', len(m['资源']))"
|
||||||
76
pyproject.toml
Normal file
76
pyproject.toml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# muse 应用包:安装、命令、依赖与检查配置。
|
||||||
|
# 边界:只约束安装、构建或编辑配置;不规范化作者正文、不替代业务权限与验收。
|
||||||
|
# 运行时依赖随所属工作包逐步加入(W03 数据库、W06 模型传输、W07 HTTP 等),
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "muse"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Muse:AI 驱动的长篇创作工具(单作者模块化单体)"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115",
|
||||||
|
"httpx>=0.28",
|
||||||
|
"itsdangerous>=2.2",
|
||||||
|
"jsonschema>=4.23",
|
||||||
|
"psycopg[binary]>=3.2",
|
||||||
|
"pydantic>=2.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"uvicorn>=0.30",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
muse = "muse.__main__:main"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.3",
|
||||||
|
"pytest-timeout>=2.3",
|
||||||
|
"ruff>=0.8",
|
||||||
|
"pyright>=1.1.390",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 资源发布包(运行资源.zip、清单.json、工作台/)是 gitignore 的生成物:
|
||||||
|
# 存在时打入 sdist/wheel(artifacts 同时作用于两目标),未生成时不阻塞全新环境安装。
|
||||||
|
[tool.hatch.build]
|
||||||
|
artifacts = ["src/muse/资源/**", "!src/muse/资源/迁移/**"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["/src", "/数据库/迁移", "/数据库/初始化", "/pyproject.toml", "/uv.lock", "/README.md"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
|
"数据库/迁移" = "muse/资源/迁移"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/muse"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
# 只收集新版用例目录;旧测试(tests/skills、tests/architecture 英文目录)仍由旧环境
|
||||||
|
# (.venv-旧 + 旧 harness)运行,两套收集互不接管。
|
||||||
|
testpaths = ["tests/单元", "tests/契约", "tests/集成", "tests/架构"]
|
||||||
|
# 架构/契约/集成/单元 是目录级组织;完整用例身份绑定登记文件与符号。
|
||||||
|
addopts = "--strict-markers --timeout=120"
|
||||||
|
markers = [
|
||||||
|
"数据库: 显式外部执行需要 MUSE_TEST_DATABASE_URL;缺配置失败",
|
||||||
|
"网络: 需要真实网络访问;必须显式标记",
|
||||||
|
"真实模型: 需要真实模型调用与预算授权;必须显式标记",
|
||||||
|
"浏览器: 需要真实浏览器与后端;必须显式标记",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
src = ["src", "tests", "工具"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I", "B", "UP"]
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
||||||
|
include = ["src", "工具"]
|
||||||
|
pythonVersion = "3.12"
|
||||||
|
typeCheckingMode = "standard"
|
||||||
8
src/muse/__init__.py
Normal file
8
src/muse/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""muse 应用包身份。
|
||||||
|
|
||||||
|
只承载包版本与身份,不放业务逻辑;业务在各业务模块的 接口.py 暴露。
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
6
src/muse/__main__.py
Normal file
6
src/muse/__main__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Python 模块入口,与已安装的 muse 命令使用同一 CLI。"""
|
||||||
|
|
||||||
|
from muse.接入.cli.入口 import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
38
src/muse/共享/__init__.py
Normal file
38
src/muse/共享/__init__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""共享基础:少量稳定身份与不可变引用。
|
||||||
|
|
||||||
|
边界:仅少量不可变基础类型;不导入业务模块、不新增通用业务框架。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from muse.共享.时间 import 假时钟, 时钟, 真实时钟
|
||||||
|
from muse.共享.标识 import (
|
||||||
|
任务标识,
|
||||||
|
作品标识,
|
||||||
|
命令标识,
|
||||||
|
审阅版本标识,
|
||||||
|
标识不合法,
|
||||||
|
校验标识,
|
||||||
|
)
|
||||||
|
from muse.共享.版本 import 版本引用, 计算内容哈希
|
||||||
|
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||||||
|
from muse.共享.错误 import Muse错误, 环境缺失错误, 资源缺失错误, 配置错误
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"任务标识",
|
||||||
|
"作品标识",
|
||||||
|
"命令标识",
|
||||||
|
"审阅版本标识",
|
||||||
|
"标识不合法",
|
||||||
|
"校验标识",
|
||||||
|
"内容用途",
|
||||||
|
"调用身份",
|
||||||
|
"用途",
|
||||||
|
"假时钟",
|
||||||
|
"时钟",
|
||||||
|
"真实时钟",
|
||||||
|
"版本引用",
|
||||||
|
"计算内容哈希",
|
||||||
|
"Muse错误",
|
||||||
|
"配置错误",
|
||||||
|
"环境缺失错误",
|
||||||
|
"资源缺失错误",
|
||||||
|
]
|
||||||
63
src/muse/共享/时间.py
Normal file
63
src/muse/共享/时间.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""时钟和期限类型。
|
||||||
|
|
||||||
|
业务代码只依赖 时钟 协议;测试用 假时钟 固定时间,运行装配注入 真实时钟。
|
||||||
|
期限计算只在此处出现一种解释。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class 时钟(Protocol):
|
||||||
|
"""给业务与租约用的时钟协议。"""
|
||||||
|
|
||||||
|
def 现在(self) -> datetime: ...
|
||||||
|
|
||||||
|
|
||||||
|
class 真实时钟:
|
||||||
|
"""生产用 UTC 时钟。"""
|
||||||
|
|
||||||
|
def 现在(self) -> datetime:
|
||||||
|
return datetime.now(UTC)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class 假时钟:
|
||||||
|
"""测试用可设定的 UTC 时钟;推进时间必须显式调用 推进。"""
|
||||||
|
|
||||||
|
_当前: datetime = field(default_factory=lambda: datetime(2026, 1, 1, tzinfo=UTC))
|
||||||
|
|
||||||
|
def 现在(self) -> datetime:
|
||||||
|
return self._当前
|
||||||
|
|
||||||
|
def 设定(self, 时刻: datetime) -> None:
|
||||||
|
if 时刻.tzinfo is None:
|
||||||
|
时刻 = 时刻.replace(tzinfo=UTC)
|
||||||
|
self._当前 = 时刻
|
||||||
|
|
||||||
|
def 推进(self, 时长: timedelta | float) -> None:
|
||||||
|
"""按 timedelta 或秒数前进;不允许倒退(倒退会破坏期限语义)。"""
|
||||||
|
增量 = timedelta(seconds=时长) if isinstance(时长, (int, float)) else 时长
|
||||||
|
新时刻 = self._当前 + 增量
|
||||||
|
if 新时刻 < self._当前:
|
||||||
|
raise ValueError("假时钟不允许倒退")
|
||||||
|
self._当前 = 新时刻
|
||||||
|
|
||||||
|
|
||||||
|
def 截止于(基准: datetime, 时长: timedelta) -> datetime:
|
||||||
|
"""计算期限:基准 + 时长,统一 UTC。"""
|
||||||
|
if 基准.tzinfo is None:
|
||||||
|
基准 = 基准.replace(tzinfo=UTC)
|
||||||
|
return 基准 + 时长
|
||||||
|
|
||||||
|
|
||||||
|
def 单调秒() -> float:
|
||||||
|
"""单调时钟秒数;用于超时与执行耗时,不受系统改时间影响。"""
|
||||||
|
return time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["时钟", "真实时钟", "假时钟", "截止于", "单调秒"]
|
||||||
35
src/muse/共享/标识.py
Normal file
35
src/muse/共享/标识.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""少量稳定身份与不可变引用。
|
||||||
|
|
||||||
|
身份是跨模块共用的不可变字符串引用;构造时即校验,杜绝空串与空白身份
|
||||||
|
进入业务层。这里不生成身份(身份由数据库与业务模块在写入时产生)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import NewType
|
||||||
|
|
||||||
|
作品标识 = NewType("作品标识", str)
|
||||||
|
任务标识 = NewType("任务标识", str)
|
||||||
|
命令标识 = NewType("命令标识", str)
|
||||||
|
审阅版本标识 = NewType("审阅版本标识", str)
|
||||||
|
|
||||||
|
|
||||||
|
class 标识不合法(ValueError):
|
||||||
|
"""身份为空、空白或包含控制字符。"""
|
||||||
|
|
||||||
|
|
||||||
|
def 校验标识(值: str, *, 名称: str = "标识") -> str:
|
||||||
|
"""校验并原样返回身份字符串;不合法即抛 标识不合法。"""
|
||||||
|
if not isinstance(值, str) or not 值.strip() or any(ord(ch) < 0x20 for ch in 值):
|
||||||
|
raise 标识不合法(f"{名称}不合法:必须是非空且不含控制字符的字符串")
|
||||||
|
return 值
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"作品标识",
|
||||||
|
"任务标识",
|
||||||
|
"命令标识",
|
||||||
|
"审阅版本标识",
|
||||||
|
"标识不合法",
|
||||||
|
"校验标识",
|
||||||
|
]
|
||||||
33
src/muse/共享/版本.py
Normal file
33
src/muse/共享/版本.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""revision、内容哈希与版本引用。
|
||||||
|
|
||||||
|
revision 是单调递增的整数版本号;内容哈希是 UTF-8 字节的 SHA-256 十六进制。
|
||||||
|
版本引用在模块间只有一种解释:先比 revision,再核对哈希。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True, order=True)
|
||||||
|
class 版本引用:
|
||||||
|
"""指向某个对象某个版本的不可变引用。"""
|
||||||
|
|
||||||
|
revision: int
|
||||||
|
内容哈希: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.revision < 0:
|
||||||
|
raise ValueError("revision 不能为负数")
|
||||||
|
哈希 = self.内容哈希
|
||||||
|
if len(哈希) != 64 or any(ch not in "0123456789abcdef" for ch in 哈希):
|
||||||
|
raise ValueError("内容哈希必须是 64 位十六进制 SHA-256")
|
||||||
|
|
||||||
|
|
||||||
|
def 计算内容哈希(文本: str) -> str:
|
||||||
|
"""按 UTF-8 字节计算 SHA-256 十六进制哈希;正文、规划与快照统一用它。"""
|
||||||
|
return hashlib.sha256(文本.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["版本引用", "计算内容哈希"]
|
||||||
60
src/muse/共享/调用身份.py
Normal file
60
src/muse/共享/调用身份.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""作者、任务、用途和授权上下文。
|
||||||
|
|
||||||
|
合同依据:接口契约/任务工具与事件.md。
|
||||||
|
- run_purpose(用途)表示执行隔离:production / evaluation / maintenance。
|
||||||
|
- content_purpose(内容用途)表示内容消费:planning / generation / detection / extraction。
|
||||||
|
- 两者不能混用:评测写手是 evaluation + generation,按 generation 裁剪字段、
|
||||||
|
受评测数据库权限约束。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from muse.共享.标识 import 任务标识, 校验标识
|
||||||
|
|
||||||
|
|
||||||
|
class 用途(StrEnum):
|
||||||
|
"""执行隔离用途。"""
|
||||||
|
|
||||||
|
生产 = "production"
|
||||||
|
评测 = "evaluation"
|
||||||
|
维护 = "maintenance"
|
||||||
|
|
||||||
|
|
||||||
|
class 内容用途(StrEnum):
|
||||||
|
"""内容消费用途。"""
|
||||||
|
|
||||||
|
规划 = "planning"
|
||||||
|
生成 = "generation"
|
||||||
|
检测 = "detection"
|
||||||
|
抽取 = "extraction"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class 调用身份:
|
||||||
|
"""一次业务调用携带的稳定身份与用途。
|
||||||
|
|
||||||
|
模型工具没有作者确认身份;作者身份只随作者显式动作传入。
|
||||||
|
人工命令的任务引用可空,不虚构一个S02后台任务。
|
||||||
|
"""
|
||||||
|
|
||||||
|
作者: str | None
|
||||||
|
任务: 任务标识 | None
|
||||||
|
用途: 用途
|
||||||
|
内容用途: 内容用途
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.作者 is not None:
|
||||||
|
校验标识(self.作者, 名称="作者")
|
||||||
|
if self.任务 is not None:
|
||||||
|
校验标识(self.任务, 名称="任务")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def 允许写正式内容(self) -> bool:
|
||||||
|
"""只有生产用途且携带作者身份才可能写正式内容(仍需各模块授权检查)。"""
|
||||||
|
return self.用途 is 用途.生产 and self.作者 is not None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["用途", "内容用途", "调用身份"]
|
||||||
60
src/muse/共享/错误.py
Normal file
60
src/muse/共享/错误.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""共用错误结构。
|
||||||
|
|
||||||
|
错误携带稳定错误码、中文说明与可重试性;凭据值禁止进入错误消息与上下文。
|
||||||
|
接入层(HTTP/CLI)把这些字段映射为对外错误响应,不另造第二套错误合同。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class Muse错误(Exception):
|
||||||
|
"""muse 业务与基础设施错误的基类。
|
||||||
|
|
||||||
|
:param 错误码: 稳定英文代码,接入层据此映射 HTTP 状态与 CLI 退出码。
|
||||||
|
:param 说明: 面向作者的中文说明,直陈事实与后果。
|
||||||
|
:param 可重试: 该错误在同一输入下重试是否有意义。
|
||||||
|
:param 上下文: 排查所需的最小事实;禁止放入凭据值。
|
||||||
|
"""
|
||||||
|
|
||||||
|
错误码: str = "MUSE_ERROR"
|
||||||
|
可重试: bool = False
|
||||||
|
|
||||||
|
def __init__(self, 说明: str, *, 上下文: dict[str, Any] | None = None) -> None:
|
||||||
|
super().__init__(说明)
|
||||||
|
self.说明 = 说明
|
||||||
|
self.上下文: dict[str, Any] = dict(上下文 or {})
|
||||||
|
|
||||||
|
def 呈现(self) -> dict[str, Any]:
|
||||||
|
"""给接入层与日志的确定性结构;不包含凭据值。"""
|
||||||
|
return {
|
||||||
|
"code": self.错误码,
|
||||||
|
"message": self.说明,
|
||||||
|
"target_ref": None,
|
||||||
|
"retryable": self.可重试,
|
||||||
|
"details": self.上下文,
|
||||||
|
"actions": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class 配置错误(Muse错误):
|
||||||
|
"""配置缺失、不合法或互相冲突;发生在启动边界,不回退隐式默认。"""
|
||||||
|
|
||||||
|
错误码 = "MUSE_CONFIG"
|
||||||
|
|
||||||
|
|
||||||
|
class 环境缺失错误(Muse错误):
|
||||||
|
"""所需外部环境(隔离库、真实模型授权等)未提供。"""
|
||||||
|
|
||||||
|
错误码 = "MUSE_ENV_MISSING"
|
||||||
|
可重试 = True
|
||||||
|
|
||||||
|
|
||||||
|
class 资源缺失错误(Muse错误):
|
||||||
|
"""安装包内带发布身份的资源缺失或哈希不符。"""
|
||||||
|
|
||||||
|
错误码 = "MUSE_RESOURCE_MISSING"
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Muse错误", "配置错误", "环境缺失错误", "资源缺失错误"]
|
||||||
219
src/muse/启动.py
Normal file
219
src/muse/启动.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
"""选择并注入数据库、宿主、模块和流程实现。
|
||||||
|
|
||||||
|
合同依据:文件设计/后端-装配与接入.md。
|
||||||
|
|
||||||
|
- 环境实现的选择集中在此唯一装配位置;更换宿主或连接实现不改业务规则。
|
||||||
|
- 只配置和注入模块、宿主、连接、资源与流程;不承载创作规则。
|
||||||
|
- 数据库工厂随配置与用途注入,取得连接时才访问外部;模型与角色循环读取任务已绑定配置。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
from muse.上下文.接口 import 上下文服务
|
||||||
|
from muse.任务运行.接口 import (
|
||||||
|
任务服务,
|
||||||
|
原文服务,
|
||||||
|
只读工具集,
|
||||||
|
执行上下文,
|
||||||
|
模型执行器,
|
||||||
|
角色策略目录,
|
||||||
|
证据服务,
|
||||||
|
调用计价,
|
||||||
|
配置版本管理,
|
||||||
|
预算管理,
|
||||||
|
领取凭证,
|
||||||
|
)
|
||||||
|
from muse.作品规划.接口 import 作品服务, 创作进度服务, 探索服务, 登记作品参与者, 规划服务
|
||||||
|
from muse.共享.时间 import 时钟, 真实时钟
|
||||||
|
from muse.共享.错误 import Muse错误, 配置错误
|
||||||
|
from muse.基础设施.受控文件 import 受控文件
|
||||||
|
from muse.基础设施.宿主.Pi import Pi宿主
|
||||||
|
from muse.基础设施.宿主.直接调用 import 直接宿主, 直接角色循环
|
||||||
|
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||||
|
from muse.基础设施.模型.HTTP传输 import HTTP传输
|
||||||
|
from muse.接入.只读工具 import 领取只读工具
|
||||||
|
from muse.故事世界.接口 import 故事服务, 登记故事参与者, 解析本书规划引用
|
||||||
|
from muse.正式变更.接口 import 参与者目录, 正式变更服务
|
||||||
|
from muse.正文写作.接口 import 正文服务, 登记正文参与者
|
||||||
|
from muse.编排.接口 import 流程登记
|
||||||
|
from muse.配置 import 应用配置
|
||||||
|
|
||||||
|
|
||||||
|
class 数据库实现(Protocol):
|
||||||
|
"""取得按装配用途绑定的数据库连接。"""
|
||||||
|
|
||||||
|
def 连接(self, *, 只读: bool = False, autocommit: bool = False) -> psycopg.Connection: ...
|
||||||
|
|
||||||
|
def 检查连接(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class 装配未完成(Muse错误):
|
||||||
|
"""所需实现尚未注入;启动如实失败而不是静默降级。"""
|
||||||
|
|
||||||
|
错误码 = "MUSE_ASSEMBLY_INCOMPLETE"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class 应用装配:
|
||||||
|
"""已注入的实现集合;业务模块只依赖这里的成员。"""
|
||||||
|
|
||||||
|
配置: 应用配置
|
||||||
|
时钟: 时钟
|
||||||
|
数据库: 数据库实现 | None = None
|
||||||
|
任务运行: 任务服务 | None = None
|
||||||
|
原文: 原文服务 | None = None
|
||||||
|
作品: 作品服务 | None = None
|
||||||
|
正文: 正文服务 | None = None
|
||||||
|
作品进度: 创作进度服务 | None = None
|
||||||
|
故事世界: 故事服务 | None = None
|
||||||
|
规划: 规划服务 | None = None
|
||||||
|
上下文: 上下文服务 | None = None
|
||||||
|
|
||||||
|
def 要求上下文(self) -> 上下文服务:
|
||||||
|
if self.上下文 is None:
|
||||||
|
raise 装配未完成("上下文服务未注入")
|
||||||
|
return self.上下文
|
||||||
|
|
||||||
|
def 要求只读工具(self, 领取: 领取凭证) -> 只读工具集:
|
||||||
|
"""工具范围完全取自已保存任务;接入层不重新解释作者、作品或截止。"""
|
||||||
|
if self.任务运行 is None:
|
||||||
|
raise 装配未完成("任务运行服务未注入")
|
||||||
|
return 领取只读工具(self.要求上下文(), self.任务运行, 领取)
|
||||||
|
|
||||||
|
def 要求规划(self) -> 规划服务:
|
||||||
|
if self.规划 is None:
|
||||||
|
raise 装配未完成("规划服务未注入")
|
||||||
|
return self.规划
|
||||||
|
|
||||||
|
def 要求探索(self) -> 探索服务:
|
||||||
|
if self.配置.探索草稿 is None:
|
||||||
|
raise 配置错误("私人探索需要在作者配置中明确探索草稿目录")
|
||||||
|
return 探索服务(Path(self.配置.探索草稿))
|
||||||
|
|
||||||
|
def 要求故事世界(self) -> 故事服务:
|
||||||
|
if self.故事世界 is None:
|
||||||
|
raise 装配未完成("故事世界服务未注入")
|
||||||
|
return self.故事世界
|
||||||
|
|
||||||
|
def 要求作品进度(self) -> 创作进度服务:
|
||||||
|
if self.作品进度 is None:
|
||||||
|
raise 装配未完成("创作进度查询未注入")
|
||||||
|
return self.作品进度
|
||||||
|
|
||||||
|
def 要求作品(self) -> 作品服务:
|
||||||
|
if self.作品 is None:
|
||||||
|
raise 装配未完成("作品服务未注入")
|
||||||
|
return self.作品
|
||||||
|
|
||||||
|
def 要求正文(self) -> 正文服务:
|
||||||
|
if self.正文 is None:
|
||||||
|
raise 装配未完成("正文服务未注入")
|
||||||
|
return self.正文
|
||||||
|
|
||||||
|
def 要求证据(self) -> 证据服务:
|
||||||
|
return 证据服务(数据库工厂(self.配置.数据库, self.配置.运行用途))
|
||||||
|
|
||||||
|
def 要求原文(self) -> 原文服务:
|
||||||
|
if self.原文 is None:
|
||||||
|
self.原文 = 原文服务(
|
||||||
|
数据库工厂(self.配置.数据库, self.配置.运行用途),
|
||||||
|
受控文件(Path(self.配置.原文暂存)) if self.配置.原文暂存 else None,
|
||||||
|
self.时钟,
|
||||||
|
)
|
||||||
|
return self.原文
|
||||||
|
|
||||||
|
def 要求数据库(self) -> 数据库实现:
|
||||||
|
if self.数据库 is None:
|
||||||
|
raise 装配未完成(
|
||||||
|
"数据库实现未注入(W03 提供);当前构建拒绝启动业务入口",
|
||||||
|
上下文={"缺少": "数据库"},
|
||||||
|
)
|
||||||
|
return self.数据库
|
||||||
|
|
||||||
|
def 要求模型执行器(self, 上下文: 执行上下文, 计价: 调用计价) -> 模型执行器:
|
||||||
|
数据库 = 数据库工厂(self.配置.数据库, self.配置.运行用途)
|
||||||
|
版本 = 配置版本管理(数据库).读取任务绑定(上下文.领取.任务ID)
|
||||||
|
内容 = 版本.内容
|
||||||
|
if 内容.资源发布身份 != self.配置.资源发布身份:
|
||||||
|
raise 配置错误("任务运行配置与当前安装资源不一致")
|
||||||
|
if self.任务运行 is None:
|
||||||
|
raise 装配未完成("任务运行服务未注入")
|
||||||
|
当前 = self.任务运行.读取任务(上下文.领取.任务ID)
|
||||||
|
步骤 = next(s for s in 当前.流程.步骤 if s.步骤ID == 上下文.领取.步骤ID)
|
||||||
|
策略 = 角色策略目录.从发布包()
|
||||||
|
角色 = 策略.定义.get("aliases", {}).get(步骤.角色, 步骤.角色)
|
||||||
|
if 角色 not in 内容.角色配置:
|
||||||
|
raise 配置错误("任务角色没有对应的固定运行配置")
|
||||||
|
声明 = 内容.角色配置[角色]
|
||||||
|
提供方 = next(p for p in 内容.提供方 if p.身份 == 声明["provider"])
|
||||||
|
凭据 = next(r for r in 内容.凭据 if r.名称 == 提供方.凭据名称)
|
||||||
|
return 模型执行器(
|
||||||
|
self.任务运行,
|
||||||
|
预算管理(数据库, 内容.预算策略引用),
|
||||||
|
self.要求原文(),
|
||||||
|
self.要求证据(),
|
||||||
|
策略,
|
||||||
|
直接宿主(HTTP传输(提供方.地址, 凭据.来源, 凭据.位置, 提供方.协议)),
|
||||||
|
计价,
|
||||||
|
固定配置=版本,
|
||||||
|
)
|
||||||
|
|
||||||
|
def 要求角色循环(self, 任务ID: str) -> 直接角色循环 | Pi宿主:
|
||||||
|
版本 = 配置版本管理(数据库工厂(self.配置.数据库, self.配置.运行用途)).读取任务绑定(任务ID)
|
||||||
|
内容 = 版本.内容
|
||||||
|
if 内容.宿主 == "pi":
|
||||||
|
assert 内容.Node路径 is not None and 内容.Pi包目录 is not None
|
||||||
|
return Pi宿主(Path(内容.Node路径), Path(内容.Pi包目录), 内容.宿主版本)
|
||||||
|
if 内容.宿主版本 != "1":
|
||||||
|
raise 配置错误("未支持的直接宿主合同版本")
|
||||||
|
return 直接角色循环()
|
||||||
|
|
||||||
|
|
||||||
|
def 构建(配置: 应用配置, *, 时钟: 时钟 | None = None, 流程: 流程登记 | None = None) -> 应用装配:
|
||||||
|
"""注入按用途绑定的数据库工厂;装配与自检不建立连接。"""
|
||||||
|
数据库 = 数据库工厂(配置.数据库, 配置.运行用途)
|
||||||
|
目录 = 参与者目录()
|
||||||
|
登记作品参与者(目录, 探索服务(Path(配置.探索草稿)) if 配置.探索草稿 else None, 解析本书规划引用)
|
||||||
|
登记正文参与者(目录)
|
||||||
|
登记故事参与者(目录)
|
||||||
|
正式 = 正式变更服务(数据库, 目录)
|
||||||
|
作品 = 作品服务(数据库, 正式)
|
||||||
|
正文 = 正文服务(数据库, 正式)
|
||||||
|
任务 = 任务服务(数据库, 流程 or 流程登记())
|
||||||
|
return 应用装配(
|
||||||
|
配置=配置,
|
||||||
|
时钟=时钟 or 真实时钟(),
|
||||||
|
数据库=数据库,
|
||||||
|
任务运行=任务,
|
||||||
|
作品=作品,
|
||||||
|
正文=正文,
|
||||||
|
作品进度=创作进度服务(作品, 正文, 任务),
|
||||||
|
故事世界=故事服务(数据库, 正式),
|
||||||
|
规划=规划服务(数据库, 正式, 解析本书规划引用),
|
||||||
|
上下文=上下文服务(数据库, 任务),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def 自检(装配: 应用装配) -> dict[str, Any]:
|
||||||
|
"""进程内自检:报告已注入实现与缺口;不连接任何外部服务。"""
|
||||||
|
return {
|
||||||
|
"包版本": __import__("muse").__version__,
|
||||||
|
"资源发布身份": 装配.配置.资源发布身份,
|
||||||
|
"数据库": "已注入" if 装配.数据库 is not None else "未注入(W03)",
|
||||||
|
"宿主": "按任务已绑定配置装配",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"应用装配",
|
||||||
|
"构建",
|
||||||
|
"自检",
|
||||||
|
"装配未完成",
|
||||||
|
"数据库实现",
|
||||||
|
]
|
||||||
165
src/muse/资源加载.py
Normal file
165
src/muse/资源加载.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
"""安装包资源、发布身份与哈希核对。
|
||||||
|
|
||||||
|
合同依据:接口契约/任务工具与事件.md(生产不读取未审工作树)。
|
||||||
|
|
||||||
|
- 只读安装包内带发布身份的资源(muse.资源 包内的 清单.json 与运行资源.zip)。
|
||||||
|
- 清单与实际字节哈希不符、清单缺失即 资源缺失错误,不加载替代路径。
|
||||||
|
- 不读取 Git 工作树、tests 或当前目录;不加载任意用户路径。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
import zipfile
|
||||||
|
from importlib import resources
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from muse.共享.错误 import 资源缺失错误
|
||||||
|
|
||||||
|
_清单名 = "清单.json"
|
||||||
|
_包名 = "运行资源.zip"
|
||||||
|
_资源包 = "muse.资源"
|
||||||
|
|
||||||
|
|
||||||
|
def 解析能力声明(数据: bytes) -> tuple[dict, str]:
|
||||||
|
"""构建和运行共同读取frontmatter;正文不夹带登记元信息。"""
|
||||||
|
try:
|
||||||
|
文本 = 数据.decode("utf-8")
|
||||||
|
匹配 = re.fullmatch(r"---\r?\n(.*?)\r?\n---(?:\r?\n|$)(.*)", 文本, re.S)
|
||||||
|
if 匹配 is None:
|
||||||
|
raise ValueError
|
||||||
|
声明 = yaml.safe_load(匹配[1])
|
||||||
|
if not isinstance(声明, dict) or not 匹配[2].strip():
|
||||||
|
raise ValueError
|
||||||
|
if not all(
|
||||||
|
isinstance(声明.get(k), str) and 声明[k].strip() for k in ("id", "name", "description")
|
||||||
|
):
|
||||||
|
raise ValueError
|
||||||
|
if not re.fullmatch(r"[a-z][a-z0-9_.-]*", 声明["id"]):
|
||||||
|
raise ValueError
|
||||||
|
if (
|
||||||
|
not re.fullmatch(r"[\u3400-\u9fff]+", 声明["name"])
|
||||||
|
or unicodedata.normalize("NFC", 声明["name"]) != 声明["name"]
|
||||||
|
):
|
||||||
|
raise ValueError
|
||||||
|
if (
|
||||||
|
声明.get("category") not in {"role", "method", "operation"}
|
||||||
|
or 声明.get("contract_version") != 1
|
||||||
|
):
|
||||||
|
raise ValueError
|
||||||
|
允许字段 = {"id", "name", "category", "contract_version", "description"}
|
||||||
|
if 声明["category"] == "operation":
|
||||||
|
允许字段.add("commands")
|
||||||
|
未登记 = sorted(set(声明) - 允许字段)
|
||||||
|
if 未登记:
|
||||||
|
# 模型绑定等静态配置不得写进能力文件;角色模型由策略与配置决定。
|
||||||
|
raise ValueError(f"能力声明包含未登记字段:{','.join(未登记)}")
|
||||||
|
return 声明, 匹配[2].strip()
|
||||||
|
except (UnicodeError, yaml.YAMLError, ValueError) as 错误:
|
||||||
|
if isinstance(错误, ValueError) and str(错误).startswith("能力声明"):
|
||||||
|
raise
|
||||||
|
raise ValueError("能力资源需要稳定身份、中文名称、分类、合同版本和有效正文") from None
|
||||||
|
|
||||||
|
|
||||||
|
def 读取能力目录() -> dict:
|
||||||
|
return json.loads(打开资源("能力/目录.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def 读取能力(分类: str, 身份: str, *, 预期哈希: str | None = None) -> dict:
|
||||||
|
"""按稳定身份取得固定角色或Skill,未发布资源不回退到工作树。"""
|
||||||
|
条目 = 读取能力目录().get(分类, {}).get(身份)
|
||||||
|
if 条目 is None:
|
||||||
|
raise 资源缺失错误("能力身份未在当前安装包登记")
|
||||||
|
if 预期哈希 is not None and 条目["sha256"] != 预期哈希:
|
||||||
|
raise 资源缺失错误("能力资源与冻结哈希不符")
|
||||||
|
数据 = 打开资源(条目["resource_path"])
|
||||||
|
if _sha256_of(数据) != 条目["sha256"]:
|
||||||
|
raise 资源缺失错误("能力资源与目录摘要不符")
|
||||||
|
声明, 正文 = 解析能力声明(数据)
|
||||||
|
if 声明["id"] != 身份 or 声明["category"] != 分类:
|
||||||
|
raise 资源缺失错误("能力正文声明与目录身份不一致")
|
||||||
|
return {**条目, "正文": 正文}
|
||||||
|
|
||||||
|
|
||||||
|
def _读取清单字节() -> bytes:
|
||||||
|
try:
|
||||||
|
ref = resources.files(_资源包).joinpath(_清单名)
|
||||||
|
except ModuleNotFoundError as exc: # 资源包尚未生成
|
||||||
|
raise 资源缺失错误(
|
||||||
|
"安装包缺少资源目录;先执行 make 生成",
|
||||||
|
上下文={"包": _资源包},
|
||||||
|
) from exc
|
||||||
|
if not ref.is_file():
|
||||||
|
raise 资源缺失错误(
|
||||||
|
"安装包缺少资源清单;先执行 make 生成",
|
||||||
|
上下文={"期望文件": _清单名},
|
||||||
|
)
|
||||||
|
return ref.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def 加载清单() -> dict[str, Any]:
|
||||||
|
"""加载并校验资源清单;返回 清单.json 的确定性结构。"""
|
||||||
|
原始 = _读取清单字节()
|
||||||
|
try:
|
||||||
|
清单: dict[str, Any] = json.loads(原始)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise 资源缺失错误(f"资源清单不是合法 JSON:{exc}") from exc
|
||||||
|
if not isinstance(清单.get("发布身份"), str) or not 清单["发布身份"]:
|
||||||
|
raise 资源缺失错误("资源清单缺少 发布身份")
|
||||||
|
if not isinstance(清单.get("资源"), dict):
|
||||||
|
raise 资源缺失错误("资源清单缺少 资源 表")
|
||||||
|
return 清单
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_of(数据: bytes) -> str:
|
||||||
|
return hashlib.sha256(数据).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def 打开资源(名称: str) -> bytes:
|
||||||
|
"""按清单打开一个已登记资源的字节;未登记、缺失或哈希不符都失败。"""
|
||||||
|
清单 = 加载清单()
|
||||||
|
条目 = 清单["资源"].get(名称)
|
||||||
|
if not isinstance(条目, dict):
|
||||||
|
raise 资源缺失错误(f"资源未在清单登记:{名称}")
|
||||||
|
期望哈希 = str(条目.get("sha256", ""))
|
||||||
|
if len(期望哈希) != 64:
|
||||||
|
raise 资源缺失错误(f"资源清单哈希不合法:{名称}")
|
||||||
|
资源根 = resources.files(_资源包)
|
||||||
|
if 名称.startswith(("工作台/", "迁移/")):
|
||||||
|
文件 = 资源根.joinpath(名称)
|
||||||
|
if not 文件.is_file():
|
||||||
|
raise 资源缺失错误(f"安装包缺少目录资源:{名称}")
|
||||||
|
数据 = 文件.read_bytes()
|
||||||
|
else:
|
||||||
|
压缩包路径 = 资源根.joinpath(_包名)
|
||||||
|
if not 压缩包路径.is_file():
|
||||||
|
raise 资源缺失错误("安装包缺少运行资源包", 上下文={"期望文件": _包名})
|
||||||
|
with zipfile.ZipFile(io.BytesIO(压缩包路径.read_bytes())) as zf:
|
||||||
|
try:
|
||||||
|
数据 = zf.read(名称)
|
||||||
|
except KeyError as exc:
|
||||||
|
raise 资源缺失错误(f"资源在运行资源包中不存在:{名称}") from exc
|
||||||
|
实际哈希 = _sha256_of(数据)
|
||||||
|
if 实际哈希 != 期望哈希:
|
||||||
|
raise 资源缺失错误(
|
||||||
|
f"资源哈希与清单不符:{名称}",
|
||||||
|
上下文={"期望": 期望哈希, "实际": 实际哈希},
|
||||||
|
)
|
||||||
|
return 数据
|
||||||
|
|
||||||
|
|
||||||
|
def 核对资源() -> dict[str, Any]:
|
||||||
|
"""逐项读取并核对包内字节;清单可解析不等同资源完整。"""
|
||||||
|
清单 = 加载清单()
|
||||||
|
for 名称 in 清单["资源"]:
|
||||||
|
打开资源(名称)
|
||||||
|
return 清单
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["加载清单", "打开资源", "核对资源", "解析能力声明", "读取能力目录", "读取能力"]
|
||||||
147
src/muse/配置.py
Normal file
147
src/muse/配置.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""配置解析、校验与连接引用。
|
||||||
|
|
||||||
|
合同依据:文件设计/后端-装配与接入.md、工程配置与部署.md。
|
||||||
|
|
||||||
|
- 解析 TOML 与环境变量,校验后在启动边界暴露缺失与冲突。
|
||||||
|
- 只保存连接“引用”(环境变量名或受控存储位置),不保存凭据值。
|
||||||
|
- 缺配置或不合法策略抛 配置错误;禁止回退到隐式正式库或示例配置。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from muse.共享.调用身份 import 用途
|
||||||
|
from muse.共享.错误 import 配置错误
|
||||||
|
|
||||||
|
# 凭据引用的取值方式:值本身永远从环境变量或受控存储读取,不落入配置对象。
|
||||||
|
_允许的取值方式 = frozenset({"环境变量", "受控存储"})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class 数据库引用:
|
||||||
|
"""数据库连接引用:只记录取值方式与位置,不含连接串明文。"""
|
||||||
|
|
||||||
|
取值方式: str
|
||||||
|
位置: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.取值方式 not in _允许的取值方式:
|
||||||
|
raise 配置错误("数据库凭据取值方式不合法")
|
||||||
|
if not self.位置.strip():
|
||||||
|
raise 配置错误("数据库凭据位置不能为空")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class 服务配置:
|
||||||
|
"""单作者 HTTP 服务的访问来源与凭据位置。"""
|
||||||
|
|
||||||
|
口令文件: str
|
||||||
|
作者ID: str = "author-local"
|
||||||
|
监听地址: str = "127.0.0.1"
|
||||||
|
端口: int = 8080
|
||||||
|
公开地址: str = "http://127.0.0.1:8080"
|
||||||
|
允许来源: tuple[str, ...] = ("http://127.0.0.1:8080",)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.口令文件 or not self.作者ID:
|
||||||
|
raise 配置错误("HTTP 服务需要作者身份和口令文件引用")
|
||||||
|
if not 1 <= self.端口 <= 65535:
|
||||||
|
raise 配置错误("HTTP 端口不合法")
|
||||||
|
if not self.允许来源 or any(x == "*" for x in self.允许来源):
|
||||||
|
raise 配置错误("HTTP 写入来源必须明确登记")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class 应用配置:
|
||||||
|
"""启动所需的最小配置面;随后续工作包按合同扩展。"""
|
||||||
|
|
||||||
|
数据库: 数据库引用
|
||||||
|
资源发布身份: str
|
||||||
|
运行用途: 用途 = 用途.生产
|
||||||
|
HTTP: 服务配置 | None = None
|
||||||
|
原文暂存: str | None = None
|
||||||
|
探索草稿: str | None = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.资源发布身份.strip():
|
||||||
|
raise 配置错误("资源发布身份不能为空")
|
||||||
|
|
||||||
|
|
||||||
|
def _校验数据库节(节: dict[str, Any]) -> 数据库引用:
|
||||||
|
取值方式 = 节.get("取值方式")
|
||||||
|
位置 = 节.get("位置")
|
||||||
|
if 取值方式 not in _允许的取值方式:
|
||||||
|
raise 配置错误(
|
||||||
|
"数据库.取值方式 不合法",
|
||||||
|
上下文={"实际": 取值方式, "允许": sorted(_允许的取值方式)},
|
||||||
|
)
|
||||||
|
if not isinstance(位置, str) or not 位置.strip():
|
||||||
|
raise 配置错误("数据库.位置 不能为空")
|
||||||
|
return 数据库引用(取值方式=取值方式, 位置=位置)
|
||||||
|
|
||||||
|
|
||||||
|
def 解析配置(数据: dict[str, Any]) -> 应用配置:
|
||||||
|
"""从已加载的 TOML 字典解析并校验应用配置;不合法即失败。"""
|
||||||
|
if "数据库" not in 数据:
|
||||||
|
raise 配置错误("配置缺少 [数据库] 节")
|
||||||
|
if "资源" not in 数据 or not isinstance(数据["资源"].get("发布身份"), str):
|
||||||
|
raise 配置错误("配置缺少 [资源].发布身份")
|
||||||
|
发布身份: str = 数据["资源"]["发布身份"]
|
||||||
|
if not 发布身份.strip():
|
||||||
|
raise 配置错误("[资源].发布身份 不能为空")
|
||||||
|
try:
|
||||||
|
运行用途 = 用途(数据.get("运行", {}).get("用途", 用途.生产.value))
|
||||||
|
except ValueError:
|
||||||
|
raise 配置错误("运行.用途 必须为 production、evaluation 或 maintenance") from None
|
||||||
|
服务 = 数据.get("HTTP")
|
||||||
|
try:
|
||||||
|
HTTP = 服务配置(**服务) if 服务 is not None else None
|
||||||
|
except TypeError as exc:
|
||||||
|
raise 配置错误("HTTP 配置字段不合法") from exc
|
||||||
|
return 应用配置(
|
||||||
|
数据库=_校验数据库节(数据["数据库"]),
|
||||||
|
资源发布身份=发布身份,
|
||||||
|
运行用途=运行用途,
|
||||||
|
HTTP=HTTP,
|
||||||
|
原文暂存=数据.get("文件", {}).get("原文暂存"),
|
||||||
|
探索草稿=数据.get("文件", {}).get("探索草稿"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def 读取配置(路径: str | Path) -> 应用配置:
|
||||||
|
"""读取并校验 TOML 配置文件;文件缺失、解析失败与校验失败均为 配置错误。"""
|
||||||
|
文件 = Path(路径)
|
||||||
|
if not 文件.is_file():
|
||||||
|
raise 配置错误(
|
||||||
|
"配置文件不存在",
|
||||||
|
上下文={"路径": str(文件), "获取位置": "复制 配置/应用.example.toml 后填写"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with 文件.open("rb") as fh:
|
||||||
|
数据 = tomllib.load(fh)
|
||||||
|
except tomllib.TOMLDecodeError as exc:
|
||||||
|
raise 配置错误(f"配置不是合法 TOML:{exc}", 上下文={"路径": str(文件)}) from exc
|
||||||
|
配置 = 解析配置(数据)
|
||||||
|
if 配置.数据库.取值方式 == "受控存储":
|
||||||
|
位置 = Path(配置.数据库.位置)
|
||||||
|
if not 位置.is_absolute():
|
||||||
|
配置 = replace(
|
||||||
|
配置, 数据库=replace(配置.数据库, 位置=str((文件.parent / 位置).resolve()))
|
||||||
|
)
|
||||||
|
if 配置.HTTP is not None and not Path(配置.HTTP.口令文件).is_absolute():
|
||||||
|
配置 = replace(
|
||||||
|
配置,
|
||||||
|
HTTP=replace(配置.HTTP, 口令文件=str((文件.parent / 配置.HTTP.口令文件).resolve())),
|
||||||
|
)
|
||||||
|
if 配置.原文暂存 is not None and not Path(配置.原文暂存).is_absolute():
|
||||||
|
配置 = replace(配置, 原文暂存=str((文件.parent / 配置.原文暂存).resolve()))
|
||||||
|
if 配置.探索草稿 is not None and not Path(配置.探索草稿).is_absolute():
|
||||||
|
配置 = replace(配置, 探索草稿=str((文件.parent / 配置.探索草稿).resolve()))
|
||||||
|
return 配置
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["数据库引用", "应用配置", "服务配置", "解析配置", "读取配置"]
|
||||||
423
tests/架构/test_技能与文档索引.py
Normal file
423
tests/架构/test_技能与文档索引.py
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
"""技能与文档索引行为验证。
|
||||||
|
|
||||||
|
保护:技能目录合同(SKILL.md 完整、名称唯一且全中文、frontmatter 一致)
|
||||||
|
与活跃文档索引(目录.md 标准索引、链接可达)。
|
||||||
|
|
||||||
|
已实现子集(W02):目录与链接检查、技能目录基础合同。
|
||||||
|
依赖技能承接(W13+)、角色目录(W06)与旧库参数守卫(W03/W07)的用例
|
||||||
|
在对应包落地后逐例实现。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(仓库根 / "工具"))
|
||||||
|
from 维护索引 import 检查技能目录, 检查目录链接 # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _写能力样本(根: Path, 相对: str, 身份: str, 名称: str) -> Path:
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
文件 = 根 / 相对
|
||||||
|
文件.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
声明 = {
|
||||||
|
"id": 身份,
|
||||||
|
"name": 名称,
|
||||||
|
"category": "method",
|
||||||
|
"contract_version": 1,
|
||||||
|
"description": "合成方法",
|
||||||
|
}
|
||||||
|
文件.write_text(
|
||||||
|
"---\n" + yaml.safe_dump(声明, allow_unicode=True) + "---\n\n只提出有依据的规划方法。\n"
|
||||||
|
)
|
||||||
|
return 文件
|
||||||
|
|
||||||
|
|
||||||
|
def test_passing_project_is_independent_of_current_repository__a189e4(tmp_path, monkeypatch):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
根 = tmp_path / "独立项目"
|
||||||
|
文件 = _写能力样本(根, ".agent/skills/alpha/SKILL.md", "alpha", "合成方法")
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
目录, 资源 = 编译能力资源(根, [文件.relative_to(根).as_posix()], set())
|
||||||
|
assert set(目录["method"]) == {"alpha"}
|
||||||
|
assert len(资源) == 1 and 资源["能力/method/alpha.md"] == 文件.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_skill_path_is_accepted_from_manifest__c9cb85(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
相对 = ".agent/skills/planning/alpha/SKILL.md"
|
||||||
|
文件 = _写能力样本(tmp_path, 相对, "alpha", "合成方法")
|
||||||
|
目录, 资源 = 编译能力资源(tmp_path, [相对], set())
|
||||||
|
assert 目录["method"]["alpha"]["resource_path"] == "能力/method/alpha.md"
|
||||||
|
assert 资源["能力/method/alpha.md"] == 文件.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_only_policy_rejects_english_manifest_name__c089ce(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
_写能力样本(tmp_path, "SKILL.md", "alpha", "alpha")
|
||||||
|
with pytest.raises(ValueError, match="中文名称"):
|
||||||
|
编译能力资源(tmp_path, ["SKILL.md"], set())
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_only_policy_allows_new_native_chinese_skill__b72bcc(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
a = _写能力样本(tmp_path, "规划/规划作品/SKILL.md", "plan-work", "规划作品")
|
||||||
|
b = _写能力样本(tmp_path, "写作/新增技能/SKILL.md", "new-method", "新增技能")
|
||||||
|
目录, 资源 = 编译能力资源(tmp_path, [p.relative_to(tmp_path).as_posix() for p in (a, b)], set())
|
||||||
|
assert {v["name"] for v in 目录["method"].values()} == {"规划作品", "新增技能"}
|
||||||
|
assert len(资源) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_normalized_skill_name_is_rejected__5b50cc(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
_写能力样本(tmp_path, "SKILL.md", "alpha", "e\u0301-name")
|
||||||
|
with pytest.raises(ValueError, match="中文名称"):
|
||||||
|
编译能力资源(tmp_path, ["SKILL.md"], set())
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_name_with_decorative_punctuation_is_rejected__984f56(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
_写能力样本(tmp_path, "SKILL.md", "story", "故事结构!")
|
||||||
|
with pytest.raises(ValueError, match="中文名称"):
|
||||||
|
编译能力资源(tmp_path, ["SKILL.md"], set())
|
||||||
|
|
||||||
|
|
||||||
|
def test_资源按稳定身份编译且移动不改变身份__a66001(tmp_path):
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
文件 = tmp_path / "初始.md"
|
||||||
|
文件.write_text(
|
||||||
|
"---\nid: writer\nname: 写手\ncategory: role\ncontract_version: 1\n"
|
||||||
|
"description: 合成职责\n---\n\n只执行固定任务。\n"
|
||||||
|
)
|
||||||
|
目录, 资源 = 编译能力资源(tmp_path, ["初始.md"], set())
|
||||||
|
assert 目录["role"]["writer"]["name"] == "写手"
|
||||||
|
assert 资源[目录["role"]["writer"]["resource_path"]] == 文件.read_bytes()
|
||||||
|
文件.rename(tmp_path / "迁移.md")
|
||||||
|
新目录, _ = 编译能力资源(tmp_path, ["迁移.md"], set())
|
||||||
|
assert 新目录["role"]["writer"]["sha256"] == 目录["role"]["writer"]["sha256"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_技能只声明已实现命令且重复身份拒绝__a66002(tmp_path):
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
文件 = tmp_path / "SKILL.md"
|
||||||
|
文件.write_text(
|
||||||
|
"---\nid: inspect-task\nname: 查看任务\ncategory: operation\ncontract_version: 1\n"
|
||||||
|
"description: 合成操作\ncommands: [read_task]\n---\n\n按授权读取任务。\n"
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="命令"):
|
||||||
|
编译能力资源(tmp_path, ["SKILL.md"], set())
|
||||||
|
目录, _ = 编译能力资源(tmp_path, ["SKILL.md"], {"read_task"})
|
||||||
|
assert 目录["operation"]["inspect-task"]["commands"] == ["read_task"]
|
||||||
|
(tmp_path / "副本.md").write_bytes(文件.read_bytes())
|
||||||
|
with pytest.raises(ValueError, match="重复"):
|
||||||
|
编译能力资源(tmp_path, ["SKILL.md", "副本.md"], {"read_task"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_运行按发布目录读取角色正文和命令__a66003():
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from muse.任务运行.接口 import 角色策略目录
|
||||||
|
from muse.共享.错误 import 资源缺失错误
|
||||||
|
from muse.资源加载 import 读取能力, 读取能力目录
|
||||||
|
|
||||||
|
目录 = 读取能力目录()
|
||||||
|
assert set(目录["role"]) == {"writer", "planner", "extractor", "detector", "judge"}
|
||||||
|
角色 = 读取能力("role", "writer", 预期哈希=目录["role"]["writer"]["sha256"])
|
||||||
|
assert 角色["name"] == "写手" and "生成阶段" in 角色["正文"]
|
||||||
|
assert not 角色["正文"].startswith("---")
|
||||||
|
策略 = 角色策略目录.从发布包()
|
||||||
|
assert 策略.读取角色提示("writer") == 角色["正文"]
|
||||||
|
assert len(策略.资源发布身份) == 64
|
||||||
|
操作 = 读取能力("operation", "inspect-task")
|
||||||
|
assert "control_task" in 操作["commands"]
|
||||||
|
with pytest.raises(资源缺失错误):
|
||||||
|
读取能力("role", "writer", 预期哈希="0" * 64)
|
||||||
|
|
||||||
|
|
||||||
|
def _建技能(
|
||||||
|
根: Path, 分组: str, 名称: str, *, front名: str | None = None, 正文: str | None = None
|
||||||
|
) -> Path:
|
||||||
|
目录 = 根 / ".agent" / "skills" / 分组 / 名称
|
||||||
|
目录.mkdir(parents=True, exist_ok=True)
|
||||||
|
if 正文 is None:
|
||||||
|
有效名 = front名 if front名 is not None else 名称
|
||||||
|
正文 = f"---\nname: {有效名}\ndescription: 合成技能说明\n---\n\n# {名称}\n\n方法正文。\n"
|
||||||
|
(目录 / "SKILL.md").write_text(正文, encoding="utf-8")
|
||||||
|
return 目录
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_content_directories_have_standard_indexes__b35be1() -> None:
|
||||||
|
"""一级内容目录可从根索引发现,且使用固定五列表头。"""
|
||||||
|
根 = 仓库根 / ".agent"
|
||||||
|
根索引 = (根 / "目录.md").read_text()
|
||||||
|
表头 = "| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |"
|
||||||
|
for 目录 in sorted(p for p in 根.iterdir() if p.is_dir()):
|
||||||
|
索引 = 目录 / "目录.md"
|
||||||
|
assert 索引.is_file(), f"缺少一级目录索引:{目录.name}"
|
||||||
|
assert 索引.read_text().startswith(表头), f"未使用固定五列表头:{索引}"
|
||||||
|
assert f"{目录.name}/目录.md" in 根索引, f"一级目录不可发现:{目录.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_markdown_links_resolve__3734a6() -> None:
|
||||||
|
"""活跃执行知识(.agent 树)的 Markdown 相对链接可达。"""
|
||||||
|
问题: list[str] = []
|
||||||
|
import re
|
||||||
|
|
||||||
|
链接 = re.compile(r"\[[^\]]*\]\(([^)#\s]+)[)#]")
|
||||||
|
for 文件 in (仓库根 / ".agent").rglob("*.md"):
|
||||||
|
if "__pycache__" in 文件.parts:
|
||||||
|
continue
|
||||||
|
base = 文件.parent
|
||||||
|
for 序, 行 in enumerate(文件.read_text(encoding="utf-8").splitlines(), 1):
|
||||||
|
if 行.strip().startswith("|"):
|
||||||
|
continue # 目录表行由 检查目录链接 覆盖
|
||||||
|
for 地址 in 链接.findall(行):
|
||||||
|
if 地址.startswith(("http://", "https://", "mailto:")):
|
||||||
|
continue
|
||||||
|
if not (base / 地址).exists():
|
||||||
|
问题.append(f"{文件.relative_to(仓库根)}:{序} 链接不存在:{地址}")
|
||||||
|
assert 问题 == [], "\n".join(问题[:10])
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_directory_without_skill_file_fails__e8450b(tmp_path: Path) -> None:
|
||||||
|
"""技能目录缺少 SKILL.md 时被报告。"""
|
||||||
|
目录 = tmp_path / ".agent" / "skills" / "写作" / "无文件技能"
|
||||||
|
目录.mkdir(parents=True)
|
||||||
|
(目录 / "references.md").write_text("只有资料", encoding="utf-8")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("缺少 SKILL.md" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_skill_file_is_reported__1d2b76(tmp_path: Path) -> None:
|
||||||
|
"""空 SKILL.md 被报告,不静默放行。"""
|
||||||
|
_建技能(tmp_path, "写作", "空技能", 正文="---\n")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("为空" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_frontmatter_name_must_match_skill_directory__703c7b(tmp_path: Path) -> None:
|
||||||
|
"""frontmatter name 必须与技能目录名一致。"""
|
||||||
|
_建技能(tmp_path, "写作", "真名技能", front名="别名技能")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("name 与目录不一致" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_frontmatter_names_are_reported__eb80b5(tmp_path: Path) -> None:
|
||||||
|
"""同名技能(跨分组)被报告。"""
|
||||||
|
_建技能(tmp_path, "写作", "重复技能")
|
||||||
|
_建技能(tmp_path, "规划", "重复技能")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("重复" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_skill_name_is_accepted__de3efe(tmp_path: Path) -> None:
|
||||||
|
"""全中文技能名通过全部目录合同。"""
|
||||||
|
_建技能(tmp_path, "写作", "中文技能")
|
||||||
|
assert 检查技能目录(tmp_path) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_only_policy_rejects_english_skill_name__d5c460(tmp_path: Path) -> None:
|
||||||
|
"""英文技能名被中文唯一政策拒绝。"""
|
||||||
|
_建技能(tmp_path, "写作", "english-skill")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("全中文" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_normalized_skill_name_is_rejected__a87a41(tmp_path: Path) -> None:
|
||||||
|
"""非 NFC 规范形式的技能名被拒绝。"""
|
||||||
|
非规范 = "组合e\u0301音技能" # e + 组合变音符:NFC 会合成 é,与原名不同
|
||||||
|
assert unicodedata.normalize("NFC", 非规范) != 非规范
|
||||||
|
_建技能(tmp_path, "写作", 非规范)
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("NFC" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_name_with_whitespace_is_rejected__b68ab9(tmp_path: Path) -> None:
|
||||||
|
"""含空白的技能名被拒绝。"""
|
||||||
|
_建技能(tmp_path, "写作", "带 空白技能")
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("含空白" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_skills_directory_fails_closed__5acb68(tmp_path: Path) -> None:
|
||||||
|
"""技能根目录缺失时明确失败,不静默跳过。"""
|
||||||
|
问题 = 检查技能目录(tmp_path)
|
||||||
|
assert any("不存在" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_matches_disk_and_manifest__000974() -> None:
|
||||||
|
"""每个分组的 目录.md 索引与磁盘上的技能目录一一对应(不多不少)。"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
技能根 = 仓库根 / ".agent" / "skills"
|
||||||
|
链接 = re.compile(r"\]\(([^/]+)/SKILL\.md\)")
|
||||||
|
for 分组 in sorted(p for p in 技能根.iterdir() if p.is_dir()):
|
||||||
|
目录文件 = 分组 / "目录.md"
|
||||||
|
assert 目录文件.is_file(), f"分组缺目录:{分组.name}"
|
||||||
|
索引名 = set(链接.findall(目录文件.read_text(encoding="utf-8")))
|
||||||
|
磁盘名 = {p.name for p in 分组.iterdir() if p.is_dir() and (p / "SKILL.md").is_file()}
|
||||||
|
assert 索引名 == 磁盘名, (
|
||||||
|
f"分组 {分组.name} 索引与磁盘不一致:"
|
||||||
|
f"索引多出 {sorted(索引名 - 磁盘名)},磁盘多出 {sorted(磁盘名 - 索引名)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_仓库当前树通过目录链接检查__b7c8d9() -> None:
|
||||||
|
"""当前仓库 目录.md 索引链接全部可达。"""
|
||||||
|
assert 检查目录链接(仓库根) == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# W06 承接:能力目录与角色合同(原 SkillCatalogTest)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _角色字节(身份: str, 名称: str, 额外: str = "") -> bytes:
|
||||||
|
return (
|
||||||
|
f"---\nid: {身份}\nname: {名称}\ncategory: role\n"
|
||||||
|
f"contract_version: 1\ndescription: 合成角色\n{额外}---\n\n你是{名称},按合同执行。\n"
|
||||||
|
).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_role_catalog_is_exact_and_host_neutral__569f09() -> None:
|
||||||
|
"""已发布角色目录恰好五类且宿主中立:frontmatter 不携带任何模型绑定。"""
|
||||||
|
from muse.资源加载 import 解析能力声明, 读取能力目录
|
||||||
|
|
||||||
|
目录 = 读取能力目录()["role"]
|
||||||
|
assert set(目录) == {"writer", "planner", "extractor", "detector", "judge"}
|
||||||
|
from muse.资源加载 import 打开资源
|
||||||
|
|
||||||
|
for 身份, 条目 in 目录.items():
|
||||||
|
声明, 正文 = 解析能力声明(打开资源(条目["resource_path"]))
|
||||||
|
assert 声明["id"] == 身份 and 声明["category"] == "role"
|
||||||
|
assert "model" not in 声明 and "provider" not in 声明, f"{身份} 携带静态模型绑定"
|
||||||
|
assert 正文.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_catalog_ignores_directory_index__4da912(tmp_path: Path) -> None:
|
||||||
|
"""能力目录只收登记文件;目录索引不是能力,不进入发布包。"""
|
||||||
|
import sys as _sys
|
||||||
|
|
||||||
|
_sys.path.insert(0, str(仓库根 / "工具"))
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
角色 = ("writer", "planner", "extractor", "detector", "judge")
|
||||||
|
名称 = {
|
||||||
|
"writer": "写手",
|
||||||
|
"planner": "规划员",
|
||||||
|
"extractor": "知识抽取员",
|
||||||
|
"detector": "检测员",
|
||||||
|
"judge": "评委",
|
||||||
|
}
|
||||||
|
for r in 角色:
|
||||||
|
(tmp_path / "角色").mkdir(exist_ok=True)
|
||||||
|
(tmp_path / "角色" / f"{名称[r]}.md").write_bytes(_角色字节(r, 名称[r]))
|
||||||
|
(tmp_path / "角色" / "目录.md").write_text(
|
||||||
|
"| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |\n"
|
||||||
|
"|------|----------|----------|----------|----------|\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
目录, 资源 = 编译能力资源(tmp_path, [f"角色/{名称[r]}.md" for r in 角色], set())
|
||||||
|
assert set(目录["role"]) == set(角色)
|
||||||
|
assert len(资源) == 5 # 索引文件未被扫描进包
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_contract_is_central_and_complete__a74ee6() -> None:
|
||||||
|
"""角色合同与策略是唯一中心:五角色、固定模型策略与正文提示都齐备。"""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from muse.任务运行.角色策略 import 角色策略目录
|
||||||
|
from muse.资源加载 import 读取能力目录
|
||||||
|
|
||||||
|
定义 = yaml.safe_load((仓库根 / "配置" / "角色策略.yaml").read_text(encoding="utf-8"))
|
||||||
|
策略 = 角色策略目录(定义, 角色资源=读取能力目录()["role"])
|
||||||
|
合同 = (仓库根 / "muse" / "sot" / "角色合同.md").read_text(encoding="utf-8")
|
||||||
|
for 角色, 中文名 in {
|
||||||
|
"writer": "网文写手",
|
||||||
|
"detector": "检测员",
|
||||||
|
"judge": "质量评委",
|
||||||
|
"extractor": "知识抽取员",
|
||||||
|
}.items():
|
||||||
|
assert 角色 in 合同 and 中文名 in 合同
|
||||||
|
for 身份 in ("writer", "planner", "judge"):
|
||||||
|
assert 定义["roles"][身份]["model_policy"] == "fixed"
|
||||||
|
for 别名, 正名 in 定义.get("aliases", {}).items():
|
||||||
|
assert 策略.读取角色提示(别名) == 策略.读取角色提示(正名)
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_frontmatter_rejects_static_model_binding__7d0984() -> None:
|
||||||
|
"""角色 frontmatter 出现 model 等未登记字段直接拒绝。"""
|
||||||
|
from muse.资源加载 import 解析能力声明
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="未登记字段"):
|
||||||
|
解析能力声明(_角色字节("writer", "写手", 额外="model: claude-opus\n"))
|
||||||
|
with pytest.raises(ValueError, match="未登记字段"):
|
||||||
|
解析能力声明(_角色字节("planner", "规划员", 额外="provider: primary\n"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_catalog_is_valid__0af776() -> None:
|
||||||
|
"""发布目录内每个能力都能按登记哈希读取且声明与目录一致。"""
|
||||||
|
from muse.资源加载 import 读取能力, 读取能力目录
|
||||||
|
|
||||||
|
目录 = 读取能力目录()
|
||||||
|
总数 = sum(len(v) for v in 目录.values())
|
||||||
|
assert 总数 > 0
|
||||||
|
for 分类, 条目组 in 目录.items():
|
||||||
|
for 身份, 条目 in 条目组.items():
|
||||||
|
资源 = 读取能力(分类, 身份, 预期哈希=条目["sha256"])
|
||||||
|
assert 资源["name"] == 条目["name"]
|
||||||
|
assert 资源["正文"].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_directory_and_name_must_match__29d9f3(tmp_path: Path) -> None:
|
||||||
|
"""能力身份由声明而非文件位置决定:入包路径从身份派生,声明与目录一致。"""
|
||||||
|
import sys as _sys
|
||||||
|
|
||||||
|
_sys.path.insert(0, str(仓库根 / "工具"))
|
||||||
|
from muse.资源加载 import 解析能力声明
|
||||||
|
from 构建资源包 import 编译能力资源
|
||||||
|
|
||||||
|
# 源文件名与中文名无关;入包后由身份定位,不再允许目录名充当身份。
|
||||||
|
(tmp_path / "旧位置").mkdir()
|
||||||
|
源 = tmp_path / "旧位置" / "legacy-plan.md"
|
||||||
|
源.write_bytes(_角色字节("writer", "写手"))
|
||||||
|
目录, 资源 = 编译能力资源(tmp_path, ["旧位置/legacy-plan.md"], set())
|
||||||
|
assert "能力/role/writer.md" in 资源
|
||||||
|
声明, _ = 解析能力声明(资源["能力/role/writer.md"])
|
||||||
|
assert (声明["id"], 声明["name"]) == ("writer", "写手")
|
||||||
|
assert 目录["role"]["writer"]["resource_path"] == "能力/role/writer.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_skill_name_is_accepted__16a67a() -> None:
|
||||||
|
"""中文名称的能力声明被接受并原样保留。"""
|
||||||
|
from muse.资源加载 import 解析能力声明
|
||||||
|
|
||||||
|
声明, 正文 = 解析能力声明(_角色字节("writer", "写手"))
|
||||||
|
assert 声明["name"] == "写手"
|
||||||
|
assert "按合同执行" in 正文
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_frontmatter_key_is_rejected__432390() -> None:
|
||||||
|
"""未知 frontmatter 键被拒绝;合法键集内仍通过。"""
|
||||||
|
from muse.资源加载 import 解析能力声明
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="未登记字段"):
|
||||||
|
解析能力声明(_角色字节("judge", "评委", 额外="temperature: 0.7\n"))
|
||||||
|
声明, _ = 解析能力声明(_角色字节("judge", "评委"))
|
||||||
|
assert set(声明) == {"id", "name", "category", "contract_version", "description"}
|
||||||
330
tests/架构/test_模块依赖.py
Normal file
330
tests/架构/test_模块依赖.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
"""模块依赖行为验证。
|
||||||
|
|
||||||
|
保护:安装产物边界与依赖方向——业务只经公共接口、Skill 不携带实现、
|
||||||
|
接入不直写表、共享基础不反向依赖业务、装配层不承载创作规则。
|
||||||
|
|
||||||
|
已实现子集(W02):
|
||||||
|
- 安装包在任意 cwd 可导入且导入不产生外部连接(含旧树同名包遮挡守卫);
|
||||||
|
- 技能目录不注入 sys.path、不携带实现脚本;
|
||||||
|
- src/muse 活跃运行代码不依赖外部专有 CLI;
|
||||||
|
- 共享基础类型不导入业务模块;__main__ 只是薄命令入口。
|
||||||
|
|
||||||
|
依赖 W06/W07/W22/W24 的用例(宿主适配、协议、角色路由、旧库参数守卫、
|
||||||
|
技能目录合同)在对应包落地后逐例实现,不以形状检查代替。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import muse
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parents[2]
|
||||||
|
源码根 = 仓库根 / "src" / "muse"
|
||||||
|
|
||||||
|
|
||||||
|
def _遍历py(根: Path) -> list[Path]:
|
||||||
|
return sorted(p for p in 根.rglob("*.py") if "__pycache__" not in p.parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _导入名(文件: Path) -> set[str]:
|
||||||
|
"""AST 提取文件内全部 import 的顶层模块名。"""
|
||||||
|
树 = ast.parse(文件.read_text(encoding="utf-8"))
|
||||||
|
名单: set[str] = set()
|
||||||
|
for 节点 in ast.walk(树):
|
||||||
|
if isinstance(节点, ast.Import):
|
||||||
|
for 别名 in 节点.names:
|
||||||
|
名单.add(别名.name.split(".")[0])
|
||||||
|
elif isinstance(节点, ast.ImportFrom) and 节点.module and 节点.level == 0:
|
||||||
|
名单.add(节点.module.split(".")[0])
|
||||||
|
return 名单
|
||||||
|
|
||||||
|
|
||||||
|
def test_skills_do_not_syspath_into_shared_implementations__f4e7a4() -> None:
|
||||||
|
"""技能目录是方法与操作说明,不携带实现脚本,不注入 sys.path。"""
|
||||||
|
技能根 = 仓库根 / ".agent" / "skills"
|
||||||
|
脚本 = [p for p in 技能根.rglob("*.py") if "__pycache__" not in p.parts]
|
||||||
|
assert 脚本 == [], [str(p.relative_to(仓库根)) for p in 脚本]
|
||||||
|
for md in 技能根.rglob("*.md"):
|
||||||
|
文本 = md.read_text(encoding="utf-8")
|
||||||
|
assert "sys.path" not in 文本, f"技能文档出现 sys.path 注入:{md.relative_to(仓库根)}"
|
||||||
|
|
||||||
|
|
||||||
|
def _字符串常量(文件: Path) -> list[str]:
|
||||||
|
"""AST 提取全部字符串常量,排除模块/类/函数 docstring。"""
|
||||||
|
树 = ast.parse(文件.read_text(encoding="utf-8"))
|
||||||
|
文档: set[int] = set()
|
||||||
|
for 节点 in ast.walk(树):
|
||||||
|
if isinstance(节点, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||||
|
主体 = 节点.body
|
||||||
|
if 主体 and isinstance(主体[0], ast.Expr) and isinstance(主体[0].value, ast.Constant):
|
||||||
|
文档.add(id(主体[0].value))
|
||||||
|
return [
|
||||||
|
节点.value
|
||||||
|
for 节点 in ast.walk(树)
|
||||||
|
if isinstance(节点, ast.Constant) and isinstance(节点.value, str) and id(节点) not in 文档
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_does_not_import_skills__ba6ad5() -> None:
|
||||||
|
"""src/mude 不以代码依赖 .agent 技能目录(文档中的合同引用不算依赖)。"""
|
||||||
|
for 文件 in _遍历py(源码根):
|
||||||
|
for 值 in _字符串常量(文件):
|
||||||
|
assert ".agent" not in 值, f"源码以路径方式引用 .agent:{文件.relative_to(仓库根)}"
|
||||||
|
assert "skills/" not in 值, f"源码引用技能路径:{文件.relative_to(仓库根)}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_runtime_has_no_claude_cli_dependency__1ec1b5() -> None:
|
||||||
|
"""活跃运行代码不依赖专有外部 CLI(claude/dsh 等);子进程只允许工程工具。"""
|
||||||
|
for 文件 in _遍历py(源码根):
|
||||||
|
树 = ast.parse(文件.read_text(encoding="utf-8"))
|
||||||
|
for 节点 in ast.walk(树):
|
||||||
|
if isinstance(节点, ast.Call) and getattr(节点.func, "attr", "") == "run":
|
||||||
|
for 参数 in 节点.args:
|
||||||
|
if isinstance(参数, ast.List):
|
||||||
|
for 元素 in 参数.elts:
|
||||||
|
if isinstance(元素, ast.Constant) and isinstance(元素.value, str):
|
||||||
|
值 = 元素.value
|
||||||
|
assert not (值.startswith("claude") or 值.startswith("dsh")), (
|
||||||
|
f"依赖专有 CLI:{文件.relative_to(仓库根)} -> {值}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_does_not_import_muse_business__5d7edd() -> None:
|
||||||
|
"""共享纯基础不反向导入业务;启动装配可以绑定具体实现。"""
|
||||||
|
业务模块 = {
|
||||||
|
"作品规划",
|
||||||
|
"故事世界",
|
||||||
|
"资料研究",
|
||||||
|
"知识方法",
|
||||||
|
"正文写作",
|
||||||
|
"审校修订",
|
||||||
|
"作者经验",
|
||||||
|
"交付连载",
|
||||||
|
"上下文",
|
||||||
|
"效果评测",
|
||||||
|
}
|
||||||
|
基础文件 = list((源码根 / "共享").glob("*.py")) + [
|
||||||
|
源码根 / "配置.py",
|
||||||
|
源码根 / "资源加载.py",
|
||||||
|
]
|
||||||
|
assert 基础文件, "共享与装配文件缺失"
|
||||||
|
for 文件 in 基础文件:
|
||||||
|
树 = ast.parse(文件.read_text(encoding="utf-8"))
|
||||||
|
for 节点 in ast.walk(树):
|
||||||
|
if isinstance(节点, ast.ImportFrom) and 节点.module:
|
||||||
|
顶层 = 节点.module.split(".")[0] == "muse"
|
||||||
|
if 顶层 and len(节点.module.split(".")) > 1:
|
||||||
|
第二层 = 节点.module.split(".")[1]
|
||||||
|
assert 第二层 not in 业务模块, (
|
||||||
|
f"基础层导入业务模块:{文件.relative_to(仓库根)} -> {节点.module}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_agent_task_is_thin_cli__b141f1() -> None:
|
||||||
|
"""python -m muse 是薄入口:只做参数解析与转交,无业务逻辑与连接。"""
|
||||||
|
文件 = 源码根 / "__main__.py"
|
||||||
|
文本 = 文件.read_text(encoding="utf-8")
|
||||||
|
导入 = _导入名(文件)
|
||||||
|
assert 导入 <= {"__future__", "argparse", "sys", "pathlib", "muse"}, 导入
|
||||||
|
for 禁止 in ("socket", "psycopg", "http", "urllib", "subprocess"):
|
||||||
|
assert 禁止 not in 文本, f"命令入口包含 {禁止}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 新增用例(W02 补充):安装与导入边界
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_from_any_cwd_without_external_connections__e0a1b2() -> None:
|
||||||
|
"""任意 cwd 导入 muse 全部顶层模块且不产生任何 socket 连接(安装验收)。"""
|
||||||
|
代码 = (
|
||||||
|
"import socket\n"
|
||||||
|
"def _拒绝(self, 地址):\n"
|
||||||
|
" raise AssertionError(f'导入期间尝试连接 {地址}')\n"
|
||||||
|
"socket.socket.connect = _拒绝\n"
|
||||||
|
"import muse, muse.启动, muse.配置, muse.资源加载, muse.共享\n"
|
||||||
|
"print(muse.__version__)\n"
|
||||||
|
)
|
||||||
|
进程 = subprocess.run(
|
||||||
|
[sys.executable, "-c", 代码], cwd="/", capture_output=True, text=True, timeout=60
|
||||||
|
)
|
||||||
|
assert 进程.returncode == 0, 进程.stderr
|
||||||
|
assert 进程.stdout.strip() == muse.__version__
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_resolves_to_new_package_not_old_tree__c1d2e3() -> None:
|
||||||
|
"""import muse 解析到 src/muse 安装包,不被旧树同名目录遮挡。"""
|
||||||
|
代码 = "import muse\nprint(muse.__file__)\n"
|
||||||
|
进程 = subprocess.run(
|
||||||
|
[sys.executable, "-c", 代码],
|
||||||
|
cwd=仓库根,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
assert 进程.returncode == 0, 进程.stderr
|
||||||
|
assert "src/muse" in 进程.stdout, 进程.stdout
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# W13 承接:作者自然语言入口与角色路由合同(原 AuthorInstructionContractTest)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
作者根 = 仓库根 / ".agent" / "作者"
|
||||||
|
场景根 = 作者根 / "场景"
|
||||||
|
标准索引表头 = (
|
||||||
|
"| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |\n"
|
||||||
|
"|------|----------|----------|----------|----------|"
|
||||||
|
)
|
||||||
|
场景清单 = ("定故事", "排故事", "塑人物", "写下一章", "改正文", "查作品")
|
||||||
|
技能链接 = re.compile(r"\[([^\]\n]+)\]\(([^)\n]+/SKILL\.md)\)")
|
||||||
|
frontmatter名称 = re.compile(r"^name:\s*(.+?)\s*$", re.MULTILINE)
|
||||||
|
# 已有新版实现并登记进运行资源包的技能:场景路由必须指向新版路径,不再回到旧 muse 树。
|
||||||
|
已替代技能 = {
|
||||||
|
"完善故事基础设定": ".agent/skills/操作/完善故事基础设定/SKILL.md",
|
||||||
|
"决定正文候选去留": ".agent/skills/操作/决定正文候选去留/SKILL.md",
|
||||||
|
"确认规划候选": ".agent/skills/操作/确认规划候选/SKILL.md",
|
||||||
|
}
|
||||||
|
退役技能别名 = {
|
||||||
|
"生成下一章",
|
||||||
|
"局部重写",
|
||||||
|
"场景扩写",
|
||||||
|
"正文润色",
|
||||||
|
"作品定盘",
|
||||||
|
"全书规划",
|
||||||
|
"章级细纲",
|
||||||
|
"全书解析",
|
||||||
|
"章后抽取",
|
||||||
|
"一致性检测",
|
||||||
|
"质量评分",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _章节(文本: str, 标题: str) -> str:
|
||||||
|
匹配 = re.search(
|
||||||
|
rf"^## {re.escape(标题)}\s*$\n(?P<正文>.*?)(?=^## |\Z)",
|
||||||
|
文本,
|
||||||
|
flags=re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
assert 匹配 is not None, f"缺少章节:## {标题}"
|
||||||
|
return 匹配.group("正文")
|
||||||
|
|
||||||
|
|
||||||
|
def test_author_layer_has_discoverable_structure__41d4cb() -> None:
|
||||||
|
"""作者层文件完整且各级索引可发现;路由不依赖技能自动发现。"""
|
||||||
|
期望 = {
|
||||||
|
作者根 / "目录.md",
|
||||||
|
作者根 / "指令.md",
|
||||||
|
作者根 / "原生中文写作.md",
|
||||||
|
场景根 / "目录.md",
|
||||||
|
*(场景根 / f"{名称}.md" for 名称 in 场景清单),
|
||||||
|
}
|
||||||
|
缺失 = sorted(str(p.relative_to(仓库根)) for p in 期望 if not p.is_file())
|
||||||
|
assert 缺失 == [], "作者指令层文件不完整:\n" + "\n".join(缺失)
|
||||||
|
|
||||||
|
根索引 = (仓库根 / ".agent" / "目录.md").read_text(encoding="utf-8")
|
||||||
|
项目入口 = (仓库根 / "AGENTS.md").read_text(encoding="utf-8")
|
||||||
|
作者索引 = (作者根 / "目录.md").read_text(encoding="utf-8")
|
||||||
|
场景索引 = (场景根 / "目录.md").read_text(encoding="utf-8")
|
||||||
|
assert "作者/目录.md" in 根索引
|
||||||
|
assert ".agent/作者/指令.md" in 项目入口
|
||||||
|
assert 作者索引.startswith(标准索引表头)
|
||||||
|
assert 场景索引.startswith(标准索引表头)
|
||||||
|
assert "[指令.md](指令.md)" in 作者索引
|
||||||
|
assert "[原生中文写作.md](原生中文写作.md)" in 作者索引
|
||||||
|
assert "[目录.md](场景/目录.md)" in 作者索引
|
||||||
|
for 场景 in 场景清单:
|
||||||
|
assert f"[{场景}.md]({场景}.md)" in 场景索引
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenes_route_only_to_registered_chinese_skills__5e199d() -> None:
|
||||||
|
"""场景路由只指向真实存在且 frontmatter 名称一致的技能;已替代技能不再路由旧实现。"""
|
||||||
|
登记能力 = {
|
||||||
|
Path(条目).name
|
||||||
|
for 条目 in json.loads((仓库根 / "工具" / "资源登记.json").read_text(encoding="utf-8"))[
|
||||||
|
"能力文件"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for 场景 in 场景清单:
|
||||||
|
源 = 场景根 / f"{场景}.md"
|
||||||
|
路由章节 = _章节(源.read_text(encoding="utf-8"), "正式技能")
|
||||||
|
for 标签, 原目标 in 技能链接.findall(路由章节):
|
||||||
|
目标 = (源.parent / 原目标).resolve()
|
||||||
|
assert 目标.is_file(), f"{源}: 技能链接不存在:{原目标}"
|
||||||
|
匹配 = frontmatter名称.search(目标.read_text(encoding="utf-8"))
|
||||||
|
assert 匹配 is not None, f"{目标}: 缺少 frontmatter name"
|
||||||
|
assert 标签 == 匹配.group(1).strip(), f"{源}: 链接文字必须使用正式技能名"
|
||||||
|
相对 = 目标.relative_to(仓库根).as_posix()
|
||||||
|
if 标签 in 已替代技能:
|
||||||
|
assert 相对 == 已替代技能[标签], (
|
||||||
|
f"{源}: {标签} 已有新版实现,应路由 {已替代技能[标签]} 而不是 {相对}"
|
||||||
|
)
|
||||||
|
if "muse/" not in 相对:
|
||||||
|
assert Path(相对).name in 登记能力, f"{源}: 未登记的新版技能:{相对}"
|
||||||
|
assert 标签 not in 退役技能别名
|
||||||
|
# 作者层不是技能:不得出现作者入口冒充技能登记。
|
||||||
|
assert 登记能力.isdisjoint({"目录.md"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_interaction_and_native_chinese_contracts_are_complete__7239c7() -> None:
|
||||||
|
"""交互边界与原生中文写作合同的关键短语在作者层保持完整。"""
|
||||||
|
入口 = (作者根 / "指令.md").read_text(encoding="utf-8")
|
||||||
|
for 必含 in (
|
||||||
|
"一轮只问一个关键问题",
|
||||||
|
"二至三个中文选项",
|
||||||
|
"采用",
|
||||||
|
"修改",
|
||||||
|
"丢弃",
|
||||||
|
"Shadow",
|
||||||
|
"Canonical",
|
||||||
|
"只给正文",
|
||||||
|
"主会话不代替角色",
|
||||||
|
):
|
||||||
|
assert 必含 in 入口, f"作者指令缺少交互合同短语:{必含}"
|
||||||
|
|
||||||
|
写作 = (作者根 / "原生中文写作.md").read_text(encoding="utf-8")
|
||||||
|
for 必含 in (
|
||||||
|
"中文语序",
|
||||||
|
"具体名词、动作和关系优先",
|
||||||
|
"作者声音、叙述者声音和角色声音",
|
||||||
|
"不得凭空增加",
|
||||||
|
"润色不得偷改情节事实",
|
||||||
|
"扩写不得新开场景或改变落点",
|
||||||
|
"重写不得越过作者指定范围",
|
||||||
|
"精确诊断",
|
||||||
|
):
|
||||||
|
assert 必含 in 写作, f"原生中文写作合同缺少:{必含}"
|
||||||
|
|
||||||
|
诊断场景 = (场景根 / "查作品.md").read_text(encoding="utf-8")
|
||||||
|
修改场景 = (场景根 / "改正文.md").read_text(encoding="utf-8")
|
||||||
|
assert "只查不改" in 诊断场景
|
||||||
|
assert "作者指定范围" in 修改场景
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_prompts_route_with_registered_skill_names__4bead8() -> None:
|
||||||
|
"""五个角色文件与角色合同一致并登记进运行资源包;角色层不使用退役技能别名。"""
|
||||||
|
登记 = json.loads((仓库根 / "工具" / "资源登记.json").read_text(encoding="utf-8"))
|
||||||
|
能力 = set(登记["能力文件"])
|
||||||
|
角色合同 = {
|
||||||
|
"writer": "写手",
|
||||||
|
"planner": "规划员",
|
||||||
|
"extractor": "知识抽取员",
|
||||||
|
"detector": "检测员",
|
||||||
|
"judge": "评委",
|
||||||
|
}
|
||||||
|
for 角色ID, 名称 in 角色合同.items():
|
||||||
|
相对 = f".agent/角色/{名称}.md"
|
||||||
|
assert 相对 in 能力, f"角色未登记进运行资源包:{相对}"
|
||||||
|
文本 = (仓库根 / 相对).read_text(encoding="utf-8")
|
||||||
|
头部 = 文本.split("---")[1]
|
||||||
|
assert f"id: {角色ID}" in 头部
|
||||||
|
assert f"name: {名称}" in 头部
|
||||||
|
assert "contract_version:" in 头部
|
||||||
|
for 别名 in 退役技能别名:
|
||||||
|
assert f"`{别名}`" not in 文本, f"{相对}: 角色文件引用退役技能别名 {别名}"
|
||||||
582
tests/架构/test_测试依赖与执行证据.py
Normal file
582
tests/架构/test_测试依赖与执行证据.py
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
"""测试依赖与执行证据的逐例行为验证。
|
||||||
|
|
||||||
|
保护:用例选择、结构化报告、离线依赖阻断、超时与失败区分、
|
||||||
|
空收集与缺环境不静默、登记范围与执行证据的拒绝语义。
|
||||||
|
|
||||||
|
环境:离线;受限子进程和临时测试项目(复制仓库真实 conftest 后在
|
||||||
|
临时项目中运行 pytest,验证同一套隔离与守卫语义,不是旧文件迁移)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parents[2]
|
||||||
|
真实conftest = 仓库根 / "tests" / "conftest.py"
|
||||||
|
|
||||||
|
|
||||||
|
class 临时项目:
|
||||||
|
"""受限临时 pytest 项目:复制真实 conftest,写入指定用例文件。"""
|
||||||
|
|
||||||
|
def __init__(self, 根: Path) -> None:
|
||||||
|
self.根 = 根
|
||||||
|
shutil.copyfile(真实conftest, 根 / "conftest.py")
|
||||||
|
|
||||||
|
def 写(self, 相对: str, 内容: str) -> Path:
|
||||||
|
目标 = self.根 / 相对
|
||||||
|
目标.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
目标.write_text(textwrap.dedent(内容), encoding="utf-8")
|
||||||
|
return 目标
|
||||||
|
|
||||||
|
def 运行(self, *参数: str, 环境: dict[str, str] | None = None) -> tuple[int, dict]:
|
||||||
|
报告 = self.根 / "报告.xml"
|
||||||
|
完整 = [sys.executable, "-m", "pytest", "-q", f"--junitxml={报告}", *参数]
|
||||||
|
清单 = self.根 / "用例清单.json"
|
||||||
|
if not 清单.exists():
|
||||||
|
目标 = []
|
||||||
|
for 文件 in self.根.rglob("test_*.py"):
|
||||||
|
for 节点 in ast.walk(ast.parse(文件.read_text())):
|
||||||
|
if isinstance(节点, ast.FunctionDef) and 节点.name.startswith("test_"):
|
||||||
|
后缀 = 节点.name.rsplit("__", 1)[-1]
|
||||||
|
目标.append(
|
||||||
|
{
|
||||||
|
"case_id": f"TC-x-{后缀}000000",
|
||||||
|
"file": 文件.relative_to(self.根).as_posix(),
|
||||||
|
"symbol": 节点.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
清单.write_text(json.dumps({"cases": [], "new_cases": 目标}, ensure_ascii=False))
|
||||||
|
完整.extend(["--用例清单", str(清单)])
|
||||||
|
进程 = subprocess.run(
|
||||||
|
完整,
|
||||||
|
cwd=self.根,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=180,
|
||||||
|
env={**(_清理环境()), **(环境 or {})},
|
||||||
|
)
|
||||||
|
结果: dict = {"退出码": 进程.returncode, "stdout": 进程.stdout, "stderr": 进程.stderr}
|
||||||
|
if 报告.exists():
|
||||||
|
结果["报告"] = 解析junit(报告)
|
||||||
|
return 进程.returncode, 结果
|
||||||
|
|
||||||
|
|
||||||
|
def _清理环境() -> dict[str, str]:
|
||||||
|
"""子进程环境:剔除数据库变量,避免宿主环境泄漏进隔离语义。"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
环境 = dict(os.environ)
|
||||||
|
环境.pop("MUSE_TEST_DATABASE_URL", None)
|
||||||
|
return 环境
|
||||||
|
|
||||||
|
|
||||||
|
def 解析junit(路径: Path) -> dict:
|
||||||
|
树 = ET.parse(路径)
|
||||||
|
根 = 树.getroot()
|
||||||
|
用例 = []
|
||||||
|
for 节点 in 根.iter("testcase"):
|
||||||
|
条目: dict = {"名称": 节点.get("name", ""), "类": 节点.get("classname", "")}
|
||||||
|
for 子 in 节点:
|
||||||
|
if 子.tag not in {"failure", "error", "skipped"}:
|
||||||
|
continue
|
||||||
|
条目["结果"] = 子.tag
|
||||||
|
条目["消息"] = (子.get("message") or "")[:300]
|
||||||
|
if 子.text:
|
||||||
|
条目["正文"] = 子.text[:300]
|
||||||
|
用例.append(条目)
|
||||||
|
汇总 = 根.find(".//testsuite")
|
||||||
|
return {
|
||||||
|
"用例": 用例,
|
||||||
|
"数量": int(汇总.get("tests", "0")) if 汇总 is not None else len(用例),
|
||||||
|
"失败": int(汇总.get("failures", "0")) if 汇总 is not None else 0,
|
||||||
|
"跳过": int(汇总.get("skipped", "0")) if 汇总 is not None else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def 项目(tmp_path: Path) -> 临时项目:
|
||||||
|
return 临时项目(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 用例选择与结构化报告
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_selector_alias_and_intersection_are_reported_as_json__a5c7d4(项目: 临时项目) -> None:
|
||||||
|
"""--case 与标记取交集只选中目标用例;结构化报告精确到单个用例。"""
|
||||||
|
项目.写(
|
||||||
|
"test_样本.py",
|
||||||
|
"""
|
||||||
|
def test_目标行为__aa0001():
|
||||||
|
assert 1 + 1 == 2
|
||||||
|
|
||||||
|
def test_另一行为__bb0002():
|
||||||
|
assert 2 + 2 == 4
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
退出码, 结果 = 项目.运行("--case", "TC-x-aa0001000000")
|
||||||
|
assert 退出码 == 0, 结果["stderr"]
|
||||||
|
报告 = 结果["报告"]
|
||||||
|
assert 报告["数量"] == 1, 报告
|
||||||
|
assert 报告["用例"][0]["名称"] == "test_目标行为__aa0001"
|
||||||
|
assert "结果" not in 报告["用例"][0] # 通过:无 failure/skipped 节点
|
||||||
|
|
||||||
|
|
||||||
|
def test_offline_dependency_is_blocked_without_running_child__09d22d(项目: 临时项目) -> None:
|
||||||
|
"""数据库标记用例在缺环境时被阻断:子进程不执行、全跳过不算通过。"""
|
||||||
|
项目.写(
|
||||||
|
"test_数据库用例.py",
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.数据库
|
||||||
|
def test_需要数据库__cc0003(隔离数据库URL):
|
||||||
|
Path("child_ran").write_text("ran", encoding="utf-8")
|
||||||
|
assert 隔离数据库URL
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
退出码, 结果 = 项目.运行("--case", "TC-x-cc0003000000")
|
||||||
|
# 全跳过 → 守卫改写为退出码 5,不计通过;跳过原因可见
|
||||||
|
assert 退出码 == 5, (退出码, 结果["stderr"])
|
||||||
|
报告 = 结果["报告"]
|
||||||
|
assert 报告["用例"][0]["结果"] == "skipped"
|
||||||
|
assert "MUSE_TEST_DATABASE_URL" in 报告["用例"][0]["消息"]
|
||||||
|
# 子进程没有执行:没有任何副作用文件
|
||||||
|
assert not (项目.根 / "child_ran").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_requires_runs_and_still_reports_dependency__08eb1b(项目: 临时项目) -> None:
|
||||||
|
"""提供隔离库引用后数据库用例真实执行;依赖来源在结果中可见。"""
|
||||||
|
项目.写(
|
||||||
|
"test_数据库用例.py",
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.数据库
|
||||||
|
def test_获得隔离引用__cc0003(隔离数据库URL):
|
||||||
|
assert 隔离数据库URL == "postgresql://synthetic-test"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
退出码, 结果 = 项目.运行(
|
||||||
|
"--case",
|
||||||
|
"TC-x-cc0003000000",
|
||||||
|
环境={"MUSE_TEST_DATABASE_URL": "postgresql://synthetic-test"},
|
||||||
|
)
|
||||||
|
assert 退出码 == 0, (退出码, 结果["stderr"])
|
||||||
|
assert 结果["报告"]["数量"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonzero_and_timeout_have_distinct_structured_statuses__306973(项目: 临时项目) -> None:
|
||||||
|
"""断言失败与超时都以非零退出,且失败消息可区分。"""
|
||||||
|
项目.写(
|
||||||
|
"test_失败.py",
|
||||||
|
"""
|
||||||
|
def test_断言失败__dd0004():
|
||||||
|
assert 1 == 2, "合成失败"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
项目.写(
|
||||||
|
"test_睡眠.py",
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
def test_超时睡眠__ee0005():
|
||||||
|
time.sleep(30)
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
码1, 结果1 = 项目.运行("--case", "TC-x-dd0004000000")
|
||||||
|
assert 码1 == 1
|
||||||
|
断言消息 = 结果1["报告"]["用例"][0]["消息"]
|
||||||
|
assert "合成失败" in 断言消息
|
||||||
|
|
||||||
|
码2, 结果2 = 项目.运行("--case", "TC-x-ee0005000000", "--timeout=2")
|
||||||
|
assert 码2 == 1
|
||||||
|
超时正文 = 结果2["报告"]["用例"][0].get("正文", "") + 结果2["报告"]["用例"][0].get("消息", "")
|
||||||
|
assert "Timeout" in 超时正文 or "timeout" in 超时正文
|
||||||
|
# 两者消息可区分:超时消息不含合成失败标记
|
||||||
|
assert "合成失败" not in 超时正文
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_matches_and_missing_path_are_not_silent__ae03b5(项目: 临时项目) -> None:
|
||||||
|
"""未知 case 选择与非空选择的空收集都以非零退出并说明原因。"""
|
||||||
|
项目.写(
|
||||||
|
"test_存在.py",
|
||||||
|
"""
|
||||||
|
def test_存在用例__ff0006():
|
||||||
|
assert True
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
码1, 结果1 = 项目.运行("--case", "TC-x-0000zz9999")
|
||||||
|
assert 码1 != 0
|
||||||
|
assert "--case" in 结果1["stderr"] or "--case" in 结果1["stdout"]
|
||||||
|
|
||||||
|
码2, 结果2 = 项目.运行("tests/不存在的路径")
|
||||||
|
assert 码2 in (4, 5) # 缺失路径或空收集:非零且不静默
|
||||||
|
assert "no tests" in (结果2["stdout"] + 结果2["stderr"]).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_manifest_and_selector_requirement_are_structured__f7150b(项目: 临时项目) -> None:
|
||||||
|
"""非法清单与未登记选择分别失败,错误原因可区分。"""
|
||||||
|
项目.写("test_任意.py", "def test_任意用例__000007():\n assert True\n")
|
||||||
|
清单 = 项目.根 / "用例清单.json"
|
||||||
|
清单.write_text("{broken", encoding="utf-8")
|
||||||
|
码, 结果 = 项目.运行("--case", "TC-x-000007000000")
|
||||||
|
assert 码 == 4
|
||||||
|
assert "用例清单" in 结果["stderr"]
|
||||||
|
清单.write_text('{"cases": [], "new_cases": []}', encoding="utf-8")
|
||||||
|
码, 结果 = 项目.运行("--case", "TC-x-000007000000")
|
||||||
|
assert 码 == 4
|
||||||
|
assert "未登记" in 结果["stderr"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_offline_is_explicit_opt_in_and_output_is_summary_only__265060(项目: 临时项目) -> None:
|
||||||
|
"""默认运行按标记剔除外部环境用例;报告只含离线用例。"""
|
||||||
|
项目.写(
|
||||||
|
"test_离线.py",
|
||||||
|
"""
|
||||||
|
def test_离线用例__110008():
|
||||||
|
assert True
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
项目.写(
|
||||||
|
"test_外部.py",
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.数据库
|
||||||
|
def test_外部用例__220009(隔离数据库URL):
|
||||||
|
assert 隔离数据库URL
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
码, 结果 = 项目.运行("-m", "not 数据库")
|
||||||
|
assert 码 == 0, (码, 结果["stderr"])
|
||||||
|
名称 = [u["名称"] for u in 结果["报告"]["用例"]]
|
||||||
|
assert 名称 == ["test_离线用例__110008"], 名称
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_exit_without_execution_evidence_fails_closed__50f80b(项目: 临时项目) -> None:
|
||||||
|
"""空用例文件与纯跳过都不允许零退出冒充通过。"""
|
||||||
|
项目.写(
|
||||||
|
"test_空.py",
|
||||||
|
"""
|
||||||
|
说明 = "本文件没有任何测试函数"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
项目.写(
|
||||||
|
"test_跳过.py",
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_跳过用例__33000a():
|
||||||
|
pytest.skip("合成跳过")
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
# 只选空文件:无收集 → 退出码 5
|
||||||
|
码1, 结果1 = 项目.运行("test_空.py")
|
||||||
|
assert 码1 == 5
|
||||||
|
# 纯跳过:守卫改写退出码为 5,不计通过
|
||||||
|
码2, 结果2 = 项目.运行("test_跳过.py")
|
||||||
|
assert 码2 == 5, (码2, 结果2["stdout"])
|
||||||
|
assert "零实际执行" in 结果2["stderr"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 登记范围(生成范围)检查:工具/维护索引.py 的同一语义
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
sys.path.insert(0, str(仓库根 / "工具"))
|
||||||
|
from 维护索引 import 检查用例身份 # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def 写清单(根: Path, 目标: list[dict]) -> Path:
|
||||||
|
清单 = 根 / "清单.json"
|
||||||
|
清单.write_text(
|
||||||
|
json.dumps({"cases": [{"target_cases": 目标}], "new_cases": []}, ensure_ascii=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return 清单
|
||||||
|
|
||||||
|
|
||||||
|
def test_独立用例身份可绑定原生参数行__e2000b(tmp_path: Path) -> None:
|
||||||
|
项目 = 临时项目(tmp_path)
|
||||||
|
项目.写(
|
||||||
|
"test_参数.py",
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize("输入", [1, 2], ids=["first", "second"])
|
||||||
|
def test_参数行为__abcdef(输入):
|
||||||
|
assert 输入 in (1, 2)
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
项目.写(
|
||||||
|
"用例清单.json",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cases": [],
|
||||||
|
"new_cases": [
|
||||||
|
{
|
||||||
|
"case_id": "NC-first",
|
||||||
|
"file": "test_参数.py",
|
||||||
|
"symbol": "test_参数行为__abcdef",
|
||||||
|
"parameter_ids": ["first"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-second",
|
||||||
|
"file": "test_参数.py",
|
||||||
|
"symbol": "test_参数行为__abcdef",
|
||||||
|
"parameter_ids": ["second"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
退出, 结果 = 项目.运行("--case", "NC-first")
|
||||||
|
assert 退出 == 0, 结果
|
||||||
|
树 = ET.parse(tmp_path / "报告.xml")
|
||||||
|
行 = 树.findall(".//testcase")
|
||||||
|
assert len(行) == 1 and 行[0].attrib["name"].endswith("[first]")
|
||||||
|
assert 行[0].find(".//property[@name='case_id']").attrib["value"] == "NC-first"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_rejects_unregistered_disk_asset__69440b(tmp_path: Path) -> None:
|
||||||
|
"""磁盘上存在但清单未登记的用例被拒绝(未登记资产)。"""
|
||||||
|
测试文件 = tmp_path / "tests" / "registered" / "test_登记.py"
|
||||||
|
测试文件.parent.mkdir(parents=True)
|
||||||
|
测试文件.write_text("def test_已登记__44000b():\n assert True\n", encoding="utf-8")
|
||||||
|
未登记 = tmp_path / "tests" / "new" / "test_未登记.py"
|
||||||
|
未登记.parent.mkdir(parents=True)
|
||||||
|
未登记.write_text("def test_未登记__55000c():\n assert True\n", encoding="utf-8")
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-44000b000000",
|
||||||
|
"file": "tests/registered/test_登记.py",
|
||||||
|
"symbol": "test_已登记__44000b",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True)
|
||||||
|
assert any("test_未登记__55000c" in p and "未在用例清单登记" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_uses_manifest_scan_roots__20f4fa(tmp_path: Path) -> None:
|
||||||
|
"""自定义扫描根下的已登记用例被接受(无问题)。"""
|
||||||
|
测试文件 = tmp_path / "custom-tests" / "test_自定义.py"
|
||||||
|
测试文件.parent.mkdir(parents=True)
|
||||||
|
测试文件.write_text("def test_自定义用例__66000d():\n assert True\n", encoding="utf-8")
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-66000d000000",
|
||||||
|
"file": "custom-tests/test_自定义.py",
|
||||||
|
"symbol": "test_自定义用例__66000d",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_rejects_manifest_extra_asset__b4901f(tmp_path: Path) -> None:
|
||||||
|
"""清单登记的目标在磁盘上不存在时被拒绝。"""
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-77000e000000",
|
||||||
|
"file": "tests/removed/test_已删除.py",
|
||||||
|
"symbol": "test_已删除__77000e",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True)
|
||||||
|
assert any("目标不存在" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_accepts_existing_behavior_eval_entry__5b8bb2(tmp_path: Path) -> None:
|
||||||
|
"""已存在的真实模型评测用例登记被接受(含环境标记)。"""
|
||||||
|
测试文件 = tmp_path / "tests" / "evals" / "test_评测.py"
|
||||||
|
测试文件.parent.mkdir(parents=True)
|
||||||
|
测试文件.write_text(
|
||||||
|
"import pytest\n\n@pytest.mark.真实模型\ndef test_评测用例__88000f():\n assert True\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-88000f000000",
|
||||||
|
"file": "tests/evals/test_评测.py",
|
||||||
|
"symbol": "test_评测用例__88000f",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_rejects_missing_behavior_eval_entry__fe3fba(tmp_path: Path) -> None:
|
||||||
|
"""登记的真实模型评测用例文件缺失时被拒绝。"""
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-990010000000",
|
||||||
|
"file": "tests/evals/test_缺失评测.py",
|
||||||
|
"symbol": "test_缺失评测__990010",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True)
|
||||||
|
assert any("目标不存在" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_ignores_non_test_helpers_under_test_roots__14f941(tmp_path: Path) -> None:
|
||||||
|
"""测试根下的非 test_ 辅助函数不触发登记检查。"""
|
||||||
|
测试文件 = tmp_path / "tests" / "helpers" / "test_辅助.py"
|
||||||
|
测试文件.parent.mkdir(parents=True)
|
||||||
|
测试文件.write_text(
|
||||||
|
"def 辅助函数():\n"
|
||||||
|
" return 1\n"
|
||||||
|
"\n"
|
||||||
|
"\n"
|
||||||
|
"def test_真用例__a10011():\n"
|
||||||
|
" assert 辅助函数() == 1\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-a10011000000",
|
||||||
|
"file": "tests/helpers/test_辅助.py",
|
||||||
|
"symbol": "test_真用例__a10011",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_blocks_empty_and_print_only_scripts_before_child__36f79e(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""无可判定语句的 print 型用例在执行前即被登记检查拒绝。"""
|
||||||
|
空文件 = tmp_path / "tests" / "empty" / "test_空断言.py"
|
||||||
|
空文件.parent.mkdir(parents=True)
|
||||||
|
空文件.write_text("def test_空用例__b20012():\n print('只有输出')\n", encoding="utf-8")
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-b20012000000",
|
||||||
|
"file": "tests/empty/test_空断言.py",
|
||||||
|
"symbol": "test_空用例__b20012",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True)
|
||||||
|
assert any("无可判定语句" in p for p in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_scope_accepts_supported_python_shapes__7c1382(tmp_path: Path) -> None:
|
||||||
|
"""函数式与类式用例形状都被识别并核对。"""
|
||||||
|
测试文件 = tmp_path / "tests" / "shapes" / "test_形状.py"
|
||||||
|
测试文件.parent.mkdir(parents=True)
|
||||||
|
测试文件.write_text(
|
||||||
|
"class Test类式:\n"
|
||||||
|
" def test_类方法__c30013(self):\n"
|
||||||
|
" assert True\n"
|
||||||
|
"\n"
|
||||||
|
"\n"
|
||||||
|
"def test_函数式__d30014():\n"
|
||||||
|
" assert True\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-c30013000000",
|
||||||
|
"file": "tests/shapes/test_形状.py",
|
||||||
|
"symbol": "test_类方法__c30013",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_id": "TC-x-d30014000000",
|
||||||
|
"file": "tests/shapes/test_形状.py",
|
||||||
|
"symbol": "test_函数式__d30014",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert 检查用例身份(tmp_path, 清单, 核对缺失目标=True, 全树扫描=True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_完整身份不接受冒用后缀__a02001(项目: 临时项目) -> None:
|
||||||
|
"""未登记 ID 与真实符号后缀相同,也不能选择执行。"""
|
||||||
|
项目.写("test_样本.py", "def test_目标__aa0001():\n assert True\n")
|
||||||
|
码, 结果 = 项目.运行("--case", "XX-forged-aa0001000000")
|
||||||
|
assert 码 == 4
|
||||||
|
assert "未登记" in 结果["stderr"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_完整身份绑定文件与符号__a02002(tmp_path: Path) -> None:
|
||||||
|
"""另一文件冒用已登记后缀不能冒充目标用例。"""
|
||||||
|
(tmp_path / "test_冒用.py").write_text("def test_冒用__aa0001():\n assert True\n")
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[{"case_id": "TC-x-aa0001000000", "file": "test_原例.py", "symbol": "test_原例__aa0001"}],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单, 全树扫描=True)
|
||||||
|
assert any("未在用例清单登记" in 条 for 条 in 问题), 问题
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.数据库
|
||||||
|
@pytest.mark.parametrize("阶段", ["收集", "测试体", "子进程"], ids=["collection", "call", "child"])
|
||||||
|
def test_离线进程拒绝真实数据库连接__a02005(项目: 临时项目, 隔离数据库URL: str, 阶段: str) -> None:
|
||||||
|
"""先确认隔离库可达,再由未标记的子测试验证 OS 阻断 libpq。"""
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
with psycopg.connect(隔离数据库URL) as 连:
|
||||||
|
assert 连.execute("SELECT 1").fetchone() == (1,)
|
||||||
|
连接代码 = "import os, psycopg; psycopg.connect(os.environ['MUSE_TEST_DATABASE_URL'])"
|
||||||
|
if 阶段 == "收集":
|
||||||
|
内容 = 连接代码 + "\ndef test_试连接__aa0001():\n assert True\n"
|
||||||
|
elif 阶段 == "子进程":
|
||||||
|
内容 = (
|
||||||
|
"import subprocess, sys\n"
|
||||||
|
"def test_试连接__aa0001():\n"
|
||||||
|
f" subprocess.run([sys.executable, '-c', {连接代码!r}], check=True)\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
内容 = "def test_试连接__aa0001():\n " + 连接代码 + "\n"
|
||||||
|
项目.写("test_连接.py", 内容)
|
||||||
|
码, 结果 = 项目.运行(环境={"MUSE_TEST_DATABASE_URL": 隔离数据库URL})
|
||||||
|
assert 码 in (1, 2)
|
||||||
|
assert "Operation not permitted" in 结果["stdout"] + 结果["stderr"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_已执行目标丢失不被过渡期忽略__a02006(tmp_path: Path) -> None:
|
||||||
|
清单 = 写清单(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"case_id": "NC-existing-test",
|
||||||
|
"file": "tests/架构/test_已执行.py",
|
||||||
|
"symbol": "test_已执行__aa0001",
|
||||||
|
"verification_state": "通过",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
问题 = 检查用例身份(tmp_path, 清单)
|
||||||
|
assert any("目标不存在" in x for x in 问题), 问题
|
||||||
682
uv.lock
generated
Normal file
682
uv.lock
generated
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-doc"
|
||||||
|
version = "0.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.15.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "26.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.7.22"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.141.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.19"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema"
|
||||||
|
version = "4.26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "jsonschema-specifications" },
|
||||||
|
{ name = "referencing" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema-specifications"
|
||||||
|
version = "2025.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "referencing" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "muse"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "itsdangerous" },
|
||||||
|
{ name = "jsonschema" },
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyright" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-timeout" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "fastapi", specifier = ">=0.115" },
|
||||||
|
{ name = "httpx", specifier = ">=0.28" },
|
||||||
|
{ name = "itsdangerous", specifier = ">=2.2" },
|
||||||
|
{ name = "jsonschema", specifier = ">=4.23" },
|
||||||
|
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.0" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
|
{ name = "uvicorn", specifier = ">=0.30" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyright", specifier = ">=1.1.390" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3" },
|
||||||
|
{ name = "pytest-timeout", specifier = ">=2.3" },
|
||||||
|
{ name = "ruff", specifier = ">=0.8" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg"
|
||||||
|
version = "3.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/73/8fb739d0f6bba247b9b93c9840c402a4f88545be5f1d4b02b23366371c00/psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4", size = 166508, upload-time = "2026-08-31T22:45:43.151Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/2e/d0a645bcaadde68bd6d93c43f02f14b0191bdda367ce3f7722abe3da744a/psycopg-3.3.5-py3-none-any.whl", hash = "sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e", size = 213598, upload-time = "2026-08-31T22:39:02.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
binary = [
|
||||||
|
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg-binary"
|
||||||
|
version = "3.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/83/ba396428a4fb6b70f0dd41315ad86a2c14441b7214afd47b2d49cb450b78/psycopg_binary-3.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25105f9b46bdf2a30fcb67f56976ed66f6855941ae16bc024192609b917d493c", size = 4700169, upload-time = "2026-08-31T22:41:39.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/56/b23c5978e55cf4effdc5a3e13a17d69a580a487993e01b7c3768dd24281c/psycopg_binary-3.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0249c3e960cdee686000eb77169fb6590105c05bacc37e057ccdffdcd8e6ebde", size = 4763037, upload-time = "2026-08-31T22:41:47.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/d8/b41108bfe194b4098076c8b873b05ec2ca9582445eccfd9075970d7b948e/psycopg_binary-3.3.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5698ab5941a4d138c30fef858588e651fe7d583280cd6e41832825ad9e747750", size = 5546232, upload-time = "2026-08-31T22:41:55.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/d1/0f244dfef389e52e9dc3056f2a9033d1f6901e97d24d9a9c8b836e32ab6b/psycopg_binary-3.3.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672", size = 5227752, upload-time = "2026-08-31T22:42:03.776Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/88/a4781365f09807fb91435e2d00096f3f6ae5d06bce15ef3c3c385dc22772/psycopg_binary-3.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2a61e8147902771df7efe14062a3c8736347850d0d8befcf048235752504f2e", size = 6824658, upload-time = "2026-08-31T22:42:13.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/f1/072c4a46287644694731b3e40fab120ebacf6d153cce7ebf7a5b208f5561/psycopg_binary-3.3.5-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f7e1e45aad410e20de45df2b159df68ff6c8dbf47a3501f806c4489b27f4ad2b", size = 5061439, upload-time = "2026-08-31T22:42:18.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/d6/1776a95c16941b8bbce89407cc7cf9ea3fd557efb503a40873c9e2b6394f/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c09775c549b40b274206e1b043c5e5b5af39666e85c98382a30bd05d23ab677b", size = 4588586, upload-time = "2026-08-31T22:42:25.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/64/44ec87b9a74faebe966856307b65c0d35ee6321ce509cdd0e8d6a3f5338b/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cf0e5e63ee86098299c673992053d556c489ba9ae6aca6cb6e24d16a8e0b09e6", size = 4265161, upload-time = "2026-08-31T22:42:31.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/88/cf181df5651395a80afc520f5fefac72beb035c0c9d58ab7fcc880c9b221/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c065531e8c1815276f50dbfa283e3a7f022671414cdda6fa9a16794dd53b28f9", size = 3998727, upload-time = "2026-08-31T22:42:38.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/1e/c72a1107647db4bb3f534c7fe1b57b1957d9c099e795f5aa81f4a2e0c312/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9853b832b7b916e02ef68e0d5403a7dab2d5c1ddfe94f22b1b155eb862622f", size = 4310119, upload-time = "2026-08-31T22:42:46.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/8d/452620608cafff164737e20b42ebffee8151b865ee171ba0d6692a560a44/psycopg_binary-3.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:35885e333020fc152d27bea1a494bef13b2e68f6fd92b6229015e93539152008", size = 3648197, upload-time = "2026-08-31T22:42:51.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/1c/e718752cc63cf4e99e4a10fd36e3a3364dabdd0819484a24c0d79fbb9685/psycopg_binary-3.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4", size = 4704421, upload-time = "2026-08-31T22:42:59.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/cf/a0e748e27c09b92738e4460582d121ba1908be3e36791e150f435e54b332/psycopg_binary-3.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f", size = 4765054, upload-time = "2026-08-31T22:43:05.421Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/62/0cbac0266d56c94dd1f702d9af2b5d54bf80b6e658048f4f2c5bd63dd7e3/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7", size = 5547137, upload-time = "2026-08-31T22:43:11.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/3a/73c6f8871f38fc07a9c0b4cbc9467beb116a2783cecf87dfa53900a396dc/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0", size = 5227577, upload-time = "2026-08-31T22:43:20.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/7b/9f17b9f4d297b774dc574199c4dcd02dadc32a00c4056265918de5c70635/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552", size = 6824606, upload-time = "2026-08-31T22:43:33.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/86/d84dadd94a004dbbb43ce0579f1f766fdc6b8cbef745090e24bea77b5283/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586", size = 5060854, upload-time = "2026-08-31T22:43:42.258Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/d0/e5078be2c7d2490c0d6cd4cb7b3601aec44be2fa8b3fe927e9419b06004f/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e", size = 4589511, upload-time = "2026-08-31T22:43:50.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/01/08dfb5b18fa482e025864fd91a022310d0782c42e4cf5dda5d0e010b6790/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311", size = 4268144, upload-time = "2026-08-31T22:43:56.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/b9/cb01dc1d63f3241b49b2ca7fb9f98d0f5c76127f0dbb9440635a6ad0233e/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab", size = 4001036, upload-time = "2026-08-31T22:44:04.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/49/7c17dd832c05b380562ff2ff5f6ab2bcaeca8b7fff2c2b355854368a5bdb/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16", size = 4313112, upload-time = "2026-08-31T22:44:12.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/2c/b0b2f887185d6a2ec0b3bef948cc07656d2d1a5d96fa7f2bb03f6ef06ca4/psycopg_binary-3.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b", size = 3647313, upload-time = "2026-08-31T22:44:19.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/7f/4e2395da194558533bd9c31f35e4dc58ecbbae6a7176b0d2f72d629e8a51/psycopg_binary-3.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f8b132c7243ef5f503f0b6f986bf16d38a51b0df1c6ba2577743f128be03e3", size = 4712612, upload-time = "2026-08-31T22:44:25.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/94/fdb2093c8ccd7048449156db526ad746740cf34fe9c01e5dc1b7a7a8b257/psycopg_binary-3.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0cac998b9b1e82dec853d2e53b3d34d56a525cf231f9441a636cfd5992929a9", size = 4775139, upload-time = "2026-08-31T22:44:36.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/52/5195e87960715f7be2005761b72d56fce4e7757d5333fd40384f071c2de1/psycopg_binary-3.3.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:479b96fd78149cfa10369dc53fbfb89ee729be13146b584a23dbc7e164c0cf1e", size = 5556807, upload-time = "2026-08-31T22:44:42.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/42/2d616210a91e1327516ed5ae71961aaa31bb740e4a61d7190f2221685a40/psycopg_binary-3.3.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f45d77e398542ce0937d9fa3cd9d84e9c5fc6b34c50a66404ae840bada312750", size = 5236206, upload-time = "2026-08-31T22:44:47.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/2e/e54b0d4cc263b3526e3728bb50a69660d5e79e52255ccd2a1a71e40e6f9a/psycopg_binary-3.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98a388509306e5e08a4203253ac52846bc1b034e5cbd0ae6da1211593cc28594", size = 6838066, upload-time = "2026-08-31T22:44:55.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/80/ec22a110f81a44c411965982097efeee86006bdd5a1f51f628318106c84c/psycopg_binary-3.3.5-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab39e2794b95af61a2ff69e33e5ab6ac5df36e9ffea9a3b18e38b2aaca8c5ad5", size = 5072036, upload-time = "2026-08-31T22:45:02.838Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/55/7bc3c3ac769ab4fe0c619f2179b4aab3ae043f4d478283dd8279d24c0be4/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9c071bf78e5c2e6efa40bc9089a954d7b41221347a72f35c6bf2d8c96e632f75", size = 4604058, upload-time = "2026-08-31T22:45:10.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/69/8e7414f7dc10b2959e664330cdcf393e412f67355975dafa876cef265264/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:14fdfd65a96ecbd8b586d14546105641f4a6ac7cbe335c786830ea4de94bbe60", size = 4284766, upload-time = "2026-08-31T22:45:17.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/72/33f293c1d3ee9114f47e9ef4880f31c9de864e84a9b09454c26e106c25ed/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8dbd694f3741dd4ac5bc60b70e17f7841aefb3f0f38cef4d2756de270e03af43", size = 4011958, upload-time = "2026-08-31T22:45:24.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/c9/8e38840e5a7d006987bbc9acb29951912253fa50549a4db92b3aa535f089/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14f432430fd9e1a9e7d9ab2fe14956c77f5d074ebdc556a1ad04e9a1bd3fca04", size = 4323273, upload-time = "2026-08-31T22:45:32.973Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/c7/b7ebf601c307f93e7c4c4ebac0edc9db3b2729ca038efe700a18f86b5517/psycopg_binary-3.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:df209e64674a34b41662c67fdc8b4e0ffd77d2136393790691d086a09f9a6cab", size = 3745885, upload-time = "2026-08-31T22:45:40.537Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.13.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyright"
|
||||||
|
version = "1.1.411"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-timeout"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "referencing"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpds-py"
|
||||||
|
version = "2026.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.16.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
|
||||||
|
]
|
||||||
16
web/eslint.config.js
Normal file
16
web/eslint.config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist", "node_modules"] },
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
plugins: { "react-hooks": reactHooks },
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
13
web/index.html
Normal file
13
web/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Muse 工作台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!-- 入口 src/main.tsx 随 W07 工作台框架落地;此前本文件只登记工程骨架 -->
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
web/package.json
Normal file
46
web/package.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "muse-workbench",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Muse 作者工作台(React/TS);业务页面随工作包接入真实后端",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"typecheck": "tsc -b --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.102.8",
|
||||||
|
"@tiptap/extension-unique-id": "3.31.3",
|
||||||
|
"@tiptap/pm": "3.31.3",
|
||||||
|
"@tiptap/react": "3.31.3",
|
||||||
|
"@tiptap/starter-kit": "3.31.3",
|
||||||
|
"openapi-fetch": "^0.17.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^7.18.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.15.0",
|
||||||
|
"@playwright/test": "^1.49.0",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
|
"@testing-library/user-event": "^14.6.7",
|
||||||
|
"@types/node": "22.20.1",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.15.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
|
"prettier": "^3.9.6",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.15.0",
|
||||||
|
"vite": "^6.0.1",
|
||||||
|
"vitest": "^2.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
web/playwright.config.ts
Normal file
16
web/playwright.config.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
// 真实浏览器旅程:对真后端与隔离库执行中文输入与实际点击。
|
||||||
|
// 用例随 P2 起逐包登记在 web/tests/旅程/;此前本配置只登记环境合同。
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/旅程",
|
||||||
|
timeout: 60_000,
|
||||||
|
retries: 0,
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.MUSE_WORKBENCH_URL || "http://127.0.0.1:5173",
|
||||||
|
locale: "zh-CN",
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
launchOptions: {executablePath: process.env.MUSE_BROWSER_EXECUTABLE},
|
||||||
|
},
|
||||||
|
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
|
||||||
|
});
|
||||||
3849
web/pnpm-lock.yaml
generated
Normal file
3849
web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
web/tsconfig.json
Normal file
21
web/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src", "tests"]
|
||||||
|
}
|
||||||
20
web/vite.config.ts
Normal file
20
web/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// 作者工作台构建配置:开发代理指向本地后端(W07 起接入真实路由)。
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://127.0.0.1:8080",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
12
web/vitest.config.ts
Normal file
12
web/vitest.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
// 组件与单元检查:只证明组件范围;真实浏览器旅程由 Playwright 配置承担。
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: ["tests/设置.ts"],
|
||||||
|
include: ["tests/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
||||||
|
// 无用例时如实失败(不冒充通过)
|
||||||
|
passWithNoTests: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
185
工具/构建资源包.py
Normal file
185
工具/构建资源包.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"""生成运行资源与发布清单。
|
||||||
|
|
||||||
|
工程阶段的生成或预检入口:
|
||||||
|
- 从唯一登记输入(工具/资源登记.json)打包 src/muse/资源/运行资源.zip;
|
||||||
|
- 写出 src/muse/资源/清单.json(发布身份 + 每个资源的相对路径与 sha256);
|
||||||
|
- 投影 web/dist → src/muse/资源/工作台/(web 构建存在时)。
|
||||||
|
|
||||||
|
边界:只打包已登记生产资源;不读取未审工作树内容、不打包 tests/.git/当前目录;
|
||||||
|
生成物幂等可重现(固定排序、固定压缩时间戳)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import tomllib
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from muse.资源加载 import 解析能力声明
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parent.parent
|
||||||
|
登记文件 = 仓库根 / "工具" / "资源登记.json"
|
||||||
|
资源目录 = 仓库根 / "src" / "muse" / "资源"
|
||||||
|
压缩包名 = "运行资源.zip"
|
||||||
|
清单名 = "清单.json"
|
||||||
|
_固定时间戳 = (2026, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def 读取登记() -> dict[str, Any]:
|
||||||
|
if not 登记文件.is_file():
|
||||||
|
raise SystemExit(f"资源登记不存在:{登记文件}(打包前必须先登记生产资源)")
|
||||||
|
登记: Any = json.loads(登记文件.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(登记, dict) or "资源" not in 登记:
|
||||||
|
raise SystemExit("资源登记不合法:缺少 资源 表")
|
||||||
|
return 登记
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(文件: Path) -> str:
|
||||||
|
return hashlib.sha256(文件.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def 编译能力资源(
|
||||||
|
根: Path, 文件清单: list[str], 已实现命令: set[str]
|
||||||
|
) -> tuple[dict, dict[str, bytes]]:
|
||||||
|
目录: dict[str, dict] = {"role": {}, "method": {}, "operation": {}}
|
||||||
|
资源: dict[str, bytes] = {}
|
||||||
|
for 相对 in sorted(文件清单):
|
||||||
|
文件 = (根 / 相对).resolve()
|
||||||
|
if not 文件.is_relative_to(根.resolve()) or not 文件.is_file():
|
||||||
|
raise ValueError("能力资源必须是登记根内的实际文件")
|
||||||
|
数据 = 文件.read_bytes()
|
||||||
|
声明, _ = 解析能力声明(数据)
|
||||||
|
分类, 身份 = 声明["category"], 声明["id"]
|
||||||
|
if 身份 in 目录[分类]:
|
||||||
|
raise ValueError("能力稳定身份重复")
|
||||||
|
命令 = 声明.get("commands", [])
|
||||||
|
if not isinstance(命令, list) or any(
|
||||||
|
not isinstance(v, str) or v not in 已实现命令 for v in 命令
|
||||||
|
):
|
||||||
|
raise ValueError("能力声明包含未实现命令")
|
||||||
|
if 分类 == "operation" and not 命令:
|
||||||
|
raise ValueError("操作技能必须声明已实现命令")
|
||||||
|
路径 = f"能力/{分类}/{身份}.md"
|
||||||
|
资源[路径] = 数据
|
||||||
|
目录[分类][身份] = {
|
||||||
|
"id": 身份,
|
||||||
|
"name": 声明["name"],
|
||||||
|
"description": 声明["description"],
|
||||||
|
"contract_version": 声明["contract_version"],
|
||||||
|
"commands": 命令,
|
||||||
|
"resource_path": 路径,
|
||||||
|
"sha256": hashlib.sha256(数据).hexdigest(),
|
||||||
|
}
|
||||||
|
return 目录, 资源
|
||||||
|
|
||||||
|
|
||||||
|
def 打包() -> dict[str, Any]:
|
||||||
|
登记 = 读取登记()
|
||||||
|
发布身份 = str(登记.get("发布身份", ""))
|
||||||
|
if not 发布身份.strip():
|
||||||
|
raise SystemExit("资源登记缺少 发布身份")
|
||||||
|
条目: list[dict[str, str]] = 登记["资源"]
|
||||||
|
能力资源: dict[str, bytes] = {}
|
||||||
|
if 登记.get("能力文件"):
|
||||||
|
from muse.接入.http.应用 import 导出合同
|
||||||
|
|
||||||
|
合同 = 导出合同()
|
||||||
|
命令 = {
|
||||||
|
值["operationId"]
|
||||||
|
for 路径 in 合同["paths"].values()
|
||||||
|
for 值 in 路径.values()
|
||||||
|
if "operationId" in 值
|
||||||
|
}
|
||||||
|
目录, 能力资源 = 编译能力资源(仓库根, 登记["能力文件"], 命令)
|
||||||
|
能力资源["能力/目录.json"] = json.dumps(
|
||||||
|
目录, ensure_ascii=False, sort_keys=True, indent=2
|
||||||
|
).encode()
|
||||||
|
资源目录.mkdir(parents=True, exist_ok=True)
|
||||||
|
压缩包路径 = 资源目录 / 压缩包名
|
||||||
|
|
||||||
|
已打包: dict[str, str] = {}
|
||||||
|
with zipfile.ZipFile(压缩包路径, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for 条 in sorted(条目, key=lambda x: str(x.get("打包路径", ""))):
|
||||||
|
源 = 仓库根 / str(条["源路径"])
|
||||||
|
打包路径 = str(条["打包路径"])
|
||||||
|
if not 源.is_file():
|
||||||
|
raise SystemExit(f"登记资源不存在:{源}")
|
||||||
|
信息 = zipfile.ZipInfo(打包路径, date_time=_固定时间戳)
|
||||||
|
信息.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
信息.external_attr = 0o644 << 16
|
||||||
|
zf.writestr(信息, 源.read_bytes())
|
||||||
|
已打包[打包路径] = _sha256(源)
|
||||||
|
for 路径, 数据 in sorted(能力资源.items()):
|
||||||
|
if 路径 in 已打包:
|
||||||
|
raise ValueError("生成能力资源与直接登记路径重复")
|
||||||
|
信息 = zipfile.ZipInfo(路径, date_time=_固定时间戳)
|
||||||
|
信息.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
信息.external_attr = 0o644 << 16
|
||||||
|
zf.writestr(信息, 数据)
|
||||||
|
已打包[路径] = hashlib.sha256(数据).hexdigest()
|
||||||
|
|
||||||
|
# 可编辑安装和正式 wheel 使用相同的包内迁移入口。
|
||||||
|
迁移源 = 登记.get("迁移目录")
|
||||||
|
if 迁移源:
|
||||||
|
源目录 = 仓库根 / 迁移源
|
||||||
|
if not 源目录.is_dir():
|
||||||
|
raise SystemExit("登记的迁移目录不存在")
|
||||||
|
迁移资源 = 资源目录 / "迁移"
|
||||||
|
if 迁移资源.exists():
|
||||||
|
shutil.rmtree(迁移资源)
|
||||||
|
迁移资源.mkdir()
|
||||||
|
for 文件 in sorted(源目录.glob("V*.sql")):
|
||||||
|
shutil.copyfile(文件, 迁移资源 / 文件.name)
|
||||||
|
已打包[f"迁移/{文件.name}"] = _sha256(文件)
|
||||||
|
|
||||||
|
# 工作台投影:web 构建产物存在时带上;不存在时如实留空。
|
||||||
|
工作台目录 = 资源目录 / "工作台"
|
||||||
|
if 工作台目录.exists():
|
||||||
|
shutil.rmtree(工作台目录)
|
||||||
|
web_dist = 仓库根 / "web" / "dist"
|
||||||
|
if web_dist.is_dir():
|
||||||
|
shutil.copytree(web_dist, 工作台目录)
|
||||||
|
for 文件 in sorted(工作台目录.rglob("*")):
|
||||||
|
if 文件.is_file():
|
||||||
|
相对 = 文件.relative_to(资源目录).as_posix()
|
||||||
|
已打包[相对] = _sha256(文件)
|
||||||
|
|
||||||
|
清单 = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"发布身份": 发布身份,
|
||||||
|
"资源": {路径: {"sha256": 哈希} for 路径, 哈希 in sorted(已打包.items())},
|
||||||
|
}
|
||||||
|
代码文件 = {
|
||||||
|
文件.relative_to(仓库根).as_posix(): _sha256(文件)
|
||||||
|
for 文件 in (仓库根 / "src/muse").rglob("*")
|
||||||
|
if 文件.suffix in {".py", ".ts"}
|
||||||
|
and "资源" not in 文件.relative_to(仓库根 / "src/muse").parts
|
||||||
|
}
|
||||||
|
代码哈希 = hashlib.sha256(json.dumps(代码文件, sort_keys=True).encode()).hexdigest()
|
||||||
|
with (仓库根 / "pyproject.toml").open("rb") as 文件:
|
||||||
|
包版本 = tomllib.load(文件)["project"]["version"]
|
||||||
|
清单.update({"代码版本": 包版本, "代码哈希": 代码哈希, "合同版本": 1})
|
||||||
|
清单["构建身份"] = hashlib.sha256(
|
||||||
|
json.dumps(清单, ensure_ascii=False, sort_keys=True).encode()
|
||||||
|
).hexdigest()
|
||||||
|
(资源目录 / 清单名).write_text(
|
||||||
|
json.dumps(清单, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
return 清单
|
||||||
|
|
||||||
|
|
||||||
|
def 主() -> int:
|
||||||
|
解析器 = argparse.ArgumentParser(description="生成运行资源与发布清单")
|
||||||
|
解析器.parse_args()
|
||||||
|
清单 = 打包()
|
||||||
|
print(f"构建资源包:发布身份 {清单['发布身份']},资源 {len(清单['资源'])} 项")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(主())
|
||||||
64
工具/生成接口.py
Normal file
64
工具/生成接口.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""从登记的实际路由工厂生成 OpenAPI;不启动应用、不连接数据库或读取凭据。
|
||||||
|
|
||||||
|
未支持的非空输入失败,生成结果由同一公开合同产生。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parent.parent
|
||||||
|
登记文件 = 仓库根 / "工具" / "生成输入.json"
|
||||||
|
|
||||||
|
|
||||||
|
def 已登记输入() -> dict[str, Any]:
|
||||||
|
"""读取生成登记;文件不存在视为空登记(如实报告,不猜测)。"""
|
||||||
|
if not 登记文件.is_file():
|
||||||
|
return {"资源": []}
|
||||||
|
数据: Any = json.loads(登记文件.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(数据, dict) or not isinstance(数据.get("资源"), list):
|
||||||
|
raise SystemExit(f"生成输入登记不合法:{登记文件}(需要 dict 含 资源 表)")
|
||||||
|
return 数据
|
||||||
|
|
||||||
|
|
||||||
|
def 主() -> int:
|
||||||
|
解析器 = argparse.ArgumentParser(description="生成 Schema、OpenAPI 和 TypeScript")
|
||||||
|
解析器.parse_args()
|
||||||
|
|
||||||
|
输入 = 已登记输入()
|
||||||
|
if not 输入["资源"]:
|
||||||
|
print("生成接口:无登记输入,未生成任何合同(业务外壳随 W04/W07 定稿后登记)")
|
||||||
|
return 0
|
||||||
|
for 条目 in 输入["资源"]:
|
||||||
|
if 条目.get("格式") != "OpenAPI" or not 条目.get("工厂") or not 条目.get("输出"):
|
||||||
|
raise SystemExit("生成接口:未接入此输入的生成器,未生成合同")
|
||||||
|
模块, 名称 = 条目["工厂"].split(":", 1)
|
||||||
|
合同 = getattr(importlib.import_module(模块), 名称)()
|
||||||
|
if not isinstance(合同, dict) or "openapi" not in 合同:
|
||||||
|
raise SystemExit("生成接口:工厂未返回 OpenAPI 合同")
|
||||||
|
输出 = 仓库根 / 条目["输出"]
|
||||||
|
输出.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
输出.write_text(json.dumps(合同, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
客户端 = 仓库根 / "web/src/接口/生成/客户端.ts"
|
||||||
|
客户端.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
客户端.write_text(
|
||||||
|
"// 由工具/生成接口.py生成;稳定外壳来自同一OpenAPI。\n"
|
||||||
|
'import createClient from "openapi-fetch";\n'
|
||||||
|
'import type { paths } from "./类型";\n'
|
||||||
|
'import { 传输 } from "../传输";\n\n'
|
||||||
|
"export const 客户端 = createClient<paths>({\n"
|
||||||
|
" baseUrl: globalThis.location.origin, fetch: 传输,\n"
|
||||||
|
' credentials: "same-origin"\n'
|
||||||
|
"});\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"生成接口:{条目['输出']}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(主())
|
||||||
11
工具/生成输入.json
Normal file
11
工具/生成输入.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"说明": "接口生成器的唯一登记输入。每个条目声明稳定业务外壳的位置与生成目标;空登记时生成器不产出任何合同。业务外壳随 W04(元数据投影)与 W07(HTTP 合同)定稿后登记。",
|
||||||
|
"资源": [
|
||||||
|
{
|
||||||
|
"格式": "OpenAPI",
|
||||||
|
"工厂": "muse.接入.http.应用:导出合同",
|
||||||
|
"输出": "docs/接口契约/生成/openapi.generated.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
279
工具/维护索引.py
Normal file
279
工具/维护索引.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
"""索引生成、查重及检查。
|
||||||
|
|
||||||
|
工程阶段的生成或预检入口:
|
||||||
|
- 核对 Markdown 索引(目录.md)表内相对链接有效;
|
||||||
|
- 核对技能目录(.agent/skills/ 下分组)中每个技能有 SKILL.md,名称不重复;
|
||||||
|
- 核对测试符号与用例清单的 case 身份一致:仓库内每个 test_ 符号必须带
|
||||||
|
``__<case6>`` 后缀且能在清单定位;清单登记的目标符号必须在树上真实存在;
|
||||||
|
只有 print 没有 assert/raise 的用例在执行前即被标记(反假绿)。
|
||||||
|
|
||||||
|
边界:只报告差异与失败,不改变业务数据、不自行批准发布。
|
||||||
|
生成物与唯一输入一致;命令可独立重跑。所有检查函数接受显式根目录,
|
||||||
|
供测试在临时项目上验证同一语义。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
仓库根 = Path(__file__).resolve().parent.parent
|
||||||
|
_目录表行 = re.compile(r"^\|\s*([^|]+)\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|")
|
||||||
|
_用例后缀 = re.compile(r"__([0-9a-f]{6})$")
|
||||||
|
_忽略目录片段 = frozenset(
|
||||||
|
{".venv", ".venv-旧", ".git", "node_modules", "__pycache__", ".agents.local", "研究依据"}
|
||||||
|
)
|
||||||
|
# 新版用例目录:旧测试树(tests/skills、tests/adapters、tests/architecture 英文目录等)
|
||||||
|
# 由旧环境运行,不进入新版用例身份检查
|
||||||
|
_新版用例目录 = ("单元", "契约", "集成", "架构")
|
||||||
|
|
||||||
|
|
||||||
|
def _在忽略目录(路径: Path, 根: Path) -> bool:
|
||||||
|
try:
|
||||||
|
相对 = 路径.relative_to(根)
|
||||||
|
except ValueError:
|
||||||
|
return True
|
||||||
|
return any(片段 in 相对.parts for 片段 in _忽略目录片段)
|
||||||
|
|
||||||
|
|
||||||
|
def 检查目录链接(根: Path) -> list[str]:
|
||||||
|
"""核对仓库内 目录.md 的表格相对链接;返回问题列表。"""
|
||||||
|
问题: list[str] = []
|
||||||
|
for 目录文件 in 根.rglob("目录.md"):
|
||||||
|
if _在忽略目录(目录文件, 根):
|
||||||
|
continue
|
||||||
|
for 行号, 行 in enumerate(目录文件.read_text(encoding="utf-8").splitlines(), 1):
|
||||||
|
匹配 = _目录表行.match(行)
|
||||||
|
if not 匹配:
|
||||||
|
continue
|
||||||
|
地址 = 匹配.group(3).split("#")[0].strip()
|
||||||
|
if not 地址 or 地址.startswith(("http://", "https://")):
|
||||||
|
continue
|
||||||
|
目标 = (目录文件.parent / 地址).resolve()
|
||||||
|
if not 目标.exists():
|
||||||
|
问题.append(f"{目录文件.relative_to(根)}:{行号} 链接不存在:{地址}")
|
||||||
|
return 问题
|
||||||
|
|
||||||
|
|
||||||
|
def _读frontmatter名(文件: Path) -> str | None:
|
||||||
|
"""读 SKILL.md frontmatter 的 name 字段;无 frontmatter 返回 None。"""
|
||||||
|
文本 = 文件.read_text(encoding="utf-8")
|
||||||
|
if not 文本.startswith("---"):
|
||||||
|
return None
|
||||||
|
结束 = 文本.find("\n---", 3)
|
||||||
|
if 结束 < 0:
|
||||||
|
return None
|
||||||
|
for 行 in 文本[3:结束].splitlines():
|
||||||
|
if 行.startswith("name:"):
|
||||||
|
return 行[5:].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _含中文(名称: str) -> bool:
|
||||||
|
return any("\u4e00" <= ch <= "\u9fff" for ch in 名称)
|
||||||
|
|
||||||
|
|
||||||
|
def 检查技能目录(根: Path) -> list[str]:
|
||||||
|
"""核对 .agent/skills 分组:每个技能有非空 SKILL.md、名称不重复、
|
||||||
|
frontmatter name 与目录一致、目录名全中文且规范化(NFC、无空白)。"""
|
||||||
|
技能根 = 根 / ".agent" / "skills"
|
||||||
|
if not 技能根.is_dir():
|
||||||
|
return ["技能目录不存在:.agent/skills(缺失即失败,不静默跳过)"]
|
||||||
|
问题: list[str] = []
|
||||||
|
已见: dict[str, str] = {}
|
||||||
|
for 分组 in sorted(p for p in 技能根.iterdir() if p.is_dir()):
|
||||||
|
for 技能目录 in sorted(p for p in 分组.iterdir() if p.is_dir()):
|
||||||
|
名称 = 技能目录.name
|
||||||
|
相对 = str(技能目录.relative_to(根))
|
||||||
|
技能文件 = 技能目录 / "SKILL.md"
|
||||||
|
if not 技能文件.is_file():
|
||||||
|
问题.append(f"技能缺少 SKILL.md:{相对}")
|
||||||
|
continue
|
||||||
|
正文 = 技能文件.read_text(encoding="utf-8").strip()
|
||||||
|
if not 正文 or 正文 == "---":
|
||||||
|
问题.append(f"SKILL.md 为空:{相对}")
|
||||||
|
continue
|
||||||
|
front名 = _读frontmatter名(技能文件)
|
||||||
|
if front名 is None:
|
||||||
|
问题.append(f"SKILL.md 缺少 frontmatter name:{相对}")
|
||||||
|
elif front名 != 名称:
|
||||||
|
问题.append(f"frontmatter name 与目录不一致:{相对}(name: {front名})")
|
||||||
|
if not _含中文(名称):
|
||||||
|
问题.append(f"技能名称必须全中文(政策 chinese_only):{相对}")
|
||||||
|
if 名称 != unicodedata.normalize("NFC", 名称):
|
||||||
|
问题.append(f"技能名称不是 NFC 规范形式:{相对}")
|
||||||
|
if any(ch.isspace() for ch in 名称):
|
||||||
|
问题.append(f"技能名称含空白:{相对}")
|
||||||
|
if 名称 in 已见:
|
||||||
|
问题.append(f"技能名称重复:{名称}({已见[名称]} 与 {相对})")
|
||||||
|
else:
|
||||||
|
已见[名称] = 相对
|
||||||
|
return 问题
|
||||||
|
|
||||||
|
|
||||||
|
def 解析测试符号(文件: Path) -> list[ast.FunctionDef]:
|
||||||
|
"""用 AST 提取 test_ 开头的函数与方法;不执行任何代码。"""
|
||||||
|
树 = ast.parse(文件.read_text(encoding="utf-8"))
|
||||||
|
return [
|
||||||
|
节点
|
||||||
|
for 节点 in ast.walk(树)
|
||||||
|
if isinstance(节点, ast.FunctionDef) and 节点.name.startswith("test_")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _有判定语句(节点: ast.FunctionDef) -> bool:
|
||||||
|
"""函数体内是否有 assert、raise 或 pytest.fail/skip/xfail 调用。"""
|
||||||
|
for 子 in ast.walk(节点):
|
||||||
|
if isinstance(子, ast.Assert):
|
||||||
|
return True
|
||||||
|
if isinstance(子, ast.Raise):
|
||||||
|
return True
|
||||||
|
if isinstance(子, ast.Call) and isinstance(子.func, ast.Attribute):
|
||||||
|
if 子.func.attr in {"fail", "skip", "xfail", "raises"}:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def 检查用例身份(
|
||||||
|
根: Path,
|
||||||
|
清单路径: Path | None = None,
|
||||||
|
*,
|
||||||
|
核对缺失目标: bool = False,
|
||||||
|
全树扫描: bool = False,
|
||||||
|
) -> list[str]:
|
||||||
|
"""核对测试符号与用例清单双向一致:
|
||||||
|
1. 新版用例目录内符号按完整文件、函数或前端用例名绑定清单;
|
||||||
|
2. 用例体必须包含可判定语句(assert/raise/fail/skip/xfail);
|
||||||
|
3. 核对缺失目标=True 时,清单登记的 (file, symbol) 必须在树上真实存在
|
||||||
|
已实施条目始终反向核对;过渡期只容许未实施目标暂缺。
|
||||||
|
"""
|
||||||
|
if 清单路径 is None:
|
||||||
|
清单路径 = 根 / "docs" / "系统架构" / "新版设计" / "验证设计" / "测试用例清单.json"
|
||||||
|
问题: list[str] = []
|
||||||
|
if not 清单路径.is_file():
|
||||||
|
return [f"用例清单不存在:{清单路径}"]
|
||||||
|
清单: dict[str, Any] = json.loads(清单路径.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
登记符号: dict[str, str] = {}
|
||||||
|
已实施目标: set[str] = set()
|
||||||
|
for 用例 in 清单.get("cases", []):
|
||||||
|
for 目标 in 用例.get("target_cases") or []:
|
||||||
|
case_id = str(目标.get("case_id", ""))
|
||||||
|
文件 = str(目标.get("file", ""))
|
||||||
|
符号 = str(目标.get("symbol", ""))
|
||||||
|
if 文件 and 符号:
|
||||||
|
登记符号[f"{文件}::{符号}"] = case_id
|
||||||
|
if 目标.get("verification_state") in {"通过", "已实现未执行", "失败"}:
|
||||||
|
已实施目标.add(f"{文件}::{符号}")
|
||||||
|
for 新例 in 清单.get("new_cases", []):
|
||||||
|
case_id = str(新例.get("case_id", ""))
|
||||||
|
符号 = str(新例.get("symbol", ""))
|
||||||
|
文件 = str(新例.get("file", ""))
|
||||||
|
if 文件 and 符号:
|
||||||
|
登记符号[f"{文件}::{符号}"] = case_id
|
||||||
|
if 新例.get("verification_state") in {"通过", "已实现未执行", "失败"}:
|
||||||
|
已实施目标.add(f"{文件}::{符号}")
|
||||||
|
|
||||||
|
树上符号: set[str] = set()
|
||||||
|
候选 = sorted(根.glob("**/test_*.py")) if 全树扫描 else sorted(根.glob("tests/**/test_*.py"))
|
||||||
|
for 测试文件 in 候选:
|
||||||
|
if _在忽略目录(测试文件, 根):
|
||||||
|
continue
|
||||||
|
if not 全树扫描:
|
||||||
|
# 只检查新版用例目录;旧测试树由旧环境运行
|
||||||
|
try:
|
||||||
|
相对路径 = 测试文件.relative_to(根 / "tests")
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if not 相对路径.parts or 相对路径.parts[0] not in _新版用例目录:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
相对 = 测试文件.relative_to(根).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
for 节点 in 解析测试符号(测试文件):
|
||||||
|
树上符号.add(f"{相对}::{节点.name}")
|
||||||
|
匹配 = _用例后缀.search(节点.name)
|
||||||
|
if not 匹配:
|
||||||
|
问题.append(f"测试符号缺少用例后缀:{相对}::{节点.name}")
|
||||||
|
if f"{相对}::{节点.name}" not in 登记符号:
|
||||||
|
问题.append(f"测试文件与符号未在用例清单登记:{相对}::{节点.name}")
|
||||||
|
if not _有判定语句(节点):
|
||||||
|
问题.append(f"用例无可判定语句(print 不能代替断言):{相对}::{节点.name}")
|
||||||
|
|
||||||
|
前端测试 = sorted(
|
||||||
|
[*(根 / "web/tests").rglob("*.test.ts*"), *(根 / "web/tests").rglob("*.spec.ts*")]
|
||||||
|
)
|
||||||
|
if 前端测试:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# TypeScript AST 只解析静态用例声明,不执行测试或加载其运行依赖。
|
||||||
|
脚本 = """
|
||||||
|
import ts from 'typescript';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
const files = JSON.parse(fs.readFileSync(0, 'utf8'));
|
||||||
|
const found = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'),
|
||||||
|
ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||||
|
function visit(node) {
|
||||||
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)
|
||||||
|
&& ['it', 'test'].includes(node.expression.text)
|
||||||
|
&& node.arguments[0] && ts.isStringLiteral(node.arguments[0]))
|
||||||
|
found.push([file, node.arguments[0].text]);
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
}
|
||||||
|
visit(source);
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(found));
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
结果 = subprocess.run(
|
||||||
|
["node", "--input-type=module", "-e", 脚本],
|
||||||
|
cwd=根 / "web",
|
||||||
|
input=json.dumps([str(p) for p in 前端测试]),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=20,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
for 文件, 符号 in json.loads(结果.stdout):
|
||||||
|
键 = f"{Path(文件).relative_to(根).as_posix()}::{符号}"
|
||||||
|
树上符号.add(键)
|
||||||
|
if 键 not in 登记符号:
|
||||||
|
问题.append(f"前端用例未在清单登记:{键}")
|
||||||
|
except (OSError, ValueError, subprocess.SubprocessError):
|
||||||
|
问题.append("前端用例解析失败;需要已锁定的 Node 与 TypeScript 工具链")
|
||||||
|
|
||||||
|
for 键, case_id in sorted(登记符号.items()):
|
||||||
|
if 核对缺失目标 or 键 in 已实施目标:
|
||||||
|
if 键 not in 树上符号:
|
||||||
|
问题.append(f"用例清单登记的目标不存在:{键}({case_id})")
|
||||||
|
return 问题
|
||||||
|
|
||||||
|
|
||||||
|
def 主() -> int:
|
||||||
|
解析器 = argparse.ArgumentParser(description="索引生成、查重及检查")
|
||||||
|
解析器.add_argument("--写入", action="store_true", help="执行索引写入(当前为幂等检查)")
|
||||||
|
解析器.add_argument("--检查", action="store_true", help="只检查并报告差异")
|
||||||
|
参数 = 解析器.parse_args()
|
||||||
|
if 参数.写入:
|
||||||
|
print("索引写入:当前无可写生成物,执行幂等检查")
|
||||||
|
|
||||||
|
问题 = 检查目录链接(仓库根) + 检查技能目录(仓库根) + 检查用例身份(仓库根)
|
||||||
|
if 问题:
|
||||||
|
for 条 in 问题:
|
||||||
|
print(f"索引问题:{条}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print("索引检查通过:目录链接、技能目录与用例身份均一致")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(主())
|
||||||
28
工具/资源登记.json
Normal file
28
工具/资源登记.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"说明": "运行资源包的唯一登记输入。每个条目:源路径(相对仓库根)与打包路径(zip 内相对路径,工作台文件用 工作台/ 前缀之外的稳定路径)。生产资源随 W04/W06/W13/W20/W24 逐包登记;本文件是唯一输入,运行资源.zip 与 清单.json 只由它产生。",
|
||||||
|
"发布身份": "muse-foundation-r2",
|
||||||
|
"资源": [
|
||||||
|
{
|
||||||
|
"源路径": "配置/角色策略.yaml",
|
||||||
|
"打包路径": "策略/角色.yaml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"源路径": ".agent/skills/操作/完善故事基础设定/references/交付结构.json",
|
||||||
|
"打包路径": "规划/探索交付结构.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"迁移目录": "数据库/迁移",
|
||||||
|
"能力文件": [
|
||||||
|
".agent/角色/写手.md",
|
||||||
|
".agent/角色/规划员.md",
|
||||||
|
".agent/角色/知识抽取员.md",
|
||||||
|
".agent/角色/检测员.md",
|
||||||
|
".agent/角色/评委.md",
|
||||||
|
".agent/skills/操作/维护元数据结构/SKILL.md",
|
||||||
|
".agent/skills/操作/查看与恢复任务/SKILL.md",
|
||||||
|
".agent/skills/操作/决定正文候选去留/SKILL.md",
|
||||||
|
".agent/skills/操作/完善故事基础设定/SKILL.md",
|
||||||
|
".agent/skills/操作/确认规划候选/SKILL.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
配置/测试.example.toml
Normal file
14
配置/测试.example.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 隔离测试与真实调用的显式配置模板(不含凭据值)。
|
||||||
|
# 复制为 配置/测试.toml 或按环境变量注入;缺配置必须失败,不回退示例值。
|
||||||
|
|
||||||
|
["测试"."数据库"]
|
||||||
|
# 隔离 PostgreSQL 连接串的环境变量名(值不入仓、不入文档)
|
||||||
|
"连接环境变量" = "MUSE_TEST_DATABASE_URL"
|
||||||
|
|
||||||
|
["测试"."隔离"]
|
||||||
|
# 用途隔离:测试进程只允许 evaluation / maintenance 用途
|
||||||
|
"允许用途" = ["evaluation", "maintenance"]
|
||||||
|
|
||||||
|
["测试"."模型"]
|
||||||
|
# 真实模型调用默认关闭;需要时显式授权并记录预算
|
||||||
|
"真实调用" = false
|
||||||
Loading…
x
Reference in New Issue
Block a user