Skip to content

Commit c412a47

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14462-skill-map-reconciliation
2 parents 735fc9d + 1bc3c09 commit c412a47

11 files changed

Lines changed: 1213 additions & 163 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/plugin-sharing": minor
3+
---
4+
5+
fix(plugin-sharing): one refused grant no longer aborts a sharing rule's reconcile pass — its stale-row revocations still run (#14754)
6+
7+
After #14484 `sys_record_share` is `tenant-scoped` in the #13491 ledger, so on a
8+
walled install an organization-less system insert on it is refused loudly with
9+
`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (#8844). `SharingService.grant`
10+
resolves the organization on every path that can; a platform-global sharing
11+
rule (`organization_id = null`, its sweep unscoped) materialising a grant onto
12+
an organization-LESS record resolves none, and meets that refusal.
13+
14+
`SharingRuleService.reconcile` / `reconcileForRecord` had no per-grant catch, so
15+
the refusal propagated and **that rule's pass aborted mid-loop**. Two things
16+
were lost, and they are not equally serious:
17+
18+
- the remaining grants — recoverable, the next pass writes them;
19+
- **the stale-row revocations of that pass** — not recoverable by waiting,
20+
because every subsequent pass meets the same organization-less record and
21+
dies in the same place. A stale over-grant of that rule therefore persisted
22+
indefinitely, and the record kept aborting the pass until it was repaired by
23+
hand. That is the security-relevant half.
24+
25+
Measured while pinning this, and it sharpens the point: the engine returns
26+
organization-less rows **last** in a rule's criteria sweep (the driver's
27+
NULL-org compatibility arm is appended to the scoped arm). So a refused grant
28+
is nearly always one of the final attempts of a pass, and what an abort
29+
destroyed was hardly ever "the remaining grants" — it was almost entirely the
30+
revoke loop that runs after the whole upsert loop.
31+
32+
Both loops now attempt each grant individually. A refusal is logged with the
33+
rule, object, record, recipient and the engine's own code, counted, and the
34+
pass **continues** — the remaining grants and, above all, the stale-row
35+
revocations still run.
36+
37+
**The catch is deliberately narrow.** Only
38+
`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` is absorbed; every other error
39+
rethrows unchanged. A catch-all would swallow real defects and report a pass
40+
that "completed" having written nothing. It would also silently retire a
41+
reviewed decision: `record-share-organization-stamp.test.ts` deliberately pins
42+
the abort on the OTHER error a reconcile pass can meet here — the scoped update
43+
half answering `RECORD_NOT_FOUND` for a row stamped with a different
44+
organization — which the 2026-09-02 contract review left standing on "loud
45+
beats a wrong count". Those three pins are unchanged and still green.
46+
47+
**Why `minor` rather than `patch`.** The repair is a bug fix, but it reports
48+
through a new key. `reconcile` / `reconcileForRecord` / `evaluateRule` /
49+
`evaluateAllForRecord` now return `SharingRuleReconcilePassResult` — the spec's
50+
`SharingRuleEvaluationResult` plus `grantsRefused: number` — and that type is
51+
newly exported from the package index. Purely additive: the contract in
52+
`@objectstack/spec` is untouched, its six declared fields are unchanged, and a
53+
consumer typed against `ISharingRuleService` keeps compiling as it did. Same
54+
shape as `fix(runtime): tell an action handler when its caller-scope record load
55+
was refused` (#14143), which shipped `minor` for the same reason.
56+
57+
`grantsRefused > 0` does **not** mean the pass failed. It means the pass met a
58+
record it cannot grant on and carried on — which is the whole point.
59+
60+
**Wire surface — declared, not lifted.** `grantsRefused` reaches the wire.
61+
`POST /api/v1/sharing/rules/:idOrName/evaluate` is a ledgered **SDK** route —
62+
`packages/rest/src/rest-route-ledger.ts:390`, the row carrying
63+
`disposition: 'sdk'` and `client: 'shares.rules.evaluate'` — and its REST handler
64+
passes the service return value through **unfiltered**
65+
(`packages/rest/src/rest-server.ts:11108``:11109`:
66+
`const result = await svc.evaluateRule(req.params.idOrName, context ?? {})`
67+
followed by `res.json(result)`). So the seventh key is on the response body every
68+
caller of that route already receives. The SDK method declares
69+
`SharingRuleEvaluationResult` as its resolved type
70+
(`packages/client/src/index.ts:4766`, unwrapped at `:4771` through
71+
`unwrapResponse` parameterised on that same type), and that type is the spec's
72+
six-field contract — so the **declared client type cannot name the seventh key**.
73+
That is a client-type **lag**, not a contract break: the key is additive on the
74+
wire, every declared field is unchanged, and a consumer typed against
75+
`SharingRuleEvaluationResult` keeps compiling exactly as before. Lifting the type
76+
is not this PR's to do — `SharingRuleEvaluationResult` lives in
77+
`@objectstack/spec`, a `domain:spec` single-owner file — so the lag is declared
78+
here and tracked as the follow-up #14969, which lifts `grantsRefused?: number`
79+
(optional) into `SharingRuleEvaluationResult`.

.claude/skills/pm-dispatch/references/dispatch-runbook.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ monitor 按自己的 deadline 触发;身份 = 三元组 `(issue, 分支, PR head
240240
点名单当天现取仍会漏(实测漏的门恰被改动打红过)—— dev 对实际改动路径重取补跑。
241241
**去重/看板读数**:取一次(查询式 + 时间戳)整批共用,
242242
派发令写「当既有事实用,只复核其后的增量,⛔ 不重跑」。dev 侧对价住 os-dev 定义。
243+
- **四轴框架整段现取现粘、逐单随派发词下发**(维护者 2026-09-03 裁):内部 PM 副本 =
244+
`.claude/skills/pm-dispatch/SKILL.md` `## 升级与决策` 段的四轴框架块(黑体句起、推荐约束句止),
245+
整段逐字粘贴,⛔ 不重打、⛔ 不摘句;派发那刻现取,把该副本的 commit 与 md5 记进派发词。
246+
⛔ 仓内任何文件不得另立第三份(`check:skill-frame-sync` 全树扫);os-dev 定义没带就停下索取。
243247
- **文件面两句原文**:「预期落点是 X;若实测表明真正的生产者在别包,报备后按生产者侧修
244248
(落点与理由写进报告和 PR 正文),⛔ 不在消费者侧打补丁」—— 只写一个路径名,
245249
是要求 dev 在守约与修对之间二选一;跨包常等于跨车道,PM 事后补跨座位声明。

.claude/skills/pm-dispatch/references/platform-readings.md

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@
2929
即队列入口否决(冲突对象是**当前** main);draft PR 恒回 `draft`;判头脏的决断读数是零配额本
3030
地试合并:fetch PR ref 后 `git merge-tree --write-tree origin/main <ref>`(冲突文件直接列出;随 fetch
3131
老化,重跑先 fetch;与 enable 序列的队列 ref/update-branch 探针互补,彼答入队与暗冲突、此在挂
32-
前答头脏)。入队决策点才 `get` 一次:挂了 flip 定点(landing-operations)到点读,⛔ 不又查又等。
32+
前答头脏)。⚠️ 它只答「默认文本合并」会不会冲突:不套用 `.gitattributes` 的 merge driver
33+
⇒ 两侧同动一条 `merge=os-regen` 路径时系统性低报,方向危险(报干净而真合并会停;2026-09-03
34+
实测 0 冲突 vs `dirty`)—— 这类 diff 由 `mergeable_state` 或 scratch worktree 里的真 `git merge`
35+
夺。入队决策点才 `get` 一次:挂了 flip 定点(landing-operations)到点读,⛔ 不又查又等。
3336
- **状态核验用最小字段**(search/list + `fields`)或等事件,整对象 `get` 留给入队决策点;
3437
门禁放行判据 = 承载门禁族 job 的 conclusion,聚合(`blocked`/`dirty`)
35-
只作阴性筛查再定位,放行按名定向读单条 job,⛔ 不拉全表(按名定位失败才拉)。
38+
只作阴性筛查再定位,放行按名定向读单条 job,⛔ 不拉全表(按名定位失败才拉);⚠️ 订阅
39+
来的 `check_suite.completed` 按名读到时农场早已绿 —— 它是**唤醒**不是放行读数(2026-09-02)。
3640
- **转 draft 不是可靠的踢队手段 —— 两向相反实测,处置按最坏走**:本仓转 draft 同时掉
3741
auto-merge 与队列成员资格(均不自动恢复,转正后重挂);objectui 2026-08-25 已入队 PR 转 draft 条目
3842
保位、~40 分钟后队列照样合并(仓别/时机未分辨)。补救:转 draft + disable 都做(本仓
@@ -62,8 +66,13 @@
6266
带逼出暗冲突(它不踢已挂 PR、只是永不入队);② `merge_pull_request` 回 405「Pull Request is in the
6367
merge queue」= 在队(2026-08-28 两席各一次,均零 ref),405「Merge commits are not allowed」= 不在队,改
6468
挂 auto-merge。⛔ enable 与其验证间永不插 `disable`(入队 webhook 乱序迟到,会撤掉真实入
65-
队)。**踢出成因两则**:兄弟抢先落地 ⇒ `MERGE_CONFLICT`;缺必需批准 ⇒ 治理守卫 merge_group
66-
`CI_FAILURE`
69+
队)。**进度**`GET /actions/runs?event=merge_group`,⚠️ 批次分支按**批内最后一个 PR** 命名 ⇒
70+
自己的 PR 号不在运行列表里不等于没在跑(2026-09-01/09-02)。**踢出成因两则**:兄弟抢先落地
71+
`MERGE_CONFLICT`;缺必需批准 ⇒ 治理守卫 merge_group
72+
`CI_FAILURE`。在场时它还答**位置**:名形 `…/pr-<PR>-<PARENT>``<PARENT>` 是前一条目的 tip
73+
⇒ 从根在当前 `origin/main` 的那条起走父-tip 链即真实排队序(`ls-remote` 的字典序无意义),零
74+
配额、答 timeline 答不了的「我排第几」,⛔ 只读现在、不重建历史(2026-09-03 实测五条链:自
75+
以为被踢的 PR 实为第三位)。
6776
- **队列踢出先认签名再决定重投**:已知 flaky 核对失败签名一致 ⇒ 原样重投;
6877
止血修复合入后**同一签名再现就不再是那条 flaky**,是新问题必须重新诊断,
6978
⛔ 禁止条件反射式重投;第三种签名:本 PR 名下**没有任何** `merge_group` run 且批次同伴的 run
@@ -96,6 +105,11 @@
96105
**,⛔ 不据他席、他日读数推本席,唯一安全读法即本行探针。**门关着时的降级梯**:①
97106
git 先行(「零成本等价物」);② 公开仓 payload 档(下方「公开仓」条);③ MCP:search 定向一
98107
击、列表**单标签**读全 + 本地求交(`labels` 是 OR,见 MCP 参数条);④ 等重置。
108+
- **两通道的信封在配额、权限、传输三样上都不同 ⇒ 任一侧的拒绝只是那一侧的读数**:限
109+
流 / 403 / 传输失败都要**试过另一侧**才说得出「我没手段」(2026-09-03 实
110+
测:`POST /actions/runs/{id}/rerun-failed-jobs` REST 回 403 而 MCP 回 201;job 日志只有 MCP 取得回,REST 侧
111+
转 blob 存储 `http=000`;同分钟 MCP 限流而 REST core 满 15000)⇒ MCP 限流先探 REST 再定退避,⛔ 不
112+
据一侧限流把整个平台的写都停掉。
99113
- **默认读序 git → payload → REST → MCP/GraphQL**(2026-08-23 策略翻转,2026-08-25 增补 payload 档;
100114
⚠️ REST 档以**本班 repo-scoped 探针绿**为前提 —— 前提就住本行,403 会话改按降级梯读)。
101115
list/查重/卡与 PR 读/标签回读默认走容器 curl 的 REST 通道 —— App installation token,core
@@ -131,21 +145,34 @@
131145
点重置)优于指数退避,⛔ 绝不忙轮询;文档载明未实测:条件请求答 `304` 不计 core 池。
132146
- **公开仓零配额读法两档,payload 档优先 —— 只有 body 精确**:单卡页 `/issues/N` 内嵌 JSON 载
133147
**原始 body**,零配额 —— 取含 `bodyHTML``script[type="application/json"]` 块,读
134-
`payload.preloadedQueries[0].result.data.repository.issue.body``frontTimelineItems`/`backTimelineItems`
135-
边界:⛔ 只因仓库公开成立;⛔ 覆盖单卡读、**不覆盖 issue search**(搜索页无 SSR 结果);⛔ 永
148+
`payload.preloadedQueries[0].result.data.repository.issue.body``frontTimelineItems`/`backTimelineItems`。⚠️
149+
评论表与标量字段**都不可信**:时间线只渲染**前 15 项**`hasNextPage: true`(实测 12 条评论
150+
只回 3 条、4–12 重取仍缺),`assignees` 实测为陈旧空值而权威读非空,`labels`/`state` 由此**
151+
疑、未实测**(⛔ 不写成已测)⇒ ⛔ 认领承载字段(`assignees``pm:*` 标签、`state`)永不从本档
152+
读;⛔ **本档的缺席不是读数** —— 要么 `hasNextPage: false`、要么第二通道,自己刚写的按原
153+
写入通道回读(2026-09-03 三例,失效方向是空转与重发)。边界:⛔ 只因仓库公开成立;⛔ 覆盖
154+
单卡读、**不覆盖 issue search**(搜索页无 SSR 结果);⛔ 永
136155
不拿渲染列表定规模(静默只显一页,实测 12 vs 权威 147)。渲染层 WebFetch 仍在(~15 分缓存、
137156
有损)。
138-
- **查重先 `search_issues`**(2026-08-18 实测:单次调用按 issue body 内文本命中且 `total_count` 精确;
139-
「search 对本会话不可用」的继承说法实测为**** —— 继承说法不是读数,
140-
复述必带实测日期):body 文本匹配是 repo-scoped `list` 做不到的,`list` + 对照组降为回退。
157+
- **查重先 `search_issues`,并按它的契约拼**:它是**语义**匹配器 —— ⛔ `query` 里永不放 GitHub
158+
限定符(`repo:` / `is:` / `label:` / `in:title`),范围走 `owner` / `repo` 参数,`query` 写成描述缺陷
159+
**句子**;限定符形对每个带自由词的查询回 `total_count: 0``incomplete_results: false`,同一意
160+
图换文档形回 18 条、首条即目标(2026-09-02/09-03 两席实测)。body 文本匹配是 repo-scoped `list`
161+
做不到的,`list` + 本地扫描是**确定性回退,⛔ 不是默认**(2026-08-18 那
162+
次「`total_count` 精确」只对文档形成立;继承说法不是读数,复述必带实测日期)。
141163
- **`search_issues` 可整会话静默归零 —— 控制词一并归零**(2026-08-23 实测:某会话对**每个**
142164
询回 `total_count: 0`,含已知必中的控制词;同时刻另一会话正常 ⇒ 故障是**会话级**)。⛔
143165
**空查重结果不是读数,除非本会话内一个已知必中的控制词答了** —— ⛔ 不是「可疑时
144166
才验」:归零下空结果与真无重复逐字节同形,读作「搜过了,没有」⇒ 重复卡照开、空车道
145167
照停(实测:控制词对一张刚派发几分钟的卡回 0)。控制词回 0 ⇒ 本会话 search 已坏,**立刻
146168
换通道,⛔ 不重试**(重试只烧配额);换哪条按上面的探针与降级梯 —— 探针绿走 **REST 列
147169
表端点** `GET /repos/{o}/{r}/issues?state=open&labels=a,b&per_page=N`(core 桶、 `labels` 真 AND;⛔ 完整性
148-
自证:`&page=N` + 总数核对;`GET /search/issues` **不是**退路,见下条),403 走降级梯 MCP 档(单标签一
170+
自证:`&page=N` + 总数核对 —— ⚠️ **页码翻页在偏移 ~9,900 硬拒**:`page=100` 回 422
171+
`Pagination with the page parameter is not supported for large datasets`,与结果规模、`per_page` 都无关(453
172+
条的 `state=open` 同样在 `page=100` 拒,`per_page=1` 拒在 `page=9000`)⇒ 总体超过它就按 `sort=created`
173+
拆成 `asc` + `desc` 两趟 ≤99 页(`created`**承重**:新卡只追加到尾,页不在脚下重排),以两趟
174+
**重叠**证覆盖、对号码线闭合、缺号逐个直取核实(2026-09-01);`GET /search/issues` **不是**退
175+
路,见下条),403 走降级梯 MCP 档(单标签一
149176
次读全 + 本地求交 —— ⛔ **不是翻页手扫**:实测 226 张 open 只扫了 100 张,**不完整枚举比
150177
零结果更危险**)。
151178
- **会话代理只服务 repo-scoped 路径,`/search/*` 的 403 体解析成净零**:代理回 403 + 体
@@ -166,6 +193,8 @@
166193
GitHub 通道;配额池按身份计,换身份即清零燃烧,共享身份结构不变。
167194
- **组织侧授权变更后仓库访问逐步传播**(同一端点数分钟内 403→200);该 403
168195
体同样解析成净零 ⇒ 空车道先对仓库 `open_issues_count` 反查再信,零命中纪律覆盖 list 读。
196+
同族:**满页的首页零命中是截断不是缺席**(`GET /branches?per_page=100` 正好回 100 行而无目标
197+
⇒ 翻完再判,2026-09-03 实测)。
169198
- **MCP 参数两陷阱**:`list_issues` 多标签过滤是 **OR(并集)**不是
170199
AND —— 混入别车道同状态卡与本车道全状态卡,结果良构、失效全静默;**判据 =
171200
结果比任一输入都宽**(三席独立实测:两标签的「交集」回 135 张,而其中一个车道自身只有
@@ -174,7 +203,8 @@
174203
(交集),MCP 的 `labels` 数组是 **OR** ⇒ 要交集按上面的探针与降级梯选档,⛔ 不无条件「改走
175204
REST」。`issue_write``labels`**整组替换**不是追加 —— 同一动作内重读现值合并再写
176205
(隔轮旧读数 = 无效快照,按其回写静默剥别的标签);真追加走 REST `POST /issues/{n}/labels`,
177-
⚠️ 同样先过探针 —— 403 会话没有真追加通道,只能整组替换;写后照标签纪律回读。
206+
⚠️ 同样先过探针 —— 403 会话没有真追加通道,只能整组替换;⚠️ 摘标签也没有加法端
207+
点(`finding` 定级这类必须删标签的动作只能整组写,carve-out 保留);写后照标签纪律回读。
178208
- **PR 标签的三条读腿全盲、两条静默**(2026-08-28 同日实测):① `issue_read get_labels` 传 PR 号
179209
回「Could not resolve to an Issue」—— REST「PR 也是 issue」的惯例在此方法不成立;**响亮失败即
180210
路由信号**,改走腿③。② `pull_request_read get``labels` **时缺时滞**:dev 席同一张新 PR 两读
@@ -194,7 +224,9 @@
194224
- **`Blocked-by:` 行归 BODY(单通道反向索引)**:追加按上条「解码后写回」
195225
执行;历史上寄放在评论里的行按同程序**增量**回填(⛔ 不搞批量突击 —— 限流压力);
196226
解锁扫描只 grep body,⛔ 不加常设评论读;
197-
旧「连评论一起扫(`in:comments`)」提示作废,扫描走直读(`list_issues` + `issue_read` 读 body)。
227+
旧「连评论一起扫(`in:comments`)」提示作废,扫描走直读(`list_issues` + `issue_read` 读 body)。⚠️
228+
动手前先按「空的行内代码跨度」判本次 MCP 读可不可用(见读数陷阱判截断行):命中即改
229+
**原始体**取 —— 公开仓 payload 档或门开会话的 REST raw,⛔ 不拿有损读数回写。
198230
- **`list_issue_types` 对本集成 403,而 `issue_write type:` 正常**(读权限缺口):
199231
直接写已知好值(`Bug`/`Feature`/`Task`),写侧报错才是真信号,⛔ 不先探列表定可用性(列表
200232
403 ≠「类型不可用」);非法值是响错还是静默丢弃未实测,写非已知值前先小样验证。
@@ -293,7 +325,9 @@
293325
- **档位不可用时 ⛔ 不凭记忆宣告车道阻塞,逐文件面现推 mandate**:强制档不得因不可用而
294326
降档(那正是降档保险丝要拒的替换),但「本车道 fable 强制」多是过宽的回忆 ——
295327
`dispatch-gates.mjs --tier PATH` 逐路径现推(实测:`SKILL.md``.claude/agents/**` 强制,`scripts/pm/**`
296-
`references/**` 无 ⇒ 全阻塞的三卡 fold 拆成 1 阻 3 可跑);路径线是**下限不是放行**
328+
`references/**` 无 ⇒ 全阻塞的三卡 fold 拆成 1 阻 3 可跑);路径线是**下限不是放行**。⚠️
329+
该脚本只答自己那棵树:objectui 路径回「absent from this tree」,姊妹仓的档位与 clause-② 只能
330+
手推(`check-clause2-carriers.mjs --pair` 在 CCR 容器里读 GitHub 回 403 退 3,2026-09-02 实测)。
297331
- **required checks 的名单是每仓事实**(逐 job 读判定的规则住上面队列段):objectstack 六个 ——
298332
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Dogfood Regression Gate` ·
299333
`Build Core` · `Temporal Conformance (live PG + MySQL)`;`in_progress` 不是过;advisory 门禁

0 commit comments

Comments
 (0)