Skip to content

fix(runtime,core): the action activation door switches an action no package ships, and keeps ADR-0112's code and status together - #22872

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22817-action-activation-unpackaged
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22817-action-activation-unpackaged

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22817
Clause-②: yes

POST /api/v1/actions/_activation/:object/:action now switches an action no package ships, as its docblock declares (triage direction A, 6110327411). The ledger row names the package that ships the action, and no package (package_id null) when none does. A refused ledger write keeps ADR-0112's code and status together. InMemoryMetadataActivationStore.setActive now refuses packageId: '' exactly as the durable store does.

The defect, reproduced first on a real boot

Measured on origin/main 6364375b5 with a throwaway dogfood case (showcase boot, signed-in admin; the case was deleted after the run and is not in this PR). Two actions were saved through PUT /api/v1/meta/action/:name and then switched off through the door:

Action Door answer Row
env_ping_global (no object) 503 SERVICE_UNAVAILABLE, with "Activation row for action 'env_ping_global' names the empty string as its package. Omit packageId for an item no package ships; an empty package id is not a package." none
env_ping_task (on the packaged showcase_task) 200 package_id: "com.example.showcase"
showcase_mark_done (packaged, control) 200 package_id: "com.example.showcase"
  • The 503 is the card's defect. The door wrote String(action._packageId ?? obj._packageId ?? ''), objectql's setActionActive passes the row to the store unchanged (ActionActivationProjection.setActive), the store refuses '' with a TypeError, and the door's catch turned every undeclared status into 503.
  • The card's reach says no row lands in either case. That is true only when the route object has no package either. When it has one, the row lands, and it is credited to the object's package, which does not ship the action. Triage's text for option A is "the door passes undefined for an action no package ships", so this PR covers that shape too.

What the declarations carry (same boot): an environment-authored action has _provenance: "org" and no _packageId. A packaged action has its own _packageId and _provenance: "package", whether it is embedded or standalone. 73 platform actions declared inside their code objects' actions carry no stamp of their own (sys_user:invite_user, sys_approval_request:approval_approve, sys_permission_set:clone_permission_set, ...), so the object's package is the only record of who ships them.

The fix

  • packages/runtime/src/domains/actions.ts, step 5. shippingPackageOf(declaration) asks the one "does a code package ship this?" predicate, isCodeArtifactBody (@objectstack/metadata-core, ADR-0029 D9.6). That predicate already excludes the 'sys_metadata' sentinel and tenant provenance. The rule:

    1. the action's own package, when the action is a code artifact;
    2. for an action declared inside its object's actions (reference identity with the entry resolveRouteActionDeclaration returns), the object's package, when the object is a code artifact;
    3. otherwise none, and then the row is sent with no packageId key, so the store writes null.

    A tenant-authored action is never credited to a package.

  • Byte-identity, measured. On the same showcase boot, over all 146 declarations the door can resolve, the new rule matches the old computation on 144. The two it changes are env_ping_task (com.example.showcase becomes none) and env_ping_global ('' becomes none).

  • The door's catch answers through deps.errorFromThrown(err, 503), the dispatcher's one thrown-error rule, which 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 adopted on the catalog door:

    • a record validation refusal is 400 VALIDATION_FAILED;
    • the engine's declared pair (no ledger attached: 503 SERVICE_UNAVAILABLE) is kept;
    • an undeclared throw is 503 SERVICE_UNAVAILABLE.
  • The docblock is corrected. It said "Neither does the flow toggle". That has been false since c8111a575: AutomationEngine.toggleFlow refuses a flow no package ships (409 RESOURCE_CONFLICT), because such a flow has its own server-side switch, its definition's status. The docblock now gives the measured reason the action door differs: an action has no server-side switch of its own. visible and disabled are rendering predicates that no dispatch door reads (packages/spec/liveness/action.json: objectui only). So the ledger row is the only switch an environment-authored action has.

  • packages/core/src/utils/metadata-activation-store.ts. Both stores now call one refuseEmptyPackageId, so the in-memory double cannot accept a row the durable store refuses. The durable store's message is byte-identical to before.

Is there a product reason against option A? (the stop clause)

Measured, and none was found. The flow toggle's refusal is the nearest one, and it rests on a property actions lack, as above. ADR-0126 §8 item 2 says the action leg "follows the flow legs". It lists what follows, "same sys_metadata_activation ledger, same §5 write authority, a consult point at action dispatch, a surface row", and none of those is a provenance gate. The door's authority (manage_metadata; the platform operator under group/isolated) is unchanged.

