test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine - #14687

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement
Sep 2, 2026
Merged

test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine#14687
os-sales merged 2 commits into
mainfrom
claude/issue-14491-seeder-registry-copy-measurement

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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:

Scope ruled — this is a MEASUREMENT card, and it stops at the measurement

The card offers three shapes (a)/(b)/(c) and says "measure first". It is right, and I am ruling the card down to that first half rather than putting the three-way choice to the maintainer now. Everything the direction turns on is marked unmeasured in the card's own words — whether the seeders normalise what they read, and which copy actually wins for a persisted sys_* row. Presenting a direction choice on that basis would be asking for a ruling nobody can make yet, and a facet analysis built on it would be argument without evidence.

So: on a real createStandaloneStack boot with an engine, measure and report

  1. which copy each seeder actually consumes for sharing_rule, permission and capability;
  2. the runtime type of a sharing rule's condition at the seeder — bare string, or the door's { dialect, source };
  3. whether a capability arrives without its scope default;
  4. which copy wins in the persisted sys_* row at the end.

Stop and report. Do not implement (a), (b) or (c). Pin the findings key-by-key in the divergence style PR #14398 used, then hand the numbers back to triage. If the measurement shows a real divergence, the direction is a maintainer call — it moves which copy of the permission, capability and sharing-rule declarations the security and sharing seeders trust, and that is a permission-boundary decision with a possible persisted-row consequence, which is the human floor twice over. If the measurement shows the seeders already normalise and no copy diverges, the card closes on the evidence and nobody spends a ruling on it.

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.

  1. sys_permission_set persists the UN-PARSED registry copy, byte for byte. The stored object_permissions map is deep-equal to the SchemaRegistry copy and NOT to the door's: it keeps allowRestore: true / allowPurge: false (keys the current schema drops) and carries none of allowTransfer / viewAllRecords / modifyAllRecords (the three the door defaults in). row_level_security keeps priority: 10, which the door drops.
  2. A declared sharing rule is silently DROPPED.mapRecipientType refuses 'role' — it converts nothing, because the ADR-0087 sharing-recipient-role-to-position conversion lives at the door this read bypasses — so the seeder takes its skipped (unmappable recipient) branch and no sys_sharing_rule row is written at all. The door's copy of the same declaration says position, 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, so metadataService.list(KIND) is never reached. Measured, on one real boot:

seederfirst readregistry copy non-empty?fallback reached
bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yesno
bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yesno
bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yesno
bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always emptyyes, always

engine.registry and engine._registry — the two spellings the two plugins' readDeclared use — are pinned as one and the same SchemaRegistry object, so the receivers cannot diverge. position is empty because positions is absent from METADATA_ARRAY_KEYS; roles is in that list but absent from PLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.

Q2, Q3, Q4 — key by key

Registry = what readDeclared returns. Service = what metadataService.list would have returned at the same moment. Persisted = the sys_* column after the real seeders ran.

permissionprobe_agent into sys_permission_set

keyregistry copy (consumed)service copy (door)persistedverdict
objects.crm_ticket.allowRestoretrueabsenttrue⚠️ NOT absorbed
objects.crm_ticket.allowPurgefalseabsentfalse⚠️ NOT absorbed
objects.crm_ticket.allowTransferabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.viewAllRecordsabsentfalseabsent⚠️ NOT absorbed
objects.crm_ticket.modifyAllRecordsabsentfalseabsent⚠️ NOT absorbed
rowLevelSecurity[0].priority10absent10⚠️ NOT absorbed
isDefaultabsentfalsenot a column this seeder writesn/a
_packageVersionabsent3.0.0not a column this seeder writesn/a

permissionSetRowFields serialises what it was handed; nothing between readDeclared and tryInsert re-parses it.

capabilityprobe.export into sys_capability

keyregistry copy (consumed)service copy (door)persistedverdict
scopeabsentplatformplatformabsorbed — capabilityRowFields' own default
labelExport probe datasamesameno divergence
descriptionabsentabsentCapability probe.export.seeder default, both ways
_packageVersionabsent3.0.0not a column this seeder writesn/a

Q3 answered: yes — a capability reaches the seeder with no scope. The row is nevertheless indistinguishable, because capabilityRowFields re-derives platform rather 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_rule into sys_sharing_rule

rule · keyregistry copy (consumed)service copy (door)persistedverdict
all three · conditionbare string{ dialect: 'cel', source }same criteria_json either wayabsorbed — compileCelToFilter accepts string or { source }
all three · activeabsenttruetrueabsorbed — r.active !== false
share_legacy_level · accessLevelfullediteditabsorbed — normalizeAccessLevel retired-spelling map
share_legacy_deals · sharedWith.typerolepositionNO ROW⚠️ NOT absorbed — rule dropped

