Skip to content

feat(spec): an action can show the reader already pressed it (pressed, a per-reader CEL predicate) - #28235

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-nnpg3i
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-nnpg3i

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

Requested by Tesla · project thread

Fixes #27946

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #28243 (skills/objectstack-ui/rules/actions.md, to land with the console half #28242)

维护者速读

  • 改了什么:动作(action)新增一个可选键 pressed,是一条 CEL 条件(作用域同 visible:当前记录和当前读者),为真时按钮画成"已按下"(aria-pressed),用来告诉读者"你已经投过有用"。
  • 为什么改:HotCRM 知识文章头部的「有用 / 没用」按钮无法显示读者自己已投的票,现有动作契约里没有任何"按读者状态"的键。
  • 风险与回滚:只加一个可选键,不填时解析和行为完全不变;只影响显示,不改动作是否可见、可点、可执行。布尔字面量会被拒(常量不是读者状态)。回滚即 revert 本 PR。本 PR 只是契约这一半,控制台还不画按下态,liveness 记 planned 并对作者告警;画法是后续 ui 卡。
  • 先看:packages/spec/src/ui/action.zod.ts 中紧跟 disabledReason 的 pressed 声明,packages/lint/src/validate-expressions.ts 的 checkAction 与 SERVED_ROW_SITES,ui/packages/core/src/actions/actionKeys.ts 唯一的一行清单。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ui/action-pressed.test.ts,预期 12 条全绿。
  • 需要你做的:无,席位落地。

What changes

