Skip to content

fix(runtime,core,platform-objects): the catalog activation door switches an environment-authored position or permission set, and keeps ADR-0112's code and status together - #22811

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15204-s2d-activation-door-package-less
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15204-s2d-activation-door-package-less

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15204
Clause-②: yes (widening)

Stage 2d of #15204 (claim amendment 6106681412): the security catalog's activation door switches a position or permission set the environment authored, and a ledger write it refuses answers with the status its code means (ADR-0112). This PR does not close the card: the other stages stay open.

The defect (F5 of objectui#12089 round 2), reproduced first

On origin/main 9f5eca5, the dogfood case added here failed before any fix:

AssertionError: {"success":false,"error":{"code":"VALIDATION_FAILED","message":"Package is required","httpStatus":503}}: expected 503 to be 200
  • handleCatalogActivationWrite wrote packageId: entry.packageId ?? ''. An item saved through PUT /meta/position/:name or PUT /meta/permission/:name has no _packageId, so the row carried ''.
  • sys_metadata_activation.package_id was Field.text({ required: true }), so the engine refused the row: VALIDATION_FAILED, with no status of its own.
  • The door's catch kept the thrown code but defaulted the status to 503. The code and the status disagreed.

Who writes and reads package_id (measured on 9f5eca5)

Seam Uses package_id for
catalog door (runtime/src/domains/catalog-activation.ts) writes entry.packageId ?? '' (the producer of F5)
action door (runtime/src/domains/actions.ts:223) writes the action's _packageId, else ''
flow toggle (service-automation/src/engine.ts) writes the loader's package; a flow no package ships is refused before the write
ObjectStoreMetadataActivationStore.list() (core) maps it onto packageId; the flow and action hydrators (hydrateFlowActivations, ActionActivationProjection.hydrate) read only name and active
readDisabledCatalogNames (core/src/security/resolve-authz-context.ts) not read: type, name, active
catalog-set-in-effect.ts, last-admin-guard.ts (plugin-auth), sharing-rule-service.ts (plugin-sharing) not read
objectui console at main a194b4e git grep package_id over the ledger readers: no hit

The row identity is (metadata_type, name) (the 'global' unique index). No reader decides anything from package_id.

The fix, and why option (i)

(i) package_id becomes optional, and "no package" has exactly one spelling.

  • sys_metadata_activation.package_id: required is dropped. The field's description now says it is empty when no package ships the item. The column was already nullable on disk: under ADR-0113 required is a write-time check, and NOT NULL comes only from storage.notNull. So no DDL and no migration.
  • MetadataActivationRow.packageId is optional (@objectstack/core). The store writes null when it is absent. It refuses packageId: '' with a TypeError, before any read or write. list() returns packageId only when the row names a package. So a package-less row has one spelling, and no reader can take an environment item for a package named ''.
  • The door passes entry.packageId as it is. The ?? '' is gone.
  • The door's catch answers through deps.errorFromThrown(err, 503). That is the dispatcher's one thrown-error rule (resolveThrownHttpError). A record validation refusal becomes 400 VALIDATION_FAILED. A hook refusal keeps its own pair (403 PERMISSION_DENIED). An error with no status of its own becomes 503 SERVICE_UNAVAILABLE.
  • FlowActivationRow.packageId is optional to match (type only). Without that, ObjectStoreFlowActivationStore implements FlowActivationStore no longer type-checks. This was reverse-verified: putting back packageId: string gives 10 × TS2345 in service-automation.

On the four axes:

  • Real business need. The environment-authored items are real: ruling [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A covers them, and the stage 2c door already resolves them. Measured readers: none reads package_id. A marker string (ii) would serve no reader.
  • Long-term soundness. (i) records the truth in the column: there is no package. (ii) would put a sentinel into a column described as a package id. Every future reader that compares it with a package would then have to know the dialect. That is a second spelling of "no package" next to null.
  • Preventing AI mistakes. (i) closes the hole on the producer side. The ?? '' is gone, and the store loudly refuses the '' placeholder a caller would reach for. (ii) would make the marker a magic value that an AI-written reader could easily treat as a real package.
  • No scope spread. (i) is a smaller diff: one field flag, one optional type, one refusal. No new vocabulary.

Pins

  • catalog-activation-door.test.ts (runtime, unit). New tests:
    • an environment-authored position and set, in both directions, each writing exactly { metadata_type, name, package_id: null, active };
    • an anti-vacuity check that the catalog resolves both with packageId undefined, beside a packaged control that names crm;
    • the resolver honours the row for the position and for the set;
    • a record validation refusal is 400 with VALIDATION_FAILED, and nothing is written.
  • metadata-activation-store.test.ts (core). Inserting and updating with no package write package_id: null. '' is refused before any engine call. list() reads a package-less row back with packageId absent, beside a packaged row.
  • catalog-activation-door.dogfood.test.ts (real showcase boot). Both items are saved through /meta. Anti-vacuity: the booted catalog reader resolves both with no packageId. A holder is assigned the position. The test then checks the door, the ledger row (package_id null), and resolveUserAuthzGrants dropping and restoring the grants, for the position and for the set.

Verification

  • Reproduction. On base 9f5eca5, the dogfood case was red (503 above). With the fix it is green: Tests 8 passed (8).
  • Ablation, with ablation-replace.mjs and ablation-dist-preflight.mjs. Only required: true was put back on package_id, and @objectstack/platform-objects was rebuilt (the marker in dist/ was confirmed). Result: the dogfood case went red as {"code":"VALIDATION_FAILED",...,"httpStatus":400,"details":{"fields":[{"field":"package_id","code":"required",...}]}}. That is now 400, not 503, so the code-and-status fix is shown on a real engine. Restore: the blob is byte-identical to HEAD, the build was redone, and the marker is absent from all 66 dist files.
  • Full suites of every edited package, on the pre-merge head db73adc341:
    • core: 2353 passed;
    • platform-objects: 1082;
    • service-automation: 2368;
    • runtime: 5038 passed, 19 skipped.
  • Typecheck of core, objectql, service-automation, runtime and platform-objects: green.
  • Dogfood, in full, on the merged head bf3f89075e: 245 files passed and 1 skipped; 1978 tests passed and 9 skipped.
  • Gates. dispatch-gates --commands derived 100 families, and all 100 exited 0 on bf3f89075e. --ran reconciles them: "100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED (a DERIVED zero)".
  • eslint over the 7 changed TypeScript files: clean. This is a narrowed run, not the repo-wide lint, which CI owns.

Acceptance notes

  • The action activation door is not changed. For an action no package ships, it still passes ''. That write was refused before (503 VALIDATION_FAILED, from the engine) and is still refused, with nothing written. Its answer is now 503 SERVICE_UNAVAILABLE, from the store's TypeError. Whether an environment-authored action gets a switch is a product question, not decided here. Its catch has the same shape as the one this PR fixes, but actions.ts is outside this stage's declared files.
  • The door's authority (manage_metadata, operator-only under a wall) is unchanged ([Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A). The anchors' 400, the unknown-name 404 and the last-admin 403 are unchanged.
  • objectui#12089's round-2 report and the seat note on objectui#7611 could not be read from this session: the objectui API is not attached here. F5 was taken from the restatement in claim amendment 6106681412 and reproduced independently, as above.

Generated by Claude Code

…authored position or permission set

Claude-Session: https://claude.ai/code/session_014ifLBrKenJNWpsLxhLfhUg
Co-authored-by: Claude <noreply@anthropic.com>
…hes an environment-authored position or permission set, and its refusals keep ADR-0112's code and status together

Claude-Session: https://claude.ai/code/session_014ifLBrKenJNWpsLxhLfhUg
Co-authored-by: Claude <noreply@anthropic.com>
…l, as the shared ledger row's is

Claude-Session: https://claude.ai/code/session_014ifLBrKenJNWpsLxhLfhUg
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/platform-objects, @objectstack/runtime, @objectstack/service-automation, touching 17 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite), getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree), meta.publishItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/publish, selected by route anchor /:type/:name/publish), publishItem (sdk, the bare tail of client method meta.publishItem, bound to POST /api/v1/meta/:type/:name/publish))
  • content/docs/api/error-catalog.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite))
  • content/docs/api/index.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite))
  • content/docs/concepts/metadata-lifecycle.mdx _(via /security/activation/:type/:name (route, a path literal in a comment on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/permissions/authorization.mdx _(via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow), SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite), /security/activation/:type/:name (route, a path literal in a comment on a changed line))
  • content/docs/permissions/capabilities.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow))
  • content/docs/permissions/permission-sets.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow))
  • content/docs/protocol/kernel/error-handling.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/protocol/kernel/plugin-spec.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/ui/doc-pages.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow))
  • content/docs/ui/setup-app.mdx (via packageId (symbol, a field of interface FlowActivationRow; a field of interface MetadataActivationRow))

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

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-0.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-1.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-2.mdx (via /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-3.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite))
  • content/docs/releases/v17/17-4.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite), /:type/:name/publish (route, bridged from symbol packageId — its route source's handler names it), /api/v1/packages/:packageId (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol packageId — its route source's handler names it))
  • content/docs/releases/v17/17-7.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in handleCatalogActivationWrite))

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 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 — 48 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 23419bafb742fc13fa3c22b446263bbd6041c78e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 23419bafb742fc13fa3c22b446263bbd6041c78e