Q2 answered: bare string at 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: createStandaloneStack and every plugin it composes (the artifact door MetadataPlugin({ artifactSource }), ObjectQLPlugin with its real SchemaRegistry, the real default datasource over memory://), plus the real SecurityPlugin and SharingServicePlugin — the seeders under measurement are the production ones, running in their production start(), writing through the real engine into the real sys_* tables. No engine double, and therefore no row in scripts/engine-double-contract.pinned.json (the ledger PR #14528 holds is untouched).

DECLARED: exactly one composition input — tenancy: { posture: 'single' }. createStandaloneStack composes no auth plugin, and AuthPlugin is the only registrar of the tenancy service; without it SharingServicePlugin takes its fail-safe walled default, enumerates an empty sys_organization and runs zero seeding passes (measured: ruleCount: 0, empty table) — which would measure the tenancy default instead of the read under study. single is what the open runtime's own createTenancyService resolves to when no org-scoping service is installed.

No engine-less control leg, deliberately: readDeclared falls 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. createStandaloneStack lives in packages/runtime, and packages/runtime is the only package that can import it together with both seeders' plugins: @objectstack/plugin-security is a dependency and @objectstack/plugin-sharing a 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/main2aa8456cf

All four hold, at the same line numbers triage recorded at ed44512.

premisere-takenresult
P1packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYS lists roles, permissions, capabilities, profiles, sharingRules, policies and no positions
P2plugin-security/src/bootstrap-declared-permissions.ts:157 / :315, bootstrap-declared-capabilities.ts:401exact
P3plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209 / :212registry first, service fallback — exact
P4plugin-security/src/bootstrap-declared-positions.ts:96 / :129own local readDeclared, reads position — exact

Verification — final head 78513eb10, every exit captured before any pipe

runverdict line
the new fileTest Files 1 passed (1) · Tests 13 passed (13)os-verify-lock: VERDICT command-exit 0
full @objectstack/runtime suiteTest Files 210 passed (210) · Tests 3092 passed (3092)os-verify-lock: VERDICT command-exit 0
@objectstack/runtime typecheckos-verify-lock: VERDICT command-exit 0but see below
gate family (26, re-derived on this head)23 green · 3 NOT MEASURED
git merge-tree --write-tree --name-only origin/main HEADexit 0, tree 2e607b740, no conflict paths

⚠️The package typecheck's green says nothing about this file.packages/runtime/tsconfig.json excludes **/*.test.ts, and tsc --noEmit --listFiles puts 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap, and @objectstack/runtime is a recorded TEST_DEBT entry (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, now tsc --noEmit exit 0 with 0 errors in the file — so this file adds nothing to that ledger. pnpm check:slot-lookup independently refused the erased getService results 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.mjs exit 3 — PREREQUISITE NOT MET, and its own text says so: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … 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-loads exit 3 — "Run pnpm build first. ⛔ This is NOT a pass: nothing was measured." Only the @objectstack/runtime dependency closure was built locally; 32 unrelated packages have no dist.
  • check:type-check-debt exit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.

pnpm check:engine-double-contract is green, confirming the harness needs no double. Control characters: grep -naP over the new file found none.

Housekeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd4aa4e49c6ac11f1833576d8b98fc41584b6a5dpackageMentionDocs.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

PM landing provenance — domain:services seat, session_01AUF1NoViznQK32gqpK8wS8

Marked ready for review and armed for auto-merge at 18:56:54Z (queue method: repository default). Reviewer of record: this seat, verifying against GitHub and the tree — never against the dev's report.

What was verified before arming

checkreading
CI on head 78513eb1037 check runs, all completed; every conclusion success or skipped. No failure, no in_progress.
No other open PR may claim the same single-writer pathsuccess — the mechanical fence for this file.
Part-of PR must not also close its cardsuccess — this PR is Part-of #14491 and does not close it, which is the shape triage's ruling requires.
production files touchedzero.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
changesetnone, 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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33676145614 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Queue ejection triaged — not this PR's, and I am deliberately NOT spending a manual re-queue

Queue build 33676145614 ejected this PR, on the same signature that ejected #14528 an hour ago:

@objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts
↳ AssertionError: expected 'SIGKILL' to be null

Why it is not this PR's, measured rather than asserted:

  1. The diff cannot reach it.changed_files: 1, deletions: 0, and the one file is packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Zero production files, zero CLI files. A test-only addition in @objectstack/runtime has no path to a @objectstack/cli e2e test.
  2. Cross-PR signature. The same test file has now ejected 10 PRs / 9 independent hits in 24 hours. Anchor: Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14706.
  3. Failure class. The asserted value is the kill signal the harness had to send — whether a child exited inside a time budget. By this repo's own rule for reading a queue red, that is the load-sensitive class, not an assertion on product behaviour.

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 domain:services seat (session_01AUF1NoViznQK32gqpK8wS8). I have escalated that card for grading and routing to domain:cli rather than sitting on the wait. This PR's own head 78513eb10 is green on its branch CI (37 checks, all success or skipped) and is unchanged; there is nothing here to fix and nothing to push.


Generated by Claude Code

Merged via the queue into main with commit 941952cSep 2, 2026
39 checks passed
@os-sales
os-sales deleted the claude/issue-14491-seeder-registry-copy-measurement branch September 2, 2026 20:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude