Repository navigation
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
Conversation
…side the object read; plain disabled-action sentence Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ape pins 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
…sabled-actions-beside-item
…ouble in the engine-double pinned ledger Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
📓 Docs Drift CheckThis PR changes 5 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 席位核对 · ACCEPT (lane part) · card #27842 · 2026-10-30T05:31Z
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
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V New head · 2026-10-30T05:34Z · Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 8216bec |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs: card #27842 (body, triage 6513025554, claim 6514354306, os-dev-report 6515747732), PR #27964 (body, 23-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Holds. Every claim the three docblocks make is true of the cited code: present only on an
Holds. The member's two answers are kept apart in the type ( 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 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 Implemented-by: VERDICT: PASS Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: 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
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V New head · 2026-10-30T05:46Z ·
Since Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head b3d92fe |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: 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 ① Derived judgments
② Semver levelStands as judged in record 6515936468: Should-fix, carried, not addressed on this head:
③ Boundary flags
④ Design judgmentNeither
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 Implemented-by: VERDICT: PASS Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: 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 ① Derived judgments
② Semver levelJudged 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.
Should-fix from record 6515936468 (the consumer changeset's declaration): closed on this head. ③ Boundary flags
④ Design judgment
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 Attempt to break it, by counting: the interface carries a THIRD optional member, 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 Implemented-by: VERDICT: PASS Generated by Claude Code |
|
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 |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U ⛔ main is red after this PR landed: p0 #28082 · 2026-10-30T11:57ZThis PR's landing commit Owning seat ( Generated by Claude Code |
Requested by 田騏 · project thread
Fixes #27842
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
GET /meta/object/:name(含默认缓存通道)与GET /meta/object列表,在 item 旁边新增disabledActions,列出本部署在「设置 › 打包自动化」里被关掉的打包动作;点被关动作时的 409 提示改成给终端用户看的白话句子。item.actions一字不改;没有台账的部署不出这个键,行为同今天。代价是带键的对象读取不再回 304(与 automation: a stored flow the engine refuses at cold boot is withdrawn with a warn, while the metadata read keeps serving it asstatus: 'active'— after the v18 upgrade, every refused flow reads as running #23316 的registration同一取舍)。回滚即还原本 PR。packages/metadata-protocol/src/protocol-action-activation-reads.ts(读取缝与“缺席≠空”)、packages/rest/src/meta-item-read-routes.ts(对象缓存通道的 304 让路那一处)、packages/spec/src/api/protocol.zod.ts(三个 schema 的新键与说明)。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.object-disabled-actions.test.ts,预期 11 条全绿;在 showcase 关掉showcase_mark_done后GET /api/v1/meta/object/showcase_task返回"disabledActions":["showcase_mark_done"]。disabledActions(单项读取为名字数组,列表为按对象名分组的同名键)——这是 Clause-② yes 的协议新增,键名由你定。What changes
The action twin of
registration(#23316, PR #23358), served beside the item and never inside it.@objectstack/spec, minor).GetMetaItemResponseSchemaandGetMetaItemCachedResponseSchemagain the optionaldisabledActions: string[];GetMetaItemsResponseSchemagains the optionaldisabledActions: Record<objectName, string[]>.IObjectQLEngine(contracts/objectql-engine.ts, the engine contract the dispatch doors already reach) gains the optionalgetDisabledActions(): string[] | undefined. Docblocks state the four choices below.@objectstack/objectql).ObjectQL.getDisabledActions()answers the names the engine'sActionActivationProjectionswitches off (the projection both dispatch doors consult before the 409), orundefinedwhile no ledger is attached.listDisabledActions()keeps answering[]for both cases.@objectstack/metadata-protocol).protocol-action-activation-reads.tsreadsgetServicesRegistry().get('objectql').getDisabledActionsper call, the waygetFlowRegistrationis read fromautomation.metadata-protocolimports nothing fromobjectql. The item read (served face, not on draft returns) spreadsdisabledActionsbesidelock; the list spreads it besideitems, one entry per listed non-draft object;getMetaItemCachedcarries it besidedataand skips the 304 when it is present.@objectstack/rest). The cached branch ofGET /meta/:type/:namerebuilds the envelope withdisabledActionsforobject. 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.@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 theACTION_DISABLEDcode are unchanged.ObjectQL.describeDisabledAction(), which both doors call exactly when they refuse, writes oneinfolog line with the code, the action name,sys_metadata_activationand 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
disabledActions, the triage route's spelling, on all three reads (the list keyed by object name, asregistrationsis keyed by flow name). The maintainer approves the key name in this PR (Clause-② yes).[]. Measured onregistration: when the engine answers, every served flow gets a verdict (unregisteredincluded) and the list holds one entry per listed non-draft flow (Object.fromEntriesover 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: noobjectqlservice, an engine without the member, orgetDisabledActions()returningundefinedbecause 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.item.actions[]that the projection holds, in declared order. Packaged standalone actions bound to the object are merged intoactions[]bydefineStack/registerApp(mergeActionsIntoObjects), so they are covered. The key never names more thanitem.actionsalready discloses.Tests
metadata-protocol—src/protocol.object-disabled-actions.test.ts(new, 11 cases): the names of this object only;item.actionsbyte-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 besidedatawith 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; withIf-None-Matchequal 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 realObjectQLand the real protocol through the service table; asetActionActiveflip 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 inapi-action-forward.test.tsnow call the realActionActivationProjection.describeDisabled, not a transcribed copy.spec—src/api/meta-item-response-shapes.test.ts: type-levelEqpins for the three keys, parses for names /[]/ absent, and a located refusal for a non-string name.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), anddocs/qa/platform-checklist/areas/api-backend.json(api-backend.packaged-action-disabled-dispatchacceptance[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/mainincluded):@objectstack/spec@objectstack/metadata-protocol@objectstack/objectql@objectstack/rest@objectstack/runtimeThe first runtime typecheck exited 1 on
organization-ownership-boot-guard.turso-remote.kernel.test.ts(TS2307,@objectstack/driver-tursohad no localdist). That file is not in this diff. After buildingdriver-tursothe rerun exited 0. The two dogfood files boot example apps and were not run locally; CI's Dogfood Regression Gate runs onmain'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). Thenvitest run src/protocol.object-disabled-actions.test.tsgave 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 andgit diff HEADis empty.Acceptance notes
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 composessys_metadata_activation(everyPlatformObjectsPluginboot) attaches a ledger, so every cachedGET /meta/object/:namethere 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.infoand written by the engine indescribeDisabledAction. That method is the one place both doors pass on every refusal, and it holds the host's logger. The dispatcher'sdeps.loggeris not wired onHttpDispatcher. The sentence producerActionActivationProjection.describeDisabledstays pure.SERVICE_UNAVAILABLErefusal of the activation write door still namessys_metadata_activationand 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/objectalso answers it. Left for the checklist owner. Carrier: seat.describeDisabledFlow,packages/services/service-automation/src/subflow-guards.ts) still namessys_metadata_activationand ADR-0126 §7.2 in theFLOW_DISABLEDmessage. 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/objectstackon the final head7237921e36: 119 commands, each exit code recorded before any pipe.--ran: "119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN" (exit 0).check:skill-examplesandcheck:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: nodist/for packages outside the touched closure). Afterpnpm turbo run build --filter='./packages/**', both exited 0.check:engine-double-contractexited 1 (RETAINED: the new metadata-protocol test's pinnedfindOnedouble was not yet in the ledger). Fixed with--write, which adds only that one entry toscripts/engine-double-contract.pinned.json, and committed. The rerun exited 0.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-surfaceandcheck:authorable-surfaceare all in the 119 and all exited 0.mainafter 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