⚠️ 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 23419bafb742fc13fa3c22b446263bbd6041c78e → 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: bf3f89075e9f45ef9cc81e8de6f43d6620ea5127
Local-runs: none

Stage 2d of #15204 (claim amendment 6106681412, dispatched from F5 of objectui#12089 round 2; dev report 6107337287). Read on GitHub only: the card body and all 88 comments (the 2c records 6104861792 and 6105494730 on #22736, the 2c Landed 6105781131, the stage-1 ACCEPT 6102862135 and the rulings among them), objectui comment 6106225321 (F5, the measured defect) and the seat note 6106420299 on objectui#7611, the PR body, its 9-file list (+341 / −30), the net diff against main, and the 35 check-runs on this head. main was read for every reader and writer of sys_metadata_activation.package_id: the shared store and the object, the catalog door and the action door, objectql's action-activation.ts and engine.ts, service-automation's flow-activation-store.ts and engine.ts (the toggle and the hydrator), core's resolve-authz-context.ts and security-catalog.ts, plugin-auth's last-admin-guard.ts, the CLI's ownership inventory and unmanaged-tables readers, the route ledger, http-dispatcher.ts, @objectstack/types' thrown-http-error.ts and validation-failure.ts, the two docs pages, and ADR-0112, ADR-0113, ADR-0126 §4 and ADR-0131 D3/D6/D7.

