Skip to content

fix(spec,metadata-protocol)!: the /meta door no longer authors sharing rules; Setup lists declared sharing rules - #24599

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-24550-sharing-rule-meta-door
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-24550-sharing-rule-meta-door

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #24550
Clause-②: yes (narrowing)

The console half is objectstack-ai/objectui#13050 (Setup's declared-rules page). This PR is the server half: it closes the third authoring door and adds the Setup nav entry that page needs.

What changed

1. The /meta door no longer authors sharing rules (BREAKING narrowing, launch-window minor).
sharing_rule had no metadata-type registry entry. Both /meta doors read that absence as "runtime-creatable": GET /meta/types advertised allowRuntimeCreate: true, and PUT /meta/sharing_rule/:name stored an environment-wide item that GET /sharing/rules then served and enforced. That item bypassed the same-name guard and the organization wall.

  • Mechanism, measured. The position and permission-set seal (refusePackagedBaseOverride, allowEnvironmentOverlay: false) refuses writes over a packaged name only, and both types keep allowRuntimeCreate: true. So it cannot refuse a new name. The security-catalog namespace rule judges package registration, not a /meta save. The mechanism that fits is the code-only pair capability uses (capability 无 DEFAULT_METADATA_TYPE_REGISTRY / schema 条目 —— PUT /api/v1/meta/capability/:name 接受任意 JSON 且 /meta/types 报 allowRuntimeCreate:true(与 #5271 的 api 同形) #5961): sharing_rule joins MetadataTypeSchema and DEFAULT_METADATA_TYPE_REGISTRY with allowRuntimeCreate: false and allowEnvironmentOverlay: false. Its SharingRuleSchema binding moves into the kind map. The listing and the save door now read one declaration.
  • The refusals, on every kernel, in publish and ?mode=draft:
    • a new name gets 403 NOT_CREATABLE;
    • a declared rule's name gets 403 NOT_OVERRIDABLE.
  • What they say. sharing_rule gains an ADR-0126 Regime C row in PACKAGED_BASE_REGIME: the clone is POST /api/v1/sharing/rules, and the switch is POST /api/v1/security/_activation/sharing_rule/:name. Both refusals name the two sanctioned paths. Neither prescribes the OS_METADATA_WRITABLE hatch, because for a Regime C type the hatch would re-open the source the regime closes. Both stay under the REST door's 500-character bound (truncateClientMessage). The first cut did not: the showcase boot showed the switch route cut off, and it was fixed.
  • ADR-0087: semantic entry sharing-rule-runtime-create-withdrawn. The changeset carries FROM → TO and the one-line fix: "author an organization rule, or clone the declared one".

2. The Setup nav entry. plugin-sharing adds nav_declared_sharing_rules, a type: 'url' entry like nav_permission_sets. It points at /apps/setup/metadata/sharing_rule?scope=environment and sits beside nav_sharing_rules, with the same manage_platform_settings requirement. Its labels are in platform-objects' Setup translations (en, zh-CN, ja-JP, es-ES), with source hashes recorded, the same way the plugin's other entries are labelled.

Derived artifacts: meta-url-data.generated.ts, the reference docs, the metadata-form i18n bundles (translated by hand in the three locales), the engine-double ledger, and the overlay-table count in metadata-lifecycle.mdx (with a sharing_rule row).

Item 3: measured, not built

  • The ledger's reading on /meta/sharing_rule items. The metadata protocol reads sys_metadata_activation for no type. /meta/position, /meta/permission, /meta/flow and /meta/action carry no ledger state either. Adding it for one type is a new cross-layer seam, so it does not meet "removes a client read without a new surface". Not built. Measured on the showcase: after the switch-off, the /meta item still reads active: true.
  • GET /sharing/rules/:name for a switched-off declared rule. It answers 404 RULE_NOT_FOUND (measured on the showcase). getRule falls through to declaredRules(), which drops ledger-off rules. That filter is right for enforcement, because listRules feeds every enforcement path. Reported only, with a recommendation in the report.

Verification (head 47f50805; the gate battery ran at 03ec6356, and only the changeset text and one CLI test changed after it — their families were re-run at 47f50805)

  • HTTP-door pin. packages/runtime/src/meta-sharing-rule-door.test.ts drives the real HttpDispatcher, protocol and repository on both kernel shapes. It covers a new name (publish and draft): 403 NOT_CREATABLE, no row, nothing registered, both routes named, under 500 characters. It covers a declared name: 403 NOT_OVERRIDABLE, no row. It covers /meta/types: allowRuntimeCreate: false. The control is a new position, which still saves (200). Result: 10/10.
  • Ablation. The registry entry was deleted through ablation-replace.mjs, spec was rebuilt, and ablation-dist-preflight --absent confirmed the marker was gone from 232 built files. Then 6 red / 4 green: the create cases answered 200 "Saved sharing_rule 'probe_meta_rule' (env-wide, state=active)", and the listing returned allowRuntimeCreate: true. That is the reported defect, reproduced. The declared-name case stayed green under ablation, because the package door already sealed declared names before this change. The restore was confirmed by a blob hash match and a clean tree, then a rebuild with the marker present in 16 files: 10/10 green.
  • Showcase boot (pnpm dev -- --fresh):
  • Tests:
    • spec: full suite, then the re-judged files, 216/216.
    • metadata-protocol: full suite, 20725 passed.
    • platform-objects: full suite, then the two re-judged files, 73/73.
    • plugin-sharing: full suite.
    • objectql: protocol-meta*, 100/100.
    • rest: meta-unknown-type-read-refusal, 21/21.
    • runtime: the new pin plus meta-managed-content-seal.
    • CLI: the unit-tier registry-parity tests, 40/40. The integration-tier metadata-type-schema-gate (8/8, after classifying sharing_rule as gated at sharingRules:) and organization-ownership-apply were run locally because they enumerate the registry.
    • metadata, rest, runtime, lint and client: the 16 files that read the registry, 364/364.
  • Typecheck: spec, metadata-protocol, plugin-sharing, platform-objects, runtime and objectql, all clean.
  • Gates: every family in dispatch-gates.mjs --commands, plus the 34 artifact-roster rows, with exit codes captured before any pipe: 158 of 164 exit 0. The other six are NOT MEASURED, not findings: four PR-context guards that read the workflow event (check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths, check-governed-queue-guard), and two refusals on this shallow clone (check-engine-split-ratio, check-plugin-teardown-shape --self-test). On the first battery, three real reds were fixed: check:engine-double-contract (ledger --write), check:i18n (bundles regenerated, then translated), and check:overlay-whitelist-table (registry count 28 → 29, plus a row).

Acceptance notes

  • The hatch. OS_METADATA_WRITABLE=sharing_rule still opens a create, as it does for every code-only type. It is not prescribed in any refusal. It is also the repair path for an item stored through the old door on an environment kernel, where DELETE /meta/sharing_rule/:name otherwise answers 403 NOT_CREATABLE. A host-config kernel deletes such an item without it (measured).
  • Items already stored through the old door stay served until deleted, so a database created by 18.0 keeps its data (R2). Nothing rewrites them.
  • File surface. The claim named plugin-sharing/** and "the metadata door's seal". The seal lives in packages/spec (the registry) and packages/metadata-protocol (the refusal text and the regime row). The nav labels live in packages/platform-objects. The derived ledgers and docs follow from those. Tests re-judged: spec, metadata-protocol, objectql and platform-objects pins that asserted the open door or a registry count. Nothing in plugin-webhooks changed, and feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529 touches none of these files.

Generated by Claude Code

claude added 10 commits October 19, 2026 13:40
…oned paths whole, under the REST door's 500-character bound

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
…ata forms, engine-double ledger, overlay table)

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
…d the kind union beside the narrowing

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

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/platform-objects, @objectstack/plugin-sharing, @objectstack/spec, touching 38 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/meta-spelling/metadata-url-spelling.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/ai/connect-mcp.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/api/error-catalog.mdx (via /api/v1/meta/types (route, a path literal in acceptanceCriteria))
  • content/docs/concepts/metadata-lifecycle.mdx (via DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object), ObjectStackProtocolImplementation (symbol, a top-level class), sharing_rule (symbol, a field of const object BUILTIN_METADATA_TYPE_SCHEMAS; a field of const object PACKAGED_BASE_REGIME; a field of const object UNREGISTERED_KIND_SCHEMAS; a field of const object enMetadataForms; a field of const object esESMetadataForms; a field of const object jaJPMetadataForms; a field of const object zhCNMetadataForms), sharing_rule (literal, a string literal in DEFAULT_METADATA_TYPE_REGISTRY; a string literal in META_URL_TO_SINGULAR; a string literal in MetadataTypeSchema; a string literal in REGISTRY_DECLARED_META_TYPES), /api/v1/meta/types (route, a path literal in acceptanceCriteria), /api/v1/sharing/rules (route, a path literal in CODE_ONLY_TYPE_REMEDY; a path literal in a comment in DEFAULT_METADATA_TYPE_REGISTRY; a path literal in acceptanceCriteria; a path literal in replacement; a path literal in sharing_rule; a path literal in surface), /meta/sharing_rule/:name (route, a path literal in a comment in MetadataTypeSchema))
  • content/docs/deployment/validating-metadata.mdx (via sharing_rule (symbol, a field of const object BUILTIN_METADATA_TYPE_SCHEMAS; a field of const object PACKAGED_BASE_REGIME; a field of const object UNREGISTERED_KIND_SCHEMAS; a field of const object enMetadataForms; a field of const object esESMetadataForms; a field of const object jaJPMetadataForms; a field of const object zhCNMetadataForms), sharing_rule (literal, a string literal in DEFAULT_METADATA_TYPE_REGISTRY; a string literal in META_URL_TO_SINGULAR; a string literal in MetadataTypeSchema; a string literal in REGISTRY_DECLARED_META_TYPES))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/kernel/services-checklist.mdx (via getMetaTypes (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/permissions/sharing-rules.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/plugins/adding-a-metadata-type.mdx (via BUILTIN_METADATA_TYPE_SCHEMAS (symbol, a top-level const object), DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object), getMetaTypes (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/protocol/kernel/error-handling.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/protocol/objectui/concept.mdx (via DEFAULT_METADATA_TYPE_REGISTRY (symbol, a top-level const object))
  • content/docs/ui/setup-app.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))

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

  • content/docs/releases/v12.mdx (via MetadataTypeSchema (symbol, a top-level const))
  • content/docs/releases/v15.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), /api/v1/sharing/rules (route, a path literal in CODE_ONLY_TYPE_REMEDY; a path literal in a comment in DEFAULT_METADATA_TYPE_REGISTRY; a path literal in acceptanceCriteria; a path literal in replacement; a path literal in sharing_rule; a path literal in surface))
  • content/docs/releases/v17/17-1.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/releases/v17/17-2.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))
  • content/docs/releases/v17/17-6.mdx (via manage_platform_settings (literal, a string literal in SharingServicePlugin))

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
  • 1 changed file(s) yielded no anchor (packages/spec/src/meta-spelling/metadata-url-spelling.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 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 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from dbd5ed99b94cf82671c2ee2278362df16ebd7455 — the merge of head b8e0baa72a41607e5aa7bc27f1d3dfff96f44d68 into base 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc, 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 dbd5ed99b94cf82671c2ee2278362df16ebd7455 && git checkout dbd5ed99b94cf82671c2ee2278362df16ebd7455
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc b8e0baa72a41607e5aa7bc27f1d3dfff96f44d68 && git checkout -B drift-repro 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc && git merge --no-ff b8e0baa72a41607e5aa7bc27f1d3dfff96f44d68

node scripts/docs-audit/affected-docs.mjs --json 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc

⚠️ 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 65a8eeb7d43d9248aa82ee698b78e87a2dce2ffc → 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: 47f5080549d908b875a144ea683bdffa88683442
Local-runs: none
Scope: packages/spec/src/kernel/metadata-plugin.zod.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/meta-spelling/meta-url-data.generated.ts, packages/spec/src/meta-spelling/metadata-url-spelling.ts, packages/spec/src/migrations/entries/semantic/18.sharing-rule-runtime-create-withdrawn.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/packaged-base-regime.ts, packages/metadata-protocol/src/protocol.code-only-types.test.ts, packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts, packages/metadata-protocol/src/protocol.recovery-doors-emit-mutation.test.ts, packages/plugins/plugin-sharing/src/sharing-plugin.ts, packages/runtime/src/meta-sharing-rule-door.test.ts, packages/objectql/src/protocol-meta.test.ts, packages/cli/test/metadata-type-schema-gate.test.ts, packages/spec/src/kernel/metadata-create-seeds.test.ts, packages/spec/src/kernel/metadata-type-schemas.test.ts, packages/spec/src/shared/metadata-url-spelling.test.ts, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.ts, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.source-hashes.ts, packages/platform-objects/src/apps/translations/bare-type-display-echo-decisions.test.ts, packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts, scripts/engine-double-contract.pinned.json + examples/app-showcase/src/coverage.ts, examples/app-showcase/test/coverage.test.ts, packages/objectql/src/engine-security-catalog-package-door.test.ts, packages/objectql/src/package-disable-enforcement.test.ts, packages/runtime/src/domains/meta.ts, packages/runtime/src/app-plugin.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/platform-objects/src/apps/setup.app.ts, packages/plugins/plugin-sharing/src/sharing-rule-service.ts, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts, packages/cli/src/utils/organization-ownership-apply.ts, packages/cli/src/commands/migrate/organization-ownership.ts, packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts, packages/qa/dogfood/test/upgrade-harness/populate-fixture.ts, packages/qa/dogfood/test/upgrade-harness/fixture-18.0/manifest.json, .github/workflows/ci.yml, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs

Reviewer: an isolated at-tier reviewer subagent of the adopting seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3), read-only: the diff origin/main...refs/review/pr-24599 (merge-base 04669a46), the card #24550 with claim 6268404270 and os-dev-report 6271296954, objectui#12982's report 6268367666, ruling A 6258883445, the #24421 Landed record 6260568838, PR #24428, rulings 6266750393 and 6270035914, and the head's check-runs. The head did not move during the review. Job logs were unreachable (the REST logs endpoint redirects to Azure blob storage the egress proxy refuses), so the two test reds are located from the check-run annotations plus the code they name; the type-check red carries its TS error in its annotation.

① Derived judgments

Gate verdicts on this head (they ARE the verdicts). 41 check-runs. Four failure: Type Check · consumer gates (step "Type check example apps": TS2741 Property 'sharing_rule' is missing in type … but required in type 'Record of MetadataType to KindCoverage'), its aggregate TypeScript Type Check, Test Core (4/6) (examples/app-showcase pnpm run test exit 1) and Test Core (5/6) (packages/objectql pnpm run test exit 1). Test Core (3/6) was in_progress and the Test Core aggregate queued at review time. Green: Check Changeset, Lint & Repo Gates and every Lint lane, Governed Surface Queue Guard, the three claim/closing guards, Spec property liveness, Build Core, Build Docs, Type Check · workspace / source gates / debt ledger, Test Core 1/6, 2/6, 6/6. Skipped by design on a pull_request event: Console Pin Gate, the Dogfood Regression Gate shards (ci.yml:2037), Dogfood Verify CLI, Temporal, packed-tarball. The PR is draft: true, mergeable_state: blocked.

(a) The door — closed, on both kernel shapes, with ADR-0112 codes. sharing_rule enters MetadataTypeSchema (metadata-plugin.zod.ts:190) and DEFAULT_METADATA_TYPE_REGISTRY with allowRuntimeCreate: false, allowEnvironmentOverlay: false, supportsOverlay: false, domain: 'security' (metadata-plugin.zod.ts:1254), the capability pair. saveMetaItem consults isRuntimeCreateAllowed on every topology (protocol.ts:22911-22988, the #5086 block, not behind environmentId): a new name takes codeOnlyCreateError → 403 NOT_CREATABLE; a declared name is isArtifactBacked and registryAllowsOverlay is false → codeOnlyOverrideError → 403 NOT_OVERRIDABLE, hatch open or shut. ?mode=draft goes through the same block. Both messages name the two sanctioned routes and not the hatch: CODE_ONLY_TYPE_REMEDY (protocol.ts:17643) and the regime-C branch (protocol.ts:17690, 17716) read the new PACKAGED_BASE_REGIME.sharing_rule row (packaged-base-regime.ts:205: clone POST /api/v1/sharing/rules, switch POST /api/v1/security/_activation/sharing_rule/:name). The runtime pin meta-sharing-rule-door.test.ts drives the real HttpDispatcher → handleMetadataRequest → protocol → repository on both kernels and asserts code, both routes, length under 500, no row, nothing registered, plus /meta/types allowRuntimeCreate: false and a position control (200). Measured by the dev with an ablation (6 red / 4 green with the entry removed; the declared-name case stays green because the package door already sealed declared names). I accept the mechanism measurement: refusePackagedBaseOverride / allowEnvironmentOverlay: false refuse writes over a packaged name only, so they could not close a new name; the code-only pair is the fit.

  • Other doors. The runtime meta domain accepts PUT only (domains/meta.ts:1419) plus POST /_migrate-stored (:2072, a rewrite of stored rows, not a create); REST meta-item-write-routes.ts calls the same saveMetaItem. /meta/sharing_rules (plural) folds to the singular and gets the same verdict. rollbackMetaItem / revertCommit / promoteDraftForPublish are covered by enrolling the type in protocol.recovery-doors-emit-mutation.test.ts and protocol.code-only-types.test.ts. GET /meta/types reads the registry arm (getMetaTypes, protocol.ts:8873), so it no longer advertises runtime create — pinned in the runtime test and protocol.meta-types-mint-door-agreement.test.ts.
  • System and package writers are not refused. Declarations reach the registry through registerApp → registerInMemory (app-plugin.ts:1464, ['sharingRules','sharing_rule']), never saveMetaItem; nothing in packages/metadata, app-plugin.ts or registry.ts reads allowRuntimeCreate. The organization door writes sys_sharing_rule rows. The ceremony's door.save writes position/permission, view, email_template and conflict.type (organization-ownership-apply.ts:731,1560,1798,2019) — see ③ for the conflict.type note. No 18.0 code path writes sharing_rule registry items through saveMetaItem; the dev's "none found" holds.
  • Residue. OS_METADATA_WRITABLE=sharing_rule still opens a NEW-name create (isOverlayAllowed, protocol.ts:17551) — unprescribed, dev-flagged, see ③. DELETE /meta/sharing_rule/:name of a stored residue row on an environment kernel answers 403 NOT_CREATABLE without the hatch (deleteMetaItem, protocol.ts:30580-30588); a host-config kernel deletes it. Documented in the changeset.

(b) 18.0 database — R2 holds; the harness proves nothing about this door. The fixture's runtime sharing rule is a sys_sharing_rule row written through POST /sharing/rules (populate-fixture.ts:171-182, name upgrade_harness_red_projects; manifest.json lists sys_sharing_rule; fixture-manifest.ts:46 reshapes it for step 5 only). It holds no sys_metadata row of type sharing_rule, so nothing in the fixture meets the closed door, and the PR touches no sharing-service code or table shape. For an 18.0 install that DOES hold such a row (only new names could exist: the package door already refused declared names on main): loadMetaFromDb hydrates every active environment row through hydrateOverlayIntoRegistry with no type-based skip (protocol.ts:31239-31420; the only withholding is isUnservedSealedOverlay, a row shadowing sealed managed content, which a new-name item is not), and declaredRules lists registry.listItems('sharing_rule') (declared-sharing-rules.ts:269), so the item is still read and enforced — no silent grant drop, no boot refusal, so no step-5 ceremony debt arises here. This is read off the code and the dev's save-time measurement on main; boot-time serving of a stored item at HEAD was not measured. The harness did not run on this head: CI runs the dogfood suite only off pull_request (ci.yml:2037), and the report lists no local run.

(c) The Setup nav entry — present, i18n-labelled, right URL; gating is the sibling's, not nav_permission_sets'. sharing-plugin.ts:629: nav_declared_sharing_rules, type: 'url', url: '/apps/setup/metadata/sharing_rule?scope=environment', icon: 'package', requiredPermissions: ['manage_platform_settings'], between nav_sharing_rules and nav_record_shares in group_access_control at priority 200. Labels in en/zh-CN/ja-JP/es-ES with source hashes (a3371ec9975d5b0f) in the three translated locales; the sharing_rule metadata-form display pair is in all four bundles and the echo-decision pins moved 28→29 / 673→674 with it. The dev's showcase boot measured GET /meta/app/setup serving the entry and its zh-CN label. Gating: nav_permission_sets (security-plugin.ts:1590) carries NO item-level requiredPermissions; Setup itself needs setup.access (setup.app.ts:47) and group_access_control (:74) has none. The new entry requires manage_platform_settings, the same as nav_sharing_rules / nav_record_shares, so a caller with setup.access alone sees Permission Sets but not Declared Sharing Rules. The card asked for nav_permission_sets' SHAPE (type: 'url'), which is met; the stricter gate is a reasoned choice (it matches the organization-rules sibling) — flagged in ③ for the seat, not a defect.

(d) Item 3, answered (measure only, nothing built — correct).

  • Ledger state on /meta/sharing_rule items. The metadata protocol reads sys_metadata_activation for no type (zero references in protocol.ts; /meta/position, /meta/permission, /meta/flow, /meta/action carry no ledger reading either), and a meta item's active is the declaration's authored field (declared-sharing-rules.ts:219), measured true after a switch-off. Decorating one type's /meta items from a runtime table is a new cross-layer seam, so it fails the card's bar ("removes a client read without adding a new surface"). Not built — right. If the console's two reads become a measured cost, the shape is an admin read on the sharing door that lists switched-off declared rules with active: false, leaving listRules / declaredRules() (every enforcement path) untouched.
  • GET /sharing/rules/:name for a switched-off declared rule. getRule (sharing-rule-service.ts:1143-1150) → findRuleRow (no sys_sharing_rule row) → declaredRules (:994), whose source applies readSwitchedOffDeclaredRules (:1067-1095, ledger metadata_type: 'sharing_rule', active: false), so the rule is dropped and the route answers 404 RULE_NOT_FOUND (dev measured on the showcase). The filter is right for enforcement. Recommendation concurred: a follow-up card for an admin-read projection with active: false on the by-name read only, as a Clause-②: yes (widening) change on an existing route, never reaching declaredRules().

(e) Complete feature — not on this head. The server half is whole as a design: the door is closed and pinned on the HTTP transport, the listing agrees, the nav entry is served and opens the console's EXISTING generic Setup list of a registry type in environment scope (the route nav_positions / nav_capabilities / nav_permission_sets already use at the pinned console), so it is not dormant code waiting on objectui#13050 (which enriches that page with Disable/Enable/Clone). But ruling 6270035914 ("only complete features merge") and R1 of 6266750393 ("main stays releasable") are not met by a head whose type check and two test shards are red: the enum widening (MetadataTypeSchema gains sharing_rule) and the domain: 'security' registry entry each enrolled the type in a derived population the PR did not give a fixture, and the dev ran only protocol-meta* in objectql and no example-app suite. Blocking items below. The rendered console page was NOT MEASURED (no console build in the dev's container; Console Pin Gate skipped on the PR).

② Semver level

Clause-②: yes (narrowing) on PR body line 2 and in .changeset/24550-sharing-rule-meta-door-closed.md; the two agree. The claim carried no (narrowing); the dev re-measured and flipped to yes because the diff also widens published surface (the MetadataType union gains a member, META_URL_TO_SINGULAR gains sharing_rules, the Setup nav payload gains an entry, the metadata-forms bundles gain a key) — I judge yes the honest arm. Levels: @objectstack/spec minor, @objectstack/metadata-protocol minor, @objectstack/plugin-sharing minor, @objectstack/platform-objects patch — yes takes at least minor on a moved package: met. (narrowing) is BREAKING: the body carries the BREAKING banner, launch-window minor for a breaking change (the check-changeset-no-major.mjs guard is still in the tree and Check Changeset is green, so the window is open), FROM → TO for both narrowings (the PUT in publish and draft; the /meta/types advertisement), and the one-line fix verbatim: "author an organization rule, or clone the declared one", followed by the clone + switch recipe, the declaration channel, and the residue removal. ADR-0087: exactly one marker, adr-0087: registered sharing-rule-runtime-create-withdrawn, with the semantic entry packages/spec/src/migrations/entries/semantic/18.sharing-rule-runtime-create-withdrawn.ts (surface, replacement, reason, acceptance criteria; naming follows 17.api-runtime-create-withdrawn.ts); check-adr-0087-registration reads the narrowing arm and the dev reports it green at this head. Nit, not a gate matter: the changeset header reads feat(…)! while the PR title reads fix(…)!, and platform-objects: patch although its published TranslationData / metadata-forms payloads gain keys — moot under the fixed lockstep group. Reviewed-at-tier: CONTRACT_REVIEW_TIER (published schema paths in packages/spec, and the narrowing face as asked).

③ Boundary flags

  1. The hatch (dev open question 2, for the maintainer). OS_METADATA_WRITABLE=sharing_rule still opens a new-name runtime create, as for every code-only type; no refusal names it, and it is the only delete path for residue on an environment kernel. sharing_rule grants access, so the seat may want the maintainer's word on whether regime C seals the hatch for this type (a new carve-out in isOverlayAllowed) or keeps one hatch semantics. Not blocking.
  2. Clause-② arm flip from the claim's no (narrowing) to the PR's yes (narrowing) — decided as measured; the seat should record it when adopting.
  3. Item 3b follow-up card (admin-read projection of a switched-off declared rule, yes (widening)) and item 3a's shape C — noted, not filed.
  4. Harness not run on this head. The dogfood suite is full-battery only (ci.yml:2037); the dev reported no local run of upgrade-18x-harness.dogfood.test.ts. The diff touches no reshaped table and the fixture holds no /meta-written sharing rule, so R2 is not at risk from this diff, but "it must stay green if it has landed" is unmeasured until the merge-group or post-merge battery.
  5. Rendered Setup page NOT MEASURED at the pinned console; Console Pin Gate skipped on the PR. objectui#13050 is the console half.
  6. Nav gating is manage_platform_settings (the nav_sharing_rules sibling's), stricter than nav_permission_sets (none) — the card asked for the shape, not the gate; seat's call.
  7. Ceremony conflict.type save (organization-ownership-apply.ts:1798): the metadata-choice fold saves whatever stored type a legacy organization-scoped sys_metadata conflict carries; a sharing_rule item was environment-wide on main, so none is expected, but if one ever is, that save now answers NOT_CREATABLE — the step-5/6 landing (feat(cli): 18.x step 5: the upgrade ceremony steps behind the refusal (webhook re-source, sibling moves, 9c, 8e NOT NULL) #24581/feat(cli,runtime)!: 18.x step 6: open the upgrade ceremony for v17 and 18.0 databases (and the deferred flips) #24582) should account for it.
  8. File surface beyond the claim, declared by the dev: packages/spec, packages/metadata-protocol, packages/platform-objects, docs, the engine-double ledger, re-judged pins in objectql and cli. plugin-webhooks untouched (0 paths; the feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529 fence holds).
  9. Pre-existing, out of scope: metadata-lifecycle.mdx says field rows "remain deletable", which holds on a host-config kernel only (the same environment-kernel NOT_CREATABLE on delete applies to every code-only type).

Blocking items (FAIL):

  • B1 — examples/app-showcase/src/coverage.ts:52. KIND_COVERAGE is a Record keyed by MetadataType; the PR adds sharing_rule to the enum (packages/spec/src/kernel/metadata-plugin.zod.ts:190) and no row, so the example-app type check fails (TS2741 — Type Check · consumer gates, aggregate TypeScript Type Check) and examples/app-showcase/test/coverage.test.ts:98 fails ("new registry kinds not yet in KIND_COVERAGE → sharing_rule") — Test Core (4/6). Fix: add a sharing_rule row — status: 'demonstrated', files: ['src/security/sharing-rules.ts'] (the showcase declares sharingRules; test/inert-wirings.test.ts:680 reads them), notes naming regime C — then run the showcase suite and the example-app type check.
  • B2 — packages/objectql/src/engine-security-catalog-package-door.test.ts:63-64, 71, 217. SECURITY_NON_OVERRIDABLE is derived from domain === 'security' && allowEnvironmentOverlay === false, which the new entry (metadata-plugin.zod.ts:1254) joins; BODIES has no sharing_rule, so :217 fails by name and the it.each rows at :221 and :230 (both topologies) run with no body — Test Core (5/6) (the report ran only protocol-meta* in objectql). Fix: add a spec-valid body to BODIES (type: 'criteria', object, condition, accessLevel: 'read', sharedWith: { type: 'position', value: … }, name: NAME) so the manifest's sharingRules registers through registerApp and the provenance / NOT_OVERRIDABLE rows run as for capability; then run the FULL objectql suite and re-judge packages/objectql/src/package-disable-enforcement.test.ts:88, 238, which now mints a { name, label } sharing_rule item per package off MetadataTypeSchema.options — green or given a body, measured not assumed.
  • B3 — the head's gate verdicts. Every Test Core shard (3/6 was in progress) and both type-check lanes must conclude success on the fixed head, and the PR leaves draft, before this record can be re-rendered PASS.

Implemented-by: claude/issue-24550-sharing-rule-meta-door
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: FAIL

…ty-catalog package-door pin

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
…RAGE covers the sharing_rule kind

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
…or act, not an authoring route

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
…included; unset it after the removal

Claude-Session: https://claude.ai/code/session_013BWJEDZYcJKmZYj6ApCCa7
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b8e0baa72a41607e5aa7bc27f1d3dfff96f44d68
Local-runs: none
Scope: packages/spec/src/kernel/metadata-plugin.zod.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/meta-spelling/meta-url-data.generated.ts, packages/spec/src/meta-spelling/metadata-url-spelling.ts, packages/spec/src/migrations/entries/semantic/18.sharing-rule-runtime-create-withdrawn.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/packaged-base-regime.ts, packages/metadata-protocol/src/protocol.code-only-types.test.ts, packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts, packages/metadata-protocol/src/protocol.recovery-doors-emit-mutation.test.ts, packages/plugins/plugin-sharing/src/sharing-plugin.ts, packages/runtime/src/meta-sharing-rule-door.test.ts, packages/objectql/src/protocol-meta.test.ts, packages/cli/test/metadata-type-schema-gate.test.ts, packages/spec/src/kernel/metadata-create-seeds.test.ts, packages/spec/src/kernel/metadata-type-schemas.test.ts, packages/spec/src/shared/metadata-url-spelling.test.ts, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.ts, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, packages/platform-objects/src/apps/translations/{zh-CN,ja-JP,es-ES}.source-hashes.ts, packages/platform-objects/src/apps/translations/bare-type-display-echo-decisions.test.ts, packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts, scripts/engine-double-contract.pinned.json + examples/app-showcase/src/coverage.ts, examples/app-showcase/test/coverage.test.ts, packages/objectql/src/engine-security-catalog-package-door.test.ts, packages/objectql/src/package-disable-enforcement.test.ts, packages/runtime/src/domains/meta.ts, packages/runtime/src/app-plugin.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/platform-objects/src/apps/setup.app.ts, packages/plugins/plugin-sharing/src/sharing-rule-service.ts, packages/plugins/plugin-sharing/src/declared-sharing-rules.ts, packages/cli/src/utils/organization-ownership-apply.ts, packages/cli/src/commands/migrate/organization-ownership.ts, packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts, packages/qa/dogfood/test/upgrade-harness/populate-fixture.ts, packages/qa/dogfood/test/upgrade-harness/fixture-18.0/manifest.json, .github/workflows/ci.yml, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, scripts/check-stack-collection-maps.mjs, .changeset/24550-sharing-rule-meta-door-closed.md, .changeset/pre.json, packages/spec/src/security/sharing.zod.ts, packages/spec/src/meta-spelling/manifest-collection-spelling.ts, packages/spec/src/stack.zod.ts, packages/spec/src/kernel/package-artifact.zod.ts, examples/app-showcase/src/security/sharing-rules.ts, packages/cli/src/commands/serve.ts, packages/runtime/src/organization-ownership-boot-guard.ts, .github/workflows/lint.yml, content/docs/concepts/metadata-lifecycle.mdx, content/docs/references/api/metadata.mdx, content/docs/references/kernel/metadata-plugin.mdx, content/docs/deployment/environment-variables.mdx, content/docs/references/api/protocol.mdx, docs/adr/0005-metadata-customization-overlay.md, docs/adr/0126-packaged-metadata-customization-model.md, docs/adr/0131-total-organization-ownership-no-null-organization-id.md

Reviewer: an isolated at-tier reviewer subagent of the adopting seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3), read-only. This record re-judges the prior FAIL at 47f5080549d908b875a144ea683bdffa88683442 against the five commits pushed since: fa61a225b (the merge of origin/main at 5e8000906, which carries the upgrade harness ec16dbe2e), 90e39904e (B1 and B2), 3cd2fbfa3 (the waiver drop), 9bf883707 and b8e0baa72 (changeset only). The code head is 3cd2fbfa3f3fa1f67e08fc45344105a20f656a23: git diff --stat 3cd2fbfa3 b8e0baa72 lists one file, .changeset/24550-sharing-rule-meta-door-closed.md, one line changed, so every source, test and script byte at b8e0baa72 is the byte 3cd2fbfa3's battery ran on. The merge-base moved from 04669a46 to 5e8000906; origin/main was at 973917c5795f7a440d46a15bc5cf8961af9d8703 when read. The prior record's ① judgments (a) through (d) and its ② reading stand except where this record says otherwise; (e) is re-rendered below. Sources: the diff origin/main...origin/claude/issue-24550-sharing-rule-meta-door, the ruling record 6272437439 (hatch ruling A), the dev's addendum 6273585481, the check-runs on 3cd2fbfa3 and on b8e0baa72 (read at 17:58:41Z), and the PR (draft: true, mergeable_state: blocked, 15 commits, head b8e0baa72).

① Derived judgments

B1 — resolved. examples/app-showcase/src/coverage.ts:170-176 adds the sharing_rule row to KIND_COVERAGE (a Record keyed by MetadataType, :52): status: 'demonstrated', files: ['src/security/sharing-rules.ts'], notes naming regime C, the ledger switch, the organization door and the runtime pin. The proof file exists at the head (examples/app-showcase/src/security/sharing-rules.ts, the showcase's criteria rules with position and unit_and_subordinates recipients). The two assertions that were red now have their fixture: examples/app-showcase/test/coverage.test.ts:97-98 (every registry kind has a row) and :107-112 (every demonstrated row's files exist). Gate verdicts on the code head 3cd2fbfa3: Type Check · consumer gates success (17:24:09Z, the TS2741 is gone), TypeScript Type Check success (17:31:42Z), Test Core (4/6) success (17:47:24Z). The dev's local showcase run was 431/431 and the five example apps type-checked.

B2 — resolved. packages/objectql/src/engine-security-catalog-package-door.test.ts:75-80 adds a sharing_rule entry to BODIES (:71-81): type: 'criteria', name: NAME, label: 'Probe', object: 'probe_object', condition: "record.status == 'open'", accessLevel: 'read', sharedWith: { type: 'position', value: 'probe_position' }. It is spec-valid against SharingRuleSchema (packages/spec/src/security/sharing.zod.ts:302, which is CriteriaSharingRuleSchema): the base is a strictObject (:143) with name matching the snake_case regex (:183; probe_item fits), optional label (:184), object (:188), accessLevel defaulting to read (:192), sharedWith a strict object whose type is ShareRecipientType (:126-133, position is a member) and whose value names the recipient; the criteria variant adds the literal type: 'criteria' and condition (:259-261). securityManifest() (:185-192) places it under singularToPlural('sharing_rule'), which is sharingRules (packages/spec/src/meta-spelling/manifest-collection-spelling.ts:84), the stack collection stack.zod.ts:822 declares as an array of SharingRuleSchema, so registerApp registers it under PKG and the three rows that were running without a body (:217-225 the by-name fixture demand at :223, :227-233 provenance, :236-245 the NOT_OVERRIDABLE refusal on both topologies) run as they run for capability. packages/objectql/src/package-disable-enforcement.test.ts was re-judged by measurement, not assumption: :88 derives BY_NAME_TYPES from MetadataTypeSchema.options and :238 registers { name, label } through registry.registerItem directly, so no schema parse meets the sharing_rule item; the dev ran both files by name (33/33, including "registers a stack-declared sharing_rule under its package") and the full objectql suite (448 files, 8422/8422). Gate verdict on 3cd2fbfa3: Test Core (5/6) success (17:36:31Z).

B3 — met on the code head; the changeset head's battery was still running when read. On 3cd2fbfa3: 41 check-runs, 36 success, 5 skipped, 0 failure. Required lanes, every one success: Test Core (1/6) 17:33:00Z, (2/6) 17:33:40Z, (3/6) 17:41:47Z, (4/6) 17:47:24Z, (5/6) 17:36:31Z, (6/6) 17:34:44Z, the Test Core aggregate 17:47:54Z; Type Check · workspace 17:31:35Z, Type Check · source gates 17:16:07Z, Type Check · consumer gates 17:24:09Z, Type Check · debt ledger 17:20:56Z, TypeScript Type Check 17:31:42Z. Also success: Build Core, Build Docs, Check Changeset, Lint & Repo Gates and all eight Lint lanes, Governed Surface Queue Guard, Spec property liveness, the three claim guards, Dogfood Regression Gate (aggregate). Skipped by design on a pull_request event: Console Pin Gate, the Dogfood shards, Dogfood Verify CLI, Temporal Conformance, Packed-tarball smoke. On b8e0baa72, read at 17:58:41Z (the push landed at 17:53Z): completed success — Check Changeset 17:54:48Z (the only gate whose input changed), Governed Surface Queue Guard, Spec property liveness, the three claim guards, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Auto Label, filter, Dogfood Regression Gate (aggregate), Lint · ESLint 17:58:20Z, Lint · scoped · query-options erasure 17:58:07Z, Lint · scoped · tooling self-tests 17:58:04Z, Type Check · source gates 17:58:18Z. Still in_progress: Build Core, Build Docs, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · slot lookup & corpus, Test Core (1/6) through (6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace; the aggregates Test Core, TypeScript Type Check and Lint & Repo Gates had not been created yet. Skipped by design: the same five. Because the 3cd2fbfa3 to b8e0baa72 delta is one prose line in one changeset file, the 3cd2fbfa3 verdicts are the code's verdicts, and the one gate that reads the changed line is green on b8e0baa72. No failure appeared on either head.

The waiver drop (3cd2fbfa3) — correct and complete. scripts/check-stack-collection-maps.mjs:764-790 is the STACK_COLLECTION_COVERAGE site with kindCovered: true (:777). The runner (:865-874) subtracts kindCoveredCollections (:798-803): every schema collection whose PLURAL_TO_SINGULAR image is a KIND_COVERAGE key. With sharing_rule in KIND_COVERAGE, sharingRules (whose image is sharing_rule, manifest-collection-spelling.ts:84) leaves the expected set, so it is no longer missing, and applyWaivers (:441-460) reports a missing waiver whose key is not missing as stale (:455-457), which fails the gate (:900-904, "an overstated debt misleads as much as an unrecorded one"). Removing sharingRules from the waiver (:781-782, with the one-line reason) is the fix the gate itself prescribes; the dev reproduced the red at 90e39904e and the green with the 18-assertion self-test at 3cd2fbfa3. Completeness: the only other sharingRules waiver in the script, :561, belongs to the MetadataCategoryEnum site (:540-572, packages/spec/src/kernel/package-artifact.zod.ts), which has no kindCovered subtraction and still omits the collection, so that waiver is live and is correctly kept. The gate runs from .github/workflows/lint.yml:942; every Lint lane is success on 3cd2fbfa3.

The hatch sentence (b8e0baa72, ruling A) — accurate on every operative clause, over-broad in one phrase. .changeset/24550-sharing-rule-meta-door-closed.md:21 now ends: "On an environment kernel, removing it needs OS_METADATA_WRITABLE=sharing_rule. That hatch is an explicit operator opt-in, not an authoring route: as for any code-only type, it reopens every write on the type, including creates of new names, and a sharing rule grants access. Unset it after the removal." Against packages/metadata-protocol/src/protocol.ts:

  • "On an environment kernel, removing it needs the hatch": deleteMetaItem's two-tier block is behind this.environmentId !== undefined (:30566); a stored row no package declares is not artifact-backed, isRuntimeCreateAllowed('sharing_rule') is false (:17630-17643; the entry is static with allowRuntimeCreate: false, metadata-plugin.zod.ts:1254), and isOverlayAllowed (:17582-17587) is true only through envWritableTypes(), so the delete answers 403 NOT_CREATABLE (:30623-30629) with the hatch shut and proceeds with it open. A host-config kernel skips the block. Accurate.
  • "explicit operator opt-in, not an authoring route": the hatch is the parsed env var (:17430), and no refusal for this type names it: codeOnlyCreateError omits its hatch clause for a regime-C type (:17721-17724), codeOnlyOverrideError speaks the regime sentence (:17747), which is built from the row's clone and switch routes (packaged-base-regime.ts:205-212, :247-250), and that module forbids any sentence from prescribing the hatch (:96-97, :360-361). Accurate.
  • "as for any code-only type": isOverlayAllowed carries no type carve-out (:17582-17587); sharing_rule is treated as job, agent, capability, field are. Ruling A is honored; the PR changes no behaviour here.
  • "it reopens every write on the type, including creates of new names": for a name no package ships, saveMetaItem takes channel = overlayAllowed (:23010-23014), so with the hatch set the create proceeds in publish and ?mode=draft — "creates of new names" is accurate, and it is the clause the seat asked to see. "Every write" overstates: for a declared name shipped is true and the channel is registryAllowsOverlay (:23013), false with the hatch open or shut, so the write stays 403 NOT_OVERRIDABLE (:23016-23017), and a declared item's removal is refusePackagedBaseRemoval (:30622), equally hatch-blind. The accurate scope is every write of an item no package declares: new-name creates, and updates and deletes of stored residue. The overstatement runs in the cautious direction (it tells the operator the hatch is wider than it is; the worst outcome is a refused request with an accurate refusal), so it is carried in ③, not blocking.
  • "a sharing rule grants access" and "Unset it after the removal": the type is domain: 'security' (metadata-plugin.zod.ts:1254); the advice follows from the hatch's semantics. Accurate.
  • No doc page states the opposite. content/docs/concepts/metadata-lifecycle.mdx:136 says a regime-C refusal names the sanctioned path and not the hatch; :148 says a row written through the hatch over a sealed managed item is not served; content/docs/deployment/environment-variables.mdx:321 describes the hatch as opening runtime creation of a type whose registry entry allows none, for items no managed package ships; content/docs/references/api/protocol.mdx:1574 says the same of allowEnvironmentOverlay's env source. The new sharing_rule row (metadata-lifecycle.mdx:121) does not mention the hatch. The governing ADR pages (0005, 0126, 0131) carry no sentence sealing the hatch for regime C or for sharing_rule.

The merge of main — faithful, and nothing in it moves an earlier judgment. fa61a225b's tree equals the automatic merge of its parents (git merge-tree --write-tree 5e8000906e 47f508054 produces tree 24a7c09d; git diff --stat 24a7c09d fa61a225b is empty), so no hand edit rode in. Three files were touched by both sides, content/docs/concepts/metadata-lifecycle.mdx, packages/metadata-protocol/src/protocol.ts (main's 5a871e4e4 overlay-cache work) and scripts/engine-double-contract.pinned.json, each auto-merged and then measured green by the 3cd2fbfa3 battery (the runtime pin and the metadata-protocol suites run in Test Core; the engine-double ledger gate runs in a Lint lane). ec16dbe2e (the harness) is an ancestor of the head. origin/main has since moved to 973917c5 (nine commits past the merge-base); the intersection of their paths with the branch's paths is empty.

The 18.x line — R1 to R4 hold.

  • R1 (a fresh install boots): the branch touches no boot path — nothing under packages/cli/src, nothing in packages/runtime/src but the new test, no kernel file; Build Core and every Test Core shard are green on the merged code. The dev's fresh showcase boot (pnpm dev -- --fresh) was measured at 47f50805, before the merge; after it, the harness's case 1 boots HEAD over the 18.0 fixture (not a fresh boot) 5/5 locally. CI runs no boot on a pull_request event (ci.yml:2037), so the post-merge fresh boot is the dev's harness boot plus the unchanged boot surface — see ③.
  • R2 (18.0 to HEAD harness green): packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts:3-29 asserts the four cases plus the fixture-coverage case (:80, :84, :90, :101, :122); the dev ran it locally at 3cd2fbfa3, 5/5. The branch touches no file under packages/qa, no reshaped table and no sharing-service code, and the fixture holds no sys_metadata row of type sharing_rule (prior (b) stands). CI skips the dogfood shards on the PR.
  • R3 (a v17 database is refused until the ceremony opens): harness case 4 (:122-141) pins the refusal with the guard's code and sys_metadata named; packages/runtime/src/organization-ownership-boot-guard.ts:252-258 is untouched by the branch.
  • R4 (a walled posture is refused until step 4): packages/cli/src/commands/serve.ts:6266-6282 (walledPostureRefusal, NOT_IMPLEMENTED, "arrive in an 18.x release") is untouched by the branch.

(e) Complete feature — yes, on this head. The server half is whole: the door is closed on both kernel shapes with ADR-0112 codes and pinned on the HTTP transport, /meta/types agrees, the two refusals name the two sanctioned routes under the 500-character bound, the ADR-0087 entry and changeset carry FROM, TO and the fix, the nav entry is served with four locales and opens the console's existing generic Setup list of a registry type in environment scope, and every derived population the enum widening joined (the showcase kind manifest, the security-catalog pin, the stack-collection gate, the i18n bundles, the engine-double ledger, the overlay table) now has its row. Nothing is dormant code waiting on objectui#13050, which enriches that page. The two red gates and the type check that made the prior (e) a FAIL are green on the code head. The rendered console page is still NOT MEASURED (no console build; Console Pin Gate skipped on the PR) — unchanged carry.

Merge-tree — clean. git merge-tree --write-tree origin/main b8e0baa72 with origin/main at 973917c5 writes tree 29ef2cbc and exits 0 with no conflict output.

② Semver level

Restated: Clause-②: yes (narrowing) on PR body line 2 and .changeset/24550-sharing-rule-meta-door-closed.md:10; the two agree, and yes is the honest arm (the MetadataType union, META_URL_TO_SINGULAR, the Setup nav payload and the metadata-forms bundles each gain a member). Levels (:2-5): @objectstack/spec minor, @objectstack/metadata-protocol minor, @objectstack/plugin-sharing minor, @objectstack/platform-objects patch; (narrowing) is BREAKING and the body carries the banner (:14), FROM and TO for both narrowings (:18-19), and the one-line fix verbatim (:21). The ADR-0087 marker (:12) and the semantic entry are unchanged by the new commits. One thing changed around it: the merge brought in main's c46b5f14c, which exited Changesets pre mode (.changeset/pre.json now reads "mode": "exit"), so the RC exemption in scripts/check-changeset-no-major.mjs:105-110 is off and a major is again refused outside the allow-major label (:114-116). The launch-window convention (breaking ships as minor, :58-62) is still the guard's rule — the guard is in the tree at the head, its end condition is GA (:72-78), and main's own post-exit breaking commits c3954057f and 1b4078077 carry minor — so minor remains the required arm and Check Changeset is success on both 3cd2fbfa3 (17:14:39Z) and b8e0baa72 (17:54:48Z). The nits stand: the changeset header reads feat(…)! while the PR title reads fix(…)!, and platform-objects: patch although its published payloads gain keys — moot under the fixed lockstep group. Reviewed-at-tier: CONTRACT_REVIEW_TIER.

③ Boundary flags

  1. Hatch sentence precision (non-blocking). .changeset/24550-sharing-rule-meta-door-closed.md:21 says the hatch "reopens every write on the type"; the code reopens every write of an item no package declares (protocol.ts:23010-23018, :30622), and a declared rule's name stays 403 NOT_OVERRIDABLE with the hatch open or shut. A one-phrase fix, if the seat wants it: "it reopens every write of an item no package declares, including creates of new names". The clause ruling A asked for — operator act, not an authoring route — is present and accurate.
  2. The PR is still draft: true, mergeable_state: blocked (read at 17:58Z), and the b8e0baa72 battery was still running (the pending list is in ① B3). The prior record's B3 named leaving draft as a precondition; it is the seat's or the dev's act, not a code matter, and the one gate whose input moved on b8e0baa72 is green. The seat should see the b8e0baa72 aggregates conclude before merging; no run on either head has failed.
  3. Harness and fresh boot not measured by CI on this head. The dogfood lanes are skipped on a pull_request event (ci.yml:2037); the dev's local 5/5 at 3cd2fbfa3 is the only post-merge harness measurement, and the fresh showcase boot was measured pre-merge. The diff touches no boot path, no reshaped table and no packages/qa file, so R1 and R2 are not at risk from it; the merge-group or post-merge battery is where "stays green if it has landed" is measured.
  4. Launch-window end is near. Pre mode is exited (.changeset/pre.json), and main states 18.0.0 as the GA release (8fbbd5082). If whoever closes the window retires check-changeset-no-major.mjs before this PR merges, the BREAKING narrowing grades major and the changeset must be re-judged then.
  5. Rendered Setup page NOT MEASURED at the pinned console; objectui#13050 is the console half. Unchanged.
  6. The MetadataCategoryEnum sharingRules waiver stays (check-stack-collection-maps.mjs:561): that site has no kind subtraction and still omits the collection, so the waiver is live. Recorded so the drop at :782 is not read as half done.
  7. Carries from the prior record, still open and unchanged: the Clause-② arm flip from the claim's no (narrowing) to yes (narrowing), to be recorded on adoption; the item 3b follow-up card (admin-read projection of a switched-off declared rule, yes (widening)) and item 3a's shape C, noted, not filed; nav gating is manage_platform_settings (the nav_sharing_rules sibling's), stricter than nav_permission_sets; the ceremony's conflict.type save (organization-ownership-apply.ts:1798) would answer NOT_CREATABLE for a sharing_rule conflict, none expected, for the step-5/6 landing to account for; the pre-existing metadata-lifecycle.mdx field "remain deletable" wording; plugin-webhooks untouched (0 paths; the feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529 fence holds).
  8. Main moved after the merge (5e8000906 to 973917c5, nine commits); none touches a branch path and the merge-tree is clean, so no re-merge is needed for this verdict; the merge queue battery runs on the combined tree.

Implemented-by: claude/issue-24550-sharing-rule-meta-door
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 18:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 18:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

分类:failure —— 按下面的日志分诊。

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

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

    @objectstack/runtime:test:  FAIL   local  src/domains/share-links-refusal-order.test.ts > [#23328] share-link resolve refusal order — the plugin route > [#23372] email audience: the refusal is the one
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(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,按失败测试文件聚合):

  • src/domains/share-links-refusal-order.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

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/l tests tooling

Projects

None yet

2 participants