Skip to content

fix(objectql): register stack-declared positions under their package so the save door refuses overrides - #22262

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22203-position-allow-org-override
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22203-position-allow-org-override

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22203
Clause-②: no

What changes

ObjectQL.registerApp() and the nested-plugin seam now register a stack's positions collection into the engine SchemaRegistry under the owning package. They stamp the same ADR-0010 provenance that permissions and capabilities already get (METADATA_ARRAY_KEYS, packages/objectql/src/engine.ts). The metadata save door's existing type-level packaged-base check then covers position the same way it covers the other allowOrgOverride: false types.

Landing: the producer side, in packages/objectql. The save door in packages/metadata-protocol was correct and was given the wrong input, so its code is unchanged. There is no packages/spec edit; the flag was already declared.

Measured: why the type-level check answered for one type and not the other

  • The check is already type-level. The packaged-base check reads the registry's allowOrgOverride in two places: refusePackagedBaseOverride inside saveMetaItem (environment-scoped kernel) and SysMetadataRepository.assertAllowed under the override-artifact intent (host-config kernel).
  • Its input comes from the engine registry. Both decide "a code package ships this item" through isArtifactBacked → lookupArtifactItem → SchemaRegistry.getArtifactItem. That lookup only finds entries a package registered with _packageId.
  • The input was missing for positions. The only seam that puts stack collections into that registry under their package is registerMetadataCollections over METADATA_ARRAY_KEYS. That list had permissions and capabilities. For positions it still had the retired roles spelling: ADR-0090 D3's rename reached ARTIFACT_FIELD_TO_TYPE (packages/metadata/src/plugin.ts) and never reached this list. check:stack-collection-maps recorded the absence as a waiver. The waiver's reason pointed at the metadata service's registry, not the SchemaRegistry.
  • Result. For every stack-declared position, isArtifactBacked('position', NAME) answered false. The save took the runtime-only intent, and allowRuntimeCreate: true accepted it.
  • Where the permission-set 403 comes from. On the showcase, plugin-security's packaged permission-set lock answers first. That lock is registerPackagedPermissionSetLockGate, an authoring gate registered for permission only. Under it, the type-level door also refuses a package-declared permission set. The pin below shows this with no security plugin composed. The lock stays as it is, a stricter type-specific layer on top of the type-level door.

Population of the pin, read from the registry: every domain: 'security' row with allowOrgOverride: false, which today is permission, position and capability.

Door table

Showcase (pnpm dev -- --fresh), host-config kernel, seeded admin. The same requests were sent both times:

  • without fix: this branch with the one added positions entry ablated. objectql was rebuilt, and the preflight proved the change reached dist/.
  • with fix: head 7ed88a6081 (merged origin/main f4bed58341).

The same position, permission, capability and control answers were also measured before the merge, at base 7b926f7600 and head f7d8d0e172.

item request without fix with fix
package-declared position PUT 200, saved 403 NOT_OVERRIDABLE
package-declared position PUT naming the package (?package=) 422 WRITABLE_PACKAGE_REQUIRED 403 ITEM_LOCKED
package-declared position GET by name after the PUT serves the environment row serves the package's position
built-in position (plugin-security) PUT 403 NOT_OVERRIDABLE 403 NOT_OVERRIDABLE (unchanged)
package-declared permission set PUT, with or without ?package= 403 NOT_OVERRIDABLE 403 NOT_OVERRIDABLE
package-declared capability PUT 403 NOT_OVERRIDABLE 403 NOT_OVERRIDABLE
position no package declares PUT 200 200
package-declared dashboard (allowOrgOverride: true) PUT 200 overlay 200 overlay
position list GET /meta/position 16 names 16 names, no duplicates

Boot diagnostics: 4 warnings on both boots, the same four. The seeded sys_position rows carry the declared labels and descriptions.

