W02 安装工程、启动骨架与验证环境:pyproject/uv 安装工程、启动装配骨架、conftest 用例身份与 OS 离线隔离。

按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
This commit is contained in:
zizi 2026-09-10 19:25:40 +08:00
parent 50a5522014
commit b86516505f
50 changed files with 7851 additions and 0 deletions

View 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` 状态不误删。

View File

@ -0,0 +1,9 @@
# 元数据消费规则
> 权威合同:`docs/系统架构/新版设计/数据模型/元数据与结构演进.md`、`接口契约/元数据投影与版本.md`;机械门禁:`tests/架构/test_元数据不能跨模块写值.py`(随 S04 实现落地)。
1. **实例归属业务**:元数据模块拥有模具(类型、字段、结构版本),业务模块拥有实例值;禁止经元数据接口写业务实例。
2. **稳定 ID**:类型与字段用稳定 ID 登记;禁止传任意表名列名绕过登记。
3. **只增不改**:作品级结构扩展只追加;已发布字段定义不改语义,退出用弃用标记。
4. **投影按用途**:表单、提示词、导出各按字段用途投影;收紧投影不删除已存值,未知字段不静默丢弃。
5. **结构切换并发保护**:结构升级与内容写入并发时以版本约束拒绝冲突;历史值按其当时结构解释。

View File

@ -0,0 +1,9 @@
# 审校与修订规则
> 权威合同:`docs/系统架构/新版设计/模块设计/B06-审校修订.md`、`接口契约/正文结构与中文选区.md`;机械门禁:`tests/单元/test_正文格式与选段.py` 等(随 W10/W22/W23 落地)。
1. **选区语义**:修改项按段落 ID + 段内码点区间(左闭右开)定位;重复片段不得按首次命中替换;应用前重验原文与哈希。
2. **不擅动正文形态**:全角标点、异体字、组合字符与有意空白不做自动规范化;规范化必须是显式编辑操作。
3. **AI 保护区只限自动任务**:保护区不禁止作者本人编辑自己的正文。
4. **受控修订**:修订包带文档基线、目的与允许范围;有界返修不无限循环,等待作者有明确状态。
5. **派生失败单独可见**:正文成功后的派生(事实、索引、摘要)失败不回滚正文,也不被静默吞掉。

View 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. **单一权威**:正文性定义只在所属权威文档修改;索引、投影和引用不复制第二份定义。

View 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。
违反任一条:先修依赖方向,再继续;不以"临时导入"放行。

View File

@ -0,0 +1,9 @@
# 正式变更规则
> 权威合同:`docs/系统架构/新版设计/接口契约/命令与作者决策.md`、`模块设计/S01-正式变更.md`;机械门禁:`tests/架构/test_正式写入入口.py`(随 S01 实现落地)。
1. **唯一写入口**:正式内容(正文、事实、规划、设定、决策)只能经 S01 正式变更模块提交;禁止第二条写路径、禁止裸连写库。
2. **作者确认前置**:候选必须经作者明确确认才能转正;模型与工具没有作者确认身份(`调用身份.作者` 为空的调用禁止写正式内容)。
3. **携带基线**:每个提交携带正文、候选、结构和来源版本;旧版本请求必须被拒绝且不改动作者新稿。
4. **原子与幂等**:多参与者提交要么全部生效要么全部回滚;相同 `command_id` + 相同请求幂等重放,携带不同请求必须拒绝。
5. **失败可见**:提交失败保留作者本地缓冲;服务器回执是已保存的唯一依据,不能用 UI 状态代替。

View 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 运行,两套互不接管。

View 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) | 读取规划候选并执行作者明确决定 | 使用新版规划入口 | 使用被审版本,回查正式回执 |

View File

@ -0,0 +1,9 @@
# 作者主权约束
> 权威合同:`docs/系统架构/新版设计/接口契约/命令与作者决策.md`;属不可逾越边界。
1. **先审后入**:AI 产出的一切正式内容变更先成候选,作者明确确认后才入正典;不存在自动确认。
2. **作者身份不可伪造**:写入请求的作者身份来自会话与作者动作;模型工具调用没有作者身份,不能代行确认。
3. **决策可追溯**:每次采纳、拒绝、修改都记录作者、理由与所见版本;撤销与回退走同一审计链。
4. **作者新稿不受迟到响应影响**:切章、后发请求与旧版本写入不得覆盖作者当前缓冲。
5. **私人探索边界**:探索稿生命周期独立,不因任何自动化流程被提升为正式内容或被删除。

View File

@ -0,0 +1,9 @@
# 凭据与外部调用约束
> 权威合同:`docs/系统架构/新版设计/文件设计/后端-基础设施.md`(凭据读取);属不可逾越边界。
1. **值不入库不入文**:凭据值不写入源码、文档、测试、日志、错误消息与批次证据;只记获取位置(环境变量名或受控存储路径)。
2. **受控读取**:运行时凭据经 `src/muse/基础设施/凭据读取.py` 按引用解析;缺引用即失败,不猜测。
3. **生产不读测试配置**:生产装配不加载 `配置/测试.example.toml` 派生文件;评测与维护用途使用隔离凭据。
4. **调用留痕**:真实模型调用绑定任务与尝试,保存输入输出证据(脱敏后);凭据在持久化与日志前脱敏。
5. **离线默认禁外呼**:测试与确定性脚本默认禁止网络、数据库与模型调用;需要时显式标记并使用隔离环境。

View 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/` 原文只能按授权、租约与归档合同访问;普通清理不得删除持久证据与归档。

