Skip to content

Commit e39670e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18987-self-test-floor-second-injection
2 parents 5617678 + 9ee8e35 commit e39670e

27 files changed

Lines changed: 1675 additions & 93 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
feat(spec): `spec-changes.json` ships a per-release section, verified against both tarballs (#17080)
7+
8+
Clause-②: yes (widening) — one new OPTIONAL section on a published artifact plus one new
9+
`os validate --json` key. Nothing previously present is renamed, retired or reshaped: the
10+
`aggregate` and `perMajor` records and every existing key keep their spelling and meaning.
11+
Contract-review tier.
12+
13+
`spec-changes.json` (ADR-0087 D4) is keyed to the **protocol major**, while this repo's
14+
launch-window convention ships BREAKING entries as **minors**. A consumer crossing one minor
15+
therefore reads a file whose finest question is "16 → 17" — answered long ago — with
16+
`added: 0, removed: 0`, which reads as *nothing changed*. Measured on the published tarballs:
17+
between `@objectstack/spec@17.3.0` and `17.4.0` the export surface gained **225** exports and
18+
lost **51**, and the shipped manifest reported zero of each.
19+
20+
**What ships now.** The published artifact carries a `release` section — `fromVersion` →
21+
`toVersion` at package-version resolution, with `added` / `removed` (the exports that arrived
22+
and left, each named `"<entry>: <name> (<kind>)"`) and `converted` / `migrated` (the ADR-0087
23+
D2/D3 entries first registered in that release):
24+
25+
```bash
26+
jq '.release | {fromVersion, toVersion, added: (.added | length), removed: (.removed | length)}' \
27+
node_modules/@objectstack/spec/spec-changes.json
28+
os validate --json | jq .specReleaseChanges # the same data, via the CLI
29+
```
30+
31+
**The committed copy is unchanged and stays deterministic.** The section is a function of a
32+
previously *published* tarball, so it is generated at publish time only; `check:spec-changes`
33+
keeps the registry-only projection in the tree exactly as it was.
34+
35+
**A wrong change file is worse than none, so it is gated.** Before anything reaches npm the
36+
release lane recomputes the delta from the two tarballs — the previously published one and the
37+
one about to be published — and refuses to publish when the section disagrees, naming the
38+
disagreeing exports and the direction of each disagreement. A release whose data would mislead
39+
does not ship.
40+
41+
**Absence stays distinguishable from zero.** When the previous tarball carries no export
42+
snapshot the section is omitted rather than emitted empty, and `specReleaseChanges` is `null`
43+
in exactly that case: a consumer must never read "could not be computed" as "nothing changed",
44+
which is the defect this closes.
45+
46+
New public exports on `@objectstack/spec`: `SpecReleaseChangesSchema`,
47+
`SpecReleaseSurfaceSchema`, `composeReleaseChanges`, and the types `SpecReleaseChanges`,
48+
`SpecReleaseSurface`, `PreviousReleaseRegistries`, `ReleaseSurfaceDiff`.

‎.claude/agents/os-dev.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ model: opus
1414

1515
你是 ObjectStack 开发 agent,由 PM 派发,恰好带一张 GitHub issue。
1616
交付物两件:该 issue 实现完毕并推成 draft PR,加下方的 JSON 报告。
17-
报告交付两次,GitHub 优先:先作 issue 评论,首行是字面纯文本 `os-dev-report`。
17+
报告交付两次,GitHub 优先:同一段 JSON 先作 issue 评论,首行单独一行是纯文本 `os-dev-report`。
1818
⛔ 不用 HTML 注释写标记。再作为终报消息:PM 机械解析它,终报消息就是 JSON 本身。
1919
仓库根的 AGENTS.md 有约束力,第一次编辑前先读它。
2020
本文件只承载规则、查表数据与钩子无法机械强制的条款;教训写成自含规则,⛔ 不引卡号。
@@ -283,10 +283,10 @@ model: opus
283283
- 写 `#<n> is not addressed here`、`out of scope: #<n>` 或 `#<n> remains open`。
284284
- 卡片关系只在 PR 正文声明一次:commit ⛔ 不带卡片 trailer,其 trailer pair 一律 model-free。
285285
- 标题与散文用英文(见 AGENTS.md);引用的中文裁决保持原文不译,改写引文就是改写裁决。
286-
- 受管面(见 AGENTS.md)PR 正文带 `## 维护者速读(草稿)` 节,中文、业务角度,席位意见留空。
286+
- 受管路径全在 `.claude/skills/pm-dispatch/references/` 者为事实层,席位复审即记录;余为规则层。
287+
- 规则层 PR 正文带 `## 维护者速读(草稿)` 节,中文、业务角度,席位意见留空;事实层不欠。
287288
- 五段固定:改了什么/为什么改/风险与代价(含回滚)/席位意见/你要做的;席位定稿成评论。
288289
- 正文以 session-URL 形式的署名页脚收尾(见字节与 sanitizer 纪律节)。
289-
- `needs:contract-review` 归席位,⛔ 不挂不摘不等;报 PR 上有无与 `--pair PR-NUMBER` 退出码作读数。
290290
- 触 `skills/**`(对外发布的技能包)的 diff:PR 正文报两个读数,并默认拒绝小功能大扩写。
291291
- 两个读数缺一不可:被改文件的整文件 before/after,与整包 before/after(全部 SKILL.md 之和)。
292292
- 行数为准,姊妹门禁定义 token 计数后同报 token。
@@ -298,12 +298,13 @@ model: opus
298298
- `skip-changeset` 唯一判据:没有已发布的东西移动;已发布 = 各包 `files[]` 实际发运的内容。
299299
- 快速通道:`docs/adr/**` · `.claude/**` · `scripts/pm/**` · 仓根配置 · 私有包 · 注释,不发布。
300300
- 其余实测:构建后 grep `files[]` 所列路径找符号,带正控;符号零命中、正控命中 ⇒ 不发布。
301-
- 本仓库:标签是真实机制,打标签是你的步骤、不是 CI 的,PR 一开出就打。
301+
- 本仓库:标签是真实机制,打标签是你的默认步骤,PR 一开出就打;派发词可收窄或禁写。
302+
- 范围 = 派发词点名的标签 + 上文判据下的 `skip-changeset`;禁写或交集为空 ⇒ 零写并报告。
303+
- `needs:contract-review` 归席位,⛔ 不挂不摘不等;报 PR 上有无与 `--pair PR-NUMBER` 退出码作读数。
302304
- 写入首选加法端点(REST `POST .../issues/<n>/labels`,不碰已有标签);可达性按会话探,先探后用。
303305
- 被拒 ⇒ 报端点与状态码、席位代挂,⛔ 不报 `blocked`、不走 MCP;写后必做对比式读回。
304-
- 读回 diff 现集对 union(读集, 目标):union 有而回读缺 = 被剥的并发标签,重挂并写进报告。
305-
- 读回只检测剥除防不了,门语义标签被剥恰成绿灯;加法写同样必要不充分。
306-
- size-labeler 的整组 PUT 会抹掉正确的加法写;收尾一律读回、清单进报告;标签没了就重挂。
306+
- 读回 diff 现集对 union(读集, 目标),缺者 = 被剥的标签,重挂,清单进报告;收尾再读一次。
307+
- 读回只检测剥除防不了(size-labeler 整组 PUT),门标签被剥恰成绿灯;加法写同样必要不充分。
307308
- 关此步骤的是读回不是写入;读回只验写落了,验不出有门在读:幻影门标签读回照样成功。
308309
- objectui:同名标签对象在,零 workflow/脚本读它、豁免不了任何东西,pin 测试钉着。
309310
- 那边用空 frontmatter 的 changeset 声明,门禁判定行是权威;⛔ 永不在 objectui 施加该标签。
@@ -317,7 +318,6 @@ model: opus
317318

318319
## 干净收尾 —— 报告是你的终局动作
319320

320-
- 终报消息之前,把同一段 JSON 发成 issue 评论,首行单独一行、就是字面纯文本 `os-dev-report`。
321321
- 全文 ⛔ 不出现 HTML 注释:sanitizer 落库后吃短尖括号片段,连反引号里的也吃。
322322
- 被吃掉标记的评论对 PM 扫描不可见;HTML 注释形不是等价写法,是坏写法。
323323
- 凡要上 GitHub 的文本,尖括号形状片段一律改占位词拼写(`FIELD`、`IDENT.MEMBER` 一类)。

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
162162
- ⛔ 不用共享检出的工作树核验 main,它的 HEAD 由别的 agent 摆布。
163163
- 零命中须配同主体必中词,否则该零作废;同主体 = 同语料/路径形/包界/引法/失效形态。
164164
- 同仪器的控制词双零是仪器坏,⛔ 不读作缺席:换法重画再报;哑火仪器与干净结果同值。
165+
- 控制通过 ≠ 问题问对:零命中须写仪器可达半径与一个必在半径外的已知目标。
166+
- 半径按形态写(内容/文件名/声明/字段);所求在半径外 ⇒ 零非读数,换看得见它的仪器。
165167
- 他人据以行动的读数须带单位并答什么本来会让它不是这个值;递 dev 的恒标线索非答案。
166168
- ⛔ 自用即弃的探针不欠此税;「不是这个值」的答案取逐字工件或具名反例。
167169
- 仓不可达 ⛔ 不当查过了干净:在 issue 上贴出给对应座位的现成命令,等读数回贴再派。
@@ -171,13 +173,11 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
171173
- 自设定时器的文本以先重读状态开头,只写关键判据(若 X 则 Y),其余指针化。
172174
- 定时器文本 ⛔ 不写结论、不含未经重读即可执行的祈使句。
173175
- 放行认门禁 job 的结论(`completed: success`),⛔ 不认聚合读数。
174-
- advisory 门禁红着进 main 是共享损伤,任何车道发现都立即止血并立单。
175-
- main-red 的跳队例外与事故锚卡约定见 `references/landing-operations.md` B 节。
176+
- advisory 门禁红着进 main 是共享损伤,任何车道发现都立即止血并立单,见 landing-operations B。
176177
- dev 自己死了不等于维护者中止:子代理消失是正常死法,走死认领回收。
177178
- 维护者中止只认原话或宿主回报 stopped by the user,⛔ 不据推断立无重启条件的门。
178-
- 立卡者不查重,只附 3–5 个查重词;分诊按词查自有列表,零命中须控制词背书。
179179
- 共享基础设施修复入队前按症状复查 main,不按 issue 号。
180-
- 真撞上重复,先比数值与作用域再决定关哪个。
180+
- 立卡者不查重、只附 3–5 查重词;真撞上重复,先比数值与作用域再决定关哪个。
181181
- sweep 晋级与立卡的前提要对此刻的 main;dev 带证据的零实现停手是好产出,不当返工计。
182182
- 裁决明令的动作实施中测出对向事实 ⇒ 照字面执行,被打断行为的 pin 反转为拒绝 pin。
183183
- 对向事实下 ⛔ 同 PR 不做任何 promote/回退;pin 是反转不是删除。
@@ -363,9 +363,9 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
363363
- 父单已有子结构的:父单队列标签即可,分诊逐个展开路由、补 `Blocked-by:` 排序。
364364
- 父单是协调节点,永不派发。
365365
- 每张留一条英文审计评论(`Triage: lands in …; rationale: …`),可选带 `Size/model suggestion:` 行。
366-
- 跨仓查重(shadow 检查):跟跨仓引用 + 关键词搜各姊妹仓。
367-
- shadow 命中在飞 ⇒ `Blocked-by:` 不派;open 未认领 ⇒ 先收敛成一个派发入口。
368-
- shadow 命中已完成 ⇒ 卡可能过期。
366+
- 查重/shadow 检查先按文件/机制查本仓与姊妹仓 open 卡(含 `pm:dispatched`),再跟引用与关键词。
367+
- 同文件同缺陷 = 同一发现,不分车道:证据搬到先卡,后卡关 `duplicate_of`,⛔ 不并排派发。
368+
- 其余在飞 ⇒ `Blocked-by:` 不派;open 未认领 ⇒ 先并成一个派发入口;已完成 ⇒ 卡可能过期。
369369
- 生产者在哪是常设分诊问题:declared ≠ enforced 形状的卡先问谁在写这个字段。
370370
- 派发前按生产者的答案改卡的范围与域标签。
371371
- 发现分诊轮:`finding` 恒 = 待首次定级、定级即离标;hold 重验只在 `Restart-when:` 命中时发生。

‎.claude/skills/pm-dispatch/references/core-rules.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# PM 核心条款(人读摘要)
2-
每行一条有约束力的规则,按 PM 语料自身的章节分组,供人工复核。
3-
细节参数、事实表与操作配方仍以 SKILL.md 与同目录其它 references 为准;本文不新增规则。
2+
每行一条规则,按 SKILL.md 章节分组;细节以 SKILL.md 与其它 references 为准,⛔ 不新增规则。
43
## 红线
54
- PM 不写任何文件;⛔ 永不合自己的、红的或未复核的 PR,合并只经合并队列发生。
65
- 版本发布必须人工,发现未经人工的发布痕迹按事故立案,⛔ 不代跑补救性发布。
@@ -43,6 +42,7 @@
4342
- 判据取命令输出,⛔ 不取 API 字段字面值、不取本地工作树、不取看着相邻的日志。
4443
- 核验 main 用 fetch 后的 `origin/main`,⛔ 不用共享检出树;仓不可达 ⛔ 不当查过且干净。
4544
- 零命中须配同主体必中词,否则该零作废;同仪器的控制词双零是仪器坏,⛔ 不读作缺席。
45+
- 控制通过 ≠ 问题问对:零命中须写仪器可达半径与半径外已知目标,半径外的零非读数。
4646
- 他人据以行动的读数须带单位并答什么本来会让它不是这个值;递 dev 的恒标线索非答案。
4747
- 板面、树与队列读数一律自带 UTC 取数时刻,无时间戳的读数按未取处理。
4848
- 失效修法按序取:先删掉容许出错的构造,再让正确形态成为唯一拼写,最后才加检查。

‎.github/workflows/lint.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,18 @@ jobs:
943943
- name: A declared gate population reaches the tree
944944
run: pnpm check:declared-population-live
945945

946+
# ADR-0087 D4's per-release correctness gate (#17080). The REAL run needs
947+
# two published tarballs and npm, so it lives in the release lane and
948+
# cannot run on a PR; what runs here is its `--self-test`, which is
949+
# therefore the only thing standing between an edit to that gate and the
950+
# next release trusting it. Its batteries drive the same `verifyRelease()`
951+
# the release lane calls, over synthetic tarball trees: a section matching
952+
# both artifacts, one omitting a real export, one inventing an export, a
953+
# from/to version pointing at another release, and the two absences that
954+
# are legitimate. Fixture-only, no spawns; well under a second.
955+
- name: Per-release spec-changes gate self-test
956+
run: pnpm check:release-spec-changes
957+
946958
# PM bare-root worklist self-test (#10840). The step above proves the
947959
# dispatch derivation still WORKS; this one proves the recorded triage of
948960
# the gates that derivation structurally cannot see is still true of the

‎.github/workflows/release.yml‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -913,11 +913,19 @@ jobs:
913913
- name: Backfill spec-changes.json on the GitHub Release (ADR-0087 D4)
914914
# Ordering is load-bearing: `gh release upload` needs the Release the
915915
# step above creates.
916+
#
917+
# `--prepare` regenerates the manifest against the previously published
918+
# tarball exactly as the publish lane does, so the asset this repair
919+
# uploads carries the same per-release section the npm artifact does.
920+
# It cannot repair the npm artifact itself — that tarball is immutable —
921+
# and this lane never publishes one.
916922
if: steps.audit.outputs.releases-missing == 'true'
917923
env:
918924
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
919925
RELEASE_VERSION: ${{ steps.audit.outputs.version }}
920-
run: bash scripts/release-spec-changes.sh
926+
run: |
927+
bash scripts/release-spec-changes.sh --prepare
928+
bash scripts/release-spec-changes.sh --attach
921929
922930
# ══════════════════════════════════════════════════════════════════════════
923931
# HUMAN LANE — the ONLY job in this repository that publishes.
@@ -1222,6 +1230,35 @@ jobs:
12221230
12231231
echo "::warning::hotcrm@${HOTCRM_REF} is incompatible with the about-to-publish @objectstack/spec. ADVISORY ONLY — the publish continues. HOTCRM_REF is pre-v17; ship a migrated hotcrm release, bump it, and set BLOCKING=1 in .github/workflows/release.yml to re-arm this gate."
12241232
1233+
# ──────────────────────────────────────────────────────────────────────
1234+
# ADR-0087 D4 — the per-release section, and the gate that proves it.
1235+
#
1236+
# Both steps run BEFORE `changeset publish`, and that ordering is the
1237+
# whole design. The section has to be in the tree when the tarball is
1238+
# packed (a consumer's tooling reads `node_modules`, not a Release page),
1239+
# and a delta that disagrees with the artifacts must stop the release
1240+
# while stopping it is still free — after `changeset publish` the tarball
1241+
# is immutable and the only remaining repair is another version.
1242+
#
1243+
# `--no-git-checks` is what `changeset publish` passes to `pnpm publish`
1244+
# (its own source), so the working-tree edit `--prepare` makes does not
1245+
# block the publish. The committed copy is untouched: it stays the
1246+
# registry-only projection `check:spec-changes` gates on every PR.
1247+
# ──────────────────────────────────────────────────────────────────────
1248+
- name: Generate the per-release spec-changes section (ADR-0087 D4)
1249+
env:
1250+
RELEASE_VERSION: ${{ steps.guards.outputs.version }}
1251+
run: bash scripts/release-spec-changes.sh --prepare
1252+
1253+
- name: Correctness gate — the delta must match both tarballs
1254+
# ⛔ A failure here fails the release, by design (#17080): a wrong change
1255+
# file is worse than none, because a consumer stops looking once it has
1256+
# one. The gate names the disagreeing exports and the direction of each
1257+
# disagreement, so a held release arrives with its own diagnosis.
1258+
env:
1259+
RELEASE_VERSION: ${{ steps.guards.outputs.version }}
1260+
run: bash scripts/release-spec-changes.sh --verify
1261+
12251262
# ──────────────────────────────────────────────────────────────────────
12261263
# The publish itself. `pnpm run release` = build + build-console +
12271264
# scripts/release-publish.sh, which is `changeset publish` followed by ONE
@@ -1312,7 +1349,10 @@ jobs:
13121349
env:
13131350
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13141351
RELEASE_VERSION: ${{ steps.guards.outputs.version }}
1315-
run: bash scripts/release-spec-changes.sh
1352+
# Uploads the file the two steps above generated and verified — the
1353+
# Release asset and the npm artifact are the same bytes by construction,
1354+
# not by two runs agreeing.
1355+
run: bash scripts/release-spec-changes.sh --attach
13161356

13171357
# ══════════════════════════════════════════════════════════════════════════
13181358
# Runtime image — fed by either lane. Building an image for a version that is

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,7 @@ examples/app-crm/storage/
129129
.claude/worktrees/
130130
# an agent worktree is git plumbing, never repo content: a `.git` FILE reading `gitdir:` plus a whole second checkout
131131
.worktrees/
132+
133+
# The release lane's scratch space: the previously published tarball it unpacks
134+
# and the artifact it packs to verify the ADR-0087 D4 per-release section.
135+
.release-spec-changes/

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ Even inside your own worktree, operate defensively:
459459
2. **One feature branch + one PR per task.** Branch off `main`. **Never commit task work
460460
straight to `main`.** Name the branch after the issue it fixes: `claude/issue-<n>-<slug>`.
461461
The issue number is what makes in-flight work *discoverable* — `git ls-remote --heads
462-
origin | grep issue-<n>` is a one-command pre-check, and the Duplicate Fix Guard
462+
origin | grep -E 'issue-<n>(-|$)'` is a one-command pre-check, and the Duplicate Fix Guard
463463
workflow warns on fix PRs whose branch names no declared issue. A hit is a hint, not a
464464
claim — most heads carry no PR: ⛔ never skip a card on one, the `Claim:` comment decides.
465465

0 commit comments

Comments
 (0)