Pins

  • New: packages/objectql/src/engine-security-catalog-package-door.test.ts. It uses a real ObjectQL, a real ObjectStackProtocolImplementation and the population above, read from DEFAULT_METADATA_TYPE_REGISTRY. For each type it checks:

    • the provenance seam registers the stack-declared item under its package;
    • a save over the package-declared item is refused with envelope { code: 'NOT_OVERRIDABLE', status: 403 } and stores nothing, on both topologies.

    It also checks that the by-name read still serves the package's position after the refusal. Controls:

    • a position no package declares still saves, on both topologies;
    • email_template (allowOrgOverride: true) still saves over its packaged item, on both topologies.
  • Re-measured: packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts. Its positions case asserted the old absence on an artifact that declared no position. The probe artifact now declares one, and the case asserts the registry holds it under the artifact's package beside the six built-ins. This is a real createStandaloneStack boot with SecurityPlugin.

  • Widened: packages/objectql/src/engine-nested-plugin-collections.test.ts. positions joins the property candidates: both seams register it identically.

  • Gate: scripts/check-stack-collection-maps.mjs. The stale missing: ['positions'] waiver on METADATA_ARRAY_KEYS is removed. The gate fails on a stale waiver, and its reason was wrong about which registry it meant.

Reverse verification (ablation)

The run was committed first, then mutated through scripts/ablation-replace.mjs. The mutation removed 'positions' from METADATA_ARRAY_KEYS: anchor count 1 → 0, blob 8465f68a50a1 → c4414cf91029. objectql was then rebuilt, and scripts/ablation-dist-preflight.mjs @objectstack/objectql '"positions"' --absent --source-marker="'positions'" exited 0.

Predicted before running: 4 red in the new pin (seam, both topology refusals, by-name read) and 1 red in the runtime pin, everything else green. Measured exactly that:

  • objectql: Tests 4 failed | 47 passed (51);
  • runtime: Tests 1 failed | 12 passed (13).

Restore leg: blob == HEAD, git diff HEAD empty. objectql was rebuilt, and the preflight in default mode found the marker present in 4 built files with a clean tree. Both suites were green again: 51/51 and 13/13.

The new pin was also run at base 7b926f7600 before any fix existed. Exactly the 4 position cases were red, and the by-name read returned the environment fork.

Local verification

All at head 7ed88a6081 unless noted.

  • pnpm --filter @objectstack/objectql test: 381 files / 7536 tests passed, at 5188b2ad45. The merge brought no objectql, metadata-protocol or core change.
  • pnpm --filter @objectstack/objectql typecheck and pnpm --filter @objectstack/runtime typecheck: OK, test layers included.
  • Post-merge, targeted runs:
    • objectql pin, seam and capability-provenance suites: 58/58;
    • runtime standalone-stack, standalone-stack-seeder-declaration-copy, standalone-stack-security-registrar and app-plugin-artifact-forward-conversion: 48/48;
    • verify artifact-collections: 8/8;
    • pnpm --filter @objectstack/plugin-security test: 3739 passed, 45 skipped;
    • dogfood security-catalog-showcase, showcase-declarative-rbac-seeding, position-address-readers and rls-runner: 53/53. These resolve dist/, rebuilt at this head.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands gives 86 families, all run at 7ed88a6081 and reconciled with --ran.
    • 84 exit 0.
    • check-empty-changeset exits 1, by design. See the next section.
    • pnpm check:pm-dispatch-gates: exit 0, with all 1976 self-test cases passing. The 900 s per-gate cap in the batch runner killed it first, so it was re-run on its own with its exit code captured.
  • ESLint, narrowed:
    • What was checked: the 5 changed .ts/.mjs files, at 7ed88a6081.
    • Result: --format json reports 5 files linted, 0 errors, 0 warnings, none ignored.
    • Why the narrowing excludes nothing: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot change the verdict on any file it does not touch.
  • Integration tier, full lint and the rest of the farm: declared to CI.

A pending release note this PR corrects: please confirm

.changeset/15196-core-security-catalog-read.md (pending, @objectstack/core) says the engine registry carries "no stack-declared position, and the metadata service carries the stack-declared positions". This PR makes that sentence false, so the sentence is rewritten in place to say the engine registry also carries stack-declared positions. No other text changed.

check-empty-changeset stays red on it until a person confirms the correction. That is the gate's own route for a deliberate correction, and the file is not restored. If the seat prefers, the correction can be split into its own docs-only PR instead.

For #22220 (serial, same region)