Pins

  • packages/runtime/src/domains/action-activation-unpackaged.test.ts (new, 10 cases). It runs over the REAL objectql ActionActivationProjection and ObjectStoreActionActivationStore, writing into a recording sys_metadata_activation, so it pins the door, objectql's pass-through and core's store together:

    • an object-less environment action switches off with 200, and the row is exactly { metadata_type: 'action', name, package_id: null, active: false };
    • switching it ON first writes the same package-less row, with active: true;
    • an environment action on a packaged object gets package_id: null;
    • the sentinel stamp and a tenant-authored app.crm binding both give package_id: null;
    • the switch is honoured: dispatch then answers 409 ACTION_DISABLED and the handler never runs;
    • CONTROL: an action stamped crm still writes crm, and an unstamped action embedded in a crm object still writes crm;
    • faults: a record validation refusal answers 400 VALIDATION_FAILED and writes no row; an undeclared store failure answers 503 SERVICE_UNAVAILABLE and writes no row.

    The table double carries the store's find (bounded by limit, graded by check:objectql-double-limit) and insert, and no update. So check:engine-double-contract's pinned ledger is unchanged. The update leg for a package-less row is core's, already pinned by "UPDATES its row with no package on the way back on".

  • packages/core/src/utils/metadata-activation-store.test.ts:

    • the double refuses '' with a TypeError whose message equals the durable store's, apart from the type in the subject, and keeps nothing;
    • the double keeps a row with no package, with packageId absent.

Verification

  • Reverse verification, at the final head 58b68fe15e. The fix was committed first; actions.ts was replaced by its 6364375b5 bytes, then restored with git checkout HEAD --; blob c989d1e62c matched HEAD and git diff HEAD was empty. The new file went 7 failed | 3 passed (10). The 3 that stayed green are the two CONTROL pins and the undeclared-failure 503, which also answered 503 before the fix. The failures read exactly like the boot:

    • {"code":"SERVICE_UNAVAILABLE","message":"Activation row for action 'env_ping_global' names the empty string as its package. ...","httpStatus":503}: expected 503 to be 200;
    • expected [ 'sys_metadata' ] to deeply equal [ null ] and expected [ 'app.crm' ] to deeply equal [ null ];
    • expected 200 to be 409: the disabled action ran;
    • expected 503 to be 400.
  • Ablation of the double's refusal, with scripts/ablation-replace.mjs --delete. The anchor went from 1 to 0 and the blob changed, so the mutation landed. Result: 1 failed | 18 passed (19), with expected undefined to be an instance of TypeError. Restored: blob b8fe921e55 equals HEAD and git diff HEAD is empty. My first attempt passed --replacement "", which the tool refused before running anything, so it measured nothing.

  • Who relied on the double's leniency: nobody. Every construction site is a test: 1 objectql file and 6 service-automation files. All of them are green against the rebuilt core dist, which carries the marker: objectql 18 passed, service-automation 152 passed. No fixture needed triage.

  • The real boot after the fix: the same throwaway case answers 200 for all three. env_ping_global and env_ping_task rows carry package_id null; showcase_mark_done keeps com.example.showcase.

  • Full suites, on both the pre-merge head cf606feddf and the head merged with origin/main 55382dc02a (157a4dc138), with the same figures each time:

    • @objectstack/core: 2373 passed;
    • @objectstack/runtime: 5061 passed, 19 skipped;
    • typecheck of both is green, including check:test-typecheck, which reached the new test file.

    The final head 58b68fe15e changes only the new test file. There, the file passes 10 of 10 and @objectstack/runtime typecheck is green again.

  • Gates, at 58b68fe15e:

    • all 65 families dispatch-gates --commands derives, plus the dispatch-named check:error-status-conformance, each exited 0 with its exit code captured before any pipe;
    • --ran reconciles them: "65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3)";
    • check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3), because eight packages outside this diff had no dist/. They were built and it was rerun: "107 published require entry point(s) across 66 package(s) load".
  • eslint over the 4 changed TypeScript files at 58b68fe15e (--no-inline-config --format json): 4 files reported, 0 errors, 0 warnings, none ignored. This is a narrowed run, not the repo-wide lint, which CI owns. The narrowing is safe because eslint.config.mjs never enables type-aware linting (no parserOptions.project), so this diff cannot move a verdict on an untouched file.

Acceptance notes


Generated by Claude Code