A record page header action the reader toggles (HotCRM's mark_article_helpful / mark_article_not_helpful) could not tell the reader they already acted. This is the spec half of the card: the key, its expression lint, its docs and its liveness grade.

  • Spec (packages/spec/src/ui/action.zod.ts). ActionSchema declares pressed: EvaluatedExpressionInputSchema.optional(), placed after disabledReason. A CEL string normalizes to { dialect: 'cel', source }; the envelope is accepted as written. TRUE = the surface draws the button pressed (aria-pressed="true"); FALSE or absent = a plain button. Display only: it never gates, greys or routes the action. active, activeWhen and pressedWhen are renamed onto it by the strict-object alias table.
  • Lint (packages/lint/src/validate-expressions.ts). checkAction judges pressed in the record scope exactly as it judges visible (a bare field reference is an error, unknown fields are refused), at the build (os build / os validate) and at the object save door. 'action pressed' joins SERVED_ROW_SITES, so a block the object declares under attachedOnRead resolves there and a misspelt leaf is refused.
  • ui inventory (ui/packages/core/src/actions/actionKeys.ts, one line, seat ruling A in claim amendment 6528737693). SPEC_ACTION_KEYS lists pressed, inventory only, the description / execution precedent. actionKeys.pin.test.ts (Test UI) compares that array with the spec ActionSchema keys, so any new spec key reds Test UI without this line. ActionDef is untouched and nothing under ui/ reads the key.
  • Liveness. packages/spec/liveness/action.json row pressed: planned, evidenceScope: in-repo, authorWarn with an authorHint (measured: lintLivenessPropertiesFromLedgerDir emits liveness-planned-property on an action that sets it). liveness/object.json attachedOnRead evidence and the ObjectSchema.attachedOnRead TSDoc name pressed among the served-row sites.
  • Docs. content/docs/protocol/objectui/actions.mdx gains "The reader's own state: pressed" beside disabledReason, with a callout that the console does not draw it yet.
  • Ledgers held in step. The expression-surface ledger (packages/qa/dogfood/test/expression-conformance.ledger.ts) gains cel-action-pressed (experimental, unevaluated, PARSE ONLY / NO EVALUATOR FOUND); the evaluated-slot census (evaluated-slot-population.test.ts) goes 31 → 32 positions; the metadata form reconciliation records action.pressed as a planned-not-rendered omit; the lint key-read pin (validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 meta-test) lists pressed.
  • Generated. content/docs/references/ui/action.mdx, data/object.mdx, kernel/metadata-plugin.mdx, authorable-surface/ui.json, liveness/state-counts/action.md.
  • Changesets. .changeset/27946-action-pressed.md (@objectstack/spec minor, @objectstack/lint minor, both declaration lines) and .changeset/27946-action-keys-pressed.md (@object-ui/core patch, Clause-②: no, Metadata-protocol: none, the 26619 inventory precedent).

The shape chosen, and the alternatives not taken

  • Key name: pressed, not active. pressed is the toggle-button word the reader of the key implements (aria-pressed), and it says nothing about access; active reads as a status or an enabled flag beside visible / disabled. active is kept as a rename so the card's other spelling lands on the key.
  • No boolean arm (unlike visible / disabled). A literal true draws every reader's button pressed on every record, which is a static style, not a reader's state; it is refused at parse with the evaluated-slot sentence. Alternative not taken: reuse ActionConditionInputSchema for symmetry.
  • Where it is evaluated: on the client, by the action surface, against the row it fetched, the same evaluator and scope as visible. Alternative not taken: a server-computed pressed flag on the action (a new wire field per action per row) — it would need a second contract for what the row-level block already carries.
  • How it reads the reader's own vote: an attachedOnRead block, not a record field and not a related-record lookup. Measured: the HotCRM vote is a per-user child row (crm_article_feedback, counted in hotcrm src/service/actions/knowledge_article.actions.ts), so no column of the article can hold it, and CEL in an action predicate has no related-record lookup. What it CAN read today: ObjectSchema.attachedOnRead declares a per-caller block on served rows, and an app afterFind hook may shape the rows it returns (packages/objectql/src/verb-hook-result-shape.ts: shaping stays legal), so the article can carry viewer: { vote } for the caller and the predicate reads has(record.viewer) && record.viewer.vote == 'helpful'. The lint half makes that read checkable. Alternative not taken: a CEL related-record function (a new evaluator capability on every surface, with its own fetch).

Tests

  • packages/spec/src/ui/action-pressed.test.ts (new, 12 cases): a CEL string normalizes to the envelope; the envelope with meta is kept; absent stays absent; true / false refused on the key's path; a blank string, a blank source and an ast-only envelope refused with EVALUATED_EXPRESSION_SOURCE_REQUIRED; active, activeWhen, pressedWhen refused with a rename to pressed; an object declaring attachedOnRead: { viewer: { vote } } with a pressed action parses.
  • packages/lint/src/validate-expressions.test.ts: a bare-field pressed is one error located at action '…' pressed; the record.-qualified form passes. The validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 key-read pin lists pressed.
  • packages/lint/src/validate-expressions.attached-on-read.test.ts: pressed is a served-row site — a declared leaf passes, a misspelt leaf is refused naming reader.vot, and with the block undeclared the read is the unknown field it is at run time.
  • packages/spec/src/shared/evaluated-slot-population.test.ts: actionObject.pressed joins the census (32 positions), refusing the three blank shapes with the one sentence.

Acceptance notes

  • The console half is a follow-up domain:ui card, carried by the seat: page:header buttons and DeclaredActionsBar evaluate pressed and draw aria-pressed with a pressed face; ActionDef derives the key; the liveness row flips planned → live with authorWarn / authorHint dropped in the same edit; the expression ledger row is re-stated enforced with its fail face (an unevaluable predicate draws a plain button); the docs callout goes.
  • The HotCRM producer is a later card in that repo: declare attachedOnRead: { viewer: { vote: 'text' } } on crm_knowledge_article, attach it in an afterFind hook from the caller's crm_article_feedback row, and set pressed on both vote actions. Note for that card: the hook runs on every read path of the article (grid, search, lookups), so it should batch one feedback query per read.
  • The Studio action form does not offer the key yet, on purpose (the planned-not-rendered omit); the console card decides the row.

Verification

tests, typecheck, ablation
  • packages/spec (under the verify lock, before the merge of origin/main): vitest run src/ui/ src/shared/evaluated-slot-population.test.ts src/system/metadata-form-zod-reconciliation.test.ts src/kernel src/data/object → 235 files, 7705 passed, 1 failed (the form reconciliation asking for action.pressed); after the planned-not-rendered omit row, that file and action-pressed.test.ts → 2/2 files, 88/88 tests.
  • packages/lint: vitest run src/validate-expressions src/lint-liveness-properties src/runtime-gate.object-action-predicate-writes.test.ts → 19/19 files, 864/864 tests (first run red only on the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 key-read pin, now listing pressed).
  • ui: pnpm exec vitest run packages/core/src/actions/__tests__/actionKeys.pin.test.ts from ui/ → 1/1 file, 19/19 tests, against the spec dist rebuilt with pressed.
  • packages/qa/dogfood: test/expression-conformance.test.ts → 1/1 file, 8/8 tests.
  • pnpm --filter @objectstack/spec --filter @objectstack/lint typecheck at 64d80fe0: lock VERDICT command-exit 0.
  • Liveness author warning, measured with lintLivenessPropertiesFromLedgerDir: an action setting pressed gets one liveness-planned-property finding carrying the row's authorHint.
  • Ablation (scripts/ablation-replace.mjs, lint tests read src): deleting the checkAction line that judges pressed, anchor 1 → 0 on disk; -t pressed over both lint files → 2 failed, 456 skipped; restored, blob == HEAD and git diff HEAD empty.
generated artefacts and the merge
  • After a dts build of ./packages/** + the dogfood closure (76/76 tasks), check:generated named content/docs/references/** and liveness/state-counts/ stale; --fix regenerated both, re-check 14/14.
  • Merged origin/main 3739d8d6 (3 commits, no overlap with this diff, clean); the merge driver deferred authorable-surface/ui.json and references/data/object.mdx; after a second full build check:generated --fix rewrote them from the merged tree (main's KanbanConfig.lanes / ReportChart.seriesBy rows), commit 64d80fe0, marker cleared.
derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 64d80fe0: 107 families, each run with stdin from /dev/null, exit code recorded before any pipe (record: 27946-gates-ran-64d80fe0.list in the seat's out folder). check-plugin-teardown-shape --self-test's pinned control 621a4876 was fetched into the shallow clone first.
  • --ran reconciliation: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.

Generated by Claude Code

ActionSchema gains an optional per-reader pressed-state predicate (CEL over
the visible scope, display only, no boolean arm). The expression lint judges
it in the record scope as a served-row site, so an attachedOnRead block
resolves there. Liveness row planned; console half is a follow-up.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvXVNub8x3B5XhGFrP
…ounts for `pressed`

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvXVNub8x3B5XhGFrP
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, packages/spec/liveness/object.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, packages/spec/liveness/object.json, …) — pages documenting those are invisible to this run
  • 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 — 168 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 03069caf2c7fac33e4aa4d4a9bf13eaa85876e1e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 03069caf2c7fac33e4aa4d4a9bf13eaa85876e1e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 64d80fe090b880af120e1f3e148ec61898bb9a76 · 2026-10-30T18:29Z

…console card on the planned row

The served action schema gains `pressed`, so the declared total moves 48 -> 49
and the sample names it. The liveness row and the docs callout name the
console card that will draw the pressed face.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 21d81b676e6c09e8628d3865be4a1f4ab3c3cbb3 · 2026-10-30T18:47Z

Re-claim after the head moved from 64d80fe (claim 6530497615): delta is the /meta/types action property count 48 to 49 and the follow-up card #28242 named in the liveness row and the docs callout. Record follows on green.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibvXVNub8x3B5XhGFrP

Test UI (1/16) red on 21d81b67 is not this PR's. It fails in ui/packages/app-shell/src/views/studio-design/AutomationsPillar.problemsAgree-27799.test.tsx L211 (expected 'flows[0].nodes[1].config.fields.id' to contain "node 'write'"). Main's #28192 (3efccfd5) gave node config-value lint findings a raw path, and the test's server double passes it through unchanged. #28192 touched no ui/ path, so Test UI did not run on it. Reproduced locally on this branch; this diff touches neither file.

Tracked as #28219, fix in #28227 (open). Per the seat, the test stays out of this PR: once #28227 is on main, this branch merges origin/main and pushes so Test UI reruns.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head e83f4fa95c8b3b3a22e9ebd93ebac13fff2ae374 · 2026-10-30T19:41Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e83f4fa95c8b3b3a22e9ebd93ebac13fff2ae374
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts, packages/spec/src/data/object.zod.ts + packages/lint/src/validate-expressions.ts (checkAction, SERVED_ROW_SITES), packages/spec/liveness/action.json and object.json, packages/spec/liveness/state-counts/action.md, packages/spec/authorable-surface/ui.json, packages/qa/dogfood/test/expression-conformance.ledger.ts, packages/spec/src/shared/evaluated-slot-population.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/spec/src/ui/action-pressed.test.ts, packages/lint/src/validate-expressions.test.ts and validate-expressions.attached-on-read.test.ts, ui/packages/core/src/actions/actionKeys.ts (the one inventory line), content/docs/protocol/objectui/actions.mdx and the three regenerated reference pages, the two changesets; read for the claims: packages/spec/src/shared/expression.zod.ts (EvaluatedExpressionInputSchema, EVALUATED_EXPRESSION_SOURCE_REQUIRED), ui/packages/core/src/actions/tests/actionKeys.pin.test.ts, scripts/check-changeset-no-major.mjs (the metadata-protocol minor rule)

Inputs: card #27946 (body, claim 6528706898, surface amendment 6528737693 ruling A, os-dev-report 6530445276), PR body, the 20-file diff against merge base 3739d8d plus the delta 64d80fe to 21d81b6 (one commit, read under re-claim), the head's check-runs (read 2026-10-30T19:43Z: check-runs on e83f4fa: 65 total, 58 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 7 skipped path-filtered (18.x upgrade harness (opt-in), Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts); 0 failure, 0 in progress). Author user.login is objectstack-fleet[bot], so no login note is owed. The reviewer is not the drafter (session_01L9qibvXVNub8x3B5XhGFrP).

Delta since the 21d81b6 review (claims 6530497615 and 6530906593; re-claimed on this head): one commit, e83f4fa, a merge of origin/main 39da1bd, taken to pick up the #28227 fix for the main-red that held this head's Test UI. The PR's own hunks against the new merge base are identical to the reviewed ones (same file names, no content difference beyond hunk offsets), and none of the four main commits the merge brings (#28195, #28213, #28227, #28197) touches a file this PR touches; #28213 moves the connector instance auth union, not ActionSchema, so the /meta/types action count this PR pins at 49 is unaffected, which the head's green Test Core shards confirm. ①②③④ stand.

① Derived judgments

  • The key: pressed: EvaluatedExpressionInputSchema.optional() on the strict actionObject, after disabledReason. Read in expression.zod.ts on the head: that union normalizes a non-blank string to { dialect: 'cel', source }, accepts the EvaluatedExpressionSchema envelope, and answers EVALUATED_EXPRESSION_SOURCE_REQUIRED on a blank string, a blank source and an ast-only envelope; it has no boolean arm, unlike ActionConditionInputSchema (visible / disabled, line 877), so the PR's "no boolean literal" claim is the schema, not a comment. The describe states scope (same as visible), the reading (TRUE = drawn pressed, aria-pressed), display-only, the attachedOnRead route and the refusal. active / activeWhen / pressedWhen join the strict-object rename table, so the card's other spelling is refused with a rename, not accepted silently.

  • Lint: checkAction judges action.pressed in the record scope with the same check call as visible / disabled and the site key 'action pressed'; SERVED_ROW_SITES gains that key, so an attachedOnRead block resolves on it and a misspelt leaf is refused. No boolean skip is right: the schema already refuses one. The validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 key-read pin lists pressed beside disabled / visible, which is the pin that would otherwise red on the new read.

  • Tests hold the contract on both halves: action-pressed.test.ts (12: string, envelope, absent, true / false refused on path pressed, the three blank shapes with the sentence, the three renames, the attachedOnRead: { viewer: { vote } } object parses); validate-expressions tests (bare field errors at action 'mark_helpful' pressed, record.-qualified passes; declared leaf passes, misspelt leaf names reader.vot, undeclared block is the unknown field).

  • Ledgers in step: liveness action.pressed planned + authorWarn + authorHint with evidence naming the parse door and the lint gate (the app.json precedent for a warned planned row); state-counts 49 to 50 with planned 1; object.json attachedOnRead evidence and the ObjectSchema.attachedOnRead TSDoc now list pressed among the served-row sites (rule and gate edited as one); evaluated-slot census 31 to 32 with its ledger comment; metadata-form omit row for action.(root).pressed in the conditionalFormatting shape; expression-conformance row cel-action-pressed experimental / unevaluated, PARSE ONLY, covering ui/action.zod.ts:actionObject.pressed; authorable-surface/ui.json gains ui/Action:pressed (the Metadata-protocol: added baseline move).

  • ui/ inventory: one 'pressed' line in SPEC_ACTION_KEYS with the comment naming the follow-up; ActionDef untouched. Read in actionKeys.pin.test.ts: it walks the spec ActionSchema shape and compares with that array, so without the line this PR reds Test UI on its own, which is what the seat's amendment 6528737693 (ruling A, the description / execution precedent) sanctions.

  • Docs: actions.mdx gains the pressed section beside disabledReason with the callout that nothing draws it yet, and the interface listing gains the key; the reference pages are regenerated from the describe. Skills-docs follow-up (skills/objectstack-ui/rules/actions.md) is declared on the PR body as owed with the console half.

  • Head-move delta (64d80fe to 21d81b6, test, ledger note and docs only): the first head redded Test Core (7/8) on the /meta/types action property-count pin (protocol.meta-types-degenerate-derivation.test.ts:258, expected 49 to be 48); the new head bumps it to 49 with the [#27946] 48 → 49: pressed ledger comment and adds pressed to the sampled-key list, the arithmetic consequence of the new key; the liveness pressed row and the actions.mdx callout now name Header action shows it is already pressed: page:header and DeclaredActionsBar read action.pressed #28242. No schema, lint or ledger-shape change; ①②③④ stand.

② Semver level

minor for @objectstack/spec and @objectstack/lint, patch for @object-ui/core. The protocol an author writes against gains a key (Metadata-protocol: added), which check-changeset-no-major grades minor; the lint package gains a checked site on the same key; @object-ui/core moves an inventory array only (Clause-②: no, the #26619 precedent). Clause-②: yes (widening) on the PR body and the spec changeset is the right declaration: an action without the key parses as before, no accept set narrows (the renames refuse keys that were already refused by the strict object).

③ Boundary flags

④ Design judgment

The change holds against the code it cites and the scenario it is accepted against. Against the card: the key is an optional per-reader CEL predicate over record and the reader, on ActionSchema, with its expression lint and docs, and the ui half is explicitly a follow-up card; that is the Wanted, spec-first as the card orders the readers. Against the first consumer: the HotCRM vote is a per-user child row, and the PR's chosen route (an attachedOnRead block the serving side attaches per caller, read behind has()) is the one path CEL in an action predicate can read today; making pressed a served-row site is what makes that route checkable at build, and the undeclared-block control pins that nothing is resolved by name alone. Against the alternatives: a boolean arm would have let a constant pass as reader state, a server-computed flag would need a second wire contract per action per row, and a CEL related-record function would be a new evaluator capability on every surface; refusing each is the smaller, reversible contract. Naming it pressed (the aria-pressed word) rather than active keeps it off the access vocabulary of visible / disabled, and the rename table catches the card's spelling. The planned-not-rendered posture is carried consistently across the five ledgers that would otherwise red (liveness, census, form reconciliation, conformance, inventory pin), each with the precedent's own shape.

Implemented-by: claude/project-thread-nnpg3i
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

维护者速读

  • 改了什么: 动作(ActionSchema)新增可选键 pressed:一个基于 record 和 user 的表达式,为真时表示当前读者已经按过这个按钮(如“已标记有用”)。lint 按记录作用域检查这个表达式;挂在读取结果上的 attachedOnRead 数据也能在里面用,所以“我投过的票”这类存在子记录里的个人状态可以由 afterFind 钩子附到记录上再读。这次只落契约这一半,活性台账标为 planned,作者写了会收到“尚未生效”的提示。
  • 为什么改: 记录页头部的切换类按钮没法告诉读者“你已经投过有用了”(HotCRM 知识文章的有用/没用)。
  • 风险与回滚: 只加一个可选键,不写它的动作解析、校验和显示都不变。界面那边只在 actionKeys.ts 的键清单里登记了一行(不读它)。回滚即 revert。
  • 先看: packages/spec/src/ui/action.zod.ts(pressed)、lint 的动作表达式检查、packages/spec/liveness/action.json。
  • 两分钟验证: 跑 PR 正文“验证”一节列的 lint 动作测试,预期全绿。
  • 需要你做的: 无。不属于受管面,契约复审已 PASS,由席位落地。界面半边 Header action shows it is already pressed: page:header and DeclaredActionsBar read action.pressed #28242,技能文档 skills: teach action.pressed in the objectstack-ui actions rule #28243。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 2 (#18549) · 2026-10-30T19:47Z

Head e83f4fa95c8b3b3a22e9ebd93ebac13fff2ae374. Card #27946, claim 6528706898 + surface amendment 6528737693 (one inventory-only line in ui/packages/core/src/actions/actionKeys.ts).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 19:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 19:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 30, 2026
…-nnpg3i

# Conflicts:
#	packages/spec/src/system/metadata-form-zod-reconciliation.test.ts
…rence after main's percentDisplay and allowStageChange

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvXVNub8x3B5XhGFrP
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 20:42
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 698c3d65d604e35fb2ca98415ef35fc8dc0cdc1b · 2026-10-30T20:44Z

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 698c3d65d604e35fb2ca98415ef35fc8dc0cdc1b
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts, packages/spec/src/data/object.zod.ts + packages/lint/src/validate-expressions.ts (checkAction, SERVED_ROW_SITES), packages/spec/liveness/action.json and object.json, packages/spec/liveness/state-counts/action.md, packages/spec/authorable-surface/ui.json, packages/qa/dogfood/test/expression-conformance.ledger.ts, packages/spec/src/shared/evaluated-slot-population.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/spec/src/ui/action-pressed.test.ts, packages/lint/src/validate-expressions.test.ts and validate-expressions.attached-on-read.test.ts, ui/packages/core/src/actions/actionKeys.ts (the one inventory line), content/docs/protocol/objectui/actions.mdx and the three regenerated reference pages, the two changesets; read for the claims: packages/spec/src/shared/expression.zod.ts (EvaluatedExpressionInputSchema, EVALUATED_EXPRESSION_SOURCE_REQUIRED), ui/packages/core/src/actions/tests/actionKeys.pin.test.ts, scripts/check-changeset-no-major.mjs (the metadata-protocol minor rule)

Inputs: card #27946 (body, claim 6528706898, surface amendment 6528737693 ruling A, os-dev-report 6530445276), PR body, the 20-file diff against merge base 3739d8d plus the delta 64d80fe to 21d81b6 (one commit, read under re-claim), the head's check-runs (read 2026-10-30T21:07Z: check-runs on 698c3d6: 65 total, 58 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 7 skipped path-filtered (18.x upgrade harness (opt-in), Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts); 0 failure, 0 in progress). Author user.login is objectstack-fleet[bot], so no login note is owed. The reviewer is not the drafter (session_01L9qibvXVNub8x3B5XhGFrP).

Delta since the 21d81b6 review (claims 6530497615 and 6530906593; re-claimed on this head): one commit, e83f4fa, a merge of origin/main 39da1bd, taken to pick up the #28227 fix for the main-red that held this head's Test UI. The PR's own hunks against the new merge base are identical to the reviewed ones (same file names, no content difference beyond hunk offsets), and none of the four main commits the merge brings (#28195, #28213, #28227, #28197) touches a file this PR touches; #28213 moves the connector instance auth union, not ActionSchema, so the /meta/types action count this PR pins at 49 is unaffected, which the head's green Test Core shards confirm. ①②③④ stand.

Second delta (e83f4fa to 698c3d6; re-claimed on this head): 0119b3b merges origin/main 49a078e, which now carries #28222 (percentDisplay), #28198 (allowStageChange), #28247, #28249 and others, and 698c3d6 regenerates the two derived files the merge had resolved short (object.mdx regains main's percentDisplay rows, authorable-surface/ui.json regains ui/RecordPathProps:allowStageChange). Four files are shared with main; in each the PR's own hunks at this head are identical to the reviewed ones (only context lines moved), so the /meta/types pins now read field: 80 from main and action: 49 from this PR with no collision, the pressed omit row sits beside main's new percentDisplay row, and no other file the PR touches was changed by main. ①②③④ stand.

① Derived judgments

  • The key: pressed: EvaluatedExpressionInputSchema.optional() on the strict actionObject, after disabledReason. Read in expression.zod.ts on the head: that union normalizes a non-blank string to { dialect: 'cel', source }, accepts the EvaluatedExpressionSchema envelope, and answers EVALUATED_EXPRESSION_SOURCE_REQUIRED on a blank string, a blank source and an ast-only envelope; it has no boolean arm, unlike ActionConditionInputSchema (visible / disabled, line 877), so the PR's "no boolean literal" claim is the schema, not a comment. The describe states scope (same as visible), the reading (TRUE = drawn pressed, aria-pressed), display-only, the attachedOnRead route and the refusal. active / activeWhen / pressedWhen join the strict-object rename table, so the card's other spelling is refused with a rename, not accepted silently.

  • Lint: checkAction judges action.pressed in the record scope with the same check call as visible / disabled and the site key 'action pressed'; SERVED_ROW_SITES gains that key, so an attachedOnRead block resolves on it and a misspelt leaf is refused. No boolean skip is right: the schema already refuses one. The validate-expressions / validate-security-posture 也有同形的 spec 不声明键的 ?? 别名读法(#5009 建议 3 的核对结果) #5017 key-read pin lists pressed beside disabled / visible, which is the pin that would otherwise red on the new read.

  • Tests hold the contract on both halves: action-pressed.test.ts (12: string, envelope, absent, true / false refused on path pressed, the three blank shapes with the sentence, the three renames, the attachedOnRead: { viewer: { vote } } object parses); validate-expressions tests (bare field errors at action 'mark_helpful' pressed, record.-qualified passes; declared leaf passes, misspelt leaf names reader.vot, undeclared block is the unknown field).

  • Ledgers in step: liveness action.pressed planned + authorWarn + authorHint with evidence naming the parse door and the lint gate (the app.json precedent for a warned planned row); state-counts 49 to 50 with planned 1; object.json attachedOnRead evidence and the ObjectSchema.attachedOnRead TSDoc now list pressed among the served-row sites (rule and gate edited as one); evaluated-slot census 31 to 32 with its ledger comment; metadata-form omit row for action.(root).pressed in the conditionalFormatting shape; expression-conformance row cel-action-pressed experimental / unevaluated, PARSE ONLY, covering ui/action.zod.ts:actionObject.pressed; authorable-surface/ui.json gains ui/Action:pressed (the Metadata-protocol: added baseline move).

  • ui/ inventory: one 'pressed' line in SPEC_ACTION_KEYS with the comment naming the follow-up; ActionDef untouched. Read in actionKeys.pin.test.ts: it walks the spec ActionSchema shape and compares with that array, so without the line this PR reds Test UI on its own, which is what the seat's amendment 6528737693 (ruling A, the description / execution precedent) sanctions.

  • Docs: actions.mdx gains the pressed section beside disabledReason with the callout that nothing draws it yet, and the interface listing gains the key; the reference pages are regenerated from the describe. Skills-docs follow-up (skills/objectstack-ui/rules/actions.md) is declared on the PR body as owed with the console half.

  • Head-move delta (64d80fe to 21d81b6, test, ledger note and docs only): the first head redded Test Core (7/8) on the /meta/types action property-count pin (protocol.meta-types-degenerate-derivation.test.ts:258, expected 49 to be 48); the new head bumps it to 49 with the [#27946] 48 → 49: pressed ledger comment and adds pressed to the sampled-key list, the arithmetic consequence of the new key; the liveness pressed row and the actions.mdx callout now name Header action shows it is already pressed: page:header and DeclaredActionsBar read action.pressed #28242. No schema, lint or ledger-shape change; ①②③④ stand.

② Semver level

minor for @objectstack/spec and @objectstack/lint, patch for @object-ui/core. The protocol an author writes against gains a key (Metadata-protocol: added), which check-changeset-no-major grades minor; the lint package gains a checked site on the same key; @object-ui/core moves an inventory array only (Clause-②: no, the #26619 precedent). Clause-②: yes (widening) on the PR body and the spec changeset is the right declaration: an action without the key parses as before, no accept set narrows (the renames refuse keys that were already refused by the strict object).

③ Boundary flags

④ Design judgment

The change holds against the code it cites and the scenario it is accepted against. Against the card: the key is an optional per-reader CEL predicate over record and the reader, on ActionSchema, with its expression lint and docs, and the ui half is explicitly a follow-up card; that is the Wanted, spec-first as the card orders the readers. Against the first consumer: the HotCRM vote is a per-user child row, and the PR's chosen route (an attachedOnRead block the serving side attaches per caller, read behind has()) is the one path CEL in an action predicate can read today; making pressed a served-row site is what makes that route checkable at build, and the undeclared-block control pins that nothing is resolved by name alone. Against the alternatives: a boolean arm would have let a constant pass as reader state, a server-computed flag would need a second wire contract per action per row, and a CEL related-record function would be a new evaluator capability on every surface; refusing each is the smaller, reversible contract. Naming it pressed (the aria-pressed word) rather than active keeps it off the access vocabulary of visible / disabled, and the rename table catches the card's spelling. The planned-not-rendered posture is carried consistently across the five ledgers that would otherwise red (liveness, census, form reconciliation, conformance, inventory pin), each with the precedent's own shape.

Implemented-by: claude/project-thread-nnpg3i
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A record_header action cannot show per-reader state (an active / pressed predicate)

1 participant