This PR does not touch saveMetaItem, refusePackagedBaseOverride, packagedBaseRefusal or SysMetadataRepository.assertAllowed. The override check keeps its position and order, where its inputs come from, and its emitters. What changes is the value of one input: isArtifactBacked(type, name) now answers true for a stack-declared position. So the check now fires for positions where it already fired for permission sets: in saveMetaItem behind environmentId, and in the repository's assertAllowed on a host-config kernel. #22220's reorder of the package door against the authoring gate will see position behave like permission on the type-level path, without the plugin-security authoring lock, which is registered for permission only.

Acceptance notes (noted, not filed)


Generated by Claude Code

claude added 4 commits October 8, 2026 08:01
The metadata save door refuses a save over a package-declared item of a
type with no overlay channel, and decides "a package ships this" from the
engine SchemaRegistry entry the package registered. METADATA_ARRAY_KEYS
carried permissions and capabilities but still carried the retired
`roles` spelling instead of `positions`, so a stack-declared position
had no such entry and a save over it took the runtime-create tier.

Adds `positions` to the provenance seam, drops the stale waiver row in
check-stack-collection-maps, re-measures the seeder declaration-copy
pin on a real artifact boot, and pins every security-domain
allowOrgOverride:false type at the door on both topologies.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 2 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))
  • content/docs/getting-started/quick-start.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))
  • content/docs/kernel/services-checklist.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))
  • content/docs/permissions/authorization.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))
  • content/docs/permissions/permissions-matrix.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sharingRules (literal, a string literal in METADATA_ARRAY_KEYS))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 17 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 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 20ac611edde4da6e30354f252ec6307af2402a2e — the merge of head 7ed88a608124b912c391119d1b2d0e111ee7d5cf into base 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 20ac611edde4da6e30354f252ec6307af2402a2e && git checkout 20ac611edde4da6e30354f252ec6307af2402a2e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a 7ed88a608124b912c391119d1b2d0e111ee7d5cf && git checkout -B drift-repro 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a && git merge --no-ff 7ed88a608124b912c391119d1b2d0e111ee7d5cf

node scripts/docs-audit/affected-docs.mjs --json 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3513ac77814f5e6eb530c7b92e4797b4ae6b5e0a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7ed88a608124b912c391119d1b2d0e111ee7d5cf
Local-runs: none

Inputs: card #22203 (body; comments 6054396188 triage, 6055084143 claim, 6057851358 os-dev-report), PR #22262 (body, its 7-file list, the net diff against main — merge base f4bed58341, +326/−18), and the 34 check-runs on the head. Every required context is green (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). One red, Check Changeset, whose annotation names .changeset/15196-core-security-catalog-read.md under the #17712 foreign-changeset rule — judged in ③. main code is cited at the fetched tip 8cbe255ef6. No governed surface in the file list; head repo equals base repo; 344 changed lines.

