Skip to content

feat(spec,metadata-protocol,rest,objectql,runtime): GET /meta/object serves switched-off packaged actions beside the item, and the disabled-action refusal reads as a plain sentence - #27964

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-27842-disabled-actions-beside-item
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-27842-disabled-actions-beside-item

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #27842

Clause-②: yes (widening)
Metadata-protocol: added

维护者速读

What changes

The action twin of registration (#23316, PR #23358), served beside the item and never inside it.

  • Spec (@objectstack/spec, minor). GetMetaItemResponseSchema and GetMetaItemCachedResponseSchema gain the optional disabledActions: string[]; GetMetaItemsResponseSchema gains the optional disabledActions: Record<objectName, string[]>. IObjectQLEngine (contracts/objectql-engine.ts, the engine contract the dispatch doors already reach) gains the optional getDisabledActions(): string[] | undefined. Docblocks state the four choices below.
  • The fact (@objectstack/objectql). ObjectQL.getDisabledActions() answers the names the engine's ActionActivationProjection switches off (the projection both dispatch doors consult before the 409), or undefined while no ledger is attached. listDisabledActions() keeps answering [] for both cases.
  • The seam (@objectstack/metadata-protocol). protocol-action-activation-reads.ts reads getServicesRegistry().get('objectql').getDisabledActions per call, the way getFlowRegistration is read from automation. metadata-protocol imports nothing from objectql. The item read (served face, not on draft returns) spreads disabledActions beside lock; the list spreads it beside items, one entry per listed non-draft object; getMetaItemCached carries it beside data and skips the 304 when it is present.
  • The default door (@objectstack/rest). The cached branch of GET /meta/:type/:name rebuilds the envelope with disabledActions for object. An object's validator is folded and compared in REST (audienceFolds), so the REST-side 304 also stands down for a read that carries the key. The ETag header itself is unchanged.
  • The plain sentence (@objectstack/objectql, @objectstack/runtime). describeDisabled() now reads: "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or you can use another action." No table, no ADR, no tracker number. The 409 and the ACTION_DISABLED code are unchanged. ObjectQL.describeDisabledAction(), which both doors call exactly when they refuse, writes one info log line with the code, the action name, sys_metadata_activation and ADR-0126 §8 item 2. disabledActionRefusal's fallback for an engine with no sentence is plain as well.

The key name, and empty versus absent

  • Key name: disabledActions, the triage route's spelling, on all three reads (the list keyed by object name, as registrations is keyed by flow name). The maintainer approves the key name in this PR (Clause-② yes).
  • Absent vs []. Measured on registration: when the engine answers, every served flow gets a verdict (unregistered included) and the list holds one entry per listed non-draft flow (Object.fromEntries over every item); the key is absent only when no engine declares the member. This PR does the same. The key is absent when no ledger answered: no objectql service, an engine without the member, or getDisabledActions() returning undefined because no store is attached. It is [] when a ledger answered and none of this object's actions is off. That leaves absence to mean "unknown, keep today's behaviour" and never "nothing is switched off". The choice and its reason are in the spec docblock.
  • Which names. The names in the served item.actions[] that the projection holds, in declared order. Packaged standalone actions bound to the object are merged into actions[] by defineStack / registerApp (mergeActionsIntoObjects), so they are covered. The key never names more than item.actions already discloses.

Tests

  • metadata-protocol — src/protocol.object-disabled-actions.test.ts (new, 11 cases): the names of this object only; item.actions byte-identical with and without the key; [] for no matches and for an object with no actions; absent for no engine, an engine without the member, and an engine with no ledger; the plural type folded; other types are never asked; the execution face carries none; the list keyed by object name with [] entries, one engine call per list; a draft gets no entry; the cached door carries it beside data with the same ETag; a read that carries it is not answered 304 while the unjudged control is.
  • rest — src/meta-object-cached-disabled-actions.test.ts (new, 4 cases): the default cached object door carries the key, [] and absence; with If-None-Match equal to the issued ETag the judged read answers 200 and the unjudged control answers 304.
  • objectql — src/protocol-meta-disabled-actions.test.ts (new, 3 cases): the real ObjectQL and the real protocol through the service table; a setActionActive flip appears on the next item read, the list and the cached door; no ledger means no key. src/action-activation.test.ts: getDisabledActions (undefined / [] / names), the log line, and the plain-sentence pin.
  • runtime — src/domains/action-activation-dispatch.test.ts: the REST door's 409 + code + the first sentence + no table/ADR/tracker number; the MCP door serves the same sentence from the one producer; the no-sentence engine fallback is plain. The doubles there and in api-action-forward.test.ts now call the real ActionActivationProjection.describeDisabled, not a transcribed copy.
  • spec — src/api/meta-item-response-shapes.test.ts: type-level Eq pins for the three keys, parses for names / [] / absent, and a located refusal for a non-string name.
  • Pin sweep: every pin of the old sentence and ACTION_DISABLED's message was flipped in one pass: the two files above, packages/qa/dogfood/test/packaged-action-disabled-dispatch-live.dogfood.test.ts (sentence helper), packages/qa/dogfood/test/packaged-activation-ledger-reach.dogfood.test.ts (/ACTION_DISABLED|is disabled/ → the code), and docs/qa/platform-checklist/areas/api-backend.json (api-backend.packaged-action-disabled-dispatch acceptance[0], revision 9 → 10 with a history entry). ui/ carries no pin of the sentence or the code.

Full suites on the final head (merge of origin/main included):

Package typecheck test
@objectstack/spec 0 0 — 756 files, 22483 passed, 1 todo
@objectstack/metadata-protocol 0 0 — 283 files (4 skipped), 21854 passed, 26 skipped
@objectstack/objectql 0 0 — 493 files, 9587 passed
@objectstack/rest 0 0 — 350 files, 6759 passed, 332 skipped
@objectstack/runtime 0 0 — 481 files, 7837 passed, 19 skipped

The first runtime typecheck exited 1 on organization-ownership-boot-guard.turso-remote.kernel.test.ts (TS2307, @objectstack/driver-turso had no local dist). That file is not in this diff. After building driver-turso the rerun exited 0. The two dogfood files boot example apps and were not run locally; CI's Dogfood Regression Gate runs on main's push battery, not on this PR.

Ablation (serve path)

packages/metadata-protocol/src/protocol.ts: the item read's ...(readDisabledActions ? { disabledActions: readDisabledActions(decorated) } : {}) was replaced with /* ABLATION-27842 */ (scripts/ablation-replace.mjs). Then vitest run src/protocol.object-disabled-actions.test.ts gave 5 failed / 6 passed. Red: names on the item read; [] cases; the plural fold; the cached door carries it; no 304 while carried. Green: the absence controls, the other-type case, the execution face and the three list cases, which use their own spread. Restored by the same tool; the blob equals HEAD and git diff HEAD is empty.

Acceptance notes

  • The 304 trade, stated for the contract review. The card fixes the automation: a stored flow the engine refuses at cold boot is withdrawn with a warn, while the metadata read keeps serving it as status: 'active' — after the v18 upgrade, every refused flow reads as running #23316 trade: never in the ETag, never 304 when present. Any deployment that composes sys_metadata_activation (every PlatformObjectsPlugin boot) attaches a ledger, so every cached GET /meta/object/:name there now answers 200 with the full body. A ledger with no rows still serves []. The alternative is to fold the switched-off names into the REST-side validator, as REST already folds visibility, runtime-view and audience fingerprints. That keeps 304s and stays correct. It is not done here because the card rules it out.
  • The log line is info and written by the engine in describeDisabledAction. That method is the one place both doors pass on every refusal, and it holds the host's logger. The dispatcher's deps.logger is not wired on HttpDispatcher. The sentence producer ActionActivationProjection.describeDisabled stays pure.
  • Not changed. The 503 SERVICE_UNAVAILABLE refusal of the activation write door still names sys_metadata_activation and ADR-0126. It answers an administrator's flip, not an end user's click, and the card does not name it.
  • docs/qa/platform-checklist/areas/platform-core.json (the ledger posture note) says "the data-API read IS the only 'what is off here' surface today". After this PR, GET /meta/object also answers it. Left for the checklist owner. Carrier: seat.
  • The flow twin's ledger refusal (describeDisabledFlow, packages/services/service-automation/src/subflow-guards.ts) still names sys_metadata_activation and ADR-0126 §7.2 in the FLOW_DISABLED message. QA keys the ledger-vs-status distinction on that message (automation checklist, negative[3]), so a change there is its own decision. Carrier: seat.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final head 7237921e36: 119 commands, each exit code recorded before any pipe. --ran: "119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN" (exit 0).
  • On the first pass:
    • check:skill-examples and check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no dist/ for packages outside the touched closure). After pnpm turbo run build --filter='./packages/**', both exited 0.
    • check:engine-double-contract exited 1 (RETAINED: the new metadata-protocol test's pinned findOne double was not yet in the ledger). Fixed with --write, which adds only that one entry to scripts/engine-double-contract.pinned.json, and committed. The rerun exited 0.
    • That ledger commit derived 8 more scripts/ gates. All 8 exited 0.
  • node scripts/check-adr-0087-registration.mjs --base origin/main, check-changeset-no-major, check-empty-changeset, check:generated, check:api-surface and check:authorable-surface are all in the 119 and all exited 0.
  • Diff: 23 files, +1127 / −45 (under the 3000-line human-merge threshold).
  • Not measured here: the CI-only jobs dispatch-gates lists outside the runnable set (Build Core, Build Docs, the type-check lanes), and the Dogfood and Temporal jobs, which run on main after the merge.

Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V

…side the object read; plain disabled-action sentence

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ist sentence pins; changesets

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ouble in the engine-double pinned ledger

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/runtime, @objectstack/spec, touching 24 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/wire-format.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/data-modeling/formulas.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/deployment/production-readiness.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/kernel/contracts/data-engine.mdx (via IObjectQLEngine (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via getMetaItemCached (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/protocol/kernel/http-protocol.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/protocol/objectql/state-machine.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/ui/forms.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-1.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-5.mdx (via IObjectQLEngine (symbol, a top-level interface))
  • content/docs/releases/v17/17-7.mdx (via /meta/object/:name (route, a path literal in a comment in registerMetaItemReadRoutes; a path literal in a comment on a changed line))

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/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 79 pages)
  • the SDK route bridge reached 58 of 210 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 — 175 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 421bb6b7973ad8823e2c953c9314b15dc0312237 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 421bb6b7973ad8823e2c953c9314b15dc0312237

⚠️ 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 421bb6b7973ad8823e2c953c9314b15dc0312237 → 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

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

席位核对 · ACCEPT (lane part) · card #27842 · 2026-10-30T05:31Z

  • Shape: 23 files, +1127 / −45 at head 7237921e. disabledActions beside item on GetMetaItemResponseSchema and GetMetaItemCachedResponseSchema (string[]), and a name-keyed sibling of items on GetMetaItemsResponseSchema (Record<objectName, string[]>), all optional (packages/spec/src/api/protocol.zod.ts). The seam is IObjectQLEngine.getDisabledActions?(): string[] | undefined (packages/spec/src/contracts/objectql-engine.ts), read by metadata-protocol through the service table by name (protocol-action-activation-reads.ts), never an objectql import. describeDisabled() (packages/objectql/src/action-activation.ts) is now "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or you can use another action."; the code ACTION_DISABLED / 409 stay, and the ledger + ADR-0126 §8 go to the engine's log line. Changesets: spec minor, metadata-protocol / objectql / rest / runtime patch, Clause-②: yes (widening) / Metadata-protocol: added. Matches ruling 6512814276 (letter B) and triage 6513025554.
  • Measured choices (the card left them to the seat and its review):
  • Dev: opus under the quota-exhaustion exemption. spec / metadata-protocol / objectql / rest / runtime typecheck + tests exit 0; gates 119/119 run, 0 unmeasured (one check:engine-double-contract --write ledger entry, commit 7237921e); ablation on the item-read serve line reds 5/11 new cases (item + cached), absence and list controls stay green. Pins flipped: action-activation.test.ts, action-activation-dispatch.test.ts, api-action-forward.test.ts, two dogfood tests (not run locally; CI's push battery answers), docs/qa/platform-checklist/areas/api-backend.json (rev 9 → 10). os-dev-report 6515747732.
  • Acceptance notes:
    • describeDisabledFlow (packages/services/service-automation/src/subflow-guards.ts:50) still puts sys_metadata_activation and ADR-0126 §7.2 in the end-user FLOW_DISABLED message: the same lesson, the flow twin. Not this card (read only, not measured at a door); the seat routes it.
    • docs/qa/platform-checklist/areas/platform-core.json still says the data API is the only "what is off here" surface; after this PR the metadata read is one too. Same route.
  • Owed before any landing stroke: the CONTRACT_REVIEW_TIER review (packages/spec/src/** hunks) and the maintainer's approval of the key name; this seat cannot serve that tier now and hands the review over below.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 7237921

… 'refusal', not 'code'

The error-code provenance gate reads an object literal's 'code' property as
a wire stamp. The engine's info log line is not a wire answer; the dispatch
doors stamp ACTION_DISABLED on the envelope. Naming the log field 'refusal'
keeps the code in the log without claiming objectql stamps it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 7237921e365096628921e03d89f7bc18d3cb9d85 · 2026-10-30T05:34Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

New head · 2026-10-30T05:34Z · 8216bec4: one seat commit for the red Lint · docs & code guards row (Error-code provenance guard). The engine's info log line passed { action, code: 'ACTION_DISABLED' } as log metadata, which the guard reads as @objectstack/objectql stamping the code (engine.ts:5931). The field is now refusal, with its pin in action-activation.test.ts; the wire stamp stays the dispatch doors'. Locally: check:error-code-provenance exit 0, action-activation.test.ts 21/21, objectql typecheck exit 0. Nothing else moves; the ACCEPT above stands for this head.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 8216bec

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7237921e365096628921e03d89f7bc18d3cb9d85
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts + packages/metadata-protocol/src/protocol-action-activation-reads.ts, packages/metadata-protocol/src/protocol.ts (getMetaItem, getMetaItems, getMetaItemCached), packages/metadata-protocol/src/protocol-automation-flow-reads.ts (the seam template), packages/rest/src/meta-item-read-routes.ts (the cached object door), packages/objectql/src/engine.ts (getDisabledActions, describeDisabledAction), packages/objectql/src/action-activation.ts (ActionActivationProjection), packages/objectql/src/plugin.ts (hydrateActionActivations), packages/spec/src/contracts/automation-service.ts (getFlowRegistration), packages/spec/authorable-surface/api.json, .changeset/27842-disabled-actions-beside-item.md, .changeset/27842-disabled-actions-served.md. Read, not reviewed; the rest of the diff is the lane seat's ACCEPT at TIER_DEFAULT (comment 6515763620).

Inputs: card #27842 (body, triage 6513025554, claim 6514354306, os-dev-report 6515747732), PR #27964 (body, 23-file list, net diff against main at this head), the check-runs on this head. The PR has since moved to 8216bec4; this record judges 7237921e alone and says nothing about the later head.

① Derived judgments

  • Public surface, @objectstack/spec. Three optional keys named disabledActions: string[] on GetMetaItemResponseSchema (beside item, after registration) and on GetMetaItemCachedResponseSchema (beside data), Record of objectName to string[] on GetMetaItemsResponseSchema (beside items, after registrations). All .optional(), none defaulted, so an absent key parses as absent (pinned in meta-item-response-shapes.test.ts). Right: the card's Wanted 1 and 2, the registration / registrations template at L326 / L682 / L2318 on the base, taken one for one. The authorable-surface ledger gains exactly those three rows (api/GetMetaItemResponse:disabledActions, api/GetMetaItemCachedResponse:disabledActions, api/GetMetaItemsResponse:disabledActions); content/docs/references/api/protocol.mdx gains the two generated table rows. Right.
  • Public surface, @objectstack/spec/contracts. IObjectQLEngine.getDisabledActions?(): string[] | undefined, optional, declared next to the action members. Right: the card's Wanted 3 asked for the getFlowRegistration? shape (automation-service.ts:1381) and left the carrying contract to the seat's measurement; the engine contract is where the activation projection lives and the slot the dispatch doors already resolve (objectql, registered by ObjectQLPlugin at plugin.ts:719). The first consumer probes it (typeof read !== 'function', then Array.isArray(answer)), so the optional mark is honoured, not assumed.
  • Accept set. Unchanged. No parse narrows: the stored object body is neither read for the key nor rewritten (switchedOffActionsOf reads actions[].name and returns names; item.actions is byte-identical with and without a ledger, pinned in three packages). The triage's ⛔ (no visible / disabled rewrite riding GET → PUT) holds in the tree. Right.
  • Faces. Served face only; the execution face carries none (audience === 'served' on the list, !execution on the item read), and both draft returns of getMetaItem (protocol.ts:10944, :11034) exit before the key is computed, so the docblock's "absent on a draft read" is true of the code. A pending draft overlaid on the list gets no entry (_draft === true skipped). Right, and the same lines registration takes.
  • Absent versus []. ObjectQL.getDisabledActions() answers undefined while ActionActivationProjection.durable is false (no store attached) and disabledNames() otherwise; the seam turns a non-array into "no key". So absence = no objectql service, an engine without the member, or no ledger attached; [] = a ledger answered and no name in this object's actions[] is off. Right: the card reserved absence for "no ledger answered, never nothing is switched off", and registration makes the same split.
  • ETag / 304. The protocol never hashes the key (it is read from the delegate getMetaItem after the hash) and refuses notModified when it is present; the REST cached door, which judges an object's 304 itself (audienceFolds), adds !besideJudged to the same condition and serves the unchanged ETag header. Right by the card's Wanted 1 (the automation: a stored flow the engine refuses at cold boot is withdrawn with a warn, while the metadata read keeps serving it as status: 'active' — after the v18 upgrade, every refused flow reads as running #23316 trade, stated in the docblocks). Its cost is a ③ flag below.
  • Wire, the refusal. 409 and the ACTION_DISABLED code are unchanged at both doors; the message string is the new plain sentence from the one producer (ActionActivationProjection.describeDisabled), and disabledActionRefusal's no-sentence fallback is plain too. Every pin of the old sentence moved in one sweep (objectql, runtime, two dogfood tests, api-backend.json revision 9 → 10). Right by Wanted 4 and the triage's reading. The ledger table and ADR citation moved to one info log line in ObjectQL.describeDisabledAction — the lane's file, not a hit; see ③ for the gate it tripped.
  • getMetaItemCached return type widened with disabledActions?: string[] beside registration?. Right.
  • Gate verdicts on this head (the check-runs, read as they stood): Check Changeset success (both runs), Governed Surface Queue Guard success, PR-body guards success, Lint · repo & tooling gates success (carries check:generated, check:api-surface, check:authorable-surface), Lint · CI, release & engine gates success, Lint · PM dispatch-gates success, Lint · ESLint success, Type Check · source gates success, Spec property liveness success, Dogfood Regression Gate success, Build Docs success, Flag docs affected by code changes success. Lint · docs & code guards FAILURE (annotation: the Error-code provenance guard step, pnpm --filter @objectstack/spec check:error-code-provenance, exit 1), so Lint & Repo Gates FAILURE. Test Core, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger were still in progress and Build Core, the eight Test Core shards, Type Check UI and Build & E2E UI were cancelled when the dev pushed 8216bec4, so this head's gate roster is incomplete and red on one lane. The red lane's site is packages/objectql/src/engine.ts (the log line's metadata { action, code: 'ACTION_DISABLED' }), outside hits; nothing in the two spec files is a stamp site. This head cannot land as it stands, whatever this record says about the contract.

② Semver level

  • .changeset/27842-disabled-actions-beside-item.md: @objectstack/spec minor, Clause-②: yes (widening), Metadata-protocol: added. Right: three new optional response keys and one new optional contract member, nothing removed or narrowed; the authorable-surface ledger treats the response keys as surface rows, so added is the ledger's own word for them.
  • .changeset/27842-disabled-actions-served.md: @objectstack/metadata-protocol, @objectstack/rest, @objectstack/objectql, @objectstack/runtime patch, Clause-②: yes (widening), Metadata-protocol: added. The levels are the card's own prescription (Wanted 6: "metadata-protocol / rest / objectql patch") and the published change in each is an additive, optional widening (a wider getMetaItemCached return, a new ObjectQL.getDisabledActions(), a new sentence) — AGENTS.md's "a non-protocol change, widening or breaking, ships patch" covers that. What does not fit is the declaration line beside them: AGENTS.md says the level follows the kind alone and a kind other than none takes at least minor on a package the diff moves, and the flow twin's changeset (fix(automation,metadata-protocol,rest,spec): a stored flow the engine refuses is served beside its verdict, not as running #23358, the template this PR cites) shipped minor on @objectstack/metadata-protocol and @objectstack/service-automation for the same shape (both changelogs list it under Minor Changes at 18.0.0). So the second changeset reads added on a patch bump, which the rule does not allow. Should-fix, one line either way: set Metadata-protocol: none on the consumer changeset (the key is spec's; these four packages add no authorable key of their own, and the card's patch stands), or raise the four to minor as fix(automation,metadata-protocol,rest,spec): a stored flow the engine refuses is served beside its verdict, not as running #23358 did. Check Changeset is green on this head, so no gate holds it; the record does.
  • Clause-②: yes (widening) on the PR body and both changesets matches what the diff publishes. Right.
  • No skip-changeset, no !, no narrowing arm, no ADR-0087 marker owed. Right.

③ Boundary flags

  • Key name disabledActions (card: the seat's measurement and its contract review; triage: "the maintainer approves the key in the PR"). The spelling is consistent across the three schemas, the contract docblock, the ledger, the generated docs, the seam and every test; it is the triage route's literal. This review does not approve a name the card reserved to the maintainer — escalated: the maintainer's word in the PR is still owed.
  • [] versus absent (card: open). Answered above, ①: right, and stated in all three docblocks and the contract docblock in the same words.
  • The 304 trade on objects (the seat's flag to this review, comment 6515763620; the dev's third out-of-scope finding). Answered: the card fixed it (Wanted 1, "never part of the ETag, so a read that carries it is never answered 304"), so the PR is right to do what it does, and the docblocks on GetMetaItemResponseSchema and GetMetaItemCachedResponseSchema say so. The cost is real and larger than the flow twin's: PlatformObjectsPlugin registers sys_metadata_activation, ObjectQLPlugin attaches the ledger on every such boot, so on a default deployment every cached GET /meta/object/:name answers 200 with the full body from now on, and objects are the console's hottest metadata read. The alternative the seat names — fold the switched-off names into the REST validator the way visibility, runtime-view, intake and audience fingerprints already are (meta-item-read-routes.ts, four nested foldVisibilityFingerprintIntoEtag calls) — is correct, keeps 304s, and moves the ETag on an administrator's switch. It is not this PR's to do; escalated to the maintainer beside the key-name approval, as a follow-up card if 304s on object reads are wanted back. No defect here.
  • Deviation 1, the log line lives in ObjectQL.describeDisabledAction. Answered: the placement is sound (the one call both doors make on every refusal; HttpDispatcher.deps.logger is unwired), and the file is the lane's. But on this head that line is what check:error-code-provenance refused (①): a registered code in a non-test stamp-shaped position inside a package whose owner key does not list it. The guard's rule is a row under the stamping package's key or a recorded waiver naming the owner that holds the row; the next head's fix is not judged here, and its own record should confirm it answers that rule rather than the guard's spelling.
  • Deviation 2, no independent review was run. This record is that review, for this head.
  • Deviation 4, the two dogfood tests were not run locally. Dogfood Regression Gate ran green on this head. Answered.
  • Deviations 3 and 5 (PR footer; one engine-double-contract.pinned.json row for the new test's findOne double). Lane matters, not reviewed; the ledger row is the gate's own prescription.
  • Out-of-scope findings 1 and 2 (platform-core.json posture note now stale; describeDisabledFlow still names the ledger and ADR-0126 §7.2 in FLOW_DISABLED). Concur on both: the second is the same lesson on the flow twin and is its own card, since the automation checklist keys on that message. Out-of-scope 4 (HttpDispatcher logger unwired): observation, concur.
  • open_questions: none declared; none found.

④ Design judgment

packages/spec/src/api/protocol.zod.ts — judged against card #27842 (ruling 6512814276 letter B), the registration template on main, and the first consumer (metadata-protocol's item, list and cached reads; rest's cached object door).

Holds. Every claim the three docblocks make is true of the cited code: present only on an object read (canonicalMetaType(type) !== 'object' returns no reader, so objects is served and view, flow, app are not); [] only when a ledger answered (durable gate on the engine; the seam treats any non-array as absence); absent on a draft read (both draft returns exit first); beside item and never inside it (item.actions byte-identical, pinned); never in the ETag and never 304 when present (protocol and REST both). The list's "one entry per listed object that is not a pending draft" is disabledActionsOf line for line. The string[] element is the declarative action name, the same key the projection and the doors use, so a consumer can match it against item.actions[].name directly. Attempts to break it: a body with no actions array yields [] (a true statement — nothing of this object is off); a name switched off on another object is not named here (the per-object intersection, pinned with some_other_objects_action); a ?preview=draft item read returns before the key (:10944); the execution face carries none; the plural type folds. One cost is accepted by the card, not by this file — the 304 trade in ③. No must-fix. No should-fix in this file.

packages/spec/src/contracts/objectql-engine.ts — judged against the card's Wanted 3, the getFlowRegistration? seam it names, and the first consumer (protocol-action-activation-reads.ts) plus the implementer (ObjectQL).

Holds. The member's two answers are kept apart in the type (string[] | undefined) and in the docblock, and the implementer honours both: undefined while no store is attached, the projection's names otherwise. The docblock's account of when no store is attached matches plugin.ts (store.probe() throws → setActionActivationStore is never called, warn logged). The consumer reads it through the kernel's service table by name, per call, with a Pick of the contract type and a runtime probe, and @objectstack/metadata-protocol imports nothing from @objectstack/objectql — the card's import rule holds. Attempts to break it: the attach-then-hydrate window in plugin.ts (store attached, actionActivationAttached = true, then await hydrate()) and a hydrate fault both leave durable true over an empty projection, so the read says [] while the ledger may hold rows — but the doors dispatch by that same empty projection and the plugin logs the fault at error, so the read and the refusal still answer from one fact, which is what the card asked for; a design that said more than the doors know would be the wrong one. The projection is keyed by declarative name, not by object, so a name two packages share is switched off on both — the doors' own rule since ADR-0126 §8, inherited, not introduced.

Should-fix (this file): the interface header still reads "One member is optional by ruling, not by this default: {@link judgeFilter} (#19995 ruling C)", and the header's stated default is that members are required because ObjectQL implements every one. There are now two optional members, and the new one's docblock gives its own reason ("an engine that keeps no activation projection does not declare it") but the header does not name it. Add the second member and its warrant (card #27842 Wanted 3 under ruling 6512814276 letter B, the getFlowRegistration? shape) to that header paragraph, so the file does not contradict itself. Not a must-fix: the card asked for optional, the consumer probes, and ObjectQL implements it.

Verdict on the hits: no must-fix. Two should-fixes (the contract header; the consumer changeset's declaration line in ②). Two escalations (the key name; the 304 cost). The head's own gate roster is red on Lint · docs & code guards and incomplete, so landing waits on a later head and that head's record.

Implemented-by: claude/issue-27842-disabled-actions-beside-item
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 8216bec475d46fe55888dd1bd07bf1b00bf6459f · 2026-10-30T05:45Z


Generated by Claude Code

…d the consumer changeset declares Metadata-protocol none

The contract review on 7237921 asked for two should-fixes. The
IObjectQLEngine header now names getDisabledActions beside judgeFilter
as optional by ruling. The four consumer packages add no authorable key
of their own, so their patch changeset declares Metadata-protocol: none;
the key stays declared `added` on spec's minor changeset.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

New head · 2026-10-30T05:46Z · b3d92fee, answering the contract review on 7237921e (comment 6515936468, PASS):

Since 7237921e, the delta is these two findings-and-gate commits and nothing else, so it is owed a delta review by the same reviewer.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head b3d92fe

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8216bec475d46fe55888dd1bd07bf1b00bf6459f
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts + packages/objectql/src/engine.ts (describeDisabledAction, the line the move touches), packages/objectql/src/action-activation.test.ts (its pin), packages/spec/scripts/check-error-code-provenance.ts (the guard that was red, its header and STAMP_PATTERNS), packages/spec/src/api/error-code-ledger.zod.ts (the ACTION_DISABLED row under @objectstack/runtime, the @objectstack/objectql owner key, PROVENANCE_WAIVERS), packages/runtime/src/action-execution.ts (disabledActionRefusal, where the code is stamped), .changeset/27842-disabled-actions-served.md, .changeset/27842-disabled-actions-beside-item.md. Read, not reviewed; the rest of the diff is the lane seat's ACCEPT at TIER_DEFAULT (comment 6515763620, carried to this head by 6515810880).

Inputs: card #27842 (body, triage 6513025554, claim 6514354306, os-dev-report 6515747732; no comment has been added to the card since record 6515936468), PR #27964 (body, 23-file list, net diff against main at this head from the files endpoint, every comment), the check-runs on this head, and record 6515936468 on 7237921e. This is a DELTA record: what moved is git log 7237921e..8216bec4, exactly one commit (8216bec4, "fix(objectql): the disabled-action log line carries the refusal under 'refusal', not 'code'"), two lines in two files: packages/objectql/src/engine.ts:5931 and packages/objectql/src/action-activation.test.ts:399. Both heads share the merge-base e41642dd with origin/main, so there is no main carry-over to separate: the move is the PR's own, whole. Neither hits file moved (git diff 7237921e 8216bec4 -- packages/spec .changeset is empty), so ① to ④ below say where they stand on record 6515936468 and judge afresh only what the move touches. The PR has since moved to b3d92fee (seat comment 6515952505); this record judges 8216bec4 alone and says nothing about the later head.

① Derived judgments

  • Public surface, @objectstack/spec and @objectstack/spec/contracts. Unmoved. The three optional disabledActions keys, the IObjectQLEngine.getDisabledActions?() member, the three authorable-surface rows and the two generated doc rows stand as judged in record 6515936468. Right.
  • Accept set. Unmoved; stands as judged in record 6515936468 (no parse narrows, item.actions byte-identical, the triage's ⛔ holds). Right.
  • Faces, absent versus [], ETag / 304, the widened getMetaItemCached return type. Unmoved; stand as judged in record 6515936468.
  • Wire, the refusal. The move changes nothing on the wire: the 409 and the ACTION_DISABLED code are stamped by the two dispatch doors in @objectstack/runtime (disabledActionRefusal, action-execution.ts:930, reads only the sentence from describeDisabledAction and stamps the code from its own literal; the MCP door reaches the same helper), and the sentence ActionActivationProjection.describeDisabled produces is untouched. What moved is the engine's info log line: its metadata object is { action, refusal: 'ACTION_DISABLED' } where it was { action, code: 'ACTION_DISABLED' }; the message string still names the code, the action, sys_metadata_activation and ADR-0126 §8 item 2, and the test pin (action-activation.test.ts:399) moved with it in the same commit, still asserting the line carries the code, the name, the table and the ADR. No consumer reads that metadata (no door, no test outside the moved pin), so no accept set and no public surface changes. Right, and judged against the guard's rule in ③.
  • Gate verdicts on this head (the check-runs, read as they stood, with the two workflow runs 41009211107 and 41009211122 both completed · cancelled at 05:46–05:47Z when b3d92fee was pushed): Lint · docs & code guards success, its Error-code provenance guard step success, so Lint & Repo Gates success — the red that record 6515936468 named on 7237921e is cleared on this head. Also success: Check Changeset (both runs), Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · ESLint, the three Lint · scoped lanes, Type Check · source gates, Type Check · debt ledger, Spec property liveness, Build Core, Build Docs, Build UI Deps, Build & E2E UI, Live E2E UI, Test UI, Dogfood Regression Gate, Flag docs affected by code changes, Plan Test Core shards. Not green: Type Check · workspace cancelled at its Type check workspace packages step, Type Check · consumer gates cancelled at Check docs formula examples are valid CEL (every earlier step success, including Check @objectstack/spec public API surface and the nameability and no-any pins), so the TypeScript Type Check summary job concluded failure on "Verify every type-check lane succeeded"; the eight Test Core shards and Type Check UI are still reported in_progress on a run that is over, so they never concluded. Pending is not green: this head's gate roster is incomplete on the type-check and core-test families, and this head cannot land as it stands, whatever this record says about the contract. The roster that can land is the one on the head that follows, judged by that head's record.

② Semver level

Stands as judged in record 6515936468: @objectstack/spec minor with Clause-②: yes (widening) and Metadata-protocol: added is right for three new optional response keys and one new optional contract member; the consumer changeset's four patch bumps are the card's prescription and the published change in each is an additive widening. The move publishes nothing new (a log metadata key is not a published surface in any of the five packages), so no level changes.

Should-fix, carried, not addressed on this head: .changeset/27842-disabled-actions-served.md is byte-identical to 7237921e and still reads Metadata-protocol: added over four patch bumps, which AGENTS.md's level rule ("a kind takes at least minor on a package the diff moves, none takes patch") does not allow. Either Metadata-protocol: none on the consumer changeset (the key is spec's; these four packages add no authorable key of their own) or minor on the four as #23358 did. The seat says the next head sets none (comment 6515952505); that head's record judges it. Check Changeset is green on this head, so no gate holds it; this record does.

Clause-②: yes (widening) on the PR body and both changesets matches what the diff publishes. No skip-changeset, no !, no narrowing arm, no ADR-0087 marker owed. Right.

③ Boundary flags

  • Deviation 1, the log line in ObjectQL.describeDisabledAction, and the guard it tripped — judged afresh, since this is what the move answers. The rule, read off the guard's own header (check-error-code-provenance.ts, ADR-0112 D3): every stamp site of a REGISTERED code in packages/** non-test source must be listed under the stamping package's owner key or carry a PROVENANCE_WAIVERS entry naming the owner key that deliberately holds the row; a stamp is a code emitted — the published patterns are code: 'X' in an object literal ("a returned envelope, c.json(…)"), err.code = 'X' ("stamped onto a value about to be thrown") and a constant whose initializer is a code; the scan is declared blind to template literals, and its one "mention, not stamping" exemption is the ledger file by name. ACTION_DISABLED is registered under @objectstack/runtime (ledger row: "answered 409 by BOTH action doors", the activation-ledger consult in action-execution.ts); @objectstack/objectql's owner key does not list it and no waiver names the pair. On 7237921e the engine's log metadata spelled code: 'ACTION_DISABLED', the objlit shape, so the guard read @objectstack/objectql as a stamper with no row: red, correctly by its published patterns.
    Does the move answer the rule or the guard's spelling? The rule's subject is provenance of emission — who hands the code to a caller. The engine hands no code to anyone: disabledActionRefusal reads only the sentence and stamps ACTION_DISABLED from runtime's own literal, the MCP door goes through the same helper, and nothing reads the log metadata. So the two answers the guard offers were both wrong for this site: a row under @objectstack/objectql would record an emitter that does not exist (the runtime row's comment already names both doors and nothing else), and the "door, not the producer, names the wire vocabulary" waiver class presumes a value the door READS from the producer (AutomationResult.code for FLOW_DISABLED), which no door does here — a waiver would record a fiction, and the guard would then hold that fiction live. The line is mention, the same class as the ledger file's own exemption, and the honest shape for a mention is a key that is not a stamp key. That is what the move does: the metadata key is refusal, the message string still names the code (a template literal, outside the guard's declared sight by design, and rightly so: a log message is prose), and the pin moved with it. Answered: this answers the rule, not the spelling. Two things are read and accepted as costs, not findings: the decision has no ledger record (its record is the commit message, the pin's title and this record), and the guard itself holds the line — renaming the key back to code: reds the lane again, so the next author cannot undo it unknowingly. Observation, not a finding (lane's file, not a hit): one comment line at engine.ts:5931 saying why the key is refusal would save that author the round trip. Repo convention is not broken: no non-test logger.* call in packages/** puts a registered code under a code: metadata key, and refusal: 'RUN_NOT_FOUND' (runtime/src/domains/automation.ts:1656) is the one precedent for the spelling. The level stays info, right by AGENTS.md "Degradation log levels": a refusal handed to the caller is not a degradation.
  • Key name disabledActions — still escalated: the maintainer's word in the PR is still owed; no comment on PR feat(spec,metadata-protocol,rest,objectql,runtime): GET /meta/object serves switched-off packaged actions beside the item, and the disabled-action refusal reads as a plain sentence #27964 or card meta: GET /meta/object serves which packaged actions are switched off beside the item, and the disabled-action refusal reads as a plain sentence #27842 gives it as of the inputs read.
  • [] versus absent — answered in record 6515936468; unmoved.
  • The 304 trade on objects — escalated in record 6515936468, carried unchanged: the cost and the alternative (fold the switched-off names into the REST validator) are before the maintainer beside the key name; no defect here.
  • Deviation 2, no independent review — this record, for this head.
  • Deviation 4, the dogfood tests not run locally — Dogfood Regression Gate ran green on this head too. Answered.
  • Deviations 3 and 5, out-of-scope findings 1, 2 and 4 — stand as judged in record 6515936468; the move touches none of them.
  • open_questions: none declared; none found. The move adds nothing outside record 6515936468's findings: both changed lines are the site that record's ③ named, and nothing else in the tree differs between the two heads.

④ Design judgment

Neither hits file moved between 7237921e and 8216bec4, and nothing the move touches changes what the two files claim about their implementer: ObjectQL.getDisabledActions() is unchanged, describeDisabledAction returns the same sentence from the same producer, and the engine's behaviour toward the metadata read and the two doors is the same as the one record 6515936468 judged the docblocks against.

packages/spec/src/api/protocol.zod.ts — stands as judged in record 6515936468: holds against card #27842 (ruling 6512814276 letter B), the registration template on main and the first consumer; every docblock claim true of the cited code; no must-fix, no should-fix in this file.

packages/spec/src/contracts/objectql-engine.ts — stands as judged in record 6515936468: holds against Wanted 3, the getFlowRegistration? seam and the first consumer plus the implementer; both answers kept apart in type and docblock and honoured by ObjectQL. Should-fix, carried, not addressed on this head: line 233 still reads "One member is optional by ruling, not by this default: {@link judgeFilter} (#19995 ruling C)" while the interface now has two optional members; add getDisabledActions and its warrant (card #27842 Wanted 3 under ruling 6512814276 letter B, the getFlowRegistration? shape) to that header paragraph so the file does not contradict itself. Not a must-fix: the card asked for optional, the consumer probes, ObjectQL implements it. The seat says the next head rewrites the header to "Two members are optional by ruling" (comment 6515952505); that head's record judges it.

Verdict on the hits: no must-fix. Two should-fixes carried from record 6515936468, both still open on this head (the contract header; the consumer changeset's declaration line in ②). Two escalations carried (the key name; the 304 cost). The guard red of 7237921e is cleared on this head by a change that answers the rule. The head's own gate roster is incomplete on the type-check and core-test families (cancelled by the push of b3d92fee), so landing waits on a later head and that head's record.

Implemented-by: claude/issue-27842-disabled-actions-beside-item
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head b3d92fee1573d4f20f93de76b5344320e75bac4d · 2026-10-30T06:05Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b3d92fee1573d4f20f93de76b5344320e75bac4d
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts + packages/spec/src/contracts/automation-service.ts (getFlowRegistration?, the template the rewritten header cites), packages/objectql/src/engine.ts (the implementer of every optional member the header counts), .changeset/27842-disabled-actions-beside-item.md, .changeset/27842-disabled-actions-served.md (the line the move touches), scripts/check-changeset-no-major.mjs (the level axis that declaration answers) with scripts/pm/clause2-line.mjs (its one reader), .github/workflows/pr-automation.yml (the Check Changeset step's WHICH LEVEL prose), scripts/release-github-releases.mjs (the release body that consumes each changeset's own declaration), AGENTS.md Post-Task Checklist step 3. Read, not reviewed; the rest of the diff is the lane seat's ACCEPT at TIER_DEFAULT (comment 6515763620, carried to the later heads by 6515810880 and 6515952505).

Inputs: card #27842 (body, triage 6513025554, claim 6514354306, os-dev-report 6515747732; no comment has been added to the card since record 6516042954), PR #27964 (body, 23-file list, net diff against main at this head from the files endpoint, every one of its nine comments; zero reviews and zero review comments, no reviewer requested, still a draft), the fifty check-runs on this head, and records 6515936468 on 7237921e and 6516042954 on 8216bec4. This is a DELTA record on record 6516042954: what moved is git log 8216bec4..b3d92fee, exactly one commit, whose sole parent is 8216bec4 (no merge of main rides in it, so there is no carry-over to separate: the move is the PR's own, whole), two files, five lines in and four out — .changeset/27842-disabled-actions-served.md line 11 (Metadata-protocol: added becomes Metadata-protocol: none) and packages/spec/src/contracts/objectql-engine.ts lines 233–236 (the interface header paragraph). Nothing else in the tree differs between the two heads (git diff --stat names those two files alone), and packages/spec/src/api/protocol.zod.ts is byte-identical to the file both earlier records judged. The PR's head at the time of reading is this head; this record judges b3d92fee alone.

① Derived judgments

  • Public surface, @objectstack/spec and @objectstack/spec/contracts. Unmoved by this commit: the three optional disabledActions keys, the optional IObjectQLEngine.getDisabledActions?(): string[] | undefined member, the three authorable-surface rows and the two generated doc rows stand as judged in record 6515936468 and carried by 6516042954. The header paragraph the move rewrites is TSDoc on an exported interface, not a type: no export is added, removed or renamed, no member's optionality changes (judgeFilter? at line 318 and getDisabledActions? at line 352 were optional on 8216bec4 and are optional here), and no generated artifact moves (content/docs/references/api/protocol.mdx is the same +3/−0 hunk; Lint · repo & tooling gates, which carries check:generated, check:api-surface and check:authorable-surface, is green). Right.
  • Accept set, faces, absent versus [], ETag / 304, the widened getMetaItemCached return type, the wire refusal. Unmoved; every one stands as judged in record 6515936468 (and, for the refusal's log line, 6516042954). The move touches no code path.
  • The changeset declaration. Metadata-protocol: on a changeset body is a release carrier, not a published surface: it is read by check-changeset-no-major.mjs (the level axis) and by release-github-releases.mjs (which groups the GitHub Release body from each bullet's own lines). Changing it publishes nothing; what it declares is judged in ②.
  • Gate verdicts on this head (the fifty check-runs, read as they stood; every one is completed, none in_progress or queued, so this is the first head of the PR whose roster is whole). The ten required contexts (AGENTS.md Multi-agent discipline §7): Lint & Repo Gates success (its lanes Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · ESLint and the three Lint · scoped lanes all success — the Error-code provenance guard red of 7237921e stays cleared), TypeScript Type Check success (Type Check · workspace, Type Check · consumer gates, Type Check · source gates, Type Check · debt ledger all success — the two lanes cancelled on 8216bec4 are green here), Test Core success (all eight shards success — the eight left in_progress on 8216bec4 concluded here), Build Core success, Governed Surface Queue Guard success, Type Check UI success, Test UI success, Build & E2E UI success, Live E2E UI success, and Lint UI skipped by the filter job (no ui/ path in the diff, as on every head of this PR; its Test UI shard matrix is skipped the same way under a Test UI summary that is success). Also success: Check Changeset, PR-body guards, Check PR Size, Check Documentation Links, Spec property liveness, Build Docs, Build UI Deps, Dogfood Regression Gate, Flag docs affected by code changes, Plan Test Core shards, Auto Label. Skipped by design: the opt-in harnesses, the schedule gate, Temporal Conformance, UI Published Artifacts, Dogfood Verify CLI. GitHub reports the PR mergeable_state: clean on this head. Nothing on this head is red or pending. Right: the roster record 6516042954 said would have to come from a later head is this one.

② Semver level

Judged afresh, since the move is the consumer changeset's declaration line and the brief asks that the declaration now be consistent across every changeset and the PR body.

  • .changeset/27842-disabled-actions-beside-item.md: @objectstack/spec minor, Clause-②: yes (widening), Metadata-protocol: added. Unchanged and right, as record 6515936468 judged: three new optional response keys (three new authorable-surface/api.json rows) and one new optional contract member — a kind, so at least minor on the package the diff moves.
  • .changeset/27842-disabled-actions-served.md: @objectstack/metadata-protocol, @objectstack/rest, @objectstack/objectql, @objectstack/runtime patch, Clause-②: yes (widening), Metadata-protocol: none (the move). Right, and it closes the should-fix. The rule (AGENTS.md Post-Task Checklist step 3, mechanised by check-changeset-no-major.mjs): "The level follows the kind alone: a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch". None of these four packages adds an authorable key, value or type of its own — the key is spec's; what they publish is a wider getMetaItemCached return, a new ObjectQL.getDisabledActions(), a plain sentence — so none is the kind and patch is its level, and Clause-②: yes (widening) beside none is the shape the gate's own battery names as a non-protocol widening at patch ('@objectstack/rest': patch … Clause-②: yes … Metadata-protocol: none is a MUST_NOT_MATCH_BREAKING row in check-adr-0087-registration.mjs, and the B.rest fixture in release-github-releases.mjs). The card's own prescription (Wanted 6: "metadata-protocol / rest / objectql patch") is kept. Checked against the level gate's lanes: a declared none must grade every entry it introduces patch — all four are; added beside Clause-②: no is the contradiction, and this changeset declares yes; narrowed without the (narrowing) arm is the other, and nothing declares narrowed. Check Changeset is green on this head, which is the gate's own word.
  • The PR body: Clause-②: yes (widening) / Metadata-protocol: added. Right, and consistent with the two changesets rather than contradicted by them. The PR-body line is PR-scoped and names no package (the gate's header: "The kind is declared ONCE, FOR THE PR"; its predicate is "at least one package whose published source the diff moves graded minor or above" — @objectstack/spec, satisfied). AGENTS.md's "several ⇒ narrowed over semantic over added" makes added the PR's kind because the PR does add authorable surface; a changeset's line is that changeset's packages' kind. So the three declarations say one thing from three positions: the PR adds a key (body: added), the key is spec's (spec changeset: added, minor), the four consumers add no key of their own (consumer changeset: none, patch). This is the shape the flow twin fix(automation,metadata-protocol,rest,spec): a stored flow the engine refuses is served beside its verdict, not as running #23358 did NOT take (it shipped minor on its consumers), and the ruling that re-ruled the level axis (ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105, 2026-10-21, after fix(automation,metadata-protocol,rest,spec): a stored flow the engine refuses is served beside its verdict, not as running #23358 landed) is what makes this PR's shape the right one now, not that one.
  • What the release body will read (release-github-releases.mjs, read as the consumer of these lines): the spec bullet lands under 新增能力 by added; the consumer bullet lands under 新增能力 too, by its Clause-②: yes (classifyReleaseBullet: kind === 'added' || value === 'yes' ⇒ added), not under 其他修复 — so the served key and the plain sentence still read as new capabilities to an upgrading reader, and the none costs them nothing. Right.
  • No skip-changeset, no !, no narrowing arm, no ADR-0087 marker owed. Right.

Should-fix from record 6515936468 (the consumer changeset's declaration): closed on this head.

③ Boundary flags

  • Key name disabledActions — still escalated. The inputs on this head carry no maintainer word: the PR has zero reviews and zero review comments, no reviewer is requested, every one of its nine comments is a bot's (eight from fleet seats, one the docs-drift check), and no comment has been added to the card since the previous record. The PR body still asks for it in so many words. Not this review's to approve; nothing in the tree depends on the answer except the spelling.
  • The 304 trade on objects — escalated in record 6515936468, carried unchanged by 6516042954 and here; the move touches nothing on that path. No defect.
  • Deviation 1 (the engine's log line) and the provenance guard — answered in record 6516042954 (the refusal key answers the rule, not the guard's spelling). packages/objectql/src/engine.ts is byte-identical between 8216bec4 and this head, and Lint · docs & code guards is green again. Stands.
  • [] versus absent — answered in record 6515936468; unmoved.
  • Deviation 2 (no independent review) — this record, for this head.
  • Deviation 4 (dogfood tests not run locally) — Dogfood Regression Gate success on this head too. Answered.
  • Deviations 3 and 5, out-of-scope findings 1, 2 and 4 — stand as judged in record 6515936468; the move touches none of them.
  • The seat's new-head note (comment 6515952505) claims the delta is "these two findings-and-gate commits and nothing else". Checked against the tree, not the note: between 8216bec4 and this head there is one commit, two files, five lines in and four out, each line one of the two should-fixes; between 7237921e and this head, three files (engine.ts, its test pin, and these two). The claim is true of the tree. The move adds nothing outside the two records' findings and breaks nothing either record judged.
  • open_questions: none declared; none found.

④ Design judgment

packages/spec/src/api/protocol.zod.ts — did not move (byte-identical to 7237921e and 8216bec4); stands as judged in record 6515936468 and carried by 6516042954: holds against card #27842 (ruling 6512814276 letter B), the registration template on main and the first consumer; every docblock claim true of the cited code; no must-fix, no should-fix in this file.

packages/spec/src/contracts/objectql-engine.ts — the member, its docblock and the type stand as judged in record 6515936468 (both answers kept apart in type and docblock, honoured by ObjectQL; the consumer probes by name through the service table and imports no engine). Judged afresh here: the header paragraph, lines 233–236, which is what the move rewrites.

Holds. The header now reads "Two members are optional by ruling, not by this default: {@link judgeFilter} (#19995 ruling C) and {@link getDisabledActions} (#27842 ruling letter B, the getFlowRegistration? shape). Their callers must work with an engine that lacks them, so the optional mark states that requirement in the type." Each claim checked against the file and the cited code: both links resolve to members of this interface (judgeFilter? line 318, getDisabledActions? line 352), the same scope {@link judgeFilter} already resolved in; each member's own docblock states its optional mark and its warrant in the same terms as the header ("OPTIONAL by that ruling … probes before calling" at line 316; "Optional: an engine that keeps no activation projection does not declare it, and the read then serves no key rather than a guess" at line 350), so the file no longer contradicts itself — the should-fix's exact ask; the template cited is real and the same shape (automation-service.ts:1381, getFlowRegistration?(name: string): FlowRegistration, docblock "Optional: an automation implementation that keeps no refusal record does not declare it, and the read then serves no verdict at all rather than a guess", read by the metadata protocol through the service table by name); and "their callers must work with an engine that lacks them" is true of the one consumer (protocol-action-activation-reads.ts probes typeof read !== 'function' and then Array.isArray, as record 6515936468 read it) and of the implementer (ObjectQL implements both, so implements keeps the default's promise). The warrant's spelling, "#27842 ruling letter B": the ruling of record is comment 6512814276 on #27504, letter B, and card #27842 carries it as its Ruling-ref in its body, so the citation resolves in one hop; the repo's commoner spelling names the host ("ruling B on #14478" in ai/conversation.zod.ts:309), which would save that hop. An observation, not a finding.

Attempt to break it, by counting: the interface carries a THIRD optional member, hasObjectMiddleware?(objectName: string): boolean (line 420, #21080), which the header does not name. It was optional on main before this PR (it is at origin/main line 395 under the old "One member" header, so the enumeration was already one short when this card was claimed), its own docblock states the mark and the probe ("OPTIONAL: an engine without it cannot answer … typeof ql.hasObjectMiddleware === 'function'"), and it is optional by its consumer's need, not by a ruling — so the rewritten sentence, which counts members optional "by ruling", is exact as written, and ObjectQL implements it (engine.ts:6437). What remains is that the header's exceptions paragraph names two of the three members that depart from its REQUIRED default; the third departed on main under #21080 with no header edit, which is that card's drift, not this PR's. Observation, not a finding on this PR, and a one-clause follow-up for the lane that next touches this file: name hasObjectMiddleware and its warrant (#21080, the consumer declines on absence) in the same paragraph, so the count a reader takes from ?( matches the header's.

Should-fix from record 6515936468 (the contract header): closed on this head.

Verdict on the hits: no must-fix. Both should-fixes carried from record 6515936468 through 6516042954 are closed on this head, each by exactly the line it named, and the move adds nothing else. Two escalations carried (the key name; the 304 cost), both before the maintainer and neither a defect. The gate roster on this head is whole and green: every check-run concluded, the ten required contexts are success or filter-skipped, and the type-check and core-test families that were cancelled on 8216bec4 are success here. What this head still waits on is the maintainer's word on the key name, which the card reserved and which no record of this review supplies.

Implemented-by: claude/issue-27842-disabled-actions-beside-item
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Maintainer's word · 2026-10-30T10:31Z · 田騏 (marchtian) approved this PR as built at 2026-10-30T10:29Z (「同意」, on the seat's decision card "Approve as built", after a business-level walk through the key and its cost). This answers both escalations the contract review carried (record 6515936468 ③, carried by 6516042954 and 6516322954):

Landing check at head b3d92fee1573d4f20f93de76b5344320e75bac4d: same-shape CONTRACT_REVIEW_TIER PASS on record (6516322954), every check concluded green, and check-expected-skips exit 0 (9 skips, all in main's roster). The seat readies the PR and arms auto-merge in this act.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

⛔ main is red after this PR landed: p0 #28082 · 2026-10-30T11:57Z

This PR's landing commit 3e0fd7a8 turned main's Dogfood Regression Gate (3/3) red, deterministically. packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts:178 still asserts that the disabled-action message contains sys_metadata_activation. The door now answers the ruled plain sentence, so this is the pin #27842 owed.

Owning seat (domain:spec#4): claim #28082 this round. The stop-bleed is that one pin flip, plus any sibling sys_metadata_activation message pin in packages/qa/dogfood. Per #27047 item 6, if it is unclaimed for one round, triage escalates by name, and after that triage dispatches the stop-bleed.


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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

meta: GET /meta/object serves which packaged actions are switched off beside the item, and the disabled-action refusal reads as a plain sentence

1 participant