① Derived judgments

  1. What the door now accepts — right, and it is the accept set 2c already declared. 2c's PASS (6104861792 ①.3) judged the door's name space as the catalog reader's: registry first, then the metadata service. An item saved through PUT /meta/position/:name or PUT /meta/permission/:name was therefore in the declared accept set from the start; F5 is that step 5 (the ledger write) refused what step 4 (the declaration) admitted. On this head steps 1 to 4 are unchanged hunks: both authority gates, the shape 400, the anchors' 400 in both directions, the shared body reader, the 404, the no-reader and reader-throws 503s and the no-ledger 501. Only the write and its catch move. So POST /api/v1/security/_activation/:type/:name widens to nothing 2c's record did not already cover; it is made to hold. The row the door passes is right by construction: SecurityCatalogEntry.packageId is the definition's _packageId only when that is a non-empty string (ownerOf, security-catalog.ts), so the door can never hand the store '', and the spread passes a packaged item's package verbatim (the crm control pins it).

  2. package_id no longer required — right, and what an empty value means to every reader. ADR-0113: required is a write-time check; the column constraint is storage.notNull (schema-drift.ts compares nullability against field.storage?.notNull, and the DDL keys off the same knob), and this field declares none, so the column was nullable on disk, no DDL, no migration. The readers, each read on main:

    • readDisabledCatalogNames (core/src/security/resolve-authz-context.ts:780): one $in read by metadata_type and name, then active === false || active === 0. package_id is not read. A package-less row that is off is disabled exactly like a packaged one; it cannot read as "no row", because the row is selected by key.
    • ObjectStoreMetadataActivationStore.list(): its only consumers are hydrateFlowActivations (service-automation engine.ts:5028) and ActionActivationProjection.hydrate (objectql), and both read name and active only.
    • plugin-auth's ledger hooks guardActivationLedgerInsert / guardActivationLedgerUpdate (last-admin-guard.ts:1637): standing is keyed by type, name and active.
    • the flow toggle (engine.ts:5694) and the action door are writers only; neither reads a row back.
    • CLI: organization-ownership-inventory.ts:268 lists the ledger as column-drop for the tenant column and reads no package_id (the groupBy ['type','name','package_id'] at :317 is the sys_metadata entry); unmanaged-tables.ts reads no column.
    • Setup's object list shows package_id through highlightFields and renders blank; the console's own ledger read is the data door's list by name (F5's measurement). The dev's objectui census (no package_id reader at a194b4e) is outside this brief's inputs and is recorded as the dev's measurement.
      No reader compares a row's package with anything, so no reader can take a package-less row for a packaged one; every reader keys by (metadata_type, name) and reads active, so none can take it for "no row". Row identity stays the 'global' index over the two key columns, untouched.
  3. One spelling for "no package" — right, with one inert tolerance. setActive writes null when packageId is absent and refuses '' with a TypeError before the keyed find (pinned: zero engine calls). list() returns packageId only for a non-empty string. The extra !== '' arm on the read is a dead branch: no writer produces '' (the store refuses it; before this head required refused it; the flow store always names the loader's package), so it tolerates a spelling that cannot be on disk. Noted, not a defect.

  4. The public types. MetadataActivationRow.packageId? (@objectstack/core, export * from utils/metadata-activation-store.js) and FlowActivationRow.packageId? (@objectstack/service-automation, index.ts:36). The second is forced by the first: ObjectStoreFlowActivationStore extends ObjectStoreMetadataActivationStore implements FlowActivationStore, so list()'s row type must admit the shared row (the dev's reverse check: 10 × TS2345). objectql's ActionActivationRow = MetadataActivationRow is an alias and follows with no edit, which is why the typecheck of objectql is green with nothing changed there. A consumer that read packageId as string loses a type guarantee; no reader in the repo did, and the changeset states FROM → TO.

  5. The catch moving to deps.errorFromThrown(err, 503) — right; every refusal now keeps ADR-0112's pair. HttpDispatcher.errorFromThrown is resolveThrownHttpError (@objectstack/types) plus the 5xx leak withhold. Its precedence: status = .status → .statusCode → 400 when the throw is a validation shape → the fallback; code = VALIDATION_FAILED for a validation shape → a ledger-registered .code → the code the status derives; an unregistered .code demotes to declaredCode. Applied to this door's three throws:

    • the last-admin guard: refuse() sets code: 'PERMISSION_DENIED' and status: 403, so the pair is 403 PERMISSION_DENIED, unchanged, and the 4xx message reaches the caller intact;
    • the engine's record ValidationError (code: 'VALIDATION_FAILED', fields[], no status): 400 VALIDATION_FAILED, and details.fields now reaches the wire, which the old catch dropped. Pinned in the unit test through the real HttpDispatcher, and the dev's ablation showed the same pair with fields[package_id required] on a real engine;
    • a bare store failure: 503, SERVICE_UNAVAILABLE derived from the status, the message withheld when it looks like a leak, which is what the old deps.error(message, 503, { code }) path did too; the existing pin still passes.
      The anchors' 400, the unknown-name 404, the no-reader 503 and the no-ledger 501 are deps.error calls above the try and do not move.
  6. The published text. authorization.mdx: every added sentence is true of the code (the package recorded when there is one and package_id left empty when none; both honoured because the resolver keys by type and name; 400 VALIDATION_FAILED for a record validation failure; a hook's pair kept; 503 SERVICE_UNAVAILABLE for a store failure). The object file gains a true paragraph and a true field description. Nit, same file: the object's description ("Activation ledger for packaged metadata artifacts … one row per packaged artifact") and the name and active descriptions ("The packaged artifact's machine name", "Is the packaged artifact armed") still say "packaged" while package_id's says otherwise; those strings are served through /meta and Setup. Follow-up push.

  7. ADR alignment, stated. ADR-0126 §4's table types package_id as "the package that ships the base artifact"; ADR-0131 D6 frames regime C's bit as a managed item's; D7's parity note says an environment-authored set's active "moves into the environment-authored definition's lifecycle, since there is no row". On main the resolver reads no definition lifecycle for these two types, only the ledger (resolve-authz-context.ts 6a), so before this head an environment-authored position or set had no working switch at all. [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A, as the seat note 6106420299 and the 2c Landed record restate it, has the platform administrator switching environment-authored items from Setup through this door. The ADR prose is now narrower than main; the carrier for that text is the cutover's ADR note (docs(adr-0131): §8/D14 execution-plan note — C2's remainder and C3 land as one cutover, C7 splits into C7a/C7b, and C12 moves to the 18.x line (rulings 6094045326, 6094175435, 6094183024) #22619, Tier H; D14's execution-plan note already names [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621), not this PR.

  8. Exclusions, scope, merge. No transition piece (no row fallback, no junction read, no write-through). No change to who may write the catalog. Nothing under content/docs/releases/. Nine files: four source, three tests, one changeset, one docs page; service-automation/src/engine.ts is the one file outside the declared zone, type only, forced as ①.4 says. The merge commit bf3f8907 brings main in and adds no file of another stage; GitHub reports the PR mergeable and clean.

② Semver level

  • .changeset/15204-catalog-activation-environment-authored.md: @objectstack/runtime, @objectstack/core, @objectstack/platform-objects, @objectstack/service-automation, each minor; Clause-②: yes (widening) on its own line, the same line as the PR body's second line (AGENTS.md: yes|no plus at most one arm from the closed pair; yes takes at least minor). Right per package: runtime (the door's declared accept set now holds for environment-authored items; a record validation refusal moves 503 → 400, a status correction on a refusal, no removal); core (an optional property, and '' refused — '' never landed before either, so no end-to-end accept set shrinks); platform-objects (required dropped, an object-schema widening); service-automation (type only). @objectstack/dogfood is private; the docs page is repo-owned. No ADR-0087 marker is owed (no packages/spec disposition) and none is present. Check Changeset is green.
  • One wording defect. The last bullet is headed "Not changed: the action activation door" and then states that its answer moves from 503 VALIDATION_FAILED to 503 SERVICE_UNAVAILABLE. The door's code is unchanged; its published answer is not, and the body says so. A CHANGELOG heading that reads "not changed" over a described change misleads the reader the body is written for. The heading must say what the body says (③.2). Fix it in the follow-up push on this PR, before any Version Packages PR carries it.