…ackage ships, and keeps ADR-0112's code and status together

The door wrote `?? ''` as the package of an action no package ships, which
the store refuses, and its catch answered 503 for every undeclared status.
It also credited an action bound to a packaged object with that object's
package. The row now names the package that ships the action (the action's
own code-artifact package; for an action declared inside its object, the
object's), and none otherwise; the catch answers through the dispatcher's
thrown-error rule. The in-memory activation store refuses packageId '' as
the durable store does.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
…ller's bound and is drivable by the double gates

The table double now lives in a module-scope factory holding only the
store's find and insert, applies `limit` after the filter, and names its
table as a literal, so check:objectql-double-limit grades it; it carries
no update, so check:engine-double-contract's pinned ledger is unchanged.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/runtime, touching 9 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 69d421805896d8d598bd27a0d035e494f7de6630.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 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 — 44 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 69d421805896d8d598bd27a0d035e494f7de6630 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 69d421805896d8d598bd27a0d035e494f7de6630

⚠️ 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 69d421805896d8d598bd27a0d035e494f7de6630 → 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: 58b68fe15e0a842e178d1b59da540f60e1a466c3
Local-runs: none

Written 2026-10-11T17:49Z, inside the adopting seat's session. Inputs: card #22817 (body and all five comments: retriage 6110035816, triage first touch 6110327411 = direction A, claim 6110532290, os-dev-report 6111555023, the seat's ACCEPT 6111585171 read as a claim to verify), PR #22872 (body, 5-file list, net diff against merge-base 55382dc02a), and the 38 check-runs on the head: 34 success, 4 skipped (opt-in / path-filtered), 0 failure — Lint & Repo Gates, the four Type Check legs, Test Core x7, Check Changeset x2, Governed Surface Queue Guard, Dogfood x5, Temporal Conformance, the single-writer and claim checks all green. No governed path in the file list; head repo equals base repo; draft; mergeable_state: clean. Nothing built, run or re-run locally; every tree fact below is a git show / git grep on refs/review/pr-22872.

