Skip to content

Commit 3a3f5e0

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18068-write-identity-locks
# Conflicts: # .claude/agents/os-dev.md
2 parents a4dfd9d + 137eb00 commit 3a3f5e0

15 files changed

Lines changed: 1703 additions & 56 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`check:duration-unit-keys` refuses a duration key whose JSDoc names a unit its describe does not
6+
7+
The gate read a key's unit from `.describe()` and `.meta({ description })` only.
8+
A duration-shaped `z.number()` whose unit was written solely in the JSDoc block
9+
above it appeared in `--list` as a census row with `[prose: -]` and was never
10+
judged — and its own self-test pins *"a describe declared through
11+
`.meta({ description })` is READ — no exemption by blindness"*, which made the
12+
JSDoc blindness read as deliberate, measured coverage.
13+
14+
**Ruled 2026-09-07 (decision batch #65).** JSDoc is developer commentary, not
15+
governed prose: `.describe()` is what `content/docs/references/**` renders and
16+
what rides into the published dist, and the JSDoc stops at the source file. So
17+
the gate does **not** start reading JSDoc as a unit channel — a unit written
18+
only there still has not satisfied the rule. What it now refuses is the
19+
DIVERGENCE: the JSDoc names a unit and the describe names none (or there is no
20+
describe at all), so the two channels disagree about whether this number's unit
21+
is written anywhere a reader can reach, and the channel that is silent is the
22+
published one. New rule `unit-in-jsdoc-not-in-describe`; the remedy is to move
23+
the unit into the describe, where the existing rule then puts it in the key
24+
name.
25+
26+
⛔ **The JSDoc is read in exactly one direction: to refuse, never to satisfy.**
27+
A duration-shaped key with no unit in *either* channel is still listed and
28+
still not judged (the #14519 shape, unmoved). The new branch tests for a unit
29+
PRESENT in the JSDoc; it never tests for one absent from the describe, which is
30+
what would have made it the option the ruling declined.
31+
32+
**The population this rule adds was remediated before the rule landed.** When
33+
the gate was written it found **21** offenders. Ruling A on #15939 sequenced
34+
those out of this change and into seven per-file cards (#17780–#17786), all
35+
merged: eighteen were renames of published keys, each carrying its own ADR-0087
36+
conversion and `retiredKey()` tombstone, and the other three needed only their
37+
describe corrected. On this tree the gate reads **zero offenders** among **211**
38+
duration-shaped numeric keys across **2482** source files (6 declared `EpochMs`
39+
instants, 11 declared `externalVocabulary` mirrors). ⛔ **No offender was
40+
exempted to reach that zero** — there is no baseline in this gate by ruling, and
41+
none was added.
42+
43+
**One wrongly-recorded reason repaired, comment-only.** The blindness did not
44+
merely miss keys, it produced confident wrong prose about why they were missed:
45+
the retired-key entry for `SandboxConfig:process.timeout` said the neighbouring
46+
`RuntimeConfig.resourceLimits.timeout` was "outside the gate's population", when
47+
that key was inside the census and merely never judged — its unit lived in a
48+
source JSDoc only. That note now records the true reason, and points at the
49+
neighbour's own entry rather than describing a landed rename as pending.
50+
`registry.ts` regenerated to mirror it. The same wrong reason in the
51+
`metrics.test.ts` burn-rate pin was corrected by #17783 when it renamed that
52+
key, so nothing is owed there.
53+
54+
⛔ No published key, accept set, default or runtime behaviour moves.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ model: opus
4747
- 其余 ⛔ 不立卡:观察、死代码、未演练漂移、抛光、风格、文档 nit、命名。
4848
- 它们进 PR `## Acceptance notes`,报告 `out_of_scope_findings` 记 `noted, not filed: …`,席位 ACCEPT 读。
4949
- 写 `noted, not filed` 前先答哪一个 PR 或人会碰到这个文件;答不出就写明「承接者:无」。
50-
- 先搜再立:关键词 + 文件路径扫 open issues;并行 dev 同一小时立的卡只有这一搜能看见。
50+
- 立卡者不查重,只在卡面附 3–5 个查重词;查重归分诊席,⛔ 不扫 open issues、不拉板。
5151
- GitHub 写一律走 REST 代理(`curl` 带环境 `GITHUB_TOKEN`),署名恒 App 的 `claude[bot]`。
5252
- 写预算四笔:`git push`、一次 `POST /pulls`(draft)、`POST /issues/{n}/labels`、`os-dev-report` 评论。
5353
- ⛔ 不用任何 MCP GitHub 写工具:用户账号署名,封号即隐;⛔ 不枚举板面、不宽词搜。

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,14 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
103103
| issue 上的信号 | 含义 |
104104
|---|---|
105105
| open + 队列标签 + 无 assignee | 可派发(同卡带 `pm:retriage` 的除外);`pm:queue` 卡恒无 assignee,有即半态 |
106-
| assignee 已设 | 已认领/在飞,不是你的就永不碰;离手恒走释放 —— 四因 = 改路由、前提证伪、弃飞无接管、跨车道移交,去向 = 新标签态或车道;⛔ 不静默摘 assignee |
106+
| assignee 已设 | 已认领/在飞,不是你的就永不碰;离手恒走释放 —— 三因 = 改路由(限未派发)、前提证伪、弃飞无接管,去向 = 新标签态或车道;在飞卡跟到 MERGED,⛔ 不静默摘 assignee |
107107
| `pm:dispatched` | 已派发(派发评论记轮次),恒带 assignee;与摘 `pm:queue` 同一次标签写入成对落地 |
108108
| `needs-user-decision` | 决定待做:永不派发、除代裁通道外永不代答;维护者的收件箱 |
109109
| `pm:on-hold` | 决定已做且答案是暂不做:不派发不催;仅当带机器可读 `Restart-when:` 行才合法 |
110110
| `pm:blocked` + 正文行 `Blocked-by: #N` | 等上游:选择期跳过,#N 关闭时由解锁扫描放回;工已完、PR 被外部门禁卡住的同用本态 |
111111
| `pm:awaiting-maintainer` | 决定已做,只剩一次 GitHub 之外的人工动作:不派发不催;与其它 pm 状态标签互斥;入态恒带行首 `Maintainer-action:` 行(细则见 `references/state-machine.md`),无行即半态 |
112112
| `pm:blocking` | 有 open 下游依赖者(自 `Blocked-by:` 索引推导的缓存,⛔ 不手工挂);进选择全序 |
113-
| `pm:retriage` | 向分诊提问(改判、跨域 PR 指定车道、改路由、拆卡、裁 dev 报告留下的分叉),异议评论写明所求;与现行 `pm:*` 并存、⛔ 不摘原标;带本标签的 `pm:queue` 卡跳过派发 |
113+
| `pm:retriage` | 向分诊提问(改判、跨域 PR 指定车道、改路由、拆卡、裁 dev 报告留下的分叉),定车道与改路由限未派发卡,异议评论写明所求;与现行 `pm:*` 并存、⛔ 不摘原标;带本标签的 `pm:queue` 卡跳过派发 |
114114
| `finding` | 立卡三类内待首次定级,定级即离标;三类外关 not planned;不占队列不进收件箱 |
115115
| `target:<major>` | 发版阻塞:每个 backlog 恰好一个生产者 |
116116
| `pm:epic`(父单或 sub-issue) | 已由 epic PM 保留;其它 PM 永不取;⛔ 永不与 `pm:queue` 同挂 |

‎.claude/skills/pm-dispatch/references/lanes/ui.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
- 凡触 `packages/spec` 一律转 `domain:spec` 座位,见 SKILL.md 〈多仓协调〉。
2323
- `scripts/pm/**` 单写手恒在 objectstack 侧 ⇒ 本席的工具需求走上游卡带 `Blocked-by:` 回链。
2424
- 受管面五项含仓根发布 `skills/**`,清单、判据与禁令见 objectui AGENTS.md §9 受管面段。
25-
- ⇒ 命中即整 PR 停在 draft 等人合,⛔ 不 ready、不入队、不 auto-merge、不自合、不留批准。
25+
- ⇒ 命中即停 draft;⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地。
2626
- 未命中的 PR 按同节走合并队列落地;changeset 与版本纪律见 objectui AGENTS.md §9。
2727
- 半状态巡查在本仓有载体:定时 workflow 与 sweeper 皆在,自 objectstack 移植。
2828
- 锚 issue 未配置 ⇒ 写锚步骤 skip,发现只落 run summary,绿不等于已交付读锚腿。

‎packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts‎

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,27 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
6363
* ONLY read refusals whose log frames may be withheld here.
6464
*
6565
* `beforeEach` boots a kernel with no datasource and attaches sqlite late, then
66-
* syncs exactly four objects. Every approval decision below therefore probes
67-
* the six authz tables `resolveUserAuthzGrants` reads plus
68-
* `sys_approval_delegation`, which `ApprovalService.lookupActiveDelegation`
69-
* reads best-effort on each decision. All are fail-soft, so the reads are
70-
* EXPECTED — but the driver and the engine each log the fault on the way out.
66+
* syncs its objects. Every approval decision below probes `sys_organization`
67+
* (`ObjectQL.probeInstallOrganizations`) and `sys_approval_delegation`, which
68+
* `ApprovalService.lookupActiveDelegation` reads best-effort on each decision.
69+
* Both are fail-soft, so the reads are EXPECTED — but the driver and the engine
70+
* each log the fault on the way out.
7171
*
72-
* ⛔ MEASURED, not copied from the probers' source: a run of this file at
73-
* `logger: { level: 'info' }` emitted 25 `refused a read on '<t>'` driver lines
74-
* and 25 matching `ERROR Find operation failed` engine frames — sys_user 5 /
75-
* sys_member 4 / sys_position 4 / sys_user_position 4 /
76-
* sys_user_permission_set 4 / sys_organization 2 / sys_approval_delegation 2.
72+
* ⚠️ [#17985] The five authz tables `resolveUserAuthzGrants` reads USED TO BE on
73+
* this list. They are not any more, because this fixture now provisions them
74+
* (see `authzResolverObjects` below) — which is the outcome this channel's own
75+
* contract names: "a table that started resolving means the fixture now
76+
* provisions it". Being fail-soft, their refusal was not just noise: `tryFind`
77+
* answers `[]` for a missing table, so every approval decision here resolved
78+
* its grants from reads that never happened. ⛔ The count fell because the
79+
* reads SUCCEED, not because anything was quietened.
80+
*
81+
* ⛔ MEASURED, not copied from the probers' source. At `225197cdb`-era HEAD,
82+
* before this fixture provisioned anything, a run emitted 25 `refused a read on
83+
* '<t>'` driver lines and 25 matching engine frames — sys_user 5 / sys_member 4
84+
* / sys_position 4 / sys_user_position 4 / sys_user_permission_set 4 /
85+
* sys_organization 2 / sys_approval_delegation 2. The 21 authz ones are gone;
86+
* the remaining two channels are the ones declared below.
7787
*
7888
* ## Why a capture instead of the blanket `silent` this replaces
7989
*
@@ -87,15 +97,102 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
8797
* happen. A capture nobody asserts is a mute.
8898
*/
8999
const EXPECTED_ABSENT_PROBE_TABLES = [
90-
'sys_user',
91-
'sys_member',
92-
'sys_user_position',
93-
'sys_user_permission_set',
94-
'sys_position',
95100
'sys_organization',
96101
'sys_approval_delegation',
97102
] as const;
98103

104+
/**
105+
* ⚠️ [#17985] The authz objects a real approval decision makes core's
106+
* `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
107+
* `@objectstack/plugin-security`; they are spelled LOCALLY here, with only the
108+
* columns that resolver reads, so this fixture adds no dependency edge onto
109+
* either package (the `find-envelope-limb-removal.test.ts` /
110+
* `signup-existing-address-refusal.test.ts` precedent, applied by PR #17982 to
111+
* the two `plugin-auth` fixtures that carried the same defect).
112+
*
113+
* Columns, and why each one is here — every other column of the real objects is
114+
* deliberately absent, because no read on this path touches it:
115+
* `sys_user` id (filter), email (the RLS owner-email fallback)
116+
* `sys_member` user_id / organization_id (filters), role
117+
* `sys_user_position` user_id (filter), position, organization_id
118+
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
119+
* `sys_position` name (filter), id, active (`isRowActive`),
120+
* organization_id (the driver's tenant scope)
121+
*
122+
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
123+
* resolver reaches them only once a `sys_position` row resolves and a
124+
* permission-set id is collected, and these cases seed neither — registering
125+
* them would add fixture surface no read in this file touches. The ADR-0091
126+
* validity columns are absent for the reason `sys_member` lacks them today:
127+
* `isGrantActive` reads an absent bound as unbounded, so declaring them would
128+
* change no verdict here.
129+
*/
130+
const authzResolverObjects = [
131+
{
132+
owner: '@objectstack/plugin-auth',
133+
def: {
134+
name: 'sys_user',
135+
label: 'User',
136+
fields: {
137+
id: { name: 'id', type: 'text' as const, primaryKey: true },
138+
email: { name: 'email', type: 'text' as const },
139+
},
140+
},
141+
},
142+
{
143+
owner: '@objectstack/plugin-auth',
144+
def: {
145+
name: 'sys_member',
146+
label: 'Member',
147+
fields: {
148+
id: { name: 'id', type: 'text' as const, primaryKey: true },
149+
user_id: { name: 'user_id', type: 'text' as const },
150+
organization_id: { name: 'organization_id', type: 'text' as const },
151+
role: { name: 'role', type: 'text' as const },
152+
},
153+
},
154+
},
155+
{
156+
owner: '@objectstack/plugin-security',
157+
def: {
158+
name: 'sys_user_position',
159+
label: 'User Position',
160+
fields: {
161+
id: { name: 'id', type: 'text' as const, primaryKey: true },
162+
user_id: { name: 'user_id', type: 'text' as const },
163+
position: { name: 'position', type: 'text' as const },
164+
organization_id: { name: 'organization_id', type: 'text' as const },
165+
},
166+
},
167+
},
168+
{
169+
owner: '@objectstack/plugin-security',
170+
def: {
171+
name: 'sys_user_permission_set',
172+
label: 'User Permission Set',
173+
fields: {
174+
id: { name: 'id', type: 'text' as const, primaryKey: true },
175+
user_id: { name: 'user_id', type: 'text' as const },
176+
permission_set_id: { name: 'permission_set_id', type: 'text' as const },
177+
organization_id: { name: 'organization_id', type: 'text' as const },
178+
},
179+
},
180+
},
181+
{
182+
owner: '@objectstack/plugin-security',
183+
def: {
184+
name: 'sys_position',
185+
label: 'Position',
186+
fields: {
187+
id: { name: 'id', type: 'text' as const, primaryKey: true },
188+
name: { name: 'name', type: 'text' as const },
189+
active: { name: 'active', type: 'boolean' as const },
190+
organization_id: { name: 'organization_id', type: 'text' as const },
191+
},
192+
},
193+
},
194+
] as const;
195+
99196
/** [#11081] Shared by both kernels this file boots; asserted once in `afterAll`. */
100197
const noise = captureExpectedReadRefusals([...EXPECTED_ABSENT_PROBE_TABLES]);
101198

@@ -207,6 +304,12 @@ describe('an approval decision cascades as the deciding user (#3783)', () => {
207304
for (const def of [opportunity, SysApprovalRequest, SysApprovalAction, SysApprovalApprover]) {
208305
objectql.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
209306
}
307+
// [#17985] The authz resolver's own reads, so a decision resolves its
308+
// grants from reads that HAPPEN rather than from `tryFind`'s empty answer
309+
// for a missing table.
310+
for (const o of authzResolverObjects) {
311+
objectql.registry.registerObject(o.def as any, o.owner);
312+
}
210313
// Real DDL for all four objects — including the three sys_approval_* tables
211314
// the ApprovalService writes through.
212315
await objectql.syncSchemas();

‎packages/services/service-automation/src/paused-run-visibility.test.ts‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,103 @@ describe('#8050 merging a third source: no duplicates, and a stated precedence',
417417
* `where: { status: 'paused' }` scan the DB-backed store actually issues. That
418418
* is where "works against a Map, empty against a table" would hide.
419419
*/
420+
/**
421+
* ⚠️ [#17985] The authz objects a write here makes core's
422+
* `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
423+
* `@objectstack/plugin-security`; they are spelled LOCALLY, with only the
424+
* columns that resolver reads, so this fixture adds no dependency edge onto
425+
* either package (the `find-envelope-limb-removal.test.ts` precedent, applied
426+
* by PR #17982 to the two `plugin-auth` fixtures with the same defect).
427+
*
428+
* Without them the driver REFUSED every one of those reads — measured at
429+
* `2f1a6f696`: 5 `refused a read on` driver lines, one per table.
430+
* `tryFind` classifies a missing table as "not provisioned" and answers `[]`,
431+
* so nothing went red: the assertions below passed over grant reads that never
432+
* happened. ⛔ Registering the tables is what makes those reads SUCCEED; the
433+
* fix is not to quieten the line.
434+
*
435+
* Columns, and why each is here — every other column of the real objects is
436+
* deliberately absent, because no read on this path touches it:
437+
* `sys_user` id (filter), email (the RLS owner-email fallback)
438+
* `sys_member` user_id / organization_id (filters), role
439+
* `sys_user_position` user_id (filter), position, organization_id
440+
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
441+
* `sys_position` name (filter), id, active (`isRowActive`),
442+
* organization_id (the driver's tenant scope)
443+
*
444+
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
445+
* resolver reaches them only once a `sys_position` row resolves and a
446+
* permission-set id is collected, and nothing here seeds either. The ADR-0091
447+
* validity columns are absent for the reason `sys_member` lacks them today:
448+
* `isGrantActive` reads an absent bound as unbounded, so declaring them would
449+
* change no verdict here.
450+
*/
451+
const authzResolverObjects = [
452+
{
453+
owner: '@objectstack/plugin-auth',
454+
def: {
455+
name: 'sys_user',
456+
label: 'User',
457+
fields: {
458+
id: { name: 'id', type: 'text' as const, primaryKey: true },
459+
email: { name: 'email', type: 'text' as const },
460+
},
461+
},
462+
},
463+
{
464+
owner: '@objectstack/plugin-auth',
465+
def: {
466+
name: 'sys_member',
467+
label: 'Member',
468+
fields: {
469+
id: { name: 'id', type: 'text' as const, primaryKey: true },
470+
user_id: { name: 'user_id', type: 'text' as const },
471+
organization_id: { name: 'organization_id', type: 'text' as const },
472+
role: { name: 'role', type: 'text' as const },
473+
},
474+
},
475+
},
476+
{
477+
owner: '@objectstack/plugin-security',
478+
def: {
479+
name: 'sys_user_position',
480+
label: 'User Position',
481+
fields: {
482+
id: { name: 'id', type: 'text' as const, primaryKey: true },
483+
user_id: { name: 'user_id', type: 'text' as const },
484+
position: { name: 'position', type: 'text' as const },
485+
organization_id: { name: 'organization_id', type: 'text' as const },
486+
},
487+
},
488+
},
489+
{
490+
owner: '@objectstack/plugin-security',
491+
def: {
492+
name: 'sys_user_permission_set',
493+
label: 'User Permission Set',
494+
fields: {
495+
id: { name: 'id', type: 'text' as const, primaryKey: true },
496+
user_id: { name: 'user_id', type: 'text' as const },
497+
permission_set_id: { name: 'permission_set_id', type: 'text' as const },
498+
organization_id: { name: 'organization_id', type: 'text' as const },
499+
},
500+
},
501+
},
502+
{
503+
owner: '@objectstack/plugin-security',
504+
def: {
505+
name: 'sys_position',
506+
label: 'Position',
507+
fields: {
508+
id: { name: 'id', type: 'text' as const, primaryKey: true },
509+
name: { name: 'name', type: 'text' as const },
510+
active: { name: 'active', type: 'boolean' as const },
511+
organization_id: { name: 'organization_id', type: 'text' as const },
512+
},
513+
},
514+
},
515+
] as const;
516+
420517
describe('#8050 cold boot over the same sqlite file (full stack)', () => {
421518
let dir: string | undefined;
422519
const kernels: ObjectKernel[] = [];
@@ -444,6 +541,11 @@ describe('#8050 cold boot over the same sqlite file (full stack)', () => {
444541
});
445542
await driver.connect();
446543
ql.registerDriver(driver, true);
544+
// [#17985] The authz resolver's own reads, registered before the sync so
545+
// they are provisioned rather than refused.
546+
for (const o of authzResolverObjects) {
547+
(ql as any).registry.registerObject(o.def as never, o.owner);
548+
}
447549
await ql.syncSchemas();
448550

449551
const automation = kernel.getService<AutomationEngine>('automation');

0 commit comments

Comments
 (0)