③ Boundary flags

  1. Open question 1, (i) or (ii) → A/(i), as built. No reader reads package_id (①.2), so a marker string would serve nobody and put a second dialect into a provenance column; null says what is true.
  2. Open question 2, the action door — answered on main, and the carrier corrected. Traced: handleActionActivationWrite (runtime/src/domains/actions.ts:223) passes String(_packageId ?? '') to ql.setActionActive, which is a pass-through (objectql engine.ts:5217) to ActionActivationProjection.setActive, whose store is ObjectStoreActionActivationStore, a one-argument binding of the core store. So for an action no package ships the new TypeError is thrown there and the action door's own catch answers 503 with code: 'SERVICE_UNAVAILABLE' and the store's sentence. Before this head the same request answered 503 VALIDATION_FAILED "Package is required" (the engine's refusal under the door's wrong default status). Judgments: (a) it is a changed published answer of POST /api/v1/actions/_activation/:object/:action, and the changeset must state it as a change, not under "Not changed"; the new answer's message ("Omit packageId for an item no package ships") addresses a TypeScript caller and is served to an HTTP client, under a code whose semantics are a retryable outage for a refusal no retry clears; (b) in scope: the cause, not the fix. The cause is this PR's store refusal; the fix is in actions.ts, outside 2d's declared files, and the action door's own docblock declares "it does not require the action to be PACKAGED … a row for a runtime-authored artifact is harmless", a capability main has never delivered (refused by required before this head, by the store after it). Whether an environment-authored action gets a switch or a deliberate 4xx refusal is a product question [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 does not reach; (c) the right carrier is a filed card, not "none". Prime Directive chore: version packages #10 files a contract violation (declared, not enforced) rather than noting it; the dev's carrier: none is the wrong placement. Escalated to the seat: file the card (an environment-authored action through the action door: open the switch, or refuse it with a 4xx pair and a sentence; either way the ?? '' goes), and have the follow-up push rename the bullet. Not a FAIL reason: the door's answer for this request class was already a contradicting pair before this head, nothing lands either way, nothing newly reachable opens, the pair now agrees with itself, and metadata-lifecycle.mdx documents the action switch for packaged actions only, so no published page is falsified.
  3. Deviation: the objectui comments unread by the dev. Both are this record's inputs. F5's measured defect (POST /api/v1/security/_activation/permission/c9_r2_env_set → 503 { code: VALIDATION_FAILED, message: 'Package is required', httpStatus: 503 }, and the same for position/c9_r2_registry_only in both directions, on objectstack 490cb6d9) is exactly what the dev reproduced on 9f5eca5 and what this diff fixes. The seat note's reading of the seam (the door's ?? '' against the field's required: true, and the catch's 503 default) matches the diff's two moves. Closed.
  4. Deviation: engine.ts outside the zone — right (①.4), and the changeset names the package.
  5. Deviation: Clause-②: yes (widening) — well-formed under AGENTS.md's declaration shape.
  6. The in-memory double is now more lenient than the store it stands for. InMemoryMetadataActivationStore.setActive (and so the flow and action in-memory bindings) accepts ''. No test on main pins '' through it (the action-dispatch and store tests name no empty package), so nothing is wrong today; carrier: the same change that drops the action door's ?? '', or a one-line refusal in the double. Not blocking.
  7. Docs inside the diff's own file (①.6): the three "packaged" strings on the object. Follow-up push.
  8. Dogfood test hygiene. The new case saves two /meta items, signs a holder up and inserts a sys_user_position row that the file's afterAll does not remove; that is the base file's own posture (usp_catalog_activation_exec), and the three Dogfood Regression Gate shards and their roll-up are green on this head.
  9. Release-cut condition (a) of 6102862135, extended to 2d by 6106681412 and the seat note: met when this head lands. Landing per the amendment: on this PASS.
  10. Check-runs on this head, all complete: 35 runs, 33 success, 2 roster skips (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure. Green: all six Test Core shards and the roll-up, the three Dogfood Regression Gate shards and the roll-up, Dogfood Verify CLI, Lint & Repo Gates, Build Core, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, the four Type Check gates and TypeScript Type Check, Spec property liveness, Temporal Conformance, Governed Surface Queue Guard, Flag docs affected by code changes, Auto Label, and the five PR-form gates. No red is left to attribute.

Implemented-by: session_014ifLBrKenJNWpsLxhLfhUg
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: PASS


Generated by Claude Code

…riptions cover environment-authored items; the action door's changed answer is named

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

Copy link
Copy Markdown
Contributor Author

2d (#22811): the text-only push is checked; queued · epic PM session_01Rerax7QTjKMPCUZxQUtPFR · 2026-10-11T10:04Z

  • The head is 1e13bb28fc, one commit on top of the PASS head bf3f89075e. It touches 2 files, +4 / −4, all text:
    • The changeset's last bullet now reads "The action activation door's answer for an action no package ships changes; the action is still refused." The heading says what the body says, as record ② asked.
    • In sys-metadata-activation.object.ts, the object's description and the name and active field descriptions drop "packaged". They now hold for both packaged and environment-authored items (record ①.6).
  • No code, test or level moved, so PASS 6107624825 carries.
  • CI on 1e13bb28fc: 35 runs, 33 success and 2 roster skips, none red. mergeable: clean.
  • Next: pr_ready + automerge_enable now (ACCEPT 6107640544). The action door's remainder is Action activation door refuses an action no package ships, though its docblock says it does not require one (503, code and status mismatched) #22817.

Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

2 participants