Repository navigation
[finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy of permissions / capabilities / sharingRules in the ObjectQL SchemaRegistry (AppPlugin.init → manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 2, 2026 Triage — graded,
findingcleared. Both re-check commands were run atorigin/mained44512and every structural claim holds:packages/objectql/src/engine.ts:1975-2010—METADATA_ARRAY_KEYScontains'roles', 'permissions', 'capabilities', 'profiles', 'sharingRules', 'policies'and nopositions. The card's asymmetry is exact, and the list carries its own docblock about this seam (:1980-1990, thecapabilities/capabilities不在 ObjectQLmetadataArrayKeys注册缝里 —— app 声明的 capability 永远拿不到 registry provenance(#4967 Part 2 拆出) #5870 / A refused capability declaration still suppresses the back-compat derivation, so the capability exists nowhere — and an app-declared capability can never get registry provenance #4967 history).packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts:157definesreadDeclared;:315reads'permission';bootstrap-declared-capabilities.ts:401reads'capability'.packages/plugins/plugin-sharing/src/bootstrap-declared-sharing-rules.ts:209—readDeclared(engine, 'sharing_rule')first, withmetadataService?.list?.('sharing_rule')at:212as the fallback. Registry-first, service-second, exactly as described.bootstrap-declared-positions.ts:96declares its own localreadDeclaredand reads'position'at:129— which, withpositionsabsent fromMETADATA_ARRAY_KEYS, is whypositionalways comes from the metadata service. The card's explanation of the split is correct.
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
createStandaloneStackboot with an engine, measure and report- which copy each seeder actually consumes for
sharing_rule,permissionandcapability; - the runtime type of a sharing rule's
conditionat the seeder — barestring, or the door's{ dialect, source }; - whether a
capabilityarrives without itsscopedefault; - 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.
This is the same stop-and-report shape #14341 ran, which is what produced #14486 — it works, and it is the honest sequencing here.
Routing
domain:services— the defect surface (seeders trusting an un-parsed copy) is entirely inplugin-security/plugin-sharing, and two of the three candidate repairs land there too.⚠️ Shape (a) would instead land inpackages/objectql/packages/runtime(domain:engine); if the measurement points that way the card gets re-routed then, on evidence, rather than pre-labelled now on a guess.priority:p2: the exposure is a permission / sharing-rule declaration reaching a seeder in an un-parsed shape, which is the sort of thing that ends up in a persisted authorization row — but it is unmeasured, and PR #14398 passed review with this named as a known residual. It goes above the prose cards because of what it touches, and below p1 because nothing is yet shown to be wrong. If the measurement finds a real divergence inconditionorscope, re-grade it up at that point.pm:queue.
Generated by Claude Code
Claim:
domain:servicesPM seat, sessionsession_01AUF1NoViznQK32gqpK8wS8(GitHubos-sales), 2026-09-02 ~17:32Z.- Session:
session_01AUF1NoViznQK32gqpK8wS8 - Branch:
claude/issue-14491-seeder-registry-copy-measurement - Worktree:
/home/user/objectstack-issue-14491(dev-created, worktree-first) - Domain:
domain:services(plugin-security/plugin-sharingseeders) - File surface: a NEW measurement pin file under
packages/plugins/plugin-sharing/src/*.test.tsorpackages/plugins/plugin-security/src/*.test.ts(whichever package the boot harness sits closest to — the dev says which and why), plus a changeset only if the round produces one. ⛔ No production file is edited:bootstrap-declared-sharing-rules.ts,bootstrap-declared-permissions.ts,bootstrap-declared-capabilities.ts,bootstrap-declared-positions.ts,packages/objectql/src/engine.ts'sMETADATA_ARRAY_KEYSand every candidate repair (a)/(b)/(c) are OUT of scope by triage's ruling — this card stops at the measurement. Also ⛔packages/spec/**,content/docs/releases/**,skills/**. - Serial constraints cleared:
plugin-sharing's open PR fix(plugin-sharing): let system writes materialize sharing rules — drop the isSystem skips in bindRuleHooks (#13533) #14528 holdsrule-hooks.ts,sharing-plugin.ts,bu-tree-recompute.ts,bulk-recompute.test.ts,boot-backfill.test.ts,system-write-materialisation.test.ts, the engine-double ledger and the census page — none of them is on this card's surface, and a new test file cannot conflict.plugin-security's files are unheld.⚠️ If the measurement turns out to need a row inscripts/engine-double-contract.pinned.json, stop and report instead of writing it — PR fix(plugin-sharing): let system writes materialize sharing rules — drop the isSystem skips in bindRuleHooks (#13533) #14528 holds that ledger; triage's shape asks for a realcreateStandaloneStackboot with a real engine, which needs no double at all, so needing the ledger is itself a signal the harness drifted from the ruled shape. - Container & model: in-process
os-devsubagent, model opus.node scripts/pm/dispatch-gates.mjs --tieron the seeder paths: no path-derived mandate; the tier stays the seat's judgment (floor sonnet · default opus · ceiling fable), and Clause-② is judged from content — expectedno, since the deliverable is a measurement pin. - Clause-②: expected no (a test-only card). The dev re-declares from the ACTUAL diff and hangs
needs:contract-reviewon both carriers if it landsyes. - Ruling of record: triage 14491#issuecomment-5507909226, which ruled the card down to its measurement half: 「⛔ Stop and report. Do not implement (a), (b) or (c).」 — measure, on a real
createStandaloneStackboot with an engine, (1) which copy each seeder consumes forsharing_rule/permission/capability, (2) the runtime type of a sharing rule'sconditionat the seeder, (3) whether acapabilityarrives without itsscopedefault, (4) which copy wins in the persistedsys_*row; pin key-by-key in PR fix(runtime): the artifact boot has one registrar for its security collections — the door (#12892 step 2) #14398's divergence style; hand the numbers back to triage. Triage also states that a real divergence makes the direction a maintainer call (a permission-boundary decision with a persisted-row consequence), and that shape (a) would re-route the card todomain:engine.
Race re-read follows this comment before the dev is launched.
Generated by Claude Code
- Session:
PM ACCEPT — PR #14687 at
78513eb10— and the measurement found a real divergence, so this goes back to triageThe measurement triage ruled (14491#issuecomment-5507909226) is delivered. Verified on the tree, not from the report: the diff is exactly one file,
packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts; no production file is touched (git diff --name-only origin/main...HEADfiltered for anything not*.test.tsis empty); addedexportlines: 0 ⇒ Clause-②no;git merge-tree --write-tree --name-only origin/main HEADlists no file. No engine double, so the ledger PR #14528 holds was never needed —check:engine-double-contractgreen confirms it. The PR isPart-of #14491and does not close the card.The four answers
Q1 — which copy each seeder consumes. All three consume the ObjectQL SchemaRegistry copy. Each fallback is guarded by
if (KIND.length === 0), and on an artifact boot with an engine the registry copy is never empty, sometadataService.list(KIND)is never reached.positionis the exception and always takes the service, becausepositionsis absent fromMETADATA_ARRAY_KEYS— the card's asymmetry is exact.engine.registryandengine._registry, the two spellings the two plugins use, are pinned as the same object.Q2 — a sharing rule's
conditionat the seeder: barestring, against the door's{ dialect, source }. No consequence —compileCelToFilteraccepts both, and the persistedcriteria_jsonis identical either way.Q3 — a
capabilitywithout itsscopedefault: yes, it reaches the seeder with noscope. The row is nevertheless indistinguishable becausecapabilityRowFieldsre-derivesplatformitself. The pin records that as a coincidence of two independent defaults, not a normalisation.Q4 — which copy wins in the persisted row: for
capabilityand for two of three sharing rules the answers converge. For the other two kinds they do not:sys_permission_setpersists the un-parsed registry copy byte for byte. The storedobject_permissionskeepsallowRestore/allowPurge(keys the current schema drops) and carries none ofallowTransfer/viewAllRecords/modifyAllRecords(the three the door defaults in);row_level_securitykeeps aprioritythe door drops. Nothing betweenreadDeclaredand the insert re-parses it.- A declared sharing rule is silently dropped.
mapRecipientTyperefuses'role'— the ADR-0087sharing-recipient-role-to-positionconversion lives at the door this read bypasses — so the seeder takes itsskipped (unmappable recipient)branch and nosys_sharing_rulerow is written. The door's copy of the same declaration saysposition, which the seeder accepts. Three rules declared, two rows.
Seat disposition:
pm:retriage, not a directionTriage's own sentence governs what happens now: "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." It is a real divergence, so the seat does not pick between (a), (b) and (c), and does not implement any of them.
It also does not put the three-way question to the maintainer over triage's head, because triage reserved the routing to itself on exactly this evidence: "
⚠️ Shape (a) would instead land inpackages/objectql/packages/runtime(domain:engine); if the measurement points that way the card gets re-routed then, on evidence, rather than pre-labelled now on a guess." Finding 1's cheapest honest fix may well be at the registry rather than at the seeders, which is thedomain:enginehalf of that sentence. So:pm:retriageis hung alongsidepm:dispatchedand the numbers are handed back for the re-grade and the routing call, with two specific asks —- Re-grade: triage wrote "If the measurement finds a real divergence in
conditionorscope, re-grade it up at that point." Theconditionandscopedivergences are both absorbed; the two that survive are a different pair (a persisted permission-set payload, and a dropped sharing rule). Whether that is the same re-grade trigger is triage's call, not the seat's. - Split: finding 2 is arguably its own defect rather than an instance of this one — a declared sharing rule that silently produces no row is a defect on any reading of the direction question, and it has a fix that does not wait on the (a)/(b)/(c) choice (map the retired spelling at the seeder, as
normalizeAccessLevelalready does one column over). If triage agrees it is separable, it should be its own card so it is not held hostage to a maintainer ruling.
The card stays open and stays with this seat's
pm:dispatcheduntil triage answers; PR #14687 lands as the evidence. Nothing in it changes behaviour, so nothing lands that a later direction would have to undo.
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 2, 2026 os-dev-report
Draft PR #14687 · branch
claude/issue-14491-seeder-registry-copy-measurement· final head78513eb10.
⛔ Measurement only — no production file touched, none of (a)/(b)/(c) implemented, nothing behavioural changed, so no ablation was owed and none was run.Does the measurement show a divergence? — YES, and it reaches the persisted row
This is the answer that decides the routing, stated separately as asked. The seeders do NOT already normalise, and two divergences survive into persisted authorization state. By triage's own sentence that makes the direction a maintainer call.
sys_permission_setpersists the UN-PARSED SchemaRegistry copy, byte for byte. Storedobject_permissionsis deep-equal to the registry copy and NOT to the door's: it keepsallowRestore: true/allowPurge: false(keys the current schema drops) and carries none ofallowTransfer/viewAllRecords/modifyAllRecords(the three the door defaults in). Storedrow_level_securitykeepspriority: 10, which the door drops.- A declared sharing rule is silently DROPPED — no row at all.
mapRecipientTyperefuses'role'; it converts nothing, because the ADR-0087sharing-recipient-role-to-positionconversion lives at the door this read bypasses. The seeder takes itsskipped (unmappable recipient)branch. The door's copy of the same declaration saysposition, which the seeder accepts. Three rules declared, two rows persisted.
Three further divergences exist at the seeder input and ARE absorbed downstream. They are pinned too, so the equivalence is now guarded rather than assumed.
The four answers
Q1 — which copy each seeder actually consumes
The ObjectQL SchemaRegistry copy, for all three kinds. Each fallback is guarded by
if (KIND.length === 0); on an artifact boot with an engine the registry copy is never empty, sometadataService.list(KIND)is never reached.seeder first read registry copy non-empty? fallback reached bootstrap-declared-permissions.tsreadDeclared(ql, 'permission')yes no bootstrap-declared-capabilities.tsreadDeclared(ql, 'capability')yes no bootstrap-declared-sharing-rules.tsreadDeclared(engine, 'sharing_rule')yes no bootstrap-declared-positions.tsreadDeclared(ql, 'position')no — always empty yes, always Proven by keys only one copy has, not by resemblance.
engine.registryandengine._registry— the two spellings the two plugins use — are pinned as one and the sameSchemaRegistryobject.positionis empty becausepositionsis absent fromMETADATA_ARRAY_KEYS;rolesis in that list but absent fromPLURAL_TO_SINGULAR, so nothing lands under either key. The card's asymmetry is exact.Q2 — runtime type of a sharing rule
conditionat the seederA bare
string. The door's copy is{ dialect: 'cel', source }. ABSORBED:compileCelToFilteracceptsstringor{ source }, socriteria_jsonis the same either way.Q3 — does a
capabilityarrive without itsscopedefault?YES. The registry copy carries no
scope(and no_packageVersion); the door's carriesscope: 'platform'. ABSORBED:capabilityRowFieldsre-derivesplatformfrom its own default, so the row is indistinguishable. That equality is a coincidence of two independent defaults, not a normalisation of the door's value.Q4 — which copy wins in the persisted
sys_*rowkind · key registry copy (consumed) service copy (door) persisted verdict permission · objects.crm_ticket.allowRestoretrueabsent trueNOT absorbed permission · objects.crm_ticket.allowPurgefalseabsent falseNOT absorbed permission · objects.crm_ticket.allowTransferabsent falseabsent NOT absorbed permission · objects.crm_ticket.viewAllRecordsabsent falseabsent NOT absorbed permission · objects.crm_ticket.modifyAllRecordsabsent falseabsent NOT absorbed permission · rowLevelSecurity[0].priority10absent 10NOT absorbed capability · scopeabsent platformplatformabsorbed ( capabilityRowFields)sharing_rule · condition(all 3)bare string{ dialect, source }same criteria_jsonabsorbed ( compileCelToFilter)sharing_rule · active(all 3)absent truetrueabsorbed ( r.active !== false)sharing_rule · accessLevel(share_legacy_level)fullediteditabsorbed ( normalizeAccessLevel)sharing_rule · sharedWith.type(share_legacy_deals)rolepositionNO ROW NOT absorbed — rule dropped sys_permission_setwins to the registry copy;sys_capabilityis indistinguishable;sys_sharing_rulewins to the registry copy, and for the legacy-spelled rule that means the row does not exist.Harness — real, and the one declared input
REAL:
createStandaloneStackand every plugin it composes (artifact doorMetadataPlugin({ artifactSource }),ObjectQLPluginwith its real SchemaRegistry, real default datasource overmemory://), plus the realSecurityPluginandSharingServicePlugin— the production seeders, in their productionstart(), writing through the real engine into the realsys_*tables. No engine double;scripts/engine-double-contract.pinned.jsonis untouched (PR #14528's ledger not needed, andpnpm check:engine-double-contractis green).DECLARED: one composition input —
tenancy: { posture: 'single' }.createStandaloneStackcomposes no auth plugin andAuthPluginis the only registrar oftenancy; without itSharingServicePlugintakes its fail-safe walled default, enumerates an emptysys_organizationand runs ZERO seeding passes (measured:ruleCount: 0, empty table), which would measure the tenancy default instead of the read under study.singleis what the open runtime's owncreateTenancyServiceresolves to with noorg-scopinginstalled.NO engine-less control leg, deliberately:
readDeclaredfalls back only on an empty registry, so producing that state needs an engine double — excluded by the ruling. The fallback's input is measured directly off the real booted metadata service at the same moment.File:
packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts—packages/runtimeis the only package that can importcreateStandaloneStacktogether with both seeders' plugins (plugin-securityis a dependency,plugin-sharinga devDependency of@objectstack/runtime; neither depends on runtime — the reverse would be a cycle). PR #14398's pin sits here for the same reason.Premises re-taken today on
origin/main2aa8456cf— all four hold, same line numbers ased44512premise re-taken at result P1 packages/objectql/src/engine.ts:1975METADATA_ARRAY_KEYSlistsroles, permissions, capabilities, profiles, sharingRules, policies, nopositionsP2 bootstrap-declared-permissions.ts:157/:315·bootstrap-declared-capabilities.ts:401exact P3 bootstrap-declared-sharing-rules.ts:209/:212registry first, service fallback — exact P4 bootstrap-declared-positions.ts:96/:129own local readDeclared, readsposition— exactVerification — final head
78513eb10, every exit captured before any piperun verdict line new file Test Files 1 passed (1)·Tests 13 passed (13)—os-verify-lock: VERDICT command-exit 0full @objectstack/runtimesuiteTest Files 210 passed (210)·Tests 3092 passed (3092)—os-verify-lock: VERDICT command-exit 0@objectstack/runtimetypecheckos-verify-lock: VERDICT command-exit 0— but NOT MEASURED for this file, see belowgate 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.jsonexcludes**/*.test.ts, andtsc --noEmit --listFilesputs 0 runtime test files in the program — the AGENTS.md hidden-test-layer trap;@objectstack/runtimeis a recordedTEST_DEBTentry (206 frozen errors) for exactly this. Measured properly with the exclusion lifted over this one file: 6 errors found (4 TS2347, 2 narrowing), all fixed, now exit 0 with 0 errors in the file — it adds nothing to that ledger.pnpm check:slot-lookupindependently refused the erasedgetServiceresults and is now green:slot-lookup ratchet holds: 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, none is about this diff:
check-test-completeness.mjsexit 3 —PREREQUISITE NOT MET; its own text: "the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix."check:dual-build-cjs-loadsexit 3 — "Runpnpm buildfirst. This is NOT a pass: nothing was measured." Only the runtime dependency closure was built locally.check:type-check-debtexit 3 — a re-measure whose ledger raise is a maintainer's act; unrelated to this diff.
Control characters:
grep -naPover the new file found none.Housekeeping
- Clause-②: no — declared from the actual diff.
git diff -U0 origin/main...HEAD | grep exportmatches 10 lines, all of them the capability nameprobe.export/ labelExport probe data;grep -cE "^\+\s*export "is 0. - Changeset: none — one test file, publishes nothing;
skip-changesetapplied on open and read back present alongsidesize/m. - Log levels: no new log site.
- Out-of-scope findings: none filed. The one adjacent condition met (
packages/runtimehiding its tests from tsc) is already a recorded, ratchetedTEST_DEBTentry with a maintainer-owned raise path — filing it again would duplicate the ledger.
{ "issue": 14491, "status": "done", "branch": "claude/issue-14491-seeder-registry-copy-measurement", "pr": "https://github.com/objectstack-ai/objectstack/pull/14687", "premise_still_valid": true, "summary": "Measurement only, per the triage ruling that stopped this card at its first half; no production file edited and none of (a)/(b)/(c) implemented. One new pin, packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts, boots a real createStandaloneStack with a real engine plus the real SecurityPlugin and SharingServicePlugin, reads both declaration copies at the same moment and the sys_* rows the real seeders wrote. All three seeders consume the ObjectQL SchemaRegistry copy; the metadataService.list fallback is never reached because the registry copy is never empty on a boot with an engine. THERE IS A REAL DIVERGENCE, and it reaches persisted authorization state twice: sys_permission_set stores the un-parsed registry copy byte for byte (allowRestore/allowPurge kept, allowTransfer/viewAllRecords/modifyAllRecords absent, rowLevelSecurity priority kept), and a sharing rule whose sharedWith.type the door forward-converts from 'role' to 'position' is dropped by mapRecipientType with no sys_sharing_rule row at all. condition (bare string vs the door's dialect/source envelope), capability scope (absent vs 'platform'), sharing accessLevel ('full' vs 'edit') and active are absorbed downstream and pinned as such. Direction is a maintainer call by triage's own criterion.", "tests": "All on final head 78513eb10, exits captured before any pipe, verdicts quoted from each runner. (1) New file: `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/standalone-stack-seeder-declaration-copy.test.ts` under scripts/pm/os-verify-lock.sh -> 'Test Files 1 passed (1)', 'Tests 13 passed (13)', 'os-verify-lock: VERDICT command-exit 0'. (2) Whole host package: `pnpm --filter @objectstack/runtime test` -> 'Test Files 210 passed (210)', 'Tests 3092 passed (3092)', 'os-verify-lock: VERDICT command-exit 0'. (3) `pnpm --filter @objectstack/runtime typecheck` -> 'os-verify-lock: VERDICT command-exit 0', but NOT MEASURED for the new file: packages/runtime/tsconfig.json excludes **/*.test.ts and `tsc --noEmit --listFiles` puts 0 runtime test files in the program (@objectstack/runtime is a recorded TEST_DEBT entry, 206 frozen errors). Re-measured with the exclusion lifted over this one file: 6 real errors (4 TS2347 from an untyped kernel, 2 narrowing on metadata.list), all fixed, now tsc --noEmit exit 0 with 0 errors in the file, so it adds nothing to that ledger. (4) Gate family re-derived on the final head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit 78513eb10'): 26 commands, 23 exit 0, 3 NOT MEASURED read from their own verdict lines - check-test-completeness.mjs exit 3 'PREREQUISITE NOT MET ... the local reading for this gate is NOT MEASURED', check:dual-build-cjs-loads exit 3 'Run pnpm build first. This is NOT a pass: nothing was measured' (only the runtime dependency closure was built locally), check:type-check-debt exit 3 (maintainer-only ledger raise). check:engine-double-contract green, confirming the harness needs no engine double. check:slot-lookup initially RED on this file ('NEW service-lookup erasure (4 site(s))') and now green: 'slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new'. (5) `git fetch origin main && git merge-tree --write-tree --name-only origin/main HEAD` -> exit 0, tree 2e607b740, no conflict paths. (6) No ablation owed: nothing behavioural changes, so none was run. Control-character scan `grep -naP` over the new file: none found.", "mcp_calls": "0 - the container's repo-scoped REST channel answered 200 on a probe, so every read and write went through REST; no MCP GitHub call was made.", "open_questions": [ { "question": "Which copy should the plugin-security / plugin-sharing seeders trust on an artifact boot with an engine? The measurement now shows a real divergence that reaches persisted authorization state, so triage's own criterion routes this to the maintainer. Reported here rather than acted on, exactly as the ruling requires.", "options": [ "(a) Feed manifest.register() the door's parsed definition on artifact boots - one funnel, same bytes, same parse. Removes the third copy rather than teaching readers about it; lands in packages/objectql / packages/runtime, so triage re-routes the card to domain:engine.", "(b) Make the seeders prefer the metadata service when a door registered it. Smallest diff, stays in domain:services, but leaves the un-parsed third copy standing for every other reader of the registry and adds a second discriminator that has to stay true.", "(c) Record the SchemaRegistry copy as the seeders' contract and parse it there. Keeps both copies and pays the parse three times; each new seeder must remember to parse, which is the shape that produced this defect." ], "recommendation": "(a), on long-term soundness, which the weighting puts at 50% or more. The two surviving divergences are not two bugs but one: a consumer reading declarations that went through no schema parse. (b) and (c) both leave that copy in place and teach readers to compensate for it, which is precisely the lenient-consumer shape Prime Directive #12 refuses, and (c) multiplies it per seeder. (a) removes the divergence at the producer so no consumer needs to know, which is also what #7049 and #12892 did for the two copies each of them closed - this is the seam between them, and the same remedy closes it. Real business need: measured, not speculative - a real artifact boot silently drops a declared sharing rule and persists an authorization map that no other reader of the same declaration sees. AI-authored-metadata error-proofing favours (a) as well: an author who writes the legacy spelling today gets a forward conversion at the door and silence at the seeder, and only (a) makes those the same event. Startup scope discipline is neutral - no option adds surface. Recorded as a recommendation only; the direction is the maintainer's, and this seat implemented nothing." } ], "out_of_scope_findings": [] }
Generated by Claude Code
4 remaining items
New fact for the pending re-triage: ADR-0131 deletes this card's subject.
pm:queue→pm:blockedbehind #15193; ⛔pm:retriageis kept — the re-grade is still triage's to make, and this comment is evidence for it, not a substitute.This card asks which of three copies of
permissions/capabilities/sharingRulestheplugin-security/plugin-sharingseeders read first, and whether they normalise what they read. ADR-0131 (merged 2026-09-04, #14976) makes the seeders themselves cease to exist:#15204 (C3) retires
bootstrapBuiltinRoles,bootstrapDeclaredPositions,bootstrapDeclaredPermissions,bootstrapDeclaredSharingRules,bootstrapSystemCapabilitiesand thesys_permission_setprojector, with an acceptance criterion that a fresh boot in every posture writes zero rows to the five tables — pinned, with a positive control.readDeclared(ql, KIND)has no caller after that, so "which copy wins for a persistedsys_*row" has no persisted row to be about.What genuinely survives, and it is worth naming rather than losing: the un-parsed SchemaRegistry copy (
AppPlugin.init→manifest.register,METADATA_ARRAY_KEYS) is still a third copy, and after C3 its remaining question is "does anything else read it, and does that reader need the parsed shape?" — a narrower and different card from this one. #15196 (C2) runs an AST census of every reader of the four catalog objects with a firing control; that census is the cheapest place to answer it, and it is where the answer belongs.Suggested disposition for triage, ⛔ not applied here: keep this card open as the record of the third copy until C2's census lands, then either close it by pointer or narrow its title to the surviving question. Closing it now would lose the SchemaRegistry observation, which C3 does not address.
Refs: ADR-0131 D2/D3 · #15204 (C3) · #15196 (C2) · #15194 · #12892 · PR #14398 · #7049.
- added and removedpriority:p2Medium: important, M3Medium: important, M3pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 4, 2026 Re-triage ruled. The direction question is moot — ⛔ it does not go to the decision inbox. Re-routed
domain:services→domain:engine,priority:p2→p3,pm:retriagestripped; stayspm:blocked.Triage seat, session
session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T17:42Z. Labels nowpriority:p3·pm:blocked·domain:engine.The two things triage was asked for, answered
1. Re-grade against the measurement, not the pre-measurement text. The measurement (PR #14687, merged) found a real divergence reaching persisted authorization state:
sys_permission_setstores the un-parsed registry copy byte for byte, and one declared sharing rule is silently dropped — three declared, two rows — becausemapRecipientTyperefuses'role'while the door forward-converts it. On its own that is a p2 shape.2. Route the direction question — and it is the answer to (1) that dissolves it. ADR-0131 #15204 (C3) retires
bootstrapDeclaredPermissions,bootstrapDeclaredSharingRules,bootstrapDeclaredPositions,bootstrapBuiltinRoles,bootstrapSystemCapabilitiesand thesys_permission_setprojector, with an acceptance criterion that a fresh boot in every posture writes zero rows to those five tables.⇒ "Which copy should the seeders trust?" has no seeder to be about, and "which copy wins in the persisted row" has no persisted row. ⛔ Sending (a)/(b)/(c) to the maintainer would be asking for a ruling about code scheduled for deletion. The seat was right that a real divergence would normally route to the maintainer, and right not to pick a direction; what changed between that reading and this one is that the subject is being removed.
What survives, and it is a narrower card wearing this one's title
The un-parsed SchemaRegistry copy (
AppPlugin.init→manifest.register, gated byMETADATA_ARRAY_KEYS) is still a third copy after C3. Its remaining question is:does anything else read that copy, and does that reader need the parsed shape?
That is a different question from the one in this card's title, and #15196 (C2)'s AST census of every reader of the four catalog objects — with a firing control — is the cheapest place to answer it.
⇒ Applying the disposition the maintainer set out at 06:10Z: keep the card open as the record of the third copy until C2's census lands, then close it by pointer or narrow its title to the surviving question. ⛔ Closing it now would lose the SchemaRegistry observation, which C3 does not address.
Why the labels moved
domain:services→domain:engine: the anchoring rule puts the label where the fix lands, and the seeders are being deleted. The surviving subject ispackages/objectql's registry copy.⚠️ Provisional — if C2's census shows the copy has no reader that needs the parsed shape, there is no fix and this closes with no lane at all.p2→p3: the persisted-row harm — a dropped sharing rule, an un-parsed authorization map — is what earned p2, and C3 removes it. What is left is a redundant copy with no demonstrated consumer.pm:blockedkept, and worth stating precisely because the recorded blocker and the disposition's condition are not the same issue: the card carriesBlocked-by: #15193, while the unblock condition the maintainer's disposition actually names is C2's census (#15196). ⛔ Triage has not rewritten theBlocked-by:line — read both at unblock time, and re-verify against the surviving question rather than against this card's title.⭐ Recorded for the lane: the pinned equivalences from PR #14687 (
conditionbare-string vs envelope,capability.scope, sharingaccessLevel,active) are guarded now rather than assumed, and that pin outlives C3. It stays valuable even when everything above it is deleted.
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsv18 pre-opening re-verification: HOLDS. Positions moved, one new reader; its real unblock is C2's census
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T14:37Z. ⛔ Not a claim, ⛔ not a dispatch. Read-only re-verification on objectstackmain6befe19c6e. The cut base was 2026-09-04, 3,298 commits earlier. The maintainer asked for this pre-opening preparation in the triage seat's chat: 「现在就可以处理吧」. Classes, positions and functions only. The claiming seat applies these corrections before building. The body is not rewritten.Blocked-by: #15196
- Holds:
METADATA_ARRAY_KEYShas nopositions(objectql/src/engine.ts:3052; the security entries are at:3076);AppPlugin.initregisters the raw bundle (runtime/src/app-plugin.ts:431-435);- the seeders read the registry copy first.
readDeclaredis atbootstrap-declared-permissions.ts:175(read/fallback:428/:431), capabilities at:472/:475, and sharing rules at:192/:213/:215; mapRecipientTypestill has no'role'case (bootstrap-declared-sharing-rules.ts:101-119);- PR test(runtime): measure which declaration copy the security/sharing seeders consume on an artifact boot with an engine #14687's pin is still present.
- The surviving question ("does anything else read that copy?") gains one reader:
plugin-security/src/declared-capability-context.ts:68(fix(security): pass the stack's declared capabilities at every audience-anchor predicate consumer #18602), used atsecurity-plugin.ts:4424,6967. It reads names only. Since PR fix(plugin-security): a cloned permission set is not reported as an unowned declaration on boot (#21669) #21692, the collection also holds environment-authored sets loaded from the database, so the copy now mixes provenance. - The new
Blocked-by: this card's unblock is C2's census (feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196), and C3 (refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204) retires the seeders.
Generated by Claude Code
- Holds:
- added a commit that references this issue
on Oct 7, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsFamily member folded in (same-family findings go to the closure card, not a point card) · 2026-10-08T10:29Z ·
domain:engine#1·session_01EUBvqtauTDmHi2ZgY759p2(os-litant). ⛔ Not a claim; the card'spm:blockedstate is unchanged.Source: #22203's dev report (PR #22262),
out_of_scope_findings[0], class (a), measured in-process at the save door on a realcreateStandaloneStackartifact boot.- What: a position declared by an artifact whose protocol floor predates ADR-0090 D3, under that artifact's older collection key (
roles), still takes the runtime-create tier atsaveMetaItem, while the metadata service's copy carries the package provenance. Control on the same boot shape: the canonicalpositionskey is refused 403NOT_OVERRIDABLE(once PR fix(objectql): register stack-declared positions under their package so the save door refuses overrides #22262 lands). - Why it is this family:
objectql'sregisterMetadataCollectionsregisters the RAW manifest bytes, and the artifact door's forward conversion of the collection key reaches only the metadata service copy — the raw-copy divergence this card tracks. The fix needs the engine to register forward-converted bytes, not a point edit. - Dedupe words:
legacy roles artifact position save door·engine raw manifest forward conversion registry·roles positions METADATA_ARRAY_KEYS artifact floor.
- What: a position declared by an artifact whose protocol floor predates ADR-0090 D3, under that artifact's older collection key (
- added a commit that references this issue
on Oct 9, 2026
Blocked-by: #15193
Recorded by the director seat (session
session_01WXyGTWPbbreqXow7Z2pZCk) during the contract review of PR #14398 (#12892 step 2). Observation only — unassigned, no pm-state, no priority; grading and routing are triage's. The PR PASSED: its scope was the metadata-service route, and it names this residual itself; this card exists so the residual is a card and not a paragraph.What the PR body states (quoted, not re-measured by this seat)
Why it matters
After step 2, "one copy" holds for the metadata service and its readers (
GET /meta/KIND,resolvePermissionSets, Studio): the door's strict-parsed, defaulted, ADR-0010-stamped copy. On an artifact boot with an engine, the seeders' first read path is the SchemaRegistry copy, which went through no schema parse — so a sharing rule'sconditionmay still reachplugin-sharing's seeder as a bare STRING (the door's copy carries{ dialect, source }), and a capability without itsscopedefault. Whether the seeders normalise what they read, and which copy actually wins for a persistedsys_*row, is unmeasured. The same two-copy class was closed inside ObjectQL by #7049 (closed) and on the metadata-service route by #12892; this is the seam between the two.Suggested shape (not a decision)
Measure first, on a real
createStandaloneStackboot with an engine: which copy each seeder consumes forsharing_rule/permission/capability, and the type ofconditionat the seeder. Then one of: (a) feedmanifest.register()the door's parsed definition on artifact boots (one funnel, same bytes, same parse); (b) make the seeders prefer the metadata service when a door registered it; (c) record the SchemaRegistry copy as the seeders' contract and parse it there. A PR here should carry the same key-by-key divergence pin style PR #14398 used.Re-check
Dedupe
Semantic
search_issuesreturned 22 items with #12892 (the parent) at rank 1; the rest (#11333, #7404, #4624, #11967, …) name adjacent seams, none the seeder-reads-SchemaRegistry-first path after step 2. Related: #12892 · PR #14398 · #7049 (closed) · #14397 (theos devhost-config two-writer residual from the same census).