Repository navigation
pm-dispatch: owe the isolated contract review on three contract faces; changeset and docs prose move to the seat's ACCEPT - #21192
Conversation
…ract faces; prose faces move to the seat's ACCEPT The at-tier review face list in references/contract-review.md named five surfaces, three of them prose (`.changeset`/CHANGELOG, `content/docs/**`, `apps/docs/**`), so every PR with a releasing changeset owed an isolated CONTRACT_REVIEW_TIER subagent whenever its seat was off-tier. Per the maintainer's ruling A and the director's record settling forks 1/2/3, the isolated review is now owed on exactly three faces: a `Clause-②: yes` declaration, published schema (`packages/spec/src/**`, non-test), and governed rule text. The prose faces are checked sentence by sentence against the diff in the dispatching seat's ACCEPT, at the seat's own tier, and the ACCEPT names the sentences it checked; no second agent is spawned for them. The three sentences that restate the rule (landing-operations.md, execution-duties.md, lanes/ui.md) now point at the three faces. Line counts are unchanged in every file; contract-review.md pays its extra line by dropping the second spelled-out copy of the record-template command. Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
…face trigger reads diff or declaration The prose faces are checked in the dispatching seat's ACCEPT, but the path a seat walks at ACCEPT (execution-duties 〈复核〉 into review-checklist.md) did not say so; the duty lived only in contract-review.md and lanes/ui.md. One line in review-checklist.md 〈范围与 changeset〉 now states it: the three prose faces are checked sentence by sentence against the diff, the ACCEPT names the sentences checked, and no subagent is spawned. The line is paid in the same file by compressing the two-line dev-union head check into one: the dropped words restate the dev's own obligation (run after the last commit, quote the short sha in report and body), which the dev-agent definition carries, and the both-directions rule the section states two lines above. No ⛔ line is touched. contract-review.md's trigger now reads diff or claim declaration, since the first contract face is a Clause-② declaration in the claim, not in the diff. Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21187 body and all four comments (ruling record 5934170950, claim 5934212807, os-dev-report 5934679331 and 5934891734); PR #21192 body, file list and the net diff against ① Derived judgmentsAccept-set and public surface: the diff touches no package, no export, no schema, no script, no workflow; it implies no accept-set change and no public-surface change. The only surface that moves is governed PM rule text (Tier S,
The two lines ruled unchanged (fork 2) are byte-unchanged: Residue: a Line and byte readings at head, taken with ② Semver level
Clause-②: no ③ Boundary flags
Check-runs on the head at 2026-10-01T15:51Z: Implemented-by: VERDICT: PASS Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 36889153084 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…e policies — double accumulation, the PostgreSQL boolean cast and the empty-sum fold, hoisted into core (objectstack-ai#21042) (objectstack-ai#21209) Fixes objectstack-ai#21042 Clause-②: yes (widening) ## What this changes The analytics native-SQL strategy (`NativeSQLStrategy`, the default on a SQL driver) skipped three aggregate policies that `SqlDriver.aggregate()` applies. So one route answered different numbers, or a `500`, depending on which strategy served it. This PR follows the route ruling on objectstack-ai#21042 (comment `5925613967`): the operand policies are hoisted into `@objectstack/core`, beside `AGGREGATE_ANSWER_KIND`, and both faces read them from there. - **`packages/core/src/utils/aggregate-answer.ts`** (`@objectstack/core`, `minor`). It takes: - `AGGREGATE_ACCUMULATION`, moved from `driver-sql` with its docblock (the docblock and the table are byte-identical to the base, apart from the `export` keyword); - `aggregandColumnClass({ type, multiple })`, the one column-class predicate (`'fractional'`, `'integral'`, `'boolean'`, or none); - `POSTGRES_BOOLEAN_AGGREGAND_CAST`, the objectstack-ai#11635 cast, as a `Record` over `AggregationFunction`: `sum` / `avg` / `min` / `max` are cast, the counts never are; - `doubleAccumulationOperand(operand, dialect)`, the double operand with the dialect as a parameter; - `aggregandOperandSql(func, columnClass, dialect, operand)`, the one composition both faces emit (the cast inside, the double operand around it). No `index.ts` line was added: the module is already exported. - **`packages/drivers/driver-sql/src/sql-driver.ts`** (`patch`), in the ruled regions only. The `AGGREGATE_ACCUMULATION` table becomes a pointer plus an import. `isFractionalNumericType` is deleted. The two registry fills fill `fractionalNumericFields` through the predicate. `accumulatesInDouble` / `doubleAccumulationOperand` are replaced by `aggregandColumnClassOf`, which maps the driver's registries onto the predicate's classes. In `aggregate()`, the private boolean-cast condition and the accumulation call become one `aggregandOperandSql(funcName, class, this.dialectName, '??')`. - **`packages/services/service-analytics/src/strategies/native-sql-strategy.ts`** (`patch`), in two places. - **`resolveMeasureSql`** wraps the column it hands `AGGREGATE_SQL` / `CONDITIONAL_AGGREGATE_SQL` in `aggregandOperandSql`. The column class comes from the declaration the host already relays (`declaredValueShape`), on the object the column lives on (`columnObjectOf`, the one hop resolver). The dialect comes from `sqlDialect`, which the strategy already reads. - **The `execute` shaping point** that PR objectstack-ai#21040 added now folds a `null` measure answer to `emptyGroupValueFor(measure.type)` (`@objectstack/spec`). It does this for every measure, measure-scoped ones included, before the number presenter, in `driver-sql`'s order. The dataset door's `DatasetExecutor` fill stays, and it is idempotent on a folded row. - The one line outside those two regions is the `generateSql` call site, which now passes `ctx` to `resolveMeasureSql`. - No native copy of any policy, and no runtime hook asks the driver: the rejected (C) route was not taken. `canHandle`, `buildFieldMeta`, the hop-object sites, the filter / text-match rendering, `analytics-service.ts` and `field-read-admission.ts` are untouched. ## The card's table, before and after Measured through `AnalyticsService.query` (the cube door, which `POST /api/v1/analytics/query` relays verbatim) and `AnalyticsService.queryDataset` (the dataset door), on `AnalyticsServicePlugin` over a real ObjectQL engine and `SqlDriver`. **Native** is the plugin's own composition (`NativeSQLStrategy` answered, with one raw statement and no engine aggregate). **ObjectQL** is the same composition narrowed to `engine.aggregate`. "Before" is the strategy file at the base `d34aa58a2a`; "after" is this branch at `61aab5013a`. Neither merge since then touches the aggregate code paths, and the pins below are green at `ef1f9d8484`. Fixture: - group `f`: `frac` (a `number` column) holds 0.1 and 0.2, and `flag` holds true and false; - group `i`: `stars` (a `rating` column) holds seven 1s and two 2s, and `flag` holds 7 trues and 2 falses; - group `n`: every aggregand is NULL in all three rows. The measure-scoped measures filter on `tag = x`, which only group `f` holds. **PostgreSQL 16.13** (a private local server; the ObjectQL column is the same before and after): | measure | group | door | native before | native after | ObjectQL | |:--|:--|:--|:--|:--|:--| | `sum(frac)` | f | cube, dataset | `0.3` | `0.30000000000000004` | `0.30000000000000004` | | `avg(frac)` | f | cube, dataset | `0.15` | `0.15000000000000002` | `0.15000000000000002` | | `avg(stars)`, an integer column | i | cube, dataset | `1.222222222222222` | `1.2222222222222223` | `1.2222222222222223` | | `sum(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `7` | `7` | | `avg(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `0.7777777777777778` | `0.7777777777777778` | | `min(flag)` / `max(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `0` / `1` | `0` / `1` | | `sum(frac)`, all-NULL group | n | cube | `null` | `0` | `0` | | `sum(frac)`, all-NULL group | n | dataset | `0` (executor fill) | `0` | `0` | | `sum(flag)`, all-NULL group | n | cube | `500 DATABASE_ERROR` | `0` | `0` | | `avg(frac)`, all-NULL group | n | cube, dataset | `null` | `null` | `null` | | measure-scoped `sum(frac)`, no admitted row | i | cube | `null` | `0` | `0` | | measure-scoped `sum(frac)`, no admitted row | i | dataset | `0` (executor fill) | `0` | `0` | | measure-scoped `avg(frac)`, no admitted row | i | cube | `null` | `null` | `null` | | `count` control | n | cube, dataset | `3` | `3` | `3` | | measure-scoped `count` control | i | cube, dataset | `0` | `0` | `0` | **SQLite** (better-sqlite3): accumulation and the boolean answers already agreed on every face (`0.30000000000000004`, `0.15000000000000002`, `1.2222222222222223`, `7`, `0.7777777777777778`, `0` / `1`). The fold is the policy that diverged there: | measure | group | door | native before | native after | ObjectQL | |:--|:--|:--|:--|:--|:--| | `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | cube | `null` | `0` | `0` | | `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | dataset | `0` (executor fill) | `0` | `0` | | measure-scoped `sum(frac)`, no admitted row | i, n | cube | `null` | `0` | `0` | | measure-scoped `sum(frac)`, no admitted row | i, n | dataset | `0` (executor fill) | `0` | `0` | After the fix, the native and ObjectQL faces **differ in 0 of 144 cells** (2 drivers × 2 doors × 12 measures × 3 groups). **MySQL is NOT MEASURED**: there is no MySQL server in this container. The MySQL operand text is pinned offline: by `core`'s `aggregate-answer.test.ts`, and by the `driver-sql` move proof for the driver's own statements. ## The move proof `driver-sql`'s aggregate statements were dumped at the base, before any consumer changed. The dump covered `SqlDriver.aggregate()` for every function (`count`, `count_distinct`, `sum`, `avg`, `min`, `max`, and `count(*)`), aliased and unaliased, over 23 columns: every fractional, integral and boolean type, the `float` / `integer` / `int` aliases, multi-valued and untyped columns, and text / date / lookup / formula. It ran on SQLite, PostgreSQL and MySQL, through both registration paths (`registerObjectMetadata` and `registerExternalObject`), offline (knex `toSQL()`). The policies were then hoisted, `driver-sql` was switched to the imports, and the same dump was run again: - base dump: 1668 entries, 0 errors, md5 `8eee668372a28a7568f3eb1cc5a2bc9b`; - after dump (at `bc8aa0cc2f`): 1668 entries, md5 `8eee668372a28a7568f3eb1cc5a2bc9b`. `cmp` printed nothing: the two dumps are **byte-identical**. `sql-driver.ts` and `aggregate-answer.ts` are unchanged between `bc8aa0cc2f` and `61aab5013a`. The committed move-proof pin, `packages/drivers/driver-sql/src/sql-driver-21042-aggregate-policy-move.test.ts`, holds the captured expressions for one column of each class, on each dialect and through each registration path. It passed at the base (`192fc0010b`: 54 / 54) and passes after (54 / 54). ## Pins (committed red first, then the fix) | file | at the pins commit (`192fc0010b`, base code) | after | |:--|:--|:--| | `core` `aggregate-answer.test.ts` | 16 red (the exports did not exist) | 22 / 22 | | `service-analytics` `native-sql-aggregate-policies.test.ts` (each measure on both faces at both doors, against the engine's arithmetic; SQLite and live PostgreSQL cells) | SQLite: the cube-door folds red. PostgreSQL: accumulation, boolean `500`, folds red | 49 / 49 | | `service-analytics` `cube-measure-field-type-door.test.ts`, **the lifted skip** | PostgreSQL native `max(boolean)` red (`500`) | 23 / 23 | | `rest` `analytics-dataset-aggregate-policies-door.test.ts` (the route, both strategies) | PostgreSQL: 7 red (accumulation and booleans). SQLite green (that door already folded) | 19 / 19 | | `driver-sql` move proof | 54 / 54 | 54 / 54 | **The lifted skip:** `it.skipIf(cell.id === 'pg' && face === 'native')` in `cube-measure-field-type-door.test.ts` (from PR objectstack-ai#21128) is gone. Its comment now says why the cell runs on every cell and face. The PostgreSQL native `max_flag` cell answers `1`. `native-sql-measure-number-presentation.test.ts` gets a comment-only edit: its header said the native statement does not carry objectstack-ai#20387's accumulation, and it now points at the new pin. ## Ablations There was one ablation per policy, each predicted in writing before it ran. Each mutation was planted through `scripts/ablation-replace.mjs`, which checks that the anchor hit and that the blob changed. Each mutation was confirmed in the built `dist/` (`ablation-dist-preflight.mjs`: marker present). Each restore ran by absolute path (`git checkout HEAD`), and the file's blob was proven equal to its `HEAD` blob with `git diff HEAD` empty. After each restore the package was rebuilt, and the marker was proven absent from `dist/` with the tree clean. Every prediction held exactly. | ablation | mutation | predicted red | observed red | |:--|:--|:--|:--| | A1 accumulation | `core` `accumulatesInDouble`'s dialect gate never admits PostgreSQL or MySQL | `core` 3; `driver-sql` move proof 24 (pg and mysql × both fills × the six numeric / boolean columns); `service-analytics` 10, PostgreSQL only (both doors × `sum` / `avg(frac)`, `avg(stars)`, measure-scoped `sum` / `avg`); `rest` 3, PostgreSQL only | the same 3 / 24 / 10 / 3; SQLite cells green; `avg(flag)` green as predicted | | A2 boolean cast | `core` `aggregandOperandSql` never casts | `core` 1; move proof 4 (pg × both fills × `boolean` / `toggle`); `service-analytics` 8, PostgreSQL only; the lifted cell, PostgreSQL native and ObjectQL, 2; `rest` 4, PostgreSQL only | the same 1 / 4 / 8 / 2 / 4 | | A3 fold | the native shaping point never folds | `service-analytics` 8, cube door only (SQLite and PostgreSQL × the three all-NULL `sum`s and the measure-scoped `sum`); everything else green, the `rest` dataset-door route included, because the executor fill folds there | the same 8; `rest` 19 / 19 green | A1 and A2 show one policy reaching both faces. Each one turned `driver-sql`'s own statements red. Under A2 the ObjectQL face's `max(boolean)` cell failed too, with the driver's refusal. Under A1 the ObjectQL face answered the same exact decimal as the native face for the plain measures: the failing assertion was the engine's number, while native and ObjectQL still agreed. A **reverse type check** also ran. Passing a dialect the new type rejects (`'oracle'`) to `aggregandOperandSql` turned `service-analytics`' typecheck red (`TS2345 ... not assignable to parameter of type 'AggregandSqlDialect'`), which shows the rebuilt `core` `.d.ts` was read. The file was restored byte-identical. ## Verification (at `ef1f9d8484`, after merging `origin/main` at `cb45469e67`, which carries PR objectstack-ai#21170 and PR objectstack-ai#21173) Everything below ran as one locked script, at `ef1f9d8484`, with each exit code captured before any pipe. The live PostgreSQL 16.13 server ran at `timezone = Asia/Shanghai`, and the `driver-sql` suite ran under `TZ=America/New_York`, which are its own non-vacuity preconditions. - **Refresh after the merge:** `pnpm turbo run build --filter='!@objectstack/docs' --concurrency=1` exit 0, and `pnpm --filter @objectstack/spec check:generated` exit 0. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 67 commands from the real diff (10 paths). All 67 exited 0. Reconciliation with `--ran` and the recorded exit codes printed: `Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.` - **Typecheck:** `pnpm --filter … typecheck` exit 0 for `@objectstack/core`, `@objectstack/driver-sql`, `@objectstack/service-analytics` and `@objectstack/rest`. - **Tests, every vitest project of every touched package** (`vitest run --maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set so the live cells ran): | package / project | files | tests | |:--|:--|:--| | `core` local | 74 passed | 2120 passed | | `core` repo | 3 passed | 48 passed | | `driver-sql` | 216 passed, 3 skipped | 4300 passed, 96 skipped | | `service-analytics` | 161 passed | 3765 passed | | `rest` local | 256 passed | 5027 passed, 127 skipped | | `rest` repo | 5 passed | 179 passed, 1 skipped | - **The five pin files, run explicitly:** move proof 54 / 54, `core` 22 / 22, policies 49 / 49 (24 SQLite + 24 PostgreSQL + the oracle), field-type door 23 / 23, `rest` route 19 / 19 (9 SQLite + 9 PostgreSQL + the oracle). - **Lint, narrowed and proven:** `eslint --no-inline-config --format json` over the 9 changed code files answered 9 results, 0 errors and 0 warnings. The population is read from eslint's own config: `ESLint.isPathIgnored` answers `false` for each of the 9. The narrowing excludes nothing that could move, because `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot change the verdict on an untouched file. The repo-wide `pnpm lint` is CI's. - **The `driver-sql` live preconditions:** the first gate run used a private server at UTC, and the suite's four timezone non-vacuity cells failed by design (`… start it with timezone=Asia/Shanghai`). With the server at `Asia/Shanghai` and the process at `America/New_York` the suite is green, as listed above. - **`main` after the final merge:** `origin/main` moved 4 commits past `cb45469e67` before this PR opened (objectstack-ai#21149, objectstack-ai#21188, objectstack-ai#21195, objectstack-ai#21192). None of them touches `core`, `driver-sql`, `service-analytics` or the analytics `rest` tests. The one `packages/spec` file in the analytics area, `ui/dataset.zod.ts`, changes a comment only. They are not merged here; CI runs on the merge ref. ## Acceptance notes - **MySQL is NOT MEASURED** (no server in this container). The MySQL operand text is pinned offline in `core` and in the `driver-sql` move proof. - **The live PostgreSQL cells are not run in CI.** No CI step sets `OS_TEST_POSTGRES_URL` for `service-analytics` or `rest`. The cells above ran against a private PostgreSQL 16.13 started for this run and removed afterwards. In CI the SQLite cells run, and so do the offline `core` / move-proof pins. - **Phase 0's note on the dataset door, which is no divergence:** a measure-scoped `avg` is **absent** from a row its supplementary query reported no row for. That is `x_avg_frac` for groups `i` and `n`, on both strategies and before and after. It is not `null`. The new service pin holds this cell only to "both faces agree", not to a value. Relatedly, a dataset-door selection made only of measure-scoped measures reports only the groups their filter admits, so the pin asks each one beside the base count. - **Residual, as stated in the ruling:** a host that relays no field declarations (`declaredValueShape`), or names no SQL dialect, gets no column class or no policy. It keeps the native arithmetic it had, and a PostgreSQL boolean `sum` there still answers `500`. The plugin's own composition wires both. - **How `driver-sql` reads the class:** it reads its own registries rather than calling the predicate per column. `fractionalNumericFields` is filled by the predicate. `booleanFields` and `numericFields` are filled by the driver's coercion rules, whose populations equal the predicate's `'boolean'` and `'fractional'` ∪ `'integral'` classes. The move proof pins that equality per column class. Asking the predicate per column through the driver's `valueShapeFields` would retire `fractionalNumericFields`, but its declaration and shard-alias regions are outside the ruled surface, so this PR does not do it. - **The scan-order residual is unchanged.** On PostgreSQL and MySQL the double sums are added without compensation (`AGGREGATE_ACCUMULATION`'s docblock), so three or more fractions can still differ in the last place from SQLite and the rows path. The pins use two addends. - objectstack-ai#21129 (the presenter for a relationship-path `min` / `max`) is not addressed here. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21187
Clause-②: no
What this changes
references/contract-review.mdnamed five faces that owe an at-tier contract review, and three of them were prose:.changeset/ CHANGELOG,content/docs/**andapps/docs/**. The eligibility line inreferences/execution-duties.mdthen forbids an off-tier seat from reviewing in-seat. Together, every PR carrying a releasing changeset owed an isolatedCONTRACT_REVIEW_TIERsubagent whenever its seat was not spec or skills.This PR implements the ruling. The isolated at-tier review is now owed on exactly three contract faces:
Clause-②: yesdeclaration;packages/spec/src/**, non-test);.claude/**included; Tier S as today).The prose faces (
.changeset/ CHANGELOG,content/docs/**,apps/docs/**) are checked sentence by sentence against the diff in the dispatching seat's ACCEPT, at the seat's own tier, and the ACCEPT names the sentences it checked. No second agent is spawned for them.The change is text-only. No script or workflow implements the face list (search below), and the queue's contract-review gate reads only the two Clause-② legs (
landing-operations.md「条款②入队闸门」), which this PR does not touch.Rulings implemented
content/docs/**andapps/docs/**no longer owe an isolated review.priority:p1, landed this round.Every sentence changed (before, then after)
All five files keep their line counts. Anchors are content, not line numbers. Sentences 1 to 6 landed in
4bde1f71a; sentences 7 and 8 landed in27e976d73(review round 1).1.
references/contract-review.md, the face list (two lines become three):The line breaks fall at a source space (
governed 规则文本,散文面 ⛔), so the rendered text is unchanged by them.2.
references/contract-review.md, 「五面皆不碰」 (renamed; meaning kept for the three-face set):3.
references/contract-review.md, the independence-pair line (this is the line payment, see "Line budget"):4.
references/landing-operations.md, 「交付后复核按面欠」:5.
references/execution-duties.md, 「条款②自审资格按车道」:spec 与 skillsbecamespec/skillsonly to keep the line within the ratchet's 120-byte cap (118 bytes).6.
references/lanes/ui.md, 「条款②复核本席适用」:The ui seat is the lane whose changeset-only PRs started this card, so its line says where the prose check now sits.
7.
references/review-checklist.md〈范围与 changeset〉, the ACCEPT-side prose check (new line, review round 1). The path a seat walks at ACCEPT isexecution-duties.md〈复核〉 into this checklist, so the duty is stated here as well as incontract-review.md:The line is paid in the same file by compressing the two-line dev-union head check into one. The dropped words restate the dev's own obligation, which
.claude/agents/os-dev.mdcarries (run the union after the final commit, quote the short sha in the report and the body), and the both-directions rule that the same section states two lines above (「不一致的双向都不入账」). No ⛔ line is touched.8.
references/contract-review.md, the trigger (review round 1). The first contract face is aClause-②: yesdeclaration, which lives in the claim comment, not in the diff:Deliberately unchanged (ruling point 2)
These two lines are not prose review. Each is the record that lets a red or exceptional gate pass, so they stay exactly as they are:
references/execution-duties.md: 「改到已有.changeset/*.md的 PR ⛔ 不打skip-changeset;确认取同 head 达档 PASS 或维护者原话」references/landing-operations.md: 「DELIBERATE CORRECTION 红(check-empty-changeset):同 head 达档复核 PASS 记录即确认」Both lines grep exactly once at head, and neither is an added or removed line in the diff (the second shows up only as hunk context).
Where the rule lives: the search
Base
2c1cef334. Searched.claude/,AGENTS.mdandCLAUDE.md:五面|五处|复核面|按面|四面|三面: the rule hits only the anchors changed above. The other hits are unrelated:execution-duties.md「三面都查」 (premise check),lanes/devx.mdandlanes/spec.md(spec lane slicing), andplatform-readings.md「按面不按路由」 (footer readings).达档|CONTRACT_REVIEW_TIER|at-tier|契约复[审核]: none of these ties prose to an at-tier record.dispatch-runbook.md「无主阻塞项的契约面(含packages/spec)」 names contract faces only, andSKILL.md(the skills-hunk, Tier S and governed-definition lines) and.claude/agents/os-dev.md(Tier S landing) are about governed text.散文,content/docs|apps/docs,contract[- ]review|prose, andchangeseton the same line as复核|review|达档|复审: no further site.review-checklist.md,SKILL.mdand.claude/agents/os-dev.mdcarry no restatement of the face list, so nothing outsidereferences/**needed an edit.scripts/and.github/mention both a contract-review term and one of.changeset/content/docs/apps/docs. In each of them, 0 lines put the two within 4 lines of each other. No script reads the changed sentences. The one script that readscontract-review.mdas text (thedispatch-gates.mjsself-test: theCONTRACT_REVIEW_TIERconstant is present and the retired fuse text is absent) is unaffected, and its gate is green.0a3e540, cloud90b1af0, hotcrmfb408a7): 0 hits for五面|复核面|五处|按面欠|CONTRACT_REVIEW_TIERoutside CHANGELOGs.Line budget
references/contract-review.mdreferences/landing-operations.mdreferences/execution-duties.mdreferences/lanes/ui.mdreferences/review-checklist.mdThe new text in
contract-review.mdneeds one line more than the old face list. That line is paid by deleting content in the same file: the second spelled-out copy ofnode scripts/pm/record-recognisers.mjs --template(sentence 3). The full command is still spelled once, eleven lines above. No line was re-wrapped to free space.review-checklist.mdpays for its new line the same way (sentence 7). Every added line is at most 119 bytes.Local gates
Run at head
27e976d73, after the review-round-1 commit. The list was derived bynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(18 commands, no paths passed;check:required-contextsjoined the list withreview-checklist.md). The--ranreconciliation over the exit-coded record printed: 「18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3)」.node scripts/check-closing-keyword-parity.mjs(and--self-test)node scripts/check-comment-mask-corpus.mjsnode scripts/pm/check-harness-current.mjs --self-testpnpm --filter @objectstack/lint run check:doc-formula-expressionspnpm check:agent-test-spellingpnpm check:cross-package-test-inputspnpm check:doc-authoringpnpm check:driver-memory-censuspnpm check:gitlink-declaredpnpm check:nul-bytespnpm check:pm-governed-mergespnpm check:pm-skill-id-lintpnpm check:pm-skill-ratchetpnpm check:refd-timer-probepnpm check:required-contextspnpm check:skill-frame-syncpnpm check:watch-hint-literalNote:
check:doc-formula-expressionsfirst exited 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintwere not built in the fresh worktree), so that first run measured nothing. Afterpnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2under the shared verify lock (VERDICT command-exit 0), it ran with exit 0 and a 58-case self-test. The same happened at27e976d73in the recreated worktree: first run exit 3, rebuild (a turbo cache hit), rerun exit 0. A raw control-byte scan over the four files found nothing. The full farm is CI's.No changeset: the diff publishes nothing (
.claude/**only), so the PR takesskip-changeset.Acceptance notes
execution-duties.md〈复核〉 was not touched again, because its 「核对清单结论」 already carries the checklist into the ACCEPT..github/workflows/changeset-presence.yml,scripts/render-changeset-claims-comment.mjsthere), and this repository's tree has 0 hits for it. On objectstack, the ACCEPT check is the only prose control before a release consumes the changeset. Noted, not filed; no carrier..claude/**only). The dev does not ready, queue or arm this PR. The owning seat lands it after an isolatedCONTRACT_REVIEW_TIERreview of record PASS on the current head, with every check green.维护者速读(草稿)
改了什么:PM 规则里「哪些改动必须另起一个顶档 agent 做隔离契约复核」的清单,从五处缩到三处契约面:
Clause-②: yes申报、packages/spec/src/**(非测试)、受管规则文本(含.claude/**)。changeset/CHANGELOG 与content/docs/**、apps/docs/**的散文改由派发席在 ACCEPT 时按本席档逐句对照 diff 核对,并在 ACCEPT 里点名核过的句子,不再另起 agent。另在 ACCEPT 复核清单(review-checklist.md)补一行,写明散文面逐句核、ACCEPT 点名、不起子代理。只动.claude/skills/pm-dispatch/references/下 5 个文件共 11 行,每个文件行数不变。为什么改:卡片实测 objectui 最近 40 个合并 PR 中有 30 个带发布 changeset,按旧规则每个都要一次隔离顶档复核;单次约 23.6 万 token,那一例只多抓出一句散文偏松。您已裁 A(「我已经同意了,不需要严格的规则」),并对三个分叉答「同意,你负责派发。」。
风险与代价(含回滚):契约面(条款②申报、spec、受管面)照旧走隔离顶档复核;入队闸门只读条款②两肢,本 PR 不碰。两条「同 head 达档 PASS 即确认」的例外条款原样保留。代价是散文核对的质量落在派发席本档,靠 ACCEPT 逐句点名留痕。回滚 = revert 本 PR,不牵连任何脚本或数据。
席位意见:
你要做的:无需操作。本 PR 属 Tier S,由席位在隔离达档复核 PASS 后落地;对措辞有异议请在本 PR 留言。
Generated by Claude Code