View 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
View 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
View File

@ -0,0 +1,8 @@
# 术语规范(执行提示)
> 权威定义:`docs/系统架构/新版设计/领域模型/术语表.md`。本文件只保留执行提示,不维护第二套定义。
1. **使用前先对表**:为业务概念命名或改写文档时,先查术语表;表内已有名称直接用,不造同义词。
2. **避免使用列**:术语表每条的 _避免使用_(如把参考书称当前作品、把规划称历史事实、把候选称定稿)在代码、文档与提示词中同样适用。
3. **新旧词不混**:旧实现的英文目录名与旧模块名不进入新代码;引用旧路径时明确标注"旧"。
4. **读者是作者**:面向作者的说明用白话直陈;"Shadow/Canonical"等内部词在界面与文档中用"候选/正式"。

21
.editorconfig Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
3.12

73
Makefile Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
"""muse 应用包身份。
只承载包版本与身份,不放业务逻辑;业务在各业务模块的 接口.py 暴露。
"""
__version__ = "0.1.0"
__all__ = ["__version__"]

6
src/muse/__main__.py Normal file
View File

@ -0,0 +1,6 @@
"""Python 模块入口,与已安装的 muse 命令使用同一 CLI。"""
from muse.接入.cli.入口 import main
if __name__ == "__main__":
raise SystemExit(main())

View 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
View 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
View 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
View 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__ = ["版本引用", "计算内容哈希"]

View 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
View 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
View 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
View 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
View 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__ = ["数据库引用", "应用配置", "服务配置", "解析配置", "读取配置"]

View 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"}

View 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"{相对}: 角色文件引用退役技能别名 {别名}"

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

21
web/tsconfig.json Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,11 @@
{
"schema_version": 1,
"说明": "接口生成器的唯一登记输入。每个条目声明稳定业务外壳的位置与生成目标;空登记时生成器不产出任何合同。业务外壳随 W04(元数据投影)与 W07(HTTP 合同)定稿后登记。",
"资源": [
{
"格式": "OpenAPI",
"工厂": "muse.接入.http.应用:导出合同",
"输出": "docs/接口契约/生成/openapi.generated.json"
}
]
}

279
工具/维护索引.py Normal file
View 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
View 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"
]
}

View File

@ -0,0 +1,14 @@
# 隔离测试与真实调用的显式配置模板(不含凭据值)。
# 复制为 配置/测试.toml 或按环境变量注入;缺配置必须失败,不回退示例值。
["测试"."数据库"]
# 隔离 PostgreSQL 连接串的环境变量名(值不入仓、不入文档)
"连接环境变量" = "MUSE_TEST_DATABASE_URL"
["测试"."隔离"]
# 用途隔离:测试进程只允许 evaluation / maintenance 用途
"允许用途" = ["evaluation", "maintenance"]
["测试"."模型"]
# 真实模型调用默认关闭;需要时显式授权并记录预算
"真实调用" = false