① Derived judgments

  1. 'positions' added to METADATA_ARRAY_KEYS (packages/objectql/src/engine.ts), 'roles' kept — right. registerMetadataCollections is the one body both seams run (registerApp and the nested-plugin seam), and its registerItem(pluralToSingular(key), item, 'name', ownerId) is the only seam that stamps ADR-0010 _packageId / _provenance: 'package' on a stack collection. ObjectStackDefinitionSchema declares positions (packages/spec/src/stack.zod.ts:466, ADR-0090 D3), metadata-collection.zod.ts:85 carries the plural, and ARTIFACT_FIELD_TO_TYPE (packages/metadata/src/plugin.ts:140) already maps positions to position — the engine list was the one enumeration the D3 rename never reached. 'roles' stays: no schema-valid stack carries the key, it is waived under the gate's extra DRIFT row with six other retired kinds, and removing it is Six independent enumerations of the stack-collection set, none answerable to stack.zod.ts — one is mis-aimed and two still list retired kinds #6242 row 3's lane. Keeping it is scope discipline.

  2. The save door's accept set narrows, exactly as the type registry already declared — right. isArtifactBacked(type, name) (packages/metadata-protocol/src/protocol.ts:16424) is lookupArtifactItem over SchemaRegistry.getArtifactItem, which answers only an entry carrying a real _packageId. With the entry present, refusePackagedBaseOverride (inside saveMetaItem, environment-scoped kernel) and SysMetadataRepository.assertAllowed under the override-artifact intent (host-config kernel) refuse a save over a package-declared position with 403 NOT_OVERRIDABLE, because position is allowOrgOverride: false (packages/spec/src/kernel/metadata-plugin.zod.ts:1117, the 2026-08-08 rollback ee58392e1; ADR-0005's amendment: artifact-backed item on a type without the flag is not_overridable). The door code is untouched — the net diff has no hunk under packages/metadata-protocol. The refusal set is read off the registry (domain: 'security' with allowOrgOverride: false — permission, position, capability) and the new pin enumerates it the same way, on both topologies, with three controls (the runtime-create tier, an email_template overlay, the by-name read). The PR's door table measures 200 → 403 on the showcase; its ?package= row (422 WRITABLE_PACKAGE_REQUIRED → 403 ITEM_LOCKED) is the named-read-only-base limb the door already answers for permission: a refusal recoded truthfully, not an acceptance withdrawn.

  3. The /meta read envelope follows the door — right, and consumer-visible. servedLockState (packages/metadata-protocol/src/protocol.ts; the /meta read envelope and the diagnostics tile call it with isArtifactBacked) derives editable / deletable / lock from packagedBaseRefusal, so a stack-declared position now reads editable: false where it read editable: true. Route rule 4 (machine-readable surfaces must not lie) wants exactly this; the changeset does not name it, which is acceptable because the envelope is defined to mirror the doors ("a door that moves moves this read with it").

  4. A pre-fix environment fork stays removable — right (repair path). A row saved under a package position's name before this fix hydrates into the bare slot and still wins registry.getItem (ADR-0005 precedence). After this PR it cannot be edited (403), but refusePackagedBaseRemoval exempts position through mergesOverlayAtRead (supportsOverlay: true), so DELETE /api/v1/meta/position/NAME removes the row and removeRuntimeShadow restores the package's definition. No lock-out. The dev's S2b heads-up is accurate: the diff neither adds nor removes a refusal for built-in names, which registerBuiltinPositions registers independently of this list.

  5. The security catalog read (packages/core/src/security/security-catalog.ts) — right; names unchanged, source changes. list and resolve ask the registry first and the metadata service only for names the registry does not hold. A stack-declared position now answers source: 'registry' with packageId from the entry's _packageId instead of source: 'metadata'; the NAME set is unchanged because AppPlugin's SECURITY_FIELDS → metadata.registerInMemory (packages/runtime/src/app-plugin.ts:1007) and the artifact door's ARTIFACT_FIELD_TO_TYPE still fill the metadata-service copy, and the reader dedupes by name. The dogfood pin packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts holds names == declarations == door in three postures and its header says a position reaching the engine registry must not turn it red; Dogfood Regression Gate is green on this head. The reader is unreleased (its note is the pending 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 changeset), so this needs no @objectstack/core changeset — it needs the note corrected, which is ③'s subject.

  6. bootstrapDeclaredPositions (plugin-security) — right; rows unchanged. It seeds from the catalog read (readDeclaredPositions), projects label / description only (Audit sibling declared-metadata↔record two-store types (sys_position, sys_sharing_rule, sys_capability) per ADR-0094 addendum #2909 T2) and excludes the six built-ins by name. The registry copy is the raw stack bytes under the owning package; the metadata-service copy is the same bytes (in-memory registration) or the forward-converted artifact copy, whose conversion rewrites the collection KEY (roles → positions), not item fields. Same names, labels and descriptions; the dev measured the seeded sys_position rows and ran the plugin-security suite; Test Core is green.

  7. Other registry readers that now see stack positions — judged, none wrong. readDeclared(ql, 'position') has no caller on main (permission and capability only); listItems('position') outside the catalog read has no non-test caller. unregisterItemsByPackage and isPackageDisabled now cover a stack's positions as they cover its permission sets (a disabled package's positions leave the registry list; the catalog read already hides owner-disabled definitions, and the metadata-service copy carries no owner, so the catalog's answer for a disabled package is unchanged). narrowObjectsToPackageClosure (packages/lint/src/runtime-gate.ts) filters objects only. Cross-package collision at registerItem: on main the per-item cross-package throw is retired (registry.ts:3603), so two packages declaring one position name both register and the by-name read answers the first-registered — the answer the catalog read pins today. PR feat(objectql)!: positions, permission sets and capabilities hold one name per deployment — a second holder is refused at registration, naming both #22197 (card feat(objectql,metadata,runtime)!: refuse a package whose position, permission set or capability name is already held by an installed package, the environment catalog or a built-in (ruling Q4 = A on #15196; narrows ADR-0048 §3.4) #22135; open, not in this head) will refuse the second holder for position as ADR-0048 §3.4 now narrowed (f4bed58341, this PR's merge base) decides; this PR is what lets that gate see a stack-declared position at all — the ADR's intent, not a side effect to escalate.

  8. Public surface: no export, key, route or accepted value is added or removed; METADATA_ARRAY_KEYS is module-private. The one measured accept-set change is the refusal in 2, on a payload the published registry excluded already.

  9. Tests and the edited gate script — right. The new pin boots a real ObjectQL and a real ObjectStackProtocolImplementation over an in-memory driver that honours the caller's limit (the 5188b2ad45 fix for check:objectql-double-limit); the dev reports base-red / head-green and a committed-state ablation (anchor 1 → 0, dist preflight, 4 + 1 predicted reds, restore leg) with numbers; I did not re-run any of it — Test Core green is the gate verdict. engine-nested-plugin-collections.test.ts adds positions to the two-seam candidates (both seams run one body). standalone-stack-seeder-declaration-copy.test.ts re-measures the [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 positions case on a probe artifact that now declares one position and asserts the registry copy under the artifact's package beside the six built-ins; the old comment attributed "none of the stack's" to ADR-0131 D2, which in fact puts declared positions in the registry — the new text is the correct reading. scripts/check-stack-collection-maps.mjs: the waiver list is a ratchet ("a waiver that no longer applies FAILS"), so the missing: ['positions'] row had to leave once the key joined; the replacing comment records why its reason was wrong (it named the metadata service's registry, not the SchemaRegistry the save door reads). Lint & Repo Gates green covers the gate and its self-test.

② Semver level

.changeset/22203-position-package-door.md: @objectstack/objectql: patch, summary fix(objectql): …, Clause-②: no — right; the PR body carries the same bare line at the start of its second line.

  • Clause ② asks whether the card widens an accept set or expands the public surface (scripts/pm/clause2-line.mjs). Nothing is added — no key, export, route or accepted value (① 8). no is the truthful value.
  • The arm. The diff narrows what the /meta save door accepts (① 2), and the arm rule reads a narrowing as breaking. I judge the bare no right because the narrowed set is one the published contract already excluded: DEFAULT_METADATA_TYPE_REGISTRY (a published export of @objectstack/spec/kernel) has declared position allowOrgOverride: false since 2026-08-08, ADR-0005's table names the artifact-backed / no-flag case not_overridable, and triage graded the 200 as "the defect" and the fix as "execution, not a decision". The repo's precedents for this shape — a door that leaked past an already-declared rule, brought back to it — are graded the same way: the org-override-registry-gate: the field overlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743 field fix (the identical mechanism, isArtifactBacked blind to one type so allowOrgOverride: false was never reached; released under @objectstack/metadata-protocol Patch Changes) and the pending .changeset/22113-revert-package-code-shipped-members.md (ADR-0070 D2 read-only packages; publish 200 → 422; patch, bare Clause-②: no). The pending (narrowing) stock (22032, 22046, 22168, 22201, 15196-builtin-positions-declared-metadata) each tightens or newly declares a rule at a door; none restores one already declared. The changeset still carries the consumer's prescription ("create a position with a different name"), the half of a breaking note that matters, so a reader of either reading is served.
  • Level: a fix( that changes no public surface stays patch (the WHICH LEVEL ruling, 2026-09-04). @objectstack/runtime moves a test file only; plugin-security and core move no source. No ADR-0087 disposition is owed on a non-breaking changeset.
  • Check Changeset's red is the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 foreign-changeset rule alone (its annotation), not the level axis.

③ Boundary flags

The deliberate correction — .changeset/15196-core-security-catalog-read.md (card #15196, @objectstack/core: minor, pending): CONFIRMED, option A — keep it in this PR

Before (one sentence of the "Where it reads" bullet): “Neither holds the whole catalog: the engine registry carries the platform's own permission sets and every package manifest's catalog items but no stack-declared position, and the metadata service carries the stack-declared positions but not the platform's permission sets.”

After: “Neither holds the whole catalog: the engine registry carries the platform's own permission sets and every package manifest's catalog items, stack-declared positions included, and the metadata service carries the security collections an app registers in memory but not the platform's permission sets.”

  • Is the old sentence false once this PR lands? Yes. "but no stack-declared position" is exactly what ① 1 reverses: registerApp now registers a stack's positions into the engine SchemaRegistry under its package — pinned on a bare engine by the new objectql test's seam case and on a real createStandaloneStack boot by the runtime [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 pin. The sentence's other clause ("the metadata service carries the stack-declared positions") stays true, and the new wording keeps that fact.
  • Is the new sentence true against the diff and main? Yes. "every package manifest's catalog items, stack-declared positions included" — true for the positions key, the only spelling ObjectStackDefinitionSchema accepts (stack.zod.ts:466); the dev's measured carve-out (an artifact whose floor predates ADR-0090 D3 and spells roles, converted only on the metadata-service copy) is not a positions declaration and is handled below. "the metadata service carries the security collections an app registers in memory" — true: AppPlugin's SECURITY_FIELDS → metadata.registerInMemory (app-plugin.ts:1007–1100) and, under the artifact-door registrar, the artifact loader's ARTIFACT_FIELD_TO_TYPE; the phrase is the module doc's own (security-catalog.ts: "the stack-declared security collections an app registers in memory"). "but not the platform's permission sets" and "Neither holds the whole catalog" — unchanged facts, still true: the metadata service lacks the platform's bootstrap sets, and the registry lacks loader-held and legacy-key items.
  • Does it change anything else the note says? No. The diff is +1 / −1 on that line; frontmatter, level, Clause-②: yes (widening), the precedence bullet, the "what it does not answer" bullet and the loud-failure bullet are byte-identical. (Aside for the 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 carrier, not this PR's doing: the note's last line "Nothing calls the reader yet" is already stale on main — bootstrap-declared-positions.ts and grant-permission-set-name-backfill.ts call it; the same carrier takes the stale security-catalog.ts module doc and its construction TypeError text, as the dev noted.)
  • The red Check Changeset is the gate's own DELIBERATE CORRECTION class — its annotation on this head reads "do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back". Option B (a docs-only PR) would let the false sentence ship if the two landings reorder. This record is the confirmation the gate routes to.

Other dev flags (6057851358 deviations, open_questions, out_of_scope_findings, seat_heads_up_s2b)

Implemented-by: claude/issue-22203-position-allow-org-override
Reviewed-by: session_01EUBvqtauTDmHi2ZgY759p2

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing note: Check Changeset is red by design on this head · 2026-10-08T11:15Z

domain:engine#1 · session_01EUBvqtauTDmHi2ZgY759p2 (os-litant). Recorded before the queue, as the landing rule for a by-design red requires.

  • The gate and its cause: Check Changeset (scripts/check-empty-changeset.mjs) refuses this PR in its DELIBERATE CORRECTION class: the PR rewrites one sentence of 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's pending .changeset/15196-core-security-catalog-read.md, which this PR makes false. The script's own remedy text is "do NOT restore it -- say so on the PR and get it confirmed".
  • The confirmation: the at-tier contract review on this head, 6058620975, names the note and judges the rewritten sentence (old clause false once positions registers under its package; new sentence true against the diff and main; nothing else in the note changes).
  • Why it does not block the queue: the job runs in pr-automation.yml, triggered by pull_request only, never merge_group, and it is not in the main ruleset's required set. Every required context is success on 7ed88a6081.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 11:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 0b997ea Oct 8, 2026
35 of 36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22203-position-allow-org-override branch October 8, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants