Skip to content

Commit 555e10e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15693-clause2-no-reading-split
2 parents 46f28ef + 6c08131 commit 555e10e

8 files changed

Lines changed: 949 additions & 62 deletions

File tree

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
115115
| `pm:retriage` | 等分诊改判:与现行 `pm:*` 并存、⛔ 不摘原标;带本标签的 `pm:queue` 卡跳过派发 |
116116
| `finding` | 观察类记录,恒 = 待首次定级;定级即离标;不占队列不进收件箱 |
117117
| `target:<major>` | 发版阻塞:每个 backlog 恰好一个生产者 |
118-
| `pm:epic`(父单) | 子树已委托 epic PM;其它 PM 不把其 sub-issue 当候选 |
118+
| `pm:epic`(父单或 sub-issue) | 已由 epic PM 保留;其它 PM 永不取;⛔ 永不与 `pm:queue` 同挂 |
119119
| `pm:seat` | 座位登记贴:协议载体,不是待分诊的工作 |
120120
| `priority:p0` | 插队:可超 `batch`、破轮次立即派发;⛔ 不豁免同文件串行与认领协议 |
121121
| open PR 引用该单 | 已实现,复核中 |
@@ -296,17 +296,17 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
296296
## Epic 子树车道
297297

298298
- 大开发(父单 + sub-issue 树)整体委托一个专职 PM 会话:`/pm-dispatch epic:#<n>`
299-
- 队列 = 子树 open 未认领 sub-issue,每轮重读不缓存。
300299
- 委托信号成对落地:父单打 `pm:epic` + 正文写会话 ID 与声明的文件领地。
301-
- 其它 PM 的候选获取跳过整棵子树。
302-
- 域座位批次选择时读一次 `label:pm:epic` 索引,避开领地相交。
303-
- epic PM 不同时持有 `domain:*` 座位;认领纪律全套照做。
304-
-`packages/spec` 的 sub-issue 照旧转 spec 座位。
305-
- 衍生问题三分法,判据:不修它,epic 验收过不过得去。
306-
- in-scope ⇒ 挂父单 sub-issue 下轮自动入队。
307-
- 触 spec/公共契约 ⇒ 转 spec 座位队列,epic 侧写 `Blocked-by:`
308-
- 顺带发现 ⇒ 独立立单进修复落地仓 backlog,查重先行。
309-
- ⛔ 不借 sub-issue 通道把未分诊的卡塞进池子。
300+
- epic 立卡挂父单、打 `pm:epic`,⛔ 永不 `pm:queue`;`label:pm:epic` 即父子保留全集,域座位不取。
301+
- 队列 = 子树 open 未认领 sub-issue,每轮重读不缓存;其它 PM 候选获取跳过整棵子树。
302+
- 域座位批次选择读一次 `label:pm:epic` 索引,避开领地相交;epic PM 不兼任 `domain:*` 座位。
303+
- 认领纪律全套照做:先写标签、再 `Claim:`、再全线程重读;认领后 `pm:epic` 留在卡上。
304+
- 离开子树(交车道或转 spec 座位)同一笔标签写摘 `pm:epic``pm:queue`
305+
- `pm:epic` + `pm:queue` 同卡 = 半状态(保留兼移交);后者即已移交,取回走全套认领协议含重读。
306+
- 单车道仓(无 `domain:*`)开卡即认领合法;多车道仓域标签前置照旧,子树标记即保留。
307+
- 衍生问题三分,判据:不修它 epic 验收过不过得去;in-scope ⇒ 挂父单 sub-issue 下轮自动入队。
308+
-`packages/spec`/公共契约(sub-issue 或衍生)⇒ 照旧转 spec 座位队列,epic 侧写 `Blocked-by:`
309+
- 顺带发现 ⇒ 独立立单进修复落地仓,查重先行;⛔ 不借 sub-issue 通道塞未分诊卡进池子。
310310
- 每次分流留一行审计评论;父单维护 checklist 汇总评论,决策仍锚在具体 sub-issue。
311311
- 收尾四步、僵尸回收与领地防撞细则见 `references/seat-post-protocol.md`
312312

.claude/skills/spec-property-retirement/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ walk 看不见的属性就是 ratchet 管不到的属性。
114114

115115
墓碑条目的 note 模板(house style 原文,如 `liveness/action.json`):
116116

117-
> `REMOVED <date> (#<issue>) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-<N> conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); <what to do instead>.`
117+
> `REMOVED <date> — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-<N> conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); <what to do instead>.`
118118
119119
### ⚠ 四张 ratchet 的可见性,按路线是**相反**的 —— 拿错对照就会判错
120120

@@ -151,7 +151,7 @@ ratchet(#2978)会先开火,要求你**有意删除**对应的 manifest key;删
151151
五条惯例,树上每个墓碑都遵守 —— 逐点判定归下面那个 pin 测试,不归这段散文:
152152

153153
1. 反引号包着的**全限定**键打头 —— `` `flow.errorHandling.fallbackNodeId` ``,不是裸尾段。
154-
2. `was removed in @objectstack/spec <version> (#issue[, ADR-XXXX Dn])`
154+
2. `was removed in @objectstack/spec <version> (ADR-XXXX[ Dn])`;`#<n>` 归 schema 注释,`check:doc-authoring` 把门
155155
3. 一个破折号从句讲**它为何惰性或错误** —— "it never had an effect"、"no renderer ever read it"。
156156
4. 祈使句修复:改名写 "use `<replacement>`" + "Rename the key; the value (…) is unchanged.";删除写 "Delete
157157
the key." + **真正生效的机制是什么**

content/docs/protocol/kernel/error-handling.mdx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -346,27 +346,27 @@ deriving it from this page.
346346
- Check user has permission to see resource (row-level security)
347347
- Resource may have been deleted
348348

349-
#### `DUPLICATE_RECORD`
349+
#### `UNIQUE_VIOLATION`
350350
**HTTP Status:** 409
351-
**Meaning:** Resource with unique constraint already exists
351+
**Meaning:** The write collides with a unique constraint — a record already holds that value
352352

353353
**Example:**
354354
```json
355355
{
356-
"success": false,
357-
"error": {
358-
"code": "DUPLICATE_RECORD",
359-
"message": "Account with email 'john@acme.com' already exists",
360-
"details": {
361-
"resource": "account",
362-
"constraint": "unique",
363-
"field": "email",
364-
"value": "john@acme.com"
365-
}
366-
}
356+
"error": "A record with this email already exists",
357+
"code": "UNIQUE_VIOLATION",
358+
"field": "email",
359+
"object": "account"
367360
}
368361
```
369362

363+
The engine throws `DuplicateRecordError`, whose in-process `code` is
364+
`DUPLICATE_RECORD`; the REST door translates that envelope at the boundary, so
365+
every route answers the wire code `UNIQUE_VIOLATION` and the in-process spelling
366+
never crosses HTTP. The refusal is emitted as the flat body shown above, and
367+
`field` is best-effort — see
368+
[HTTP API](/docs/protocol/kernel/http-protocol) for the degraded shape.
369+
370370
**How to fix:**
371371
- Check for existing resource before creating
372372
- Update existing resource instead of creating new one
@@ -947,7 +947,7 @@ Even error responses can be abused:
947947
// Attacker tries to enumerate user emails
948948
for (let i = 0; i < 1000000; i++) {
949949
await register({ email: `user${i}@example.com` });
950-
// Response: "DUPLICATE_RECORD" or "VALIDATION_ERROR"
950+
// Response: "UNIQUE_VIOLATION" or "VALIDATION_ERROR"
951951
}
952952
```
953953

packages/spec/scripts/check-objectui-pin-citations.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,69 @@ const REPO = path.resolve(SPEC, '../..');
127127
const SRC = path.join(SPEC, 'src');
128128
const PIN_FILE = path.join(REPO, '.objectui-sha');
129129

130+
/**
131+
* ## The dispatch-gates declaration — the root-FILE idiom
132+
*
133+
* `scripts/pm/dispatch-gates.mjs` derives the gate families a card must run
134+
* from the paths it touches, and it learns a gate's population by scanning the
135+
* gate's own SOURCE TEXT for path-shaped literals (`extractWatchHints`). Every
136+
* path this gate reads is COMPUTED — `PIN_FILE` from `REPO`, `SRC` from
137+
* `SPEC` — so the extractor found nothing here and this gate had no population
138+
* at all. Measured on `7bf96cfd0`, before this declaration existed:
139+
*
140+
* node scripts/pm/dispatch-gates.mjs --commands .objectui-sha --repo objectstack-ai/objectstack
141+
* -> 12 commands (5 matched by path), 0 of them naming check:objectui-pin-citations
142+
*
143+
* and on the same specimen with it, 13 commands (6 matched by path), the new
144+
* one being `pnpm --filter @objectstack/spec run check:objectui-pin-citations`.
145+
*
146+
* A change set consisting only of the pin file is EXACTLY the class this gate
147+
* exists for — a pin bump is the one event that invalidates an asserting
148+
* citation — and it was the class that never derived it. The measured cost was
149+
* a full CI cycle: a pin bump ran 47 derived commands green and then redded
150+
* here on 8 stale citations, discovered a cycle late.
151+
*
152+
* ⛔ The declaration is the POPULATION, never a hand list of the citing files.
153+
* They are discovered by regex on purpose (`sourceFiles(SRC)` + `scanFile`), so
154+
* a list of today's citers is stale the moment a record is written or moved —
155+
* the same reason `check:merge-driver` refuses a hand list of generator names.
156+
*
157+
* A bare `.objectui-sha` carries no path separator and reaches the extractor as
158+
* a bare word; the trailing `/` + `**` suffix is the sanctioned escape for a
159+
* repo-ROOT file — `collapseHint` reduces it back to the literal filename, so
160+
* it claims the root file and no same-named file inside a directory. Nothing in
161+
* this tree lives under `.objectui-sha/`, so it claims no directory either.
162+
* Measured through `hintCovers`, the sole predicate:
163+
*
164+
* hintCovers('.objectui-sha/**', '.objectui-sha') -> true
165+
* collapseHint('.objectui-sha/**') -> '.objectui-sha'
166+
*
167+
* `check-doc-anchors.mjs` declares `README.md`/`ARCHITECTURE.md` this way and
168+
* `git-merge-regen.mjs` declares `package.json` this way; the sibling package-
169+
* scoped gate `check-llms-txt.ts` proves the idiom reaches a gate invoked
170+
* through a pnpm filter — dispatch-gates resolves the `--filter` script back to
171+
* this file and reads it as `gate source`, exactly as it does for that one.
172+
*
173+
* ⚠️ Provenance, NOT a lookup key. Nothing here is joined with `REPO` and
174+
* stat'd: `PIN_FILE` and `SRC` remain the paths this gate opens. The glob form
175+
* used as a path would resolve to nothing and `existsSync` would drop it
176+
* SILENTLY — the "checked nothing, reported green" disease this gate's own
177+
* vacuous-green guard exists to refuse. `selfTest` pins both halves.
178+
*/
179+
const ROOT_FILE_WATCH_HINTS = ['.objectui-sha/**'];
180+
181+
/**
182+
* The other half of the population: the SUBTREE this gate walks for citations.
183+
*
184+
* A pin bump is not the only way a citation goes stale — writing a new record,
185+
* or moving one, changes what this gate has to say about the SAME pin. The
186+
* glob sits in the FINAL segment, so `hintCovers` reaches every source beneath
187+
* `packages/spec/src` and nothing outside it, which is precisely the scope
188+
* `sourceFiles(SRC)` walks (the header's "everything outside `packages/spec/src`"
189+
* exclusion is the same boundary, stated from the other side).
190+
*/
191+
const ROOT_DIR_WATCH_HINTS = ['packages/spec/src/**'];
192+
130193
const LIST = process.argv.includes('--list');
131194

132195
/** How far past a `.objectui-sha` mention a citation may reach. */
@@ -379,6 +442,51 @@ function selfTest(): never {
379442
check(found > 0, 'discovery: found zero citations in the real tree — the matcher or the scope has moved');
380443
}
381444

445+
// ── the dispatch-gates declaration ──────────────────────────────────────
446+
// Enforcement cannot hold any of this: the declaration is read by ANOTHER
447+
// tool entirely (`extractWatchHints` in scripts/pm/dispatch-gates.mjs, off
448+
// source text), so a wrong, reworded or deleted entry runs perfectly green
449+
// here and shows up only as a dev dispatched on a `.objectui-sha` pin bump
450+
// who is never told this gate reads it — which is the failure that bought
451+
// this declaration, measured as a full CI cycle after 47 green commands.
452+
{
453+
const declared = [...ROOT_FILE_WATCH_HINTS, ...ROOT_DIR_WATCH_HINTS];
454+
const collapse = (h: string): string => h.replace(/\/\*+$/, '');
455+
456+
// The population the declaration CLAIMS is the population this gate READS.
457+
check(
458+
ROOT_FILE_WATCH_HINTS.length === 1 && collapse(ROOT_FILE_WATCH_HINTS[0]!) === path.relative(REPO, PIN_FILE),
459+
`the declared root file is the pin file this gate opens: ${ROOT_FILE_WATCH_HINTS.join(', ')} vs ${path.relative(REPO, PIN_FILE)}`,
460+
);
461+
check(
462+
ROOT_DIR_WATCH_HINTS.length === 1 && collapse(ROOT_DIR_WATCH_HINTS[0]!) === path.relative(REPO, SRC),
463+
`the declared subtree is the tree this gate walks: ${ROOT_DIR_WATCH_HINTS.join(', ')} vs ${path.relative(REPO, SRC)}`,
464+
);
465+
466+
// The separator is what makes `hintCovers` admit the entry at all — a bare
467+
// filename reaches it as a bare word. A reword back to `.objectui-sha`
468+
// leaves every other signal this gate emits green.
469+
check(
470+
declared.every((h) => h.includes('/')),
471+
`every declared entry carries a path separator: ${declared.join(', ')}`,
472+
);
473+
474+
// Provenance, never a lookup key: joined with REPO these resolve to
475+
// nothing, and `existsSync` would drop them SILENTLY.
476+
check(
477+
declared.every((h) => !existsSync(path.join(REPO, h))),
478+
`the declared spellings are provenance, not paths: ${declared.join(', ')}`,
479+
);
480+
481+
// …and the declared subtree really covers the live population, so a
482+
// narrowing of either side cannot pass unnoticed.
483+
const declaredSrc = path.join(REPO, collapse(ROOT_DIR_WATCH_HINTS[0] ?? ''));
484+
check(
485+
sourceFiles(SRC).every((f) => f.startsWith(declaredSrc + path.sep)),
486+
'every source this gate scans lies under the declared subtree',
487+
);
488+
}
489+
382490
if (failures.length) {
383491
for (const f of failures) console.error(`✗ self-test: ${f}`);
384492
console.error(`\ncheck-objectui-pin-citations --self-test: ${failures.length} failure(s).\n`);
@@ -388,7 +496,9 @@ function selfTest(): never {
388496
'✅ self-test: asserting citations are checked against the pin and historical ones are not;\n' +
389497
' a citation wrapped across comment lines is still found (both wrap positions); a sha in\n' +
390498
' neither spelling and a sha missing its backticks both FAIL; a mention naming no sha is\n' +
391-
' skipped; hex-looking prose without a digit is not a sha; a missing pin file throws.',
499+
' skipped; hex-looking prose without a digit is not a sha; a missing pin file throws; and\n' +
500+
' the dispatch-gates watch-hint declaration names the pin file and the subtree this gate\n' +
501+
' really reads, in the separator-carrying spelling `hintCovers` admits.',
392502
);
393503
process.exit(0);
394504
}

0 commit comments

Comments
 (0)