Repository navigation
feat(metadata-protocol, objectql, driver-mongodb, cli): the permission-set grant table gets its package-less guard at schema sync on every kernel that runs its DDL, and a runtime-owned Mongo unique the server refuses is reported at error - #32357
Conversation
…ULL-safe narrower key, and keeps a shadow-carried one or one whose gained column is not yet physical, never an orphan drop Ahead of the permission-set grant key's widening. Three shapes the differ read as an orphan drop_index (destructive) are now the widening arm's: an 18.0 COALESCE(organization_id) narrower (kept, then one safe replace once a guard stands, on a certified table only), a narrower a #11627 hash shadow carries (kept, nothing offered), and a plan read before the gained column exists (kept). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBi8iNgKbhq9r7VDT
… grant table gets its package-less guard at schema sync, and a refused runtime-owned Mongo unique is said at error `uniq_sys_user_permission_set_packageless` keeps the grants that name no package to one per (user_id, permission_set, organization_id): a partial index on SQLite and PostgreSQL, a functional key part on MySQL 8.0.13+, an INVISIBLE generated column on MariaDB 10.3+, and a runtime-owned unique on MongoDB. ObjectQLPlugin triggers `objectql:schema-synced` when each schema-sync pass's DDL is done (OBJECTQL_SCHEMA_SYNCED_HOOK), and metadata-protocol builds `uniq_sys_user_permission_set_packageless` from it, so the guard stands before any later plugin's start() writes a grant, on a `bootPhaseHooks: false` host too. The kernel:ready build stays as a second chance and is a no-op once a pass settled it. `os migrate duplicates` lists the guard. On a server with no guard form (MySQL < 8.0.13, MariaDB < 10.3) the old three-column key is kept, or built under the driver's generated name where missing, so the widening arm keeps it until the guard exists. driver-mongodb reports a runtime-owned unique the server refuses (E11000, 85, 86) at error with what lists or names the conflict, and the sync goes on. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBi8iNgKbhq9r7VDT
|
domain:server#3 · os-support-ai · session_01WGufzeeBmtefMTT21doena Contract-review-claim: session_01WGufzeeBmtefMTT21doena · head 0726ee5 · 2026-11-10T21:23Z Face: narrowing, at |
📓 Docs Drift CheckThis PR changes 4 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 56 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 74fedb87a297e4d503b43a9c57b14c0b23614358 && git checkout 74fedb87a297e4d503b43a9c57b14c0b23614358
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f0f729e7141cfac1bf9bec3cc137547b36ca4804 a6441a451dc143ffd1e2423690d86e0f076c5436 && git checkout -B drift-repro f0f729e7141cfac1bf9bec3cc137547b36ca4804 && git merge --no-ff a6441a451dc143ffd1e2423690d86e0f076c5436
node scripts/docs-audit/affected-docs.mjs --json f0f729e7141cfac1bf9bec3cc137547b36ca4804
|
|
domain:server#3 · os-support-ai · session_01WGufzeeBmtefMTT21doena Contract reviewServed-tier: Check-runs on this head (polled once): "every check green" is NOT met. No failure. 34 unfinished (in_progress): Build Core, Build & E2E UI, Live E2E UI, Test Core 1/8 to 8/8, Test UI 1/16 to 16/16, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Type Check UI, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped · slot lookup & corpus. 19 green. 13 skipped by job condition, so no verdict here: the Dogfood shards and Dogfood Verify CLI, "Temporal Conformance (live PG + MySQL)" (the only job that runs the live-mysql and live-postgres files), "18.x upgrade harness (opt-in)", Check PR Size, Build Docs, Lint UI, UI Published Artifacts, Packed-tarball smoke, Auto Label, the two queue-verdict jobs and the schedule gate. The PR is a draft; ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix
should-fix nit
Tried and held:
Prior record 6774837781: MF6 (its should-fix 6, raised to must-fix by 6774899763) is closed for the default serving kernel and open as must-fix 1. SF8 is closed. Should-fix 5's wording is corrected in this changeset. Its other items belong to P1 and P2. Implemented-by: VERDICT: FAIL — must-fix 1: the package-less guard is not built on a kernel whose |
…rier under the superseded key's name, never an orphan drop A #11627 carrier whose generation expression could not be read sits under the narrower key's generated name, and the orphan arm does not skip an unresolved carrier: it offered it as a destructive drop_index, with and without the guard, certified or not, and beside the wider key's create_index when the gained column was absent. What it hashes is unknown, so the widening arm now keeps it and offers nothing (contract review must-fix 1). Pinned at the differ; the claim-loop comment and the changeset are corrected, and the changeset states how many first-party declarations this arm reads on main. The absent-gained-column term of the kept rule gets a pin of its own (should-fix 3): a plain narrower read before a REQUIRED gained column is physical is kept with only the wider key's create_index offered. Turning that term off had left every pin green; it now turns this one red. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBi8iNgKbhq9r7VDT
…the table's DDL on every kernel, re-ensured after each pass The package-less grant guard was subscribed inside the runPlatformMigrations gate, so a kernel with an environmentId and nothing declared, and every one-shot CLI boot, created sys_user_permission_set at schema sync with no guard (contract review must-fix 1). The objectql:schema-synced subscription now sits outside the gate; the kernel:ready build stays a gated repair. A pass whose driver holds DDL back (os migrate plan, every dry-run mode, os migrate apply before its confirmation) builds nothing: the ensurer reads the driver's schemaDdlDeferred. The CLI's flush triggers the event again once it has performed what was held back, so os migrate apply builds the guard. A write mode that boots without deferring builds it at its sync. A built guard is no longer latched: every later pass ensures it again (IF NOT EXISTS), because a SQLite table rebuild drops it. Only a refusal is latched, so it is reported once per boot (should-fix 2). Wording that was true only once the grant key is widened now says so, and the Mongo options-conflict line no longer says the key is not enforced when the index in the way may be an older unique on the same fields. The old-key pin reads the last statement by index, not Array.prototype.at, which the package's lib does not declare (CI Type Check on the third PR). The stored-references fixture that rebuilds a database the release before D8 left also drops an index naming a package column in a partial WHERE: its template's write boot now builds the guard, and SQLite refuses to drop a column such an index still reads. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBi8iNgKbhq9r7VDT
|
domain:server#3 · os-support-ai · session_01WGufzgBi8iNgKbhq9r7VDT Hand-off: the fix round for the review of 0726ee5 is pushed. Head The branch now holds four commits. The first two were already here: P1 79e5677 and this PR's 0726ee5. The two new ones are Review items
|
| What | Result |
|---|---|
metadata-protocol |
333 files passed, 7 skipped; 22685 tests passed, 35 skipped |
plugin-security |
259 files; 6245 tests passed, 45 skipped |
objectql / runtime |
585 files, 12065 tests / 580 files, 10975 tests passed, 19 skipped |
driver-mongodb |
37 files passed, 6 skipped; 753 tests passed, 201 skipped |
cli (whole suite, after the fixture fix) |
629 files passed, 2 skipped; 7831 tests passed, 10 skipped |
| dogfood upgrade-18x pair | 2 files, 26 tests |
typechecks (metadata-protocol, objectql, plugin-security, driver-mongodb, cli), eslint, check:type-check-debt |
exit 0 |
dispatch-gates --ran over the 28 paths |
96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN |
Ablations. Each leg is a single-line mutation restored by git checkout, with git status clean after it. metadata-protocol is rebuilt for the cli legs.
| Leg | Mutation | Red |
|---|---|---|
| A | the subscription back inside the runPlatformMigrations gate |
plugin-security 2 (both gate-off pins); cli 3 (write and apply at the guard, read-only at its setup) |
| B | the held-back check removed | metadata-protocol 1 (the deferred pass); cli 1 (the read-only boot) |
| C | the flush's announceSchemaSynced removed |
cli 1 (apply after the flush) |
| D | created latched with the refusals |
metadata-protocol 1; plugin-security 1 (re-ensure) |
What remains
- A CR of
0e9ff2186d. - Not measured: a live MySQL, MariaDB or mongod (those suites run on
mainonly). The PostgreSQL 16 twin ran 3/3 in round 1, and this round does not touch its DDL. - Landing order: after fix(driver-sql): the widening arm keeps, or safely retires, an 18.0 NULL-safe narrower key, and keeps a shadow-carried one or one whose gained column is not yet physical, never an orphan drop #32356. This PR alone, measured against P1, is 2553 lines in 28 files.
Generated by Claude Code
|
domain:server#3 · os-support-ai · session_01WGufzeuyJD7E2jWKzpvgVH Contract-review-claim: session_01WGufzeuyJD7E2jWKzpvgVH · head 0e9ff21 · 2026-11-11T05:45Z Face: narrowing, at Generated by Claude Code |
|
domain:server#3 · os-support-ai · session_01WGufzgBi8iNgKbhq9r7VDT For domain:server#6, who now holds #32164. This is a finding only; I have pushed nothing.
Two ways to fix it:
Generated by Claude Code |
|
domain:server#6 · huangyiirene · session_01J8VVAnTcb4cZVkPdhkPicW Contract reviewServed-tier: Check-runs on this head (polled at 2026-11-11T06:07Z, 66 runs, none unfinished): "every check green" is NOT met. 2 failed: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentStatus of each finding of record 6779785246
must-fix
should-fix nit
Tried and held
Epic red-line read (epic #31527)
Implemented-by: VERDICT: FAIL — must-fix 1: |
… site is on the roster, with its sentinel pin The schema-sync ensurer's catch in `user-permission-set-packageless-index.ts` is the module's third `operatorFacingErrorText` call site. The roster row now says 3, and a pin drives a raw-statement fault carrying the sentinel into that catch (a driver whose raw-SQL surface cannot be read, the first thing a pass asks) and asserts its error line carries the dialect text and never the bound value. The `unsupported` case also asserts the old key's own detail, the second site, carries the dialect text. Claude-Session: https://claude.ai/code/session_01J8VVAnTcb4cZVkPdhkPicW Co-authored-by: Claude <noreply@anthropic.com>
…DDL, so a re-sync over a standing guard locks nothing Each schema-sync pass re-checks `uniq_sys_user_permission_set_packageless` (a SQLite table rebuild drops it), and that check used to be the build itself: `CREATE UNIQUE INDEX IF NOT EXISTS` on SQLite and PostgreSQL, a version read and a refused `CREATE INDEX` on MySQL, two refused `ALTER TABLE`s on MariaDB, on every `metadata:reloaded` re-sync of every serving kernel. Measured on PostgreSQL 16: over a standing index that statement waits for the table's SHARE lock behind an open write transaction, and a second writer then queues behind it, before it finds the name taken. The ensurer now reads the catalog first (an index of the guard's name on the table: `sqlite_master`, `pg_index` through `to_regclass`, `information_schema.statistics` under `DATABASE()`) and builds only when it is missing. A client with no catalog arm, a refused read or no result set builds as before: the read only saves a statement, never decides the build may be skipped. `created` still never latches, so a guard a rebuild dropped is put back by the next pass. Pins: a second pass over a standing guard sends no DDL (real SQLite, and live PostgreSQL 16 with a write open on the table under a 2 s lock_timeout); a refused read still builds; the MySQL family sends no version read and no DDL once the catalog names the guard; no arm for an unknown client. Prose: the module doc's "admits nothing the old key refused" now separates the plain key from an 18.0 store's NULL-safe form, which the widening arm keeps until the table is certified; `OBJECTQL_SCHEMA_SYNCED_HOOK` says the engine's own doors trigger nothing and no first-party composition creates the grant table through them; the changeset's per-pass sentence follows the read. Claude-Session: https://claude.ai/code/session_01J8VVAnTcb4cZVkPdhkPicW Co-authored-by: Claude <noreply@anthropic.com>
#32356 (the stacked P1) merged as 48d6b47, so its two commits on this branch (79e5677, da76f2b) now match main and fall out of the PR's diff. The one conflict, the P1 changeset, takes main's side (its corrected version); driver-sql's schema-drift.ts and its test were already byte-identical to main. Claude-Session: https://claude.ai/code/session_01J8VVAnTcb4cZVkPdhkPicW Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#6 · huangyiirene · session_01J8VVAnTcb4cZVkPdhkPicW Contract reviewServed-tier: Check-runs on this head (polled at 2026-11-11T16:29Z, 66 runs, none unfinished): 55 green, 0 failed, 11 skipped by job condition (the Dogfood shards, Dogfood Verify CLI, "Temporal Conformance (live PG + MySQL)", "18.x upgrade harness (opt-in)", Build Docs, Lint UI, UI Published Artifacts, Packed-tarball smoke, the two queue-verdict jobs, the schedule gate). Every check that ran is green. The PR is a draft; ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentStatus of each finding of record 6788505916
must-fix: none. should-fix
nit
Tried and held
Epic red-line read (epic #31527)
Implemented-by: VERDICT: PASS — the must-fix of 6788505916 is closed and the head is green. Owed outside this record: the seat's PR body, a follow-up card for the plan listing, and green live suites on |
|
domain:server#6 · huangyiirene · session_01J8VVAnTcb4cZVkPdhkPicW 席位核对 · ACCEPT + Landing · #32164 P1b · head
|
| Probe | Reading |
|---|---|
| Shape | Draft, base main. First line Part of #32164, with no closing keyword: the card stays open for P2 (#32306). #32356 landed as 48d6b477c7 and was merged into this branch (1b83b67228). The diff against main is this PR's own 28 files, +2766/−32. Clause-②: no with its Mongo runtime-owned-unique bullet, and Metadata-protocol: none. The seat holds the card by takeover 6788362280. |
| Contract review | FAIL 6779785246 → FAIL 6788505916 (0e9ff218) → fix round (9e74a636f9, 674554e99c) and the merge of main → PASS 6801350886 on this head (CONTRACT_REVIEW_TIER, delta scope, face narrowing). The epic #31527 red lines all PASS: resolution not loosened, nothing guessed (on duplicates the guard is not built and they are logged, never deleted or re-stamped), and the PR can be released on its own (on main the guard refuses nothing the three-column key does not already refuse). |
| Should-fix carried open by 6801350886 | (2) The PR body described round 1. Fixed by this seat: the body was rewritten from the fix-round tree (catalog read first, the held-back path, apply's flush announcement, the "Where" paragraph, no "Stacked on"). It was read back byte-identical, and the seat read it sentence by sentence against the diff. (4) os migrate plan does not list the guard step that apply builds. Out of scope here, filed as #32460. |
Checks on 1b83b672 |
57 success, 13 skipped, 0 failed. check-expected-skips.mjs: OK (every skip is in main's roster). check-governed-merges.mjs: NOT governed (0 of 28 paths), 2798 lines, under 3000. K = 11. |
| Owed after landing | The live MySQL/MariaDB/Postgres guard suites (*.live-mysql.test.ts, *.live-postgres.test.ts) run on main only. #32306 (P2) waits for those suites to pass on main with both P1 and P1b in. |
State: pr_ready + automerge_enable now. On MERGED, the seat posts the part-landed note on #32164. That note is what releases #32164, not this PR.
|
domain:server#6 · huangyiirene · session_01J8VVAnTcb4cZVkPdhkPicW Correction to landing comment 6801465033 · 2026-11-11T16:43Z
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43059859239 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:server#6 · huangyiirene · session_01J8VVAnTcb4cZVkPdhkPicW 席位核对 · Kicked from the queue · new signature, NOT re-queued · head
|
| Probe | Reading |
|---|---|
| Kick | Queue build 43059859239, Test Core (1/8), triage comment 6801967444. Removed from the queue at 16:55:47Z. Head unchanged. |
| Signature | packages/mcp/src/mcp-file-tools.test.ts › "read_file extracts PDF and Word text on a worker pool, off the request thread" › "out of CPU budget on the worker: the pages read so far, truncated, with the budget note, as in-process". At :786, expect(facts).toMatchObject({ content: 'text', format: 'application/pdf', truncated: true }) received content: 'none'. 1 failed, 1026 passed in @objectstack/mcp; the test took 1588 ms. |
| Initial reading | Most likely load-sensitive: the worker spent its CPU-time budget before it sent back a first page, so there was no partial text to answer with. The PR does not reach this code. @objectstack/mcp depends on core, formula, metadata-core, spec, types and lint. Of those, this PR touches only types, and only a test file there (driver-error-classification.callers.test.ts), no source. So the failing file's import closure does not intersect the diff. main push CI at 84177cf21f (16:26Z) is green. |
| Why NOT re-queued | The signature is not in the ledger. The triage comment says this PR is the only one in 24h to hit it, and no flaky card exists. Under landing-operations.md, a new signature is re-queued once only if all three facts hold. Fact 3 (the first error is a timeout, not an assertion) does not hold. This is an assertion, and its message names no duration, so the triage comment's timing-assertion exception does not apply mechanically either. Suspected new flaky test, proposed only (not added to the ledger): the CPU-budget-on-worker case in mcp-file-tools.test.ts (#31894). |
| Next | The seat is clocking out. #32164 is released with this state. The next seat re-diagnoses the signature (a few local runs of that one file on 1b83b672, then whether main or a sibling PR hits it). If it is ruled a flake, they re-queue once with label-write --land, recording the signature and the ledger basis. PASS 6801350886 and the lane ACCEPT 6801465033 still stand on this head. |
|
domain:server#2 · os-bill · session_01Utp6CaP1iy4RSEQbW6177G 席位核对 · Kick re-diagnosed · head
|
| Probe | Reading |
|---|---|
| Signature | Test Core (1/8), packages/mcp/src/mcp-file-tools.test.ts › "out of CPU budget on the worker: the pages read so far, truncated, with the budget note, as in-process". At :786, content: 'none' where 'text' was expected. |
Local runs on 1b83b672 (mcp closure built) |
5/5 green idle, and 5/5 green with 6 busy-loop processes on 4 cores. With the page count printed (a local edit, reverted, git status clean), the worker read 3, 6, 5, 5, 5 pages inside the test's 30 ms CPU budget. |
| Reading | The assertion holds only if the worker parses page 1 of the 2250-item heavy page within 30 ms of thread CPU. Locally the margin is 3 to 6 pages. A runner 3 to 6 times slower per page, or a JIT-cold first page, reads 0 pages, and the extractor then answers content: 'none'. That is the failure seen. It is a CPU-speed precondition of the experiment, not a product answer. |
| Fact 1 · import closure | @objectstack/mcp imports core, formula, metadata-core, spec and types. Of those, this PR touches one file: packages/types/src/driver-error-classification.callers.test.ts, which is a test that mcp never imports. Disjoint. |
| Fact 2 · base | No main or queue run since has failed on this file. The failed main runs since 12:00Z, scheduled 43017251989 and 43092964928, failed in other packages. |
| Fact 3 · first error | It is an assertion. Read literally, it is not a timeout, so the three-fact re-queue does not apply, and this seat does not re-queue the head unchanged. |
| K | git rev-list --count --first-parent 48d6b477..origin/main = 22, which is ≥ 15. So main is merged first in any case (landing-operations B). |
Route: the seat syncs the branch with main through the relay pr_update_branch. That is a merge commit, no rebase. The PR's own net diff is checked unchanged after the merge, with a same-head delta contract review. A fresh PR-side CI run then decides. If the queue kicks it again on this signature, the seat stops and files the suspected flake as a proposal; it does not add it to the ledger itself. Suspected new flake, for the ledger owner: mcp-file-tools.test.ts "out of CPU budget on the worker" depends on runner speed, with a 3–6 page margin measured locally.
|
domain:server#2 · os-bill · session_01Utp6CaP1iy4RSEQbW6177G Contract reviewServed-tier: Check-runs on this head, read at 2026-11-12T00:11:28Z (the head was made at 00:07:45Z): 43 runs, 12 success, 10 skipped by job condition, 21 in progress, 0 failed. Green so far: Check PR Size, Check Changeset, Governed Surface Queue Guard, PR-body guards, Lint · ESLint, Type Check · source gates, Dogfood Regression Gate, the two shard plans, Auto Label, the docs flag and the path filter. Still running: Build Core, Build UI Deps, Test Core 1/8 to 8/8, the other six Lint jobs, Type Check workspace, consumer gates and debt ledger, Live E2E UI; the Test UI shards were not yet created. This record judges the contract; it does not read a green head, and the lane ACCEPT owes that read once the runs finish. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix: none. should-fix: none new. Carried, unchanged: nit
Tried and held
Epic red-line read (epic #31527): conditions 1, 2 and 3 stand as read in 6801350886. Nothing on Implemented-by: VERDICT: PASS — the diff is byte-identical to the one that passed on |
|
domain:server#2 · os-bill · session_01Utp6CaP1iy4RSEQbW6177G 维护者速读
席位意见:ACCEPT。上次出队是 席位核对 — ACCEPT · domain:server#2 · head
|
…-ekmc12 P1 (#32356, 48d6b47) and P1b (#32357, e765a09) are on main. Every conflict was a P1/P1b file and is resolved to main's landed version; the guard migration's module doc then takes back this branch's own sentence on the grant's declared key (4b62e66). The net diff over main is P2's own: 40 files, +1686 / -126. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZ9zs2B33JD9Z2RiDE
Requested by Yibo Wang · project thread
Part of #32164
Clause-②: no
RUNTIME_OWNED_UNIQUES: the activation ledger'suniq_sys_metadata_activation_packageless, and now the grant's) that the server refuses at collection sync: on a duplicate (E11000), FROM the collection's whole sync failing (reported by the engine as "Schema sync FAILED", the declared indexes after it and the widening arm not run) TO oneerrorline naming the index, its key, the consequence and the aggregation that lists the duplicates, with the sync going on; on an options or key-spec conflict (85, 86), FROM skipped without a word TO oneerrorline naming the index and what to inspect, and saying the key is enforced only as far as the index in its way enforces it (an older unique on the same fields does). Either way the index is not built, and the widening arm keeps any older unique it stands in for, as it did. A declared index the server refuses is unchanged.Metadata-protocol: none
The second of three PRs for #32164 (the driver-sql widening arm, then this one, then the doors and the grant key). It builds the permission-set grant's package-less guard before the change that widens the grant's key needs it. While the declared key is still
(user_id, permission_set, organization_id), the guard refuses nothing that key does not already refuse, so no request's answer moves.维护者速读
sys_user_permission_set加一条运行时守卫索引uniq_sys_user_permission_set_packageless:不带包的授权,一人、一个权限集、一个组织只能有一条。凡是执行该表 DDL 的内核,都在每次 schema 同步结束时建它(ObjectQL 新增objectql:schema-synced事件),不受runPlatformMigrations开关限制;os migrate apply在确认后执行 DDL 时一并建好,os migrate plan和各种演练模式什么都不写。之后每次同步先读系统目录,守卫已在就不发任何 DDL。另外 MongoDB 上运行时自建的唯一索引被服务端拒绝时,改为一条 error 日志并继续同步。start()写授权之前、在每一种宿主上就位(契约复审 MF6)。packages/metadata-protocol/src/migrations/user-permission-set-packageless-index.ts(ensureUserPermissionSetPackagelessIndex先读目录再建,createUserPermissionSetGuardAtSchemaSync只锁定拒绝),packages/metadata-protocol/src/plugin.ts(订阅位于runPlatformMigrations开关之外),packages/cli/src/utils/schema-migrate.ts(flushSchemaDdl末尾的announceSchemaSynced)。pnpm --filter @objectstack/metadata-protocol exec vitest run src/migrations/user-permission-set-packageless-index.test.ts,预期全部通过,其中 "a later pass over a table the guard stands on reads the catalog and issues no DDL" 一条证明第二次同步不发 DDL。What changes
metadata-protocol/src/migrations/user-permission-set-packageless-index.ts, new):uniq_sys_user_permission_set_packageless,(user_id, permission_set, organization_id)over the grants whosepermission_set_packageis NULL. A partial unique index on SQLite and PostgreSQL; a functional key part over the driver's hash-shadow digest on MySQL 8.0.13+; a UNIQUE key over an INVISIBLE generateduniq_sys_user_permission_set_packageless__hashcolumn on MariaDB 10.3+. On MongoDB,driver-mongodbbuilds it at collection sync (RUNTIME_OWNED_UNIQUES,permission_set_package: null). Re-runnable, and it guesses no package: on existing duplicates the build fails and logs, aterror, the duplicated keys (ten, then a count), the consequence and the listing query; nothing is deleted or re-stamped, and the next boot tries again.objectql/src/plugin.ts,metadata-protocol/src/plugin.ts):ObjectQLPlugintriggersobjectql:schema-synced(exported asOBJECTQL_SCHEMA_SYNCED_HOOK) at the end of eachsyncRegisteredSchemaspass, with the engine, andmetadata-protocolbuilds the guard from it: inside the boot's schema-migration lock, before any later plugin'sstart(), and on abootPhaseHooks: falsehost too (the plugin triggers the event, not the kernel). Only this plugin's passes trigger it; the engine's own doors (syncSchemas,syncObjectSchema) trigger nothing, and no first-party composition creates the grant table through them. Thekernel:readybuild stays inside the boot-repair gate as a repair, for a database whose DDL came from somewhere that did not build the guard.runPlatformMigrationsgate. A kernel with anenvironmentIdand no declaration (a per-project kernel) and every one-shot CLI boot have that gate off and create the table at their own sync, so both build the guard.os migrate planand every dry-run mode hold DDL back, which the ensurer reads (schemaDdlDeferred, throughisSchemaDdlHeldBackForTable) and answersheld-back.os migrate applyholds DDL until the operator confirms; its flush then performs it and triggers the event again (DeferSchemaDdlPlugin.announceSchemaSynced), which builds the guard. A write mode that boots without deferring builds it at its sync. AskipSchemaSyncboot runs no pass; its tables come from the boot that runs their DDL.creatednever latches (a SQLite table rebuild drops every index it does not declare), so every pass looks again: an index of the guard's name on the table insqlite_master,pg_indexthroughto_regclass, orinformation_schema.statisticsunderDATABASE(). DDL runs only when the guard is missing. A client with no catalog arm, a refused read or no result set builds as before; the read only saves a statement. Only a refusal (conflict,unsupported,failed) latches, reported once per boot.uniq_sys_user_permission_set_ac248331), so the widening arm keeps it until the guard can be built. Oneerrorline says so each boot.driver-mongodb/src/mongodb-schema.ts): a refused runtime-owned unique is reported byreportRuntimeOwnedUniqueFailure(E11000: the aggregation that lists the duplicates; 85/86: inspectgetIndexes(), enforced only as far as the index in its way enforces it) and the sync goes on;SchemaSyncLoggergains an optionalerror.runtime-index-preflight.ts): the guard is listed, soos migrate duplicateslists six indexes.Tests
plugin-security/src/user-permission-set-guard-at-schema-sync.test.ts, 8 pins, SQLite): two grants naming no package, written from a later plugin'sstart()beforekernel:ready, are refused at the second, with and withoutbootPhaseHooks: false, on a kernel with anenvironmentIdand no declared gate, and on one declaringrunPlatformMigrations: false; a guard dropped after the boot is back after the next pass.cli/src/utils/schema-migrate.grant-guard.integration.test.ts, real SQLite, served composition): a write boot builds the guard; a read-only boot over a table without it leaves it absent;applyhas it absent before the flush and present after.user-permission-set-packageless-index.test.ts): held back builds nothing and latches nothing;creatednever latches; a later pass over a standing guard reads the catalog and issues no DDL; a refused catalog read still builds; a refusal latches; the MySQL family sends no version read and no DDL once the catalog names the guard; no catalog arm for an unknown client; the old key on a dialect with no form (kept, already there, cannot be built).[#21418]block of the same file): a value bound into a refused raw statement reaches no carrier at any of the module's threeoperatorFacingErrorTextsites (the CREATE's detail, the old key's detail, the ensurer's own catch);packages/types/src/driver-error-classification.callers.test.tslists the module with 3 call sites.user-permission-set-packageless-index.live-postgres.test.ts): the partial form; epic conditions 1 and 2 over real rows; the catalog read absent then present, and a second boot with a write open on the table sends no DDL under a 2 slock_timeout.driver-mongodb, recording Db): an E11000 and a code-85 refusal each give oneerrorline and the sync goes on; CONTROL: a declared index refused with E11000 still fails the sync.Acceptance notes
os migrate plandoes not list the guard (review record 6788505916, should-fix 4): sized and left for a follow-up, not done here.applybuilds the guard only from its flush, and it reaches the flush only when it has other work: its in-sync return and its nothing-safe-to-apply return (cli/src/commands/migrate/apply.ts) both come beforeflushSchemaDdl(). So a clean plan'sapplybuilds nothing, and the next serving boot builds the guard. The real gap is narrower: a plan that lists other work does not say thatapplywill also build the guard. Listing it needs a guard read in the plan boot, a new public export frommetadata-protocol, a line conditional onapplyflushing, a new--jsonfield and a served-composition integration test: about 150 to 200 lines overplan.ts,schema-migrate.ts, themetadata-protocolmodule and index, the changeset and tests. A new kind inPendingSchemaWorkwould touch driver-sql'sschema-drift.ts, fix(driver-sql): the widening arm keeps, or safely retires, an 18.0 NULL-safe narrower key, and keeps a shadow-carried one or one whose gained column is not yet physical, never an orphan drop #32356's file.created. A per-process latch on a confirmed guard would save that read, at the cost of the guard staying lost until restart when a SQLite table rebuild drops it (the closed should-fix 2 of record 6779785246); not taken.mainonly. The MySQL twin now also asserts that the catalog read sees the guard after the build and that a second boot sends no DDL.!in the title, no(narrowing)arm, and the change stays inside these packages'srcand tests.Verification
Final head 1b83b67, which carries
main48d6b47 (merged in after #32356 landed).Suites on the final head
metadata-protocol(withOS_TEST_POSTGRES_URLon a private PostgreSQL 16.15)objectqlplugin-securitydriver-mongodbtypes(test)types(test:repo, the operator-text roster)cli(--project unit)cliguard files (grant-guard, duplicates integration and contract, stored-references)Typecheck (
metadata-protocol,objectql,plugin-security,driver-mongodb,types,cli): exit 0. After the merge ofmain: fullturbo run buildexit 0,@objectstack/spec check:generatedexit 0. eslint over this round's 7 TypeScript files: exit 0. No dogfood battery is owed (no!, no(narrowing)arm).Ablations (each a single-line mutation through scripts/ablation-replace.mjs, restore proven blob == HEAD)
lock_timeout, 2046 ms).createdlatched (metadata-protocol 1, plugin-security 1).The PostgreSQL lock, measured (PostgreSQL 16.15, a private cluster)
With a write transaction open on the grant table:
CREATE UNIQUE INDEX IF NOT EXISTSover the standing guard waited and was cancelled at a 2 slock_timeout(2.03 s); the catalog read answered its row in 0.03 s. While that CREATE waited, a second writer was cancelled at 1.5 s; with no CREATE waiting, the same insert went straight through.Gates
node scripts/pm/dispatch-gates.mjs --commandsover this PR's 28 paths at 1b83b67: 96 derived. 85 ran, every one exit 0; the last 11 (check:startup-registry-verdictthroughcheck:wildcard-fallthrough,check:type-check-debtamong them) were not run locally because the run was stopped once review passed.--ran: 96 derived, 85 run, 0 NOT-MEASURED, 11 UNRUN. CI on 1b83b67: 66 check runs, 55 success, 11 skipped by job condition, 0 failed;Lint & Repo GatesandTypeScript Type Checkcover those 11 there. On 674554e,Test Core (7/8)ran the roster test green, 11 of 11.Human Minimum
packages/specholds no runtime logic. (No spec change.)pnpm lintpasses. (eslint over every touched file: exit 0.)main, one pull request. (Part of plugin-security + runtime: the grant, clone and activation doors take a permission set's package and report a two-holder name with its candidates (ADR-0150 D3/D8; before #31698 PR-2) #32164, split by seat ruling.)packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.## 维护者速读is the maintainer's Chinese summary the dispatch rules ask for.)Generated by Claude Code