test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687
Conversation
…s consume on an artifact boot (#14491) A measurement pin only — no production file is touched and no behaviour changes. One real `createStandaloneStack` boot with a real engine, both declaration copies read at the same moment, key by key, beside the `sys_*` rows the real seeders left behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…ment pin `packages/runtime`'s tsconfig excludes `**/*.test.ts`, so the package `typecheck` is green over a program this file is not in. Type-checked with the exclusion lifted instead: 4 TS2347 + 2 narrowing errors, now 0. `pnpm check:slot-lookup` also refused the erased `getService` results. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
PM landing provenance —
|
| check | reading |
|---|---|
CI on head 78513eb10 |
37 check runs, all completed; every conclusion success or skipped. No failure, no in_progress. |
No other open PR may claim the same single-writer path |
success — the mechanical fence for this file. |
Part-of PR must not also close its card |
success — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires. |
| production files touched | zero. changed_files: 1, additions: 460, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. |
| Clause-② | no. The PR body's derivation (`git diff -U0 origin/main...HEAD |
| changeset | none, skip-changeset applied — correct for a test-only diff in a repo that forbids empty-frontmatter changesets. |
Why this lands while the card stays open. Triage ruled this card down to its measurement half and said, verbatim and untranslated:
⛔ Stop and report. Do not implement (a), (b) or (c).
The diff obeys that literally: it pins the four answers and repairs nothing. So the PR lands as a recorded reading — Part-of #14491, not Fixes — and #14491 remains open carrying pm:retriage, because the measurement found a real divergence and triage's own text makes the direction a maintainer call while the routing stays triage's. The numbers were handed back on the card; this seat did not pick a direction.
Generated by Claude Code
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queueQueue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago: Why it is not this PR's, measured rather than asserted:
No fix exists to port: #14706 is a name, not a diagnosis — ungraded, unassigned, nothing in flight. On re-queueing, and why the count matters here. This PR's own batch already went green once (queue build at 19:34:55Z) and was still not merged, because a batch merges only if the whole batch passes. It was then auto-re-queued and failed again. Spending a manual re-run on top of that would add a full-queue rebuild for every PR behind mine in exchange for a coin flip, so ⛔ I am not doing it. The repository's automated queue management re-queues this PR on its own; that is the mechanism that will land it, or not, and it needs no help from me. What this PR waits on, stated so the wait is a record and not a memory: #14706, from 20:13Z, watched by the Generated by Claude Code |
Part-of #14491
⛔ A measurement, not a fix. No production file is touched, no candidate repair (a)/(b)/(c) is implemented, and nothing behavioural changes — so no ablation is owed and none was run. The card stays open; triage grades the numbers.
The ruling of record
Triage ruled this card down to its first half (14491#issuecomment-5507909226), quoted verbatim:
VERDICT: there IS a real divergence, and it reaches the persisted row in two of three kinds
Two findings survive the seeders. Both are permission-boundary facts with a persisted consequence, so by triage's own sentence the direction is a maintainer call.
sys_permission_setpersists the UN-PARSED registry copy, byte for byte. The storedobject_permissionsmap is deep-equal to the SchemaRegistry copy and NOT to the door's: it keepsallowRestore: true/allowPurge: false(keys the current schema drops) and carries none ofallowTransfer/viewAllRecords/modifyAllRecords(the three the door defaults in).row_level_securitykeepspriority: 10, which the door drops.mapRecipientTyperefuses'role'— it converts nothing, because the ADR-0087sharing-recipient-role-to-positionconversion lives at the door this read bypasses — so the seeder takes itsskipped (unmappable recipient)branch and nosys_sharing_rulerow is written at all. The door's copy of the same declaration saysposition, which the seeder accepts. Three rules declared, two rows.Three other divergences exist at the seeder input and are absorbed downstream; they are pinned too, so the equivalence is guarded rather than assumed.
Q1 — which copy each seeder consumes
All three consume the ObjectQL SchemaRegistry copy. Each fallback is guarded by
if (KIND.length === 0), and on an artifact boot with an engine the registry copy is never empty, sometadataService.list(KIND)is never reached. Measured, on one real boot:bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')bootstrap-declared-positions.tsreadDeclared(ql, 'position')engine.registryandengine._registry— the two spellings the two plugins'readDeclareduse — are pinned as one and the sameSchemaRegistryobject, so the receivers cannot diverge.positionis empty becausepositionsis absent fromMETADATA_ARRAY_KEYS;rolesis in that list but absent fromPLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.Q2, Q3, Q4 — key by key
Registry = what
readDeclaredreturns. Service = whatmetadataService.listwould have returned at the same moment. Persisted = thesys_*column after the real seeders ran.permission—probe_agentintosys_permission_setobjects.crm_ticket.allowRestoretruetrueobjects.crm_ticket.allowPurgefalsefalseobjects.crm_ticket.allowTransferfalseobjects.crm_ticket.viewAllRecordsfalseobjects.crm_ticket.modifyAllRecordsfalserowLevelSecurity[0].priority1010isDefaultfalse_packageVersion3.0.0permissionSetRowFieldsserialises what it was handed; nothing betweenreadDeclaredandtryInsertre-parses it.capability—probe.exportintosys_capabilityscopeplatformplatformcapabilityRowFields' own defaultlabelExport probe datadescriptionCapability probe.export._packageVersion3.0.0Q3 answered: yes — a capability reaches the seeder with no
scope. The row is nevertheless indistinguishable, becausecapabilityRowFieldsre-derivesplatformrather than reading the door's value. That equality is a coincidence of two independent defaults, not a normalisation; the pin records that it currently holds.sharing_ruleintosys_sharing_ruleconditionstring{ dialect: 'cel', source }criteria_jsoneither waycompileCelToFilteracceptsstringor{ source }activetruetruer.active !== falseshare_legacy_level·accessLevelfullediteditnormalizeAccessLevelretired-spelling mapshare_legacy_deals·sharedWith.typerolepositionQ2 answered: bare
stringat the seeder, against the door's{ dialect, source }— and it has no consequence, because the compiler takes both.What is real in the harness, and the one declared input
REAL:
createStandaloneStackand every plugin it composes (the artifact doorMetadataPlugin({ artifactSource }),ObjectQLPluginwith its real SchemaRegistry, the real default datasource overmemory://), plus the realSecurityPluginandSharingServicePlugin— the seeders under measurement are the production ones, running in their productionstart(), writing through the real engine into the realsys_*tables. No engine double, and therefore no row inscripts/engine-double-contract.pinned.json(the ledger PR #14528 holds is untouched).DECLARED: exactly one composition input —
tenancy: { posture: 'single' }.createStandaloneStackcomposes no auth plugin, andAuthPluginis the only registrar of thetenancyservice; without itSharingServicePlugintakes its fail-safe walled default, enumerates an emptysys_organizationand runs zero seeding passes (measured:ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study.singleis what the open runtime's owncreateTenancyServiceresolves to when noorg-scopingservice is installed.No engine-less control leg, deliberately:
readDeclaredfalls back only when the registry answers empty, so producing that state means handing a seeder an engine whose registry is empty — an engine double, which the ruling excludes. The fallback's input is measured directly instead, off the real booted metadata service at the same moment.Where the file lives, and why
packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts.createStandaloneStacklives inpackages/runtime, andpackages/runtimeis the only package that can import it together with both seeders' plugins:@objectstack/plugin-securityis a dependency and@objectstack/plugin-sharinga devDependency of@objectstack/runtime, while neither plugin depends on@objectstack/runtime— the reverse import would be a cycle. PR #14398's divergence pin sits here for the same reason.Premises, re-taken today on
origin/main2aa8456cfAll four hold, at the same line numbers triage recorded at
ed44512.packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYSlistsroles, permissions, capabilities, profiles, sharingRules, policiesand nopositionsplugin-security/src/bootstrap-declared-permissions.ts:157/:315,bootstrap-declared-capabilities.ts:401plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209/:212plugin-security/src/bootstrap-declared-positions.ts:96/:129readDeclared, readsposition— exactVerification — final head
78513eb10, every exit captured before any pipeTest Files 1 passed (1)·Tests 13 passed (13)—os-verify-lock: VERDICT command-exit 0@objectstack/runtimesuiteTest Files 210 passed (210)·Tests 3092 passed (3092)—os-verify-lock: VERDICT command-exit 0@objectstack/runtimetypecheckos-verify-lock: VERDICT command-exit 0— but see belowgit merge-tree --write-tree --name-only origin/main HEAD2e607b740, no conflict pathspackages/runtime/tsconfig.jsonexcludes**/*.test.ts, andtsc --noEmit --listFilesputs 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and@objectstack/runtimeis a recordedTEST_DEBTentry (206 frozen errors) for exactly this. Measured properly instead, with the exclusion lifted over this one file: 6 errors found (4 TS2347, 2 narrowing), all fixed, nowtsc --noEmitexit 0 with 0 errors in the file — so this file adds nothing to that ledger.pnpm check:slot-lookupindependently refused the erasedgetServiceresults and is now green (106 unswept site(s) in 25 file(s), none new).The three NOT MEASURED gates, read from each gate's own verdict line — none is a red, and none is about this diff:
check-test-completeness.mjsexit 3 —PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate familyscripts/pm/dispatch-gates.mjsderives? … the local reading for this gate is NOT MEASURED. ⛔ It is not a red, and there is nothing here to fix."check:dual-build-cjs-loadsexit 3 — "Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured." Only the@objectstack/runtimedependency closure was built locally; 32 unrelated packages have nodist.check:type-check-debtexit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.pnpm check:engine-double-contractis green, confirming the harness needs no double. Control characters:grep -naPover the new file found none.Housekeeping
git diff -U0 origin/main...HEAD | grep export): no. All 10 matches are the capability nameprobe.exportand the labelExport probe data;grep -cE "^\+\s*export "is 0.skip-changesetlabel is the mechanism, applied on open.permissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 remains open — this PR isPart-ofit, and the card is the measurement's addressee, not its outcome.🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code