diff --git a/.claude/skills/checklist-author/SKILL.md b/.claude/skills/checklist-author/SKILL.md index 6648ca0925..e488841d6c 100644 --- a/.claude/skills/checklist-author/SKILL.md +++ b/.claude/skills/checklist-author/SKILL.md @@ -28,27 +28,18 @@ metadata: `node scripts/check-platform-checklist.mjs` 下校验为绿,并按 AGENTS.md (worktree-first,PD#11)落在任务分支上。 -## 编排契约 - -1. **Worktree 先行**(PD#11):`git fetch origin main && git worktree add --no-track ../objectstack- - -b origin/main`。全部编辑都在那里做。派发任何 agent 之前先读清单现状。 -2. **五个只读 gap hunter 并行** —— 每个 SWEEP.md 角度一个(console UI / spec 枚举 / - 路由与运行时 / 内置应用 / 文档声称)。每个拿到:当前 item-id 清单、已知的 waiver - 与 blocked 项(不重复上报),以及输出契约 `surface | evidence path | coverage - verdict | proposed id | sketch | fixture?`。hunter 不写任何文件。 -3. **去重并入一个草稿登记表**(落地前删掉)。跨角度的重复命中是高优先级信号,不是噪 - 音。 -4. **按区 writer agent** —— 每个 `areas/*.json` 文件一个 agent,writer 之间永不相 - 撞;除编排者外谁都不碰 `coverage.json` 与 `scripts/`。每个测试项都遵守 README.md - 的 deep-test 契约;缺 fixture 就记 `blocked`/`knownGaps`,永不伪造覆盖。writer - 断言之前把每个 endpoint、枚举、错误码都对到源码上 —— 把本技能的简报当假设,源码 - 才是真相。 -5. **集中对账**:hunter 证明有现成 fixture 的种类一律解除 waiver(2026-08 那轮 - sweep 里六条 waiver 有四条已过期 —— 每次都重审全部 waiver),新项映射进 - `coverage.json`,新的 variants 矩阵钉上 `enumSource`(见 README「Variants stay - fresh automatically」)。 -6. **校验 + 落地**:校验器绿,再提交到任务分支。产品缺陷与文档漂移进 - `FOLLOW-UPS.md`;安全敏感的发现,没有维护者的决定**永不**公开立单。 +## 编排契约 —— 只写 SWEEP 没有的 + +六步流程照 `SWEEP.md`「How to run it」执行,⛔ 这里不复述。它没有、而这一轮必须成 +立的三条: + +1. **Worktree 先行**(PD#11):`git fetch origin main && git worktree add --no-track + ../objectstack- -b origin/main`。全部编辑落在那里;派发任何 agent + 之前先读清单现状。 +2. **只有编排者碰 `coverage.json` 与 `scripts/`。** hunter 一个文件都不写,writer 一 + 个区文件一人 —— 这是并行 agent 之间唯一的串行化依据。 +3. **本技能的简报是假设,源码才是真相** —— 每个测试项按 README.md「Item anatomy」 + 的契约写;缺 fixture 记 `blocked`/`knownGaps`,永不伪造覆盖。 ## 规模指引 diff --git a/.claude/skills/checklist-test/SKILL.md b/.claude/skills/checklist-test/SKILL.md index 4f3d33d32e..af42edace8 100644 --- a/.claude/skills/checklist-test/SKILL.md +++ b/.claude/skills/checklist-test/SKILL.md @@ -75,8 +75,9 @@ node scripts/checklist-select.mjs --json 区没跑。**默认复用**已配好的树;每轮开一个冷容器,只在这一轮必须**活得比派发会话 久**时才值(长时间浏览器运行、dogfood)。 -**按 tier 排序,钉住的先跑、按区批量。** 解析器给出的 `automated.ref` 是最便宜的切 -分: +**按 tier 排序,钉住的先跑、按区批量。** 最便宜的切分是 `automated.ref` —— 但 +`--json` **不投影它**(它只给 id · priority · surface · since · revision),按 id 到 +区文件 `areas/.json` 里读: - **Tier 1 —— 带 `automated.ref` 的项**:**按区批量**,一次 vitest 覆盖该区全部钉住 的文件,一条命令为多项取证(RUNNER 规则 6:不重证自动化已钉住的东西)。 @@ -93,16 +94,14 @@ Tier 1 批量清掉,剩下的预算才对得起 Tier 2。 `shared-browser-tab` 陷阱。 - **并行度**:API 面测试项放开并行扇出(各自端口,便宜)。浏览器项**少量并行** (2–3 个),各自端口 + 浏览器上下文 —— 超过这个数,单机 CPU 与共享显示开始互相争 - 抢。派发 runner 子代理时,**必须用 `opus`**,每个给:该项 JSON、RUNNER.md、 - dogfood 技能、自己的端口/DB、结果不进仓规则(§4)。 -- **没有子代理工具时,顺序跑 —— 并在运行记录里声明这一轮是顺序执行的。** 会话里不 - 存在 Task/子代理工具是允许的退化路径,不是阻塞。但并行的全部价值在于**读者彼此独 - 立**:悄无声息地塌缩成一个读者,findings 依然成立,**「没有别的遗漏」这个结论不再 - 成立**,而输出上看不出任何区别 —— 所以声明是强制的,不靠自觉。 -- 忠实执行每项的 `steps`;对照各自声明的 `oracle` 判定每条 `acceptance` 与每条 - `negative`,采集条款点名的 `evidence`。**服务端真相压过像素;截图确认渲染之后才查 - DOM;一个 `fail` 需要 ×2 复现 + 自动化自查 + 运行 issue 里的复现规则** - (RUNNER §rules)。 + 抢。派发 runner 子代理时,档位引当次 + `node scripts/pm/dispatch-gates.mjs --tier ` 的输出、⛔ 不凭记忆(floor + sonnet · default opus · ceiling fable);每个给:该项 JSON、RUNNER.md、dogfood 技 + 能、自己的端口/DB、结果不进仓规则(§4)。 +- **没有子代理工具时,顺序跑 —— 并在运行记录里声明这一轮是顺序执行的。** 规则与它 + 的论证住在 `checklist-author` 技能(维护者所定,一处成文);⛔ 不留第二份拷贝。 +- 忠实执行每项的 `steps`,按各自声明的 `oracle` 判定每条 `acceptance` 与 `negative` + 并采证 —— oracle 层级、证据要求与防误报自查是 RUNNER 规则 1–2,⛔ 这里不复述。 ## 3. 当这一轮教你的是关于测试项本身的东西 @@ -227,12 +226,9 @@ QA-source: #运行记录号 · area.item · 条款 或记 `blocked(environment)`;证到一半的项是 `partial`,不是 `pass`。带证据的 blocked 判定是一次成功运行;伪造的 pass 不是。 - **认证 / 授权漏洞的复现,永不发布到 GitHub 的任何地方** —— 不进运行 issue,不进跟 - 踪卡,不进评论。**本条压过 §4 的「每个 `fail` 一条复现规则」与 RUNNER 规则 2 的同 - 款要求**:记下项 id、条款,以及 `detail withheld pending maintainer`;复现留在会话 - 里,停下等维护者。「换个公开的地方贴」不是缓解 —— 跟踪卡同样是公开仓库里的公开 - issue,一次照办就把一个未认证读写洞的可用配方发布了出去。存在性公开、配方不公开的 - 发现,依然是一份完整、可行动的报告;让缺陷得到修复,从不需要把可用的 exploit 交到 - 任何人手上。 + 踪卡,不进评论。记下项 id、条款与 `detail withheld pending maintainer`,复现留在会 + 话里,停下等维护者。⚠ 这与 RUNNER 规则 2 是**同一条规则写在两处**,⛔ 两边都不是 + 对另一边的优先级主张;完整论证与它的维护者裁定在 RUNNER。 - **不把 blocked 项当可运行的跑** —— 解析器隐藏它们正为此。 - **一个选择器、一次运行、一张 issue。** release sweep 把 `since:vN` 与 `priority:P0` 作为分开的两轮跑 → 两张 issue,不要糊在一起。 diff --git a/.claude/skills/dogfood-verification/SKILL.md b/.claude/skills/dogfood-verification/SKILL.md index 7da79d57d6..d03c99ce28 100644 --- a/.claude/skills/dogfood-verification/SKILL.md +++ b/.claude/skills/dogfood-verification/SKILL.md @@ -28,10 +28,15 @@ metadata: dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行的 Claude/dogfood 会 话会抢走浏览器标签页、留下挡住导航的未保存草稿、弄脏工作树。开工前先隔离: +⚠️ 环境事实的锚点是 `docs/qa/platform-checklist/RUNNER.md` —— 本节只写它没有的那 +几条,⛔ 不留第二份拷贝。 + - [ ] **自有端口**:挑一个空闲的非默认端口(不要 3000/3001/3210)。先查: `lsof -nP -iTCP: -sTCP:LISTEN`。在 `.claude/launch.json` 加一条指向**本** 工作目录的具名配置,例如 `pnpm -C /examples/app-showcase exec objectstack dev --ui --seed-admin -p -d file:/tmp//data.db`。 +- [ ] **同时导出 `OS_PORT` —— 只给 `-p` 不够。** showcase 的自 ping 连接器读的是*环 + 境*(`SHOWCASE_SELF_URL`→`OS_PORT`→`PORT`→3000),于是 `fetch failed` 冒充出口被封。 - [ ] **自有数据**:`--seed-admin` 在空 DB 上给出 `admin@objectos.ai / admin123`。持 久化 `-d file:/tmp//data.db` 重启后仍在(适合多步配置的运行);`--fresh` 给全新首跑(退出即清)。 @@ -49,8 +54,9 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行 - [ ] Console UI 在 `/_console/`;应用在 `/_console/apps/`(如 `com.objectstack.setup`、`com.objectstack.studio`)。API 根 `/api/v1`,设置 `/api/settings`,合并后的应用/导航 `/api/v1/meta/app?id=`。 -- [ ] ⚠️ **`?id=` 键的是应用 `name`,不是包 id。** 真实 name 是 `showcase_app` / - `setup` / `studio` / `account` —— 不是上一行那种 `com.objectstack.setup` / +- [ ] ⚠️ **`?id=` 键的是应用 `name`,不是包 id。** stock boot 的 name 是 + `showcase_app` / `setup` / `account`(`studio` 不随 stock boot 装载,`?id=studio` + 理应为空 —— 详见 RUNNER)—— 不是上一行那种 `com.objectstack.setup` / `com.example.showcase`(那是包 id,只在 `/_console/apps/` 的路径段上成立)。传 包 id 得到的是 `{"items":[]}`,读起来和「应用元数据没了」一模一样 —— 最高价值 的假 P0 形状。**先不带 query 取一次 `/api/v1/meta/app`,读它真正返回的 name, @@ -63,24 +69,8 @@ dev 工作树、dev-server 端口、preview 浏览器全是**共享的**:并行 watcher 只重编译示例应用自己的 `objectstack.config.ts` / `src`,不管工作区的包。 - [ ] 所以:先做完**全部**源码编辑 → `pnpm --filter build` → `preview_stop` + `preview_start`。不要每修一处就编辑→构建→重启一遍。 -- [ ] ⚠️ **消融验证(predict-then-mutate)以最危险的方式继承这一条,而且它不是 dogfood - 专属 —— mutate 腿与 restore 腿各自都要重建,并在报告里写明重建过。** 判据是解析 - 路径:任何主体经依赖的 `exports` 解析(→ 该包的 `dist/`,且没有 vitest alias - 把 specifier 拉回源码)的测试都中招,普通单元套件一样(这批 pair 的台账是 - `scripts/check-test-source-alias.mjs` 的 `KNOWN_UNALIASED_TEST_IMPORTS`)。忘记重 - 建*修复*是假红:费一圈,但会被发现。忘记重建**消融**跑的是突变前的构建,套件保持 - **绿**,而这份绿会被记成「测试已被证明有区分度」—— 给一条可能根本红不了的断言 - 发了 - 证书,之后任何 CI 都暴露不了它(CI 构建正确,在那边永远绿);消融本就为证明**新门 - 禁能失败**时更毒 —— 那份绿读作「门禁没触发」,指向门禁坏了而不是夹具坏了,会诱 - 人 - 去弱化一条本来正常的门禁(实测:plugin-auth → core;plugin-email → - platform-objects 则是消融后 375 试假绿、重建后 4 红)。每一腿(mutate **与** - restore)都是:改动 → `pnpm --filter build` → **证明它到达了 `dist/`** → 才 - 读运行结果:`node scripts/ablation-dist-preflight.mjs ''` 只在被消费 - 的 `dist/` 真带着该状态时才退 0(消融删除守卫、以及每个 restore 腿,用 - `--absent`)。⛔ restore 腿最常被跳过 —— 留在 `dist/` 里的 marker 会让突变代码对该 - 树之后的每次运行保持生效,后面的测量量的是错的树。 +- [ ] ⚠️ **消融的 mutate 腿与 restore 腿都要重建 —— 但那条规则不是 dogfood 专属**: + 判据、preflight 与两次实测住在 `.claude/agents/os-dev.md`;⛔ 这里不留第二份拷贝。 - [ ] `dist/` 已 gitignore —— 安全;永不提交构建产物。 - [ ] **`/_console` UI 是 *vendored objectui 构建*,与框架 `dist` 是两回事。** 它由 `.objectui-sha` 钉住、按预构建 bundle 提供。已合并的 objectui 修复 —— *甚至 diff --git a/.claude/skills/spec-property-retirement/SKILL.md b/.claude/skills/spec-property-retirement/SKILL.md index f4ffa125ce..c797e6146b 100644 --- a/.claude/skills/spec-property-retirement/SKILL.md +++ b/.claude/skills/spec-property-retirement/SKILL.md @@ -51,6 +51,12 @@ preview renderer 不算消费者)与 AGENTS.md §"Touched `packages/spec`?"(八 的键,就**吸收**它:把改名折进删除,删掉改名条目。二者复合后效果不可观测,而 conversion 表的 fixture 不相交契约(§3)会因叠放而失败。先例: `agent.knowledge` 在发布前吞掉了 `topics`→`sources` 改名。 +- [ ] **台账判它 `live-elsewhere` 吗?** 本仓实测无消费者、姊妹仓真在强制执行的 + 键 —— **永不是删除候选**。单读 `dead` 会批准一次删除,而它删掉的是姊妹仓某 + 道门的输入。该核的不是删除面,是它的佐证纪律:外仓指针、 + `evidenceScope: "cross-repo"`、带日期的 `verifiedAt`、180 天过期 —— 四条由 + `packages/spec/scripts/liveness/elsewhere.mts` 执行,出处见 README 的 + `live-elsewhere` 一节。 ## 1. 裁判是构建,不是台账 @@ -78,7 +84,7 @@ dead(「两个仓都没有 form 路径的读者」),而删除打断了 `gen:sche | Schema | 路线 | 机制 | |---|---|---| | **非 `.strict()`** | `retiredKey()` 墓碑 | `packages/spec/src/shared/retired-key.ts` 的 `retiredKey(guidance)` —— `z.never({ error: () => guidance }).optional()`。两个通道:`tsc`(输入类型 `never`)与 parse(处方本身,不是 "unrecognized key")。 | -| **`.strict()`** | 删键 + guidance map | 从 shape 里删除;向某个 `*_RETIRED_KEY_GUIDANCE` record 加条目,由传给 `z.object(shape, { error: … }).strict()` 的 `z.core.$ZodErrorMap` 消费。参考:`packages/spec/src/ai/tool.zod.ts:29-93,180`。object 顶层键另见 `object.zod.ts` 的 `UNKNOWN_KEY_GUIDANCE`。 | +| **`.strict()`** | 删键 + guidance map | 从 shape 里删除;向该 schema 的 `*_RETIRED_KEY_GUIDANCE` 加条目,由 `strictObject()` 的 `guidance:` 槽消费(`shared/strict-object.ts`;整族一条走 `guidanceSets`)。样板 `ai/tool.zod.ts`,审计 `shared/alias-integrity.test.ts`。⛔ 别再手写 `$ZodErrorMap`。 | | **没人 parse 它** | 都不用 | 没人能收到的处方是噪音。有意删掉 baseline 行并在 changeset 里写明 —— 先例 #3896 与 #4834(PR #4878),都在 kernel plugin-runtime 家族。家族删除后幸存的解释块在 `packages/spec/src/kernel/index.ts`(搜 `plugin-runtime.zod`)。 | 永不从非 strict schema 上裸删一个键:zod 会静默剥掉它,你只是用一个静默 no-op 换了 @@ -100,7 +106,7 @@ liveness 门禁走的是 **schema 的 shape**,逐个属性去 - 删掉**墓碑**键的行,报 **UNCLASSIFIED**(#3896 清扫一次 14 个 —— 本节就是防它); - 留着 **strict 删除**键的行,报 **ORPHAN** 行。 -orphan 这条腿是新的(`scripts/liveness/orphans.mts`)。它落地之前这个方向从不失败 +orphan 这条腿是新的(`packages/spec/scripts/liveness/orphans.mts`)。它落地之前这个方向从不失败 —— 门禁走 schema 再查行,键已离开 shape 的行根本不会被问到,原地腐烂。report 的 `aria`/`performance` 行就这样比它们的键多活了一整个 release,靠有人恰好读到那个文件 才手工删掉。你撞上 orphan 报错而属性确实还可编写时,要修的是 **walk**,不是行: @@ -142,7 +148,7 @@ ratchet(#2978)会先开火,要求你**有意删除**对应的 manifest key;删 ### guidance 字符串怎么写 -五条惯例,树上 ~28 个墓碑全部遵守: +五条惯例,树上每个墓碑都遵守 —— 逐点判定归下面那个 pin 测试,不归这段散文: 1. 反引号包着的**全限定**键打头 —— `` `flow.errorHandling.fallbackNodeId` ``,不是裸尾段。 2. `was removed in @objectstack/spec (#issue[, ADR-XXXX Dn])`。 @@ -258,7 +264,7 @@ conversion 是消费者跟的。两个都要写。 - [ ] **i18n bundle** —— 剪掉表单输入会改变抽取出的标签:`pnpm i18n:extract` 重新生 成 `packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts` (merge 模式;退役是纯删除)。由 `pnpm check:i18n` 把门。 -- [ ] **CLI advisory lint** —— `packages/cli/src/utils/lint-liveness-properties.ts` +- [ ] **CLI advisory lint** —— `packages/lint/src/lint-liveness-properties.ts` 是台账驱动的,退役键会自动停止告警;更新它的**测试**去断言 non-warn(「strict parse 现在接管它们」)。 - [ ] **Pin 测试** —— 一条阴性,断言处方本身 @@ -279,8 +285,9 @@ conversion 是消费者跟的。两个都要写。 - [ ] **Changeset** —— `@objectstack/spec` 用 `major`。AGENTS.md:breaking changeset 必须带 FROM → TO 映射与一行修复;它作为 npm 包里的 `CHANGELOG.md` 发出,是升级中的 agent 撞上墓碑报错后 grep 的东西。 - `.changeset/tool-inert-keys-removed.md` 是样板 —— 抄它的 "The retirement - kit:" 段。 + 样板是任一条活着的 `.changeset/*-retired.md` —— 抄它的 "The retirement + kit:" 段。⚠ 并带上 ADR-0087 处置标记(AGENTS.md 的 changeset 一节; + `pnpm check:adr-0087-registration` 把门)—— 退役正是它点名的那一类。 - [ ] **`check:generated` 明确不跑的源码审计 —— 整组跑,永不单点。** 它的输出会点名 它们;陷阱是跑了五个漏了第六个。咬退役的是 `check:variant-docs`:删掉一个 discriminated union 会孤儿化它的 variant/doc-ledger 条目,本地跳过则只在 CI @@ -300,38 +307,27 @@ conversion 与测试都改了 —— 但 release-notes 行与台账 README 行 ## 5. 把门禁跑到真能失败 ```bash -cd packages/spec && pnpm build # REQUIRED first — see the dist trap below -for c in check:liveness check:empty-state check:authorable-surface check:docs \ - check:api-surface check:spec-changes check:upgrade-guide \ - check:skill-refs check:skill-docs check:skill-examples; do - pnpm -s "$c" >/dev/null 2>&1; e=$? # capture BEFORE anything else runs - printf '%-28s %s\n' "$c" "$( [ $e -eq 0 ] && echo PASS || echo FAIL )" -done -cd ../.. && pnpm check:i18n && pnpm --filter @objectstack/spec test +pnpm --filter @objectstack/spec build # REQUIRED first — 见 §6 的 dist 陷阱 +pnpm --filter @objectstack/spec check:generated # 一条命令跑齐,首个失败不挡住其余 +pnpm check:i18n && pnpm --filter @objectstack/spec test ``` -`check:liveness`、`check:empty-state`、`check:skill-examples` 没有生成器 —— 那里的 -失败是真发现,不是过期产物。 +⛔ 永不手搓一份 `check:*` 名单逐个跑:手工配对是 AGENTS.md 明禁的,而一条命令一次 +报全部过期产物,正是 CI 做不到的事。`check:generated` 的输出会点名它**不**跑的五个 +纯源码审计 —— `check:liveness`、`check:empty-state`、`check:skill-examples`、 +`check:exported-any`、`check:dual-source-exports` —— 它们没有生成器,那里的失败是 +真发现、不是过期产物,按 §4 那一行整组另跑。 ## 6. 各费过一轮红构建的陷阱 -- **过期 `dist`(一条工作线上 5+ 次假警报)。** 包从 `dist` 加载。改了 `src` 之后本 - 地套件红,通常是 dist 旧了,不是你改坏了 —— `check:api-surface` 读 - `dist/*.d.ts`,会报幻影 "breaking removals"。相信任何本地红之前、去立「main 坏 - 了」的单之前,先 `pnpm turbo run build --filter=...`。 -- **`| tail -1` 吃掉退出码。** 门禁的管道接上 `tail`,报的是管道的状态,失败的门读 - 成绿。显式抓 `exit=$?`(上面的循环就是)。一次 liveness 失败曾藏在这后面。 +⚠️ 四个陷阱不写在这里,因为它们对每个 dev 都成立、锚点在别处:过期 `dist` 的假 +红、管道吃掉退出码、串行门禁互相遮蔽、`--check` 是门而裸模式重写。锚点是 AGENTS.md +的「Touched `packages/spec`?」一节与 `.claude/agents/os-dev.md` 的「本地验证范围」; +⛔ 这里不留第二份拷贝。 + - **截断的 grep 漏掉编写者。** `| head -8` 藏掉了 `SKILL.md` 的 `defineSkill` 示 例;`examples/` 有三处 `template: true`,不是一处。先按上下文找文件,再进文件里 grep 键 —— 且把 `tsc` 和门禁当权威清扫器。 -- **串行门禁互相遮蔽。** `Check Generated Artifacts` 与 `TypeScript Type Check` 各 - 自按序跑门、停在第一个失败。提前把全部重新生成;不要一次红构建迭代一个。 -- **绿 CI 可能是休眠的门。** `check-generated` 在 `ci.yml` 里跑在 `paths` 过滤器后 - 面;路径不在过滤器里,门恰好对打破它的那些 PR 沉默(过滤器整体携带 - `packages/spec/src/**` 就是这个原因)。你新增生成产物或某产物的新输入,同一个 PR - 里把路径加上。 - **只改 conversion 的 `summary` 也会过期。** 那个字符串被逐字抄进 `spec-changes.json` 的 `to` 字段与 upgrade guide 的表行,所以纯散文改动也要 `gen:spec-changes` + `gen:upgrade-guide`,和任何别的改动一样。 -- **`--check` 模式是门;裸模式重写。** `gen:*` 修文件,`check:*` 是同一脚本断言它已 - 提交。永不靠手改生成文件去「修」一个 `check:*` 失败。