AGENTS.md · diff
git:20261004.74e40cd to git:20261004.2cef5a3
12 added, 4 removed. Audit A to A.
# Agent Instructions
This repository contains a local Destiny 2 MCP server with Bungie OAuth authentication.
For any agent, not only Codex:
- Read `README.md` first for the platform-neutral installation and verification flow.
- Read `skills/destiny2-mcp/SKILL.md` for tool routing and evidence boundaries.
- Read `skills/destiny-mcp-setup/SKILL.md` only for installation, OAuth, registration, or setup troubleshooting.
- Run `scripts/verify_mcp.py` for the real MCP handshake; do not treat registration alone as proof of readiness.
## 干活的方式(写给所有 agent,包括不继承对话上下文的)
这一节不是风格偏好,是从实际协作里攒出来的账。**子代理(`subagent`)默认看不到主会话的上下文**
(工具说明:*It does not share this conversation's context*),主会话里不写进 prompt 它就不知道 ——
所以这些规矩要落在这里,而不是留在某一次对话里。相关事故见 `docs/EXPERIENCE.md`。
- **先给计划,确认后一次做到位**;不要一段做完就停下来问。用户明确说过「不要一段完了就问我」。
只有真到了要拍板的分叉(改产品行为、动账号、两条路各有代价)才问。
- **一句话交付优于长报告**:用户要的是"能不能用、还差什么、下一步",不是过程复盘。
- **不许拿估算当结论**(与下面「声称之前先量」同源):没量过就说"没量过";不要用"应该/大概"顶替实测。
- **报告里必须写"没验成的部分"**:不许用「调用成功」顶替「功能正确」;没拿到证据就写「未取得」。
真机那轮的结论要带**原文**(上游报错、回执步骤),别转述成自己的话。
- **打真机前先确认进程已重载**:MCP 服务器是长驻进程,改完代码不重连/重启就是白测一轮
(2026-10-03 有一整轮真机测试因此作废:代码已修好,跑的却是旧进程,看到的还是旧文案)。
- **验收"树能跑"只能在新克隆/临时树里跑(`git archive <commit>` 解出的目录),两步都要**:
`python -c "import destiny_mcp.server"` 与**全量** `pytest -q`。`git ls-remote` / `ls-tree` /
`rev-list --count` 只能证明**文件在提交里**,证明不了**这棵树能跑** —— 2026-10-04 就是被这个
差别坑的:半提交(改了调用方、没提交被调用方)的树,那三条验收**全过**,新克隆却
`ImportError` + 16 条红(`analyzer` 从已经搬空的模块导入、`_build_flow` 366 > 284 上限…)。
**⚠️ 在临时树里跑之前先钉住 `PYTHONPATH=<临时树>`**:本机 `.venv` 里装着工作区的 editable 映射
(`__editable__.destiny_mcp-*.pth`),子进程(`tests/test_benchmark_equip_chain_smoke.py` 会 spawn)
会绕过临时树 import 到**工作区**的代码 —— 那样验的不是这棵树(第一次跑就被它骗过一次)。
另外:`ruff check`(CI 那套 E4/E7/E9/F)在这种断链上是**绿的** —— 它抓不到"从一个模块导入一个
它根本没有的名字",别拿 lint 绿当"树能跑"。
- **转述会失真,关键事实必须自己核**:别的 agent 给的数字/结论,写进你的 prompt 前先验证
(真机踩过:一句算错的 hash 转换被照抄进守卫夹具,测试连红好几轮;后来改成"自己去查库"才定住)。
- **单测绿 ≠ 真机对**:两者都要;只跑单测的修复不许宣布"修好了"。
- **涉及账号写入的测试**:先存快照(`loadout_assistant(intent="save")`)、测完**还原并回读核对**;
写入类改动要真机验证,别拿替身当结论。
- **能自动化的规矩都要有守门测试**,而且每个新守门都要**注入一次违规确认会变红、再逐字节恢复**
(不注入就不知道它会不会咬人)。
- **⚠️ 做注入验证前先防"旧字节码"**:`__pycache__` 的失效判据是 **(mtime, 文件大小)**,所以
**等长替换的注入会被判"没过期",实际跑的还是旧字节码**——注入看着"红了",红的却可能是别的原因
(参数拼错之类),**验证本身静默失效**。(2026-10-03 踩到:把 `"blocked"` 注入成 `"skipped"`
是等长替换,第一轮注入矩阵跑的其实是旧字节码。)
**规矩**:注入后 `touch` 源文件,并用 `PYTHONDONTWRITEBYTECODE=1` 跑;恢复后用 `cmp`/sha256
**逐字节核对**,别只看测试颜色。这条比上一条更底层:**它让"注入验证"这个动作本身失效**,
而后者是整套路数可信的地基。
- **同一文件被并发改时,先确认"谁是写者"再裁定**:多 agent 同时干活时,**报告者的自述不是写入证据**。
(2026-10-04 踩到:两个 agent 并发改 `scripts/benchmark_equip_chain.py`,我按"报告者=写者"去裁定,
判给了错的那个人 —— 真正在写的是另一个实例,报告者全程一个字节都没写。)
**规矩**:收尾裁定之前自己核一遍 `shasum -a 256 <文件>` 与 `git status --short`,拿**最终字节**
说话;别信任何一份报告的自述,也别按报告去追责或"恢复"。
- **`subagent` 派出去的进程无法寻址**:它不在 teammate 名单里,`send_message` 会回
`active teammate not found` —— **主会话看不到它的状态,只能等它汇报**。所以:
①派活前把要求写全(它中途问不了);②**别为了"怕它没在干活"而重复派同一个任务** ——
那会变成两个写者改同一个文件,正是上一条那种事故的来源。
- **"进程比提交新"这条判据有盲区:跑测期间 HEAD 会前移**。重连判据只在**重连那一刻**成立;
长任务跑到一半,别的 agent 可能又提交了几个(2026-10-04 跑真机那趟期间前移了 3 个提交)。
**正确做法**:核**"被执行的字节有没有变"** —— 看那些提交**动了什么**(只改 CHANGELOG/pyproject/
tests 就不影响)+ 被跑的文件 `mtime` 是否都早于进程启动。**别只看 `HEAD` 的哈希**。
- **测试不许依赖环境**:开发机 `.env` 里配一个正式配置项(如 `DESTINY_DEFAULT_PLAYER`)就可能让
单测变红(2026-10-04:两条测试默认假定"没配默认玩家",配上之后断言全部落空)。
**规矩**:测试要**显式钉住自己假定的配置**(`monkeypatch`),并且**配了与没配两种环境都要过**。
看见"只有我这台机器红",先怀疑环境依赖,别怀疑代码。
- 改了 `skills/**` 之后**一定**跑 `scripts/install_skill.py`:`~/.dsh/skills/` 是宿主真正读的那份,
不跑就是"文档改了但 agent 看到的还是旧版"(子代理常常会漏这一步,主会话要兜住)。
## 遇到 Bungie API 问题怎么查
官方文档一页全包:<https://bungie-net.github.io/> —— OAuth scope 表、每个端点的请求/响应、AWA 三段流程、DestinyComponentType 组件枚举,都在这一页。
要深查(离线读原文、确认字段名或端点路径)就把它拉到仓库外:
python scripts/fetch_bungie_api_docs.py # 抓到 ~/.destiny_mcp/reference/,带日期与 manifest.json(版本号/字节数/sha256)
python scripts/fetch_bungie_api_docs.py --check # 只比在线文档版本号与上次记录,看文档换没换版本(--exit-code 时不一致退出 1)
那份快照**不进 git**:它是上游产物、不是我们的口径;我们依赖的是自己的实测事实。
**实测优先于文档**:`docs/reference/bungie_api.md` 记着我们踩过的坑与真机结论(scope、AWA、free vs 付费插槽接口、组件 305 必须带清单类、写入后的同步窗口、神器三个 hash 家族…)。与官方文档冲突时,以那里的「实测」为准,并去更新那一条。
## Skill maintenance
- `skills/destiny2-mcp/` is the single source for the agent-facing guide. Nothing in it may assume one host: the same folder is loaded by hosts with a skills directory, and hosts without one get a pointer block plus the public URL advertised in the MCP handshake.
+ `skills/` holds the agent-facing guides; two of them ship to hosts today:
- - After editing anything under `skills/destiny2-mcp/`, run `scripts/install_skill.py` so the installed copy keeps up. It installs into **the host that is running you** by default (DSH: `~/.dsh/skills/`, Claude Code: `~/.claude/skills/`, Codex: `~/.codex/skills/`, detected from the environment); `--list` shows the current host, `--host <name>` picks one, `--all` installs into every detected host, `--pointer` refreshes the instruction-file pointers, and `--mcp` registers the MCP server (writes the DSH profile patch; prints the paste-ready command for the others).
+ - `skills/destiny2-mcp/` — routing and evidence boundaries (the main guide).
+ - `skills/destiny2-render/` — how to render a result as a card (allowed HTML/inline styles, icon
+ degradation, escaping, and a per-block field table that names the intent behind every field).
+ `scripts/verify_render_fields.py` resolves every path in that table against a live response.
+
+ Nothing in them may assume one host: the same folders are loaded by hosts with a skills directory, and hosts without one get a pointer block plus the public URL advertised in the MCP handshake.
+
+ - After editing anything under `skills/`, run `scripts/install_skill.py` so the installed copies keep up. It installs into **the host that is running you** by default (DSH: `~/.dsh/skills/`, Claude Code: `~/.claude/skills/`, Codex: `~/.codex/skills/`, detected from the environment); `--list` shows the current host, `--host <name>` picks one, `--all` installs into every detected host, `--pointer` refreshes the instruction-file pointers, and `--mcp` registers the MCP server (writes the DSH profile patch; prints the paste-ready command for the others).
+ - **A new skill folder must be claimed by the installer**: add it to `EXTRA_SKILLS` in `scripts/install_skill.py`. An unclaimed folder installs nowhere and nothing complains — `tests/test_skill_install.py` fails on it on purpose (`SKILLS_NOT_INSTALLED` is the ledger for folders that intentionally stay repo-only).
- A new user installs this for the agent they are talking to, not for every agent on the machine: do not spread copies into Codex and Claude "just in case".
- Never write into a directory the host manages itself (for example Cursor's `skills-cursor`); use `--target` or `--pointer <file>` for unlisted hosts.
- - `tests/test_skill_contracts.py` checks `references/routing.md` against the code: intent coverage, parameter ownership, write intents, community categories. A red test there means the document is stale, not that the check is too strict.
- - `tests/test_skill_install.py` checks the delivery layer: pointer idempotence, mirror sync, and that `config.ROUTING_GUIDE_URL` matches the installer's URL.
+ - `tests/test_skill_contracts.py` checks `references/routing.md` against the code: intent coverage, parameter ownership, write intents, community categories. It also checks the render skill: every `tool(intent=…, 参数=…)` in it must exist, and every reference file must be linked from its `SKILL.md`. A red test there means the document is stale, not that the check is too strict.
+ - `tests/test_skill_install.py` checks the delivery layer: pointer idempotence, mirror sync, that `config.ROUTING_GUIDE_URL` matches the installer's URL, and that every `skills/*/SKILL.md` folder is either installed or in the not-installed ledger.
- `destiny_mcp/error_codes.py` is the only place `error.code` may come from: literal codes live in `ErrorCode`, exception classes are turned into codes by `code_for_exception()` (the class name *is* the contract), and write failures use `write_failed(intent)`. `tests/test_error_codes.py` rejects bare string codes and pins the exception→code mapping.
- `destiny_mcp/vocabulary.py` is the only place Chinese labels live (six stats, classes, locations, elements, legacy stat names). Labels are the Manifest's official Chinese strings; input aliases may be many but must resolve to one key. `tests/test_vocabulary.py` pins the official names, checks aliases round-trip (legacy names must point at *existing* labels), and rejects re-copied tables.
- `tests/test_conclusion_paths.py` guards diagnosis/conclusion output: a conclusion module may fall back to weaker wording, but never silently — the `except` must re-raise or leave a trace (append / reason / log). Add new conclusion modules to `_CONCLUSION_MODULES` there.
- `tests/test_architecture_layers.py` guards the module graph: imports may only go downwards (`_LAYERS`), no two modules may import each other, and every `svc["…_svc"]` key used in `tools/` must be declared in `service_context.ServiceContext`. A new top-level module must be registered there, otherwise the test fails on purpose — decide its layer instead of letting the rule silently skip it.
## Setup-related tasks
For installation, reinstallation, OAuth login, Codex MCP registration, or setup troubleshooting:
1. Read `skills/destiny-mcp-setup/SKILL.md` completely before taking action.
2. Follow that Skill through a real MCP handshake and verification; registration alone is not sufficient.
3. Run `skills/destiny-mcp-setup/scripts/verify_mcp.py` and require `BUNGIE_PROFILE_CHECK=ok` plus all eight tools in the normal profile.
4. Preserve unrelated worktree changes and inspect existing `.env`, OAuth tokens, and Codex MCP entries before changing them.
Never print or request secrets in chat. This includes `.env` contents, Bungie API keys, OAuth client secrets, authorization codes, access tokens, refresh tokens, and callback URLs containing authorization codes. Ask users to enter credentials locally in `.env`.
After registering or changing the MCP server, tell the user to restart Codex or open a new task so the new server is discovered.
## 写代码的习惯
这些不是风格偏好,是踩过的坑换来的。能自动化的都配了守门测试——改代码前**先看它守什么**。
### 单一出处:一个事实只写一次
| 事实 | 唯一出处 | 守门 |
| --- | --- | --- |
| 错误码 | `destiny_mcp/error_codes.py` | `tests/test_error_codes.py`(禁止裸字符串;异常类名即契约) |
| 中文词表(六维/职业/位置/元素/旧名) | `destiny_mcp/vocabulary.py` | `tests/test_vocabulary.py`(官方名钉住、旧名必须指向存在的规范名、禁止再抄) |
| 活动统计形状 | `destiny_mcp/activity_stats.py` | `tests/test_activity_stats.py`(上游键清单当夹具,拼错或新增会红) |
| intent 取值 / 写作清单 | `tools/_requests.py` | `tests/test_skill_contracts.py`、`tests/test_ignored_parameters.py` |
| 参数归属 | `tools/_param_contracts.py` | 同上(认领了必须真读、没认领必须拒收) |
| 响应信封 | `tools/_responses.py` | 语料 `sweep` 组的信封规则(110 个 intent 全查) |
| 服务容器 | `service_context.py` | `tests/test_architecture_layers.py` |
不要复制一张表、一份映射、一段正则到第二个文件。要共享就抽模块,并加一条"禁止再抄"的扫描测试。
### 不许静默降级,也不许替调用方下结论
- **没算就不能说**:`analyze` 曾在没验证过的情况下断言"没有合法组合",而同一组约束 `recommend` 能配出 100% 达标的方案(0.1.13 修)。诊断类输出只说自己算过的东西。
- **"没探成" ≠ "试过没有"**:探测失败记 `ok: null` + `not_probed` + `reason`,不能记成 `ok: false`(`_armor_ladder` 踩过)。
- **结论路径的 `except` 必须留痕**:重抛、写进返回值、或打日志;`tests/test_conclusion_paths.py` 扫。
- **可选数据可以降级,但要带 `warnings`**;坏的可选资料不能弄坏主结果。
- **缺值给 `None`,不编 `0`**;"没查到"和"没有"是两件事(`coverage_complete`、`unverifiable_reason`、`selectable_plug_status`)。
### 禁止兼容、不做补丁
- 改响应形状**旧键一个不留**(不双写、不留过渡分支):0.2.0 活动统计从手写 8 键改行式,`activitiesEntered` 直接消失。
- 别名要么是**永久**(中文说法,如 `概况`/`重复武器`),要么**登记待删**(英文近义);一律写进 `docs/COMPATIBILITY.md`,并保证同组别名走同一段分派。
- 旧实现整段删掉,不保留"失败就退回旧路径"的分支。
### 分层与体量
- 依赖只能向下、禁止环、`svc["…_svc"]` 必须在 `ServiceContext` 里声明;新顶层模块要登记层号(`tests/test_architecture_layers.py`)。
- 模块超限先抽代码,**不抬上限**;上限只能下调(`tests/test_module_size_ratchet.py`)。
- 纯形状工厂与词表放低层;工具层只做分派、参数守卫与话术。
### 失败、写入与证据
- **失败必须是失败**:`ok=false` + `error.code` + 中文说明"缺什么/下一步";裸抛异常算 bug(schema 层拒收是例外,见 `docs/testing/TESTING_CORPUS.md` 第十二章)。
- **改账号的操作**:服务端签发候选 + 用户明确 `confirmed=true` 才写;写完回读核对。
- **数据分三档不许混**:Manifest 定义 / 你的账号 / 社区资料(不可信参考,带 `source_ref` 与更新时间;出处与致谢见 README 的 Starside 段)。
- 上游故障如实说是上游,不编、不循环重试。
### 测试与验证
- 每个新守门都要**注入一次违规、确认变红、再恢复**——不验证就不知道它会不会咬人。
- 改响应形状:跑 `pytest` + 对应语料 runner + 基线 diff。
- 单测用替身、任何机器能跑(干净 `HOME` 下也要过);真机脚本单独放,分工见 `docs/testing/TESTING_CORPUS.md` 第一张表。
- **CI 跑 ruff 的规则集是写死的**(`pyproject.toml` 的 `[tool.ruff.lint] select`,版本钉在 `ruff>=0.15,<0.16`):ruff 的默认集随版本变(0.16 把 UP/SIM/FURB 都算默认),不写死就是「本地绿、CI 红」。想收规则要显式加进 select 并一次修完。
- **CI 现在跑 ruff**(`python -m ruff check`,紧挨在 pytest 前面):它抓「未定义的 name / 未使用的 import」这类,比测试更早——真机上它抓到过 `profile_components` 用了没导入(build 取账号护甲那条路直接 NameError)。mypy 存量 411 个错,**不进 CI**;路线是「新代码先干净」:先把某个模块修到 mypy 干净,再加进 ci.yml 里那行注释的文件列表。
- **声称之前先量**:性能改动给前后对比,文档里的数字必须来自实跑。
**验证成本与省时约定**(2026-09-21 实测,写在这是为了让每个 agent 都少烧时间):
| 动作 | 实测耗时 | 什么时候才跑 |
| --- | --- | --- |
| `pytest -q`(1652 条) | ≈ 2.5 分钟 | 代码改完、提交前跑一次;**文档改动不要跑全量** |
| `pytest -q tests/test_agent_docs.py` | 5 秒 | 只动 `docs/**` 时 |
| `+ tests/test_skill_contracts.py` | 3 秒 | 动了 `skills/**` 或 intent/参数契约时 |
| `scripts/run_corpus_all_rows.py`(260 行,打真机) | ≈ 4–5 分钟 | 一个功能**只在提交前跑一次**;不要每改一处就跑 |
| `scripts/install_skill.py` | < 1 秒 | 动过 `skills/**` 之后 |
| `scripts/install_skill.py --mcp`(重连标记改成**代码版本**,DSH 随即重组重连) | < 1 秒 + 重连 ≈ 40 秒 | 要让宿主/GUI 用上新代码时;同一版本重复跑是幂等的 |
- **别把重活并发跑**:实测与其它任务并发时 pytest 从 100 秒涨到 155 秒,还会让
`tests/test_build_compute.py::test_queue_wait_is_not_charged_to_the_budget` 假失败。
- 要游戏内才能确认的事实(入口是常驻还是轮换、某个名字到底叫啥)**先问用户**,
不要围着未知先造一套表再返工。
### 注释、文档与提交
- 注释写**为什么**(根因、踩过的坑、不能改的理由),不写"做了什么":例 `a_p_i_error` 为什么长这样、`or ""` 防的是什么。
- 口径写进可执行断言,不靠人记;计划文档(`*_FORMAT_PLAN.md`)记实机证据与取舍。
- 提交信息见下面的「提交信息格式」:一行标题、一次只做一件事。
- 密钥永不进日志、提交与截图(`.env`、token、client secret)。
## 提交信息格式
一条提交 = **一行标题**,不写正文(细节在 CHANGELOG、计划文档与代码注释里)。
<类型>:<做了什么>
- 类型用中文,现有取值:`版本号`(如 `0.2.0:…`)、`修复`、`重构`、`文档`、`语料`、`CI`、`错误码`、`词表`、`信封`、`兼容面`、`结论路径`,以及计划阶段的 `护甲 P3` / `武器 P4`。
- 标题写"做了什么",不写「抓出的 XXX」这类腔调;不用 emoji、结尾不加句号。
- **一次提交只做一件事**:跨主题的改动拆成多条;发布提交也一样——内容照发,标题只留一行。
- 破坏性变更在标题里点明(例:`活动统计改为行式(破坏性)`),影响面写进 CHANGELOG。
- **推到哪**:工作分支是 `codex/bundle-starside-markdown`,`origin/main` 与它同源。发布用
(**连超时设置一起照抄**——`-c` 是 git 的**顶层选项**,必须写在 `push` **前面**):
git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=20 push origin HEAD:main HEAD:codex/bundle-starside-markdown --tags
本地那个 `main` 是 2026-09 的旧分支,`git push origin main` 会被非快进拒绝(2026-09-24 踩过)。
GitHub 直连常超时(SSL / 443)→ **失败就重试几轮**;连通时单次推送可能卡 75 秒才报
`Failed to connect to github.com port 443`,那不是命令写错。
**⚠️ 别把 `-c` 追加到 refspec 后面**:写成 `git push origin HEAD:main … -c http.lowSpeedLimit=1000`
会得到 `error: unknown switch 'c'`(2026-10-04 有 agent 连试 5 次都以为是网络问题,其实是命令位置错)。
## 代码地图
顶层模块按分层表分组列在下面。**清单那半由脚本生成,不要手改** —— 手改会被
`tests/test_agent_docs.py` 判红(它拿脚本现算的结果逐字比)。层号来自
`tests/test_architecture_layers.py` 的 `_LAYERS`:新增顶层模块先在那边登记层号,再重新生成:
.venv/bin/python scripts/gen_code_map.py
<!-- code-map:begin -->
**第 0 层:纯基础:不依赖项目里任何东西(除了彼此)**
- `destiny_mcp/__init__.py`
- `destiny_mcp/bungie_errors.py`
- `destiny_mcp/config.py`
- `destiny_mcp/error_codes.py`
- `destiny_mcp/exceptions.py`
- `destiny_mcp/logging_config.py`
- `destiny_mcp/models/`
- `destiny_mcp/utils/`
- `destiny_mcp/vocabulary.py`
**第 1 层:基础设施:Manifest / Bungie 客户端 / 账号解析 / 类型容器 / 实测事实表**
- `destiny_mcp/activity_stats.py`
- `destiny_mcp/audit.py`
- `destiny_mcp/build_contracts.py`
- `destiny_mcp/bungie_client.py`
- `destiny_mcp/bungie_stats.py`
- `destiny_mcp/data/`
- `destiny_mcp/manifest.py`
- `destiny_mcp/manifest_armor.py`
- `destiny_mcp/manifest_artifacts.py`
- `destiny_mcp/manifest_catalog.py`
- `destiny_mcp/manifest_data.py`
- `destiny_mcp/manifest_definitions.py`
- `destiny_mcp/manifest_fingerprint.py`
- `destiny_mcp/manifest_item_queries.py`
- `destiny_mcp/manifest_lookup.py`
- `destiny_mcp/manifest_names.py`
- `destiny_mcp/manifest_plugs.py`
- `destiny_mcp/manifest_search.py`
- `destiny_mcp/oauth_setup.py`
- `destiny_mcp/player_resolver.py`
- `destiny_mcp/service_context.py`
- `destiny_mcp/wishlist_data.py`
**第 2 层:领域层(纯计算,可被服务和工具复用)**
- `destiny_mcp/build/`
- `destiny_mcp/rag/`
**第 3 层:服务层:账号读写、外部数据、形状工厂**
- `destiny_mcp/services/`
**第 4 层:工具层:MCP 门面(只做分派、守卫与话术)**
- `destiny_mcp/tools/`
**第 5 层:装配层**
- `destiny_mcp/__main__.py`
- `destiny_mcp/server.py`
<!-- code-map:end -->
`legacy/`(仓库根目录)不在这张清单里,也**不进包、不参与测试与 lint**:它是 2026-09-20 剥离的
历史工具面存档(67 个低层工具 + 配装导入整块),只作查阅用。要复活里面的东西,见 `legacy/README.md`。
### 各模块一句话职责
这半**手写**、不生成:上面说"有哪些",这里说"往哪儿放、别往哪儿堆"。新增模块必须在下面补一行,
漏写会被测试判红;表里出现清单外的模块同样红(模块删了或名字写错)。可以写"贴着上限""别往里堆"这类经验。
| 模块 | 做什么 |
| --- | --- |
| `destiny_mcp/__init__.py` | 包标记与版本来源,别往里放逻辑。 |
| `destiny_mcp/config.py` | 配置唯一入口(env + dotenv);密钥只在这里读、永不打印。 |
| `destiny_mcp/error_codes.py` | `error.code` 的唯一出处;禁止裸字符串。 |
| `destiny_mcp/exceptions.py` | 异常类定义,类名即 `error.code` 契约。 |
| `destiny_mcp/logging_config.py` | 日志配置;日志里不许出现密钥。 |
| `destiny_mcp/models/` | 响应的 Pydantic 形状定义,放低层。 |
| `destiny_mcp/utils/` | 纯工具(无服务/API 依赖);带领域逻辑就该挪去 `services/`。 |
| `destiny_mcp/vocabulary.py` | 中文词表唯一出处(六维/职业/位置/元素/旧名)。 |
| `destiny_mcp/activity_stats.py` | 活动统计形状唯一样式:上游 `statId` → 行式;改口径先看 `tests/test_activity_stats.py`。 |
| `destiny_mcp/audit.py` | 每次 MCP 工具调用落盘审计,调用日志只在这里写。 |
| `destiny_mcp/build_contracts.py` | 配装契约类型(`BuildRecipe`/`CanonicalBuild`/`ExecutableBuild`),见 ADR-001;只有最后一个能执行。 |
| `destiny_mcp/bungie_errors.py` | 上游 HTTP 错误 → 领域错误的唯一翻译层(404 单独成码、503 归上游不可用);客户端与取数模块共用,禁止在别处再写一套 except。 |
| `destiny_mcp/bungie_client.py` | Bungie API 客户端门面:token 生命周期 + 各端点;活动统计端点已拆到 `bungie_stats.py`,错误映射在 `bungie_errors.py`。 |
| `destiny_mcp/bungie_stats.py` | 活动/战绩取数域:历史统计的按角色/账号级两条路;`modes` 只在按角色端点上生效、`periodType` 没有 Season(实测)。 |
| `destiny_mcp/manifest.py` | Manifest 管理器门面(查名/搜索),其余按域拆到 `manifest_*.py`;贴着 262 上限,只做聚合。 |
| `destiny_mcp/manifest_armor.py` | 护甲模组与套装加成域。 |
| `destiny_mcp/manifest_artifacts.py` | 赛季神器:列表、按名/按 hash 查询、层级与模组解析。 |
| `destiny_mcp/manifest_catalog.py` | 武器目录:按类型/稀有度/职业/属性/弹药筛条目。 |
| `destiny_mcp/manifest_data.py` | Manifest 静态常量(基址、物品类型名、社区别名)。 |
| `destiny_mcp/manifest_definitions.py` | 物品定义查询:hash 索引、原始 JSON、中英连接回退。 |
| `destiny_mcp/manifest_fingerprint.py` | Manifest 指纹:给"生成物跟不跟得上 Manifest"一个便宜判定。 |
| `destiny_mcp/manifest_item_queries.py` | 单侧连接或按 JSON 子串扫描的物品查询。 |
| `destiny_mcp/manifest_lookup.py` | 通用定义查询与命名解析(白名单表、占位、按名搜、收藏品反查)。 |
| `destiny_mcp/manifest_names.py` | 伤害/弹药/破盾/属性的中文名唯一定义处。 |
| `destiny_mcp/manifest_plugs.py` | Plug 池与 Perk 描述:插槽类别、可插条目、Perk 文本。 |
| `destiny_mcp/manifest_search.py` | 中英双语名称索引与三层匹配/模糊匹配。 |
| `destiny_mcp/oauth_setup.py` | 个人 OAuth 助手:授权码流程与令牌落盘。 |
| `destiny_mcp/player_resolver.py` | 玩家/角色解析共享逻辑(BungieName → membership)。 |
| `destiny_mcp/service_context.py` | 服务容器;`svc["…_svc"]` 的 key 必须在这里声明。 |
| `destiny_mcp/wishlist_data.py` | DIM 愿单数据获取(个人安装用)。 |
| `destiny_mcp/data/` | 纯数据表(静态常量/映射):`activity_modes.py` 是**模式词与 `modeType` 的唯一出处**(中文名去 Manifest 取,别再抄标签表);`pvp_counters.py` 是「计数器 → 模式/周期」对照表;`raids.py` 是「突袭/地牢 → 组件 1100 官方计数器 hash」表(**不是靠运行时正则抓的**:计数器名字有 `完成数`/`完成次数`/`完成` 三种写法、描述有十来种变体,两个方向都会错,所以人工核对成表,由 `tests/test_raid_report.py` 反查 Manifest 校验)。每条带实测证据,改表先看 `tests/test_activity_modes.py` / `tests/test_pvp_counters_table.py` / `tests/test_raid_report.py`。 |
| `destiny_mcp/build/` | 配装求解引擎(护甲优化):纯计算、不碰账号;`farm_target.py` 贴着 1296 上限。**调谐能不能写的判据**在 `tuning_writes.py`:`equip_mod` 的确认阶段与 `equip_build` 的模组预检共用它(2026-09-28 因"两边各写一份、只改了一边"导致 3 颗调谐被误拦而抽出)。 |
| `destiny_mcp/rag/` | 本地社区资料检索(Phase 3)。 |
| `destiny_mcp/services/` | 服务层:账号读写、外部数据、形状工厂;判断逻辑落这里,别落工具层。**职业金装的两个 roll 特性**(相对主义/唯我主义/坚忍克己)在 `services/armor_class_item.py`:读它、按名字认它、把社区模板的「A、B」写法翻回金装名都在这里。判据是 `plugCategoryHash == 1744546145`(**不是** `intrinsics` 这个标识符 —— 它下面有 1004 条,皮肤占 247 条),**别加 36 颗的静态表**:池子只能从账号读(金装特性槽是 `plugSources: 1`)。**PvP 武器榜**(逐场 PGCR 聚合)在 `services/pvp_weapon_service.py`,它的结算缓存单独在 `services/pgcr_cache.py`(两者都登记了体积上限);**游戏内生涯计数器**(profile 组件 1100)在 `services/activity_counters_service.py`:它与统计接口是两个来源,读抖动要重试、空要报 `unavailable`(不许当 0);**突袭/地牢报表**(`intent="raid_report"`)在 `services/raid_report_service.py`,它复用前者的 `read_metrics` 读同一个组件(重试与"空不是 0"的话术只有一处),副本对照表在 `data/raids.py`;**逐场索引**(`intent="raid_scan"`,报表里「全程/最短用时」两列的唯一来源)在 `services/raid_runs.py`:JSONL 只存**原始事实**(时长/`activityWasStartedFromBeginning`/`startingPhaseIndex`/完成/人数),"算不算全程"留到读时按政策判 —— 实测 PGCR **0.88 秒/场、加并发没用**(4/8/12 并发都是 1.1~1.3 场/秒),所以按副本分块续扫,**别改成一个调用扫到底**;**锻造武器模式/红框**(图鉴「模式和催化」,进度在 profile 组件 900)在 `services/pattern_service.py`,社区来源的按名索引在 `services/starside_crafting_sources.py`(见 ADR-009);口径见 `docs/reference/bungie_api.md`。 |
| `destiny_mcp/tools/` | 工具层:只做分派、参数守卫与话术;intent 取值/参数归属/响应信封三张契约表都在这层。分支响应按域放 `_*_branches.py`(如 `_counters_branches.py` = `activity_assistant(intent="counters")`)。 |
| `destiny_mcp/__main__.py` | `python -m destiny_mcp` 入口;写法要 spawn 安全(构建求解 worker 会重跑它)。 |
| `destiny_mcp/server.py` | MCP 门面装配:注册工具与握手;别塞业务逻辑。 |
## 文档索引
`docs/**/*.md` 全部登记在这里,每条一句话说"什么时候该看它"。新增文档要补一行,删文档要同步删行 ——
`tests/test_agent_docs.py` 双向核对(索引里的路径必须存在,`docs/adr/` 之外的文档必须出现在索引里)。
ADR 单独一张台账,见 `docs/adr/README.md`。
- `docs/COMPATIBILITY.md` — 改响应形状、加/删别名、删旧键之前:哪些入口必须留、哪些能删、删之前先做什么。
- `docs/EXPERIENCE.md` — 要理解或向别人解释"这个项目为什么长成这样",或开新项目想复用这套做法时看它:可迁移的架构与逻辑设计原则、踩坑账本、给未来 agent 的开工规则模板。
- `docs/benchmarks/README.md` — 引用 `docs/benchmarks/*.json` 里的性能数字之前看它:怎么读、哪些不能当结论、P0 的实测结论是什么。
- `docs/community/COMMUNITY_DATA_NOTICE.md` — 引用或再分发社区资料(Starside)之前看授权与边界。
- `docs/community/小黑盒_功能总览.md` — 面向中文玩家的八工具功能总览;写对外说明或话术时对齐口径。
- `docs/community/小黑盒_更新公告_2026-09-18.md` — 上次发文以来的**新增与修复**发布稿(计数器/生涯三档/纯 PvP 武器榜 + 一串口径修复);要发更新文章或对齐对外口径时看它。
- `docs/design/DESKTOP_SHELL.md` — 要动桌面客户端的外壳(导航模型、工作区形态、装备页尺寸、数据来源标签、Flutter 落地)之前看它:token 与尺寸出自老 webui 的 `tokens.css`/`AppShell.tsx`/`inventory.css`,附可点击原型与三张实拍图。
- `docs/plans/ARMOR_FORMAT_PLAN.md` — 动护甲载荷格式(体积口径、字段取舍)之前看实机证据与取舍。
- `docs/plans/EQUIP_FLOW_PLAN.md` — 动装备流程(候选签发 → `confirmed` → 回读)之前看它为什么长这样。
- `docs/plans/EQUIP_WRITE_PATH_PLAN.md` — 动装备**写入路径**(求解前提 → 确认复检 → 预检次序 → 写入 → 回读/回滚)之前看它:2026-10-03 那一轮逐条的证据等级(故障侧是真机原文)、**2026-10-04 真机轮的修复侧结果**(§八:回读跳过、写入回执、逐槽还原、分段实测)、验收点、仍然**未取得**的清单(§六),以及 ADR-024 结尾「三条注入」的可核对记录在哪一节。
- `docs/plans/ICON_URL_PLAN.md` — 动**图标 URL 覆盖**(哪个出口带 `icon_url`、图片 URL 从哪来)或要给模型加**HTML 渲染 skill** 之前看它:老 web(`~/项目/Destiny_MCP`)的口径、设计决定(内部只传 hash / 出口现取 / 图片只走 Bungie CDN)、44 有 / 29 台账的清点表、守门与注入矩阵、受控基线 diff、**活动副本图标走另一条道**这个缺口,以及 7 项待拍板。
- `docs/plans/HOST_COMPAT_PLAN.md` — 要支持「只发标量」的宿主(豆包 connector 这类)时看它:哪些参数收文本写法、为什么 `equip_build` 改成也能收 `execution_id`、守门在哪。
- `docs/plans/PVP_STATS_PLAN.md` — 动生涯/赛季战绩(计数器 vs 统计接口、模式与角色范围标注)之前看它为什么分三档。
- `docs/plans/LEGACY_SURFACE_REMOVAL_PLAN.md` — 要动历史工具面(`full`/`expert` profile、`ENABLE_LEGACY_TOOLS`)或配装导入入口之前看它:删什么、导入怎么搬、怎么验都在里面。
- `docs/plans/PATTERN_QUERY_PLAN.md` — 要做锻造武器模式(玩家说的红框、「模式和催化」那一页、游戏里的「模式进度 4/5」)查询之前看它:进度只在组件 900、183 vs 219 两个口径的实测证据都在里面。
- `docs/plans/ROTATION_PLAN.md` — 要做周常轮换(夜幕/宗师词缀、遗失区域、上维挑战、泉源、异域任务、突袭/地牢特色)之前看它:官方接口给了哪半、哪半只能维护周期表、锚点与诚实口径都在里面。
- `docs/plans/RESPONSE_PROJECTION_PLAN.md` — 要动「响应投影」(默认出口给行还是给整包)之前看它:两个大响应的实测基线、这轮只投影 `find`/`recommend` 与 `duplicates` 的边界、`execution_id` 作为引用协议的口径,以及冻结清单。
- `docs/plans/PERFORMANCE_PLAN.md` — 要动性能(搜索 N+1、武器目录全量展开、载荷体积、启动、并发与缓存)之前看它:六项的实测基线与验收口径,外加**第七项(装备流程的往返次数)的基线、四步方案与实测口径**。
- `docs/plans/PERSONAL_CLIENT_PLAN.md` — 要做个人客户端(Flutter + Destiny Core + Agent + 任务随航)之前看它:设想逐条对照现状的核对表、任务进度/导演地图/Manifest 体积的实测、三个必须先拍板的决定(客户端运行时、认证、Agent 放哪)与修正后的阶段顺序。
- `docs/plans/RAID_REPORT_PLAN.md` — 要做突袭/地牢战绩报表(raid.report 那种表)之前看它:截图逐行拆出来的字段与 8 种徽章、**四列指标各自的官方来源**(完成数/导师/无瑕都在组件 1100,15 个副本与截图逐字吻合;「全程」官方没有、DayOne 只有布尔值没有编号)、副本归组要自维护表、以及没验完的三项。
- `docs/plans/SOLVER_OPTIMALITY_PLAN.md` — 要动护甲求解器的**目标函数与排序**("刚好达标"、上限、偏好、调谐进搜索、可达区间)之前看它:根因的代码位置、五条待拍板口径与 P0–P6 阶段划分都在里面。
- `docs/plans/PVP_WEAPON_BOARD_PLAN.md` — 动 PvP 武器榜(PGCR 窗口聚合、成本与并发实测、模式名出处)之前看它为什么只能给"最近 N 场"。
- `docs/plans/STARSIDE_ENTITY_PLAN.md` — 要把 Starside 作者给的新归档(按 hash 的武器推荐/评语/神器关联/帧级 DPS)
接进工具面之前看它:全量字段与覆盖、hash 归一纪律、标记解析表、八条硬口径与 P0–P5 阶段都在里面。
- `docs/plans/TUNING_WRITE_PLAN.md` — 要把「代写调谐」做成正式功能(`equip_build`/`equip_mod` 写调谐)之前看它:
三条硬口径(只写组件 310 清单里的、并进 `mods` 走同一执行器、写不进去不回退)与守门/验收清单都在里面。
- `docs/plans/SUBCLASS_ARTIFACT_PLAN.md` — 动子职业/神器读写之前看计划与取舍。
- `docs/plans/WEAPON_FORMAT_PLAN.md` — 动武器载荷格式之前看计划与实机证据。
- `docs/reference/bungie_api.md` — 遇到 Bungie API 问题(scope、端点、AWA、组件号、上游错误码)时的实测事实清单;与官方文档冲突时以它为准。
- `docs/testing/TESTING_CORPUS.md` — 改响应形状、加 intent、发版之前必读:测试分层与语料总纲。
- `docs/testing/TESTING_CORPUS_FULL.md` — 要跑逐 intent 的全量回归、或查某个字段该长什么样时看它。
- `docs/testing/TESTING_PHRASES.md` — 要**手动**试功能(照着念的口语清单:一句话一个功能、组合句、必须拒绝的句子)时看它;和代码行的语料分工不同。
## 决策记录(ADR)
架构级决定写在 `docs/adr/`:索引与编号规矩在 `docs/adr/README.md`,格式照 `docs/adr/TEMPLATE.md`。
- **什么时候必须写一条**:改对外契约(响应形状、工具与 intent 面)、破坏性变更、推翻既有决定,
以及"上游就长这样"的结论(例:护甲模组必须游戏内装)。
- **写在哪**:`docs/adr/NNN-kebab-title.md`,标题行 `# ADR-NNN: 标题`;编号连续、不复用,
写完在 `docs/adr/README.md` 补一行。
- **分工**:ADR 只记**决定了什么和为什么**(连同被否掉的选项与代价);进度、实测数据、逐次取舍
仍写 `docs/plans/`,别把两件事混进一个文件。
`tests/test_agent_docs.py` 核对编号连续无重复、每条 ADR 都有 Status/Date、README 与实际文件双向一致。