Repository navigation
Commit 916531a
feat(spec): an action can show the reader already pressed it (
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoC2hwmtQyR6xez7eocpWFkd)_
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 (#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 #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
<details><summary>tests, typecheck, ablation</summary>
- `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 #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.
</details>
<details><summary>generated artefacts and the merge</summary>
- 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.
</details>
<details><summary>derived gates</summary>
- `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.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvXVNub8x3B5XhGFrP)_
---------
Co-authored-by: Claude <noreply@anthropic.com>pressed, a per-reader CEL predicate) (#28235)1 parent 4159c70 commit 916531a
21 files changed
Lines changed: 305 additions & 19 deletions
File tree
- .changeset
- content/docs
- protocol/objectui
- references
- data
- kernel
- ui
- packages
- lint/src
- metadata-protocol/src
- qa/dogfood/test
- spec
- authorable-surface
- liveness
- state-counts
- src
- data
- system
- ui
- ui/packages/core/src/actions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| 249 | + | |
249 | 250 | | |
250 | 251 | | |
251 | 252 | | |
| |||
349 | 350 | | |
350 | 351 | | |
351 | 352 | | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
352 | 373 | | |
353 | 374 | | |
354 | 375 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
503 | 503 | | |
504 | 504 | | |
505 | 505 | | |
| 506 | + | |
506 | 507 | | |
507 | 508 | | |
508 | 509 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
| 350 | + | |
350 | 351 | | |
351 | 352 | | |
352 | 353 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
208 | 230 | | |
209 | 231 | | |
210 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
926 | 926 | | |
927 | 927 | | |
928 | 928 | | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
929 | 948 | | |
930 | 949 | | |
931 | 950 | | |
| |||
3390 | 3409 | | |
3391 | 3410 | | |
3392 | 3411 | | |
3393 | | - | |
| 3412 | + | |
3394 | 3413 | | |
3395 | 3414 | | |
3396 | 3415 | | |
| |||
0 commit comments