① Derived judgments

  1. The activation door accepts an action no package ships — RIGHT. shippingPackageOf returns undefined for an environment-authored action and the row is sent without a packageId key, so ObjectStoreMetadataActivationStore.setActive writes package_id: null (its existing ?? null). The docblock at actions.ts:146 declared exactly this; triage 6110327411 directed it (option A); ADR-0126 §4 says absence of a row means active and the ledger imposes no gate, §5's write authority (manage_metadata; platform operator under group/isolated) is untouched by the diff, and §8 item 2 lists what the action leg inherits from the flow leg (ledger, write authority, consult point, surface row) and names no provenance gate. The nullable package_id was already landed and reviewed for the security catalog (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 / [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A; the platform-objects field description reads "Empty when no package ships it"). Not an ADR reversal under Prime Directive [WIP] Add Chinese version of the documentation #13: no accepted ADR says an unpackaged action must be refused; the flow toggle's refusal is automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726's own, tied to status. The response body ({ name, objectName, enabled }) and the other refusals (404, 409 RESOURCE_CONFLICT, 501) are unchanged. Pinned through the REAL HttpDispatcher, objectql projection and ObjectStoreActionActivationStore in the new test (object-less 200 + exact row; ON-first row; then 409 ACTION_DISABLED with zero executions).

  2. The package credit — RIGHT, and the deviation is inside A. Old: String(action._packageId ?? obj._packageId ?? ''), which credited a standalone environment action to its route object's package, wrote the literal 'sys_metadata' rehydration sentinel as a package, and credited a tenant-authored _packageId: 'app.crm' binding to that package. New: the ADR-0029 D9.6 predicate isCodeArtifactBody (truthy _packageId, not the sentinel, not _provenance: 'org') on the action, else on the object only when the action is the object's own actions[] entry by reference. Reference identity holds: resolveRouteActionDeclaration (action-execution.ts:2705) returns obj.actions.find(...) itself for an embedded action, so the 73 unstamped platform actions keep their object's package (CONTROL pins: stamped crm and embedded-unstamped crm). package_id means "the package that ships the artifact" (ADR-0126 §4, the field's description), so the old credit made the column lie. Readers of sys_metadata_activation.package_id, measured at the head — none decides anything from it: ActionActivationProjection.hydrate reads name and active only; the security catalog's readDisabledCatalogNames (resolve-authz-context.ts) reads metadata_type, name, active; catalog-set-in-effect.ts and last-admin-guard.ts have no package_id read; the flow engine's hydrate reads name/active; uninstall (marketplace-install-local-plugin.ts) runs domain-registered cleanups and its own comment says its packageId is the manifest id, never the ledger's — no uninstall path reads or deletes ledger rows by package; highlightFields on the platform object is display only. The changeset's "No reader decides anything from package_id" is therefore verified, not taken from the report.

  3. The catch → deps.errorFromThrown(err, 503) — RIGHT; the pairs that move, read off resolveThrownHttpError (packages/types/src/thrown-http-error.ts): (a) a ValidationError / { code: 'VALIDATION_FAILED', fields } with no status: was 503 VALIDATION_FAILED, now 400 VALIDATION_FAILED with details.fields (pinned); (b) a throw spelling .statusCode only: was the 503 fallback, now its declared status ([rest] Hook refusals carrying an explicit statusCode are not mapped by /api/v1/data — they leak as HTTP 500 INTERNAL_ERROR with no located guidance #7525's rule); (c) an unregistered .code string (a driver errno) on an undeclared 5xx: was served verbatim in the closed error.code slot, now the status-derived SERVICE_UNAVAILABLE with the spelling withheld ([Decision] Is ADR-0112's declaredCode channel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509) — the old catch violated ADR-0112's closed vocabulary here; (d) issues/userMessage now ride through. Unchanged: the engine's own { status: 503, code: 'SERVICE_UNAVAILABLE' } (no ledger attached) keeps its pair; a bare Error is 503 SERVICE_UNAVAILABLE (pinned) and still passes the 5xx leak withhold, since both the old and the new path end in this.error. A registered .code with no status still lands at the 503 fallback with that code — the shared resolver's own rule, identical to before, so nothing moves and the door now has no rule of its own, which is what the card asked for. Same exit the catalog door adopted in 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.

  4. InMemoryMetadataActivationStore.setActive refusing '' — RIGHT; no consumer relied on the leniency. One refuseEmptyPackageId called by both stores (Route & surface ownership rule 1: one owner), the durable store's message byte-identical. Consumers at the head: objectql's InMemoryActionActivationStore (used only in action-activation.test.ts, every write packageId: 'crm'); service-automation's InMemoryFlowActivationStore (six test files, none passing ''; the one non-test write, toggleFlow → flowActivationStore.setActive({ packageId: packageOwner }), follows a 409 refusal for any flow no package ships, and packagedFlowOwner returns a package id or undefined, never ''). The repo-wide grep for packageId: '' on an activation store hits only the core refusal pins. The MetadataActivationRow.packageId contract already declared '' refused; the double was the one out of contract.

  5. The new @objectstack/metadata-core import in packages/runtime — RIGHT. @objectstack/metadata-core: workspace:* is already in packages/runtime/package.json dependencies (the PR does not touch that file); isCodeArtifactBody and isTenantAuthored are exported through export * from './code-artifact-provenance.js' in its index; check:undeclared-dep-imports and the four Type Check legs are green on the head. Asking the one D9.6 predicate instead of a bare _packageId read is the right owner.

  6. The docblock correction — RIGHT. "Neither does the flow toggle" has been false since c8111a575 (2026-09-30, automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726): automation.ts:2202 documents and the door enforces 409 RESOURCE_CONFLICT for a flow no package ships, naming the flow's own status switch. packages/spec/liveness/action.json records visible and disabled as objectui-only CEL rendering predicates, and a grep of action-execution.ts, domains/actions.ts, objectql engine.ts and action-activation.ts finds no dispatch-side read of either. So the stated reason (an action has no server-side switch of its own) is measured, and the corrected sentence narrows the claim to what is enforced (Prime Directive chore: version packages #10).

  7. Stale sibling wording the widening leaves behind — noted, not a defect. Outside the claim's file surface and not touched: the sys-metadata-activation.object.ts header still says "flows and actions reach this ledger only when a package ships them"; objectql's action-activation.ts header still says "for PACKAGED ACTIONS"; and describeDisabled's served 409 sentence says "Re-enable the packaged action" — now also served for an environment-authored one (the remedy stays right). Code comments and one refusal sentence, no published page; metadata-lifecycle.mdx:140 stays true. Carried as a note for the seat (see ③).

  8. Public surface, net: @objectstack/runtime — the door's accept-set widens (unpackaged actions), one status pair corrects (503 → 400 on validation), the row credit narrows to the shipping package; response shape unchanged; no new route, no query parameter read, nothing for /discovery. @objectstack/core — InMemoryMetadataActivationStore.setActive throws TypeError on ''. No packages/objectql, no packages/spec, no docs, no governed path. The pinned objectui sibling imports nothing removed or renamed.

② Semver level

.changeset/22817-action-activation-unpackaged.md: @objectstack/runtime: minor, @objectstack/core: minor — matches what the diff publishes. The runtime change is a widening of a REST door, so at least minor; the core change brings the double into the contract the row already declared ('' refused), so it is a fix rather than a narrowing of the declared contract, and the shared minor covers it; (narrowing) would be wrong. Each bullet carries FROM → TO and the one-line fix ("omit the key"), names both subclasses, and the ADR-0112 bullet states the exact pairs. check-adr-0087-registration reads it as non-breaking, so no disposition marker is owed. PR body line 2 is the bare Clause-②: yes, the changeset Clause-②: yes (widening) — both legal spellings of the same declaration. Both Check Changeset runs on the head are green.

Clause-②: yes (widening)

③ Boundary flags

The report has open_questions: []; its eight deviations and one out_of_scope_findings entry are the flags.

  • Scope within A (no credit to the route object's package): answered RIGHT in ①2; triage's own wording ("passes undefined for an action no package ships") covers it; the changeset states it as its own bullet; the measured census (144/146 unchanged) is consistent with the CONTROL pins.
  • Route (c) pinned as 400 + undeclared 503; declared-pair 503 kept by the existing posture-gate case: answered, consistent with ①3.
  • "Back on, the row is UPDATED" not pinned at the door; update double dropped so scripts/engine-double-contract.pinned.json is untouched: acceptable. The door's ON-first row is pinned; the package-less update leg is pinned in core ("UPDATES its row with no package on the way back on"); the OFF-then-ON sequence through the store is pinned in objectql's action-activation.test.ts. Not writing the pinned ledger from a feature PR is the right call.
  • Throwaway dogfood case deleted, never committed: the file list confirms five files; answered.
  • Commit trailers: both pushed non-merge commits carry Co-authored-by: Claude and Claude-Session, the AGENTS.md pair; answered.
  • Branch 3 commits behind main, none touching the five files: CI runs the merge ref and it is green; mergeable_state: clean; answered.
  • Bare Clause-②: yes in the PR body vs the arm in the changeset: answered in ②.
  • No docs edit: verified against metadata-lifecycle.mdx:140; answered. The stale sibling comments in ①7 are outside the claim's surface (packages/objectql was forbidden to this PR) and are a comment-and-sentence drift, not a defect — the seat should carry them as an acceptance note or a small follow-up on the engine lane, ⛔ not as a change to this PR.
  • Out-of-scope finding (check-objectql-double-limit lifts identifiers from comments, tripped TDZ, answered UNJUDGED): the gate failed loud, so no verdict on this PR rests on it, and the PR is green after a comment reword. Escalated, not blocking: it is reproducible with a named landing point (identifiersIn in scripts/check-objectql-double-limit.mjs), which is Prime Directive chore: version packages #10's filing shape; the seat's "noted, not filed" should become a tooling card the next time it costs a round, or now if the seat prefers.
  • The card's stop clause ("a real product reason that an environment-authored action must not have an install-level switch"): the report's measurement supports going ahead. Actions carry no server-side switch (visible/disabled are objectui-only — ①6), so the flow toggle's reason (automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726: the flow's own status) does not transfer; ADR-0126 §8 item 2 names no provenance gate; the security catalog already switches environment-authored items (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). One property to state rather than hide, pre-existing and unchanged by this PR: the row identity is (metadata_type, name) deployment-wide (ADR-0126 §4, ADR-0131 D6), so an operator switching off an environment action by name in a group/isolated posture switches off every same-named action; refuseAmbiguousActionActivation sees the declarations the current kernel's metadata service enumerates. Environment actions on packaged objects already landed rows before this PR, so nothing newly reachable opens; the §5 operator gate holds. Not a product reason against A.
  • Who may call the door: unchanged by the diff (steps 1-4 untouched); the widening opens no new caller.

Implemented-by: claude/issue-22817-action-activation-unpackaged
Reviewed-by: session_01VoSxBQujKLZKPwK2u5ehQ6

VERDICT: PASS


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.

Action activation door refuses an action no package ships, though its docblock says it does not require one (503, code and status mismatched)

2 participants