Repository navigation
Commit 0c81694
feat(lint)!: refuse an action's copy-template findings (blank, unbound root, single-brace, logic, raw-shown ${…}) at validate, compile and the publish gate (#25702)
Fixes #25680
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Ruling-ref: `6365736632` (on #24814, the refusal half: "its own BREAKING
minor card, filed when this card lands, as #25205 is for the bulk
slot"); triage `6386348015`
The refusal half of ruling `6365736632` on #24814, verbatim: "**The
refusal half** (blank and door findings as errors) is its own BREAKING
minor card, filed when this card lands, as #25205 is for the bulk slot."
#24814 (PR #25637, `384bb7ba02`) typed an action's copy keys as ADR-0032
`template` slots and shipped every door finding as a warning. This PR
makes them errors, shaped like #25205 / PR #25451 for the bulk slot.
## What changes
- **The action copy pass refuses**
(`packages/lint/src/validate-action-copy-templates.ts`, `judgeAction`).
A blank sentence, a hole over a root the key does not bind, a
single-brace `{x}` token, a hole holding logic or an unknown formatter,
and a `${…}` token the runner shows raw are now `severity: 'error'`, and
the `ADVISORY` suffix is gone from them. The judge, the walk, the roots
and the sentences are unchanged.
- **How it reaches each door.** `objectstack validate` and `compile`
fail on an `error` from `validateStackExpressions`. The publish gate
splits `errors` from `advisories` by severity (`runtime-gate.ts`), so an
active `action`, `object` or `page` write, and a draft promotion, answer
`422 INVALID_METADATA` with an `expression-invalid` issue at the key. No
door code moves.
- **`${…}`: kept a warning only where objectui's runner still fills
it.** Measured at the pinned `.objectui-sha` `2958f17e9c` (objectui
`packages/core/src/actions/ActionRunner.ts`): `confirmMessage` still
evaluates a `confirmText` with no `{{ }}` hole through its `${…}`
expression evaluator (whose `record` is the record the action runs
against), and `composeSuccessMessage` still substitutes `${result.*}`
(and nothing else) in a `successMessage` with no `{{ }}` hole; an
`errorMessage` is shown as written (`actionCopySource`, no
substitution). So, per the triage's lead, a `${…}` token stays a
**warning** exactly when the runner fills it: a sentence with no `{{ }}`
hole whose every token is `${record.*}` (`confirmText`) or `${result.*}`
(`successMessage`). Its finding now ends "A warning, not a refusal, for
now: the console's runner still fills …". Every other `${…}` is shown
raw by the runner, so it is an error: in an `errorMessage`, beside a `{{
}}` hole or a `{x}` token, over `params` in a `confirmText`, or over
`record` in a `successMessage`.
- The roots are a spec fact beside the slot's other runner-measured
roots: `ACTION_COPY_DOLLAR_TRANSITION_ROOTS` (`@objectstack/spec/ui`).
The shape is read by the one template reader: `dollarOnlyTokenRoots`
(`@objectstack/spec/automation`, beside `closedScopeTemplateRefusal`),
so lint holds no second reading of the dialect.
- **The blank arm is kept** (`actionCopyTemplateSlot`, comment only),
following #25205's answer for the bulk slot. The parse is what the boot
load and the read path judge a stored row with; dropping the arm would
make a stored blank sentence an invalid row on load, a narrowing wider
than the three doors. The doors refuse a blank sentence in this same
change, so every finding is one kind of verdict: a door refusal over
text that parses. Door test (d) pins this for a blank and for an unbound
hole.
- **Prose made false by the flip, corrected:** the module notes
(`action-copy-template.ts` "Widening only" section,
`validate-action-copy-templates.ts`), the three copy `.describe()`s and
their regenerated references
(`content/docs/references/{ui/action,ui/component,data/object,kernel/metadata-plugin}.mdx`),
the `authoring-rules.ts` registration comment, three
`validate-expressions.ts` comments, and the dogfood
`expression-conformance.ledger.ts` rows' `enforcement` / `note` text.
`affected-docs.mjs --json origin/main` named 17 pages (4 release-owned,
untouched); none of the 13 hand-written ones states the warning-only
rule or a `${…}` copy sentence, so none changes.
- **Changeset** `.changeset/25680-action-copy-refusal.md`:
`@objectstack/lint` and `@objectstack/spec` `minor`, both declaration
lines, a FROM/TO block, and the ADR-0087 disposition `not-required
(no-migration-prescription)`, worded like #25205's.
## Producers, measured first (through the built `@objectstack/lint` at
`6021b5e837`, before the change)
- `examples/app-crm`, `app-showcase` (23 registered `successMessage`s,
29 pages), `app-todo` (2 `confirmText`, 16 `successMessage`),
`app-multi-package`: 0 findings. `@objectstack/platform-objects` (12
`confirmText`, 48 `successMessage`, as one stack of its exported
objects): 0. `@objectstack/plugin-approvals` (9 `successMessage`): 0.
Control: a planted `{{ nope.x }}` drew 1 (a warning before, an error
after).
- HotCRM `a4b3560` (read-only clone): its 20 action sentences (1
`confirmText`, 19 `successMessage`,
`src/{sales,service,marketing}/actions`) judged through the same built
pass: 0 findings, against an error for a planted `{count}`. Its bulk
`confirmText`s are #25205's pass, already measured there.
- No producer is refused, so none is converted.
## Tests (head `c69a7a6d`; final head `f03deb5f` adds only the
changeset's `Ruling-ref:` line, and the ten changeset / doc families
re-ran on it, all exit 0)
- `validate-action-copy-templates.test.ts`: every pin flips from
`warning` to `error`, plus new cases: a single-brace token and an empty
blank as errors; the four raw-shown `${…}` shapes as errors; `${…}` in a
row's `errorMessage` as an error; the transitional `${record.*}` /
`${result.*}` as warnings; and the gate block now asserts `errors` with
empty `advisories` for a page write (single-brace, unbound, blank),
errors for an action and an object write, and one advisory for each
transitional sentence. 17/17.
- `flow-text-slot-template.test.ts`: `dollarOnlyTokenRoots` answers the
roots for an all-`${…}` holeless sentence and `undefined` for no token,
a `{{ }}` hole, a bare `{x}`, logic, and an empty string.
`action-copy-template.test.ts`: the transition roots per key.
- New door block in
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
through the real `saveMetaItem`: (a) each of four texts on a page row is
refused with `INVALID_METADATA` / 422 and nothing is stored, an action
write is refused the same way, and the draft promotion is refused too;
(b) the door's finding equals `runAuthoringRules('build', …)`'s on
`rule`, `where` and `message`; (c) control: the transitional
`${record.name}` sentence saves with one advisory; (d) a stored page
with a blank or an unbound sentence loads (`invalid: 0`) and its next
save is refused, the row untouched. 10/10.
- **Ablation** (`scripts/ablation-replace.mjs`, the refuse helper's
`severity: 'error'` back to `'warning'`; blob `dcede89c7bb5` →
`60fcaccce37a`; `ablation-dist-preflight.mjs` dist reading PASSED on the
mutated build): lint `Tests 9 failed | 8 passed`, door `Tests 9 failed |
1 passed` (the passing one is control (c)). Restore leg: blob back to
HEAD, `git diff HEAD` empty, lint rebuilt, preflight `--absent` exit 0,
lint 17/17 and door 10/10 green.
- `pnpm --filter @objectstack/lint test`: 161/161 files, 7273 tests;
typecheck exit 0.
- `pnpm --filter @objectstack/spec test`: 719/719 files, 21779 passed, 1
todo; typecheck exit 0; `test:repo` 79/79 files, 1269 tests.
`check:generated` green after `--fix` regenerated api-surface,
export-origins and the references, and green again after merging
`origin/main`.
- `pnpm --filter @objectstack/metadata-protocol test` (the door's
consumer): 263 passed / 3 skipped files, 21485 passed / 19 skipped;
typecheck exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit` (`os
validate` / `compile`): 328/328 files, 4816 tests.
- Full dogfood (`pnpm --filter @objectstack/dogfood test`, owed by the
`(narrowing)` arm) at `c69a7a6d`: 295 passed / 3 skipped files, 2520
passed / 18 skipped.
- Derived gates: `dispatch-gates --commands` gives 114 families; all 114
exit 0 (three first answered exit 3, prerequisite not met, and were
re-run once their prerequisite was built or fetched:
`check-plugin-teardown-shape --self-test`, `check:skill-examples`,
`check:dual-build-cjs-loads`). `--ran` reconciles 114/114. Results are
in the os-dev-report comment.
## Acceptance notes
- `${…}` stays a warning where the runner fills it, as the triage
ordered. Refusing it is owed once objectui's `ActionRunner` retires the
`${…}` reading in `confirmMessage` and `composeSuccessMessage`. Who
carries that is a seat call; this PR names it in the report and files
nothing.
- Translated copy strings in translation bundles are still not judged,
as before this change: the pass reads the action's own slot (and an
inline locale map), the same boundary #25205 kept.
- `ActionSchema.errorMessage` and `outcomeMessages` stay outside the
family (contract review `6385469184` on PR #25637); a pin asserts a
`${…}` in `ActionSchema.errorMessage` still draws nothing.
- Contract review at `CONTRACT_REVIEW_TIER` is the seat's. No governed
surface is touched (`.claude/**`, `docs/adr/**`, `AGENTS.md`,
`skills/**`, `.github/workflows/**`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WbVg72GQsLxcNHWW17FJZA)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent fa44dd2 commit 0c81694
20 files changed
Lines changed: 408 additions & 78 deletions
File tree
- .changeset
- content/docs/references
- data
- kernel
- ui
- packages
- lint/src
- metadata-protocol/src
- qa/dogfood/test
- spec
- api-surface
- export-origins
- src
- automation
- ui
| 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 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
492 | 492 | | |
493 | 493 | | |
494 | 494 | | |
495 | | - | |
496 | | - | |
| 495 | + | |
| 496 | + | |
497 | 497 | | |
498 | 498 | | |
499 | 499 | | |
| |||
0 commit comments