Skip to content

Commit 48efe91

Browse files
fix(spec): hook condition row declares expression, not javascript (#20475)
Fixes #20439 Clause-②: no ## What `packages/spec/src/data/hook.form.ts`'s `condition` row declared `language: 'javascript'` over `HookSchema.condition`, which is `EvaluatedExpressionInputSchema` — a CEL predicate, not a script (`hook.zod.ts`: `'Predicate (CEL); hook runs only when TRUE …'`). Every sibling predicate row (`field.form.ts` / `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the formula `expression` row) already declares `language: 'expression'`. A consumer keyed on the row's declared language (objectui#10963's `CodeWidget` fix) could not tell this row apart from a real script row (`body.source`, `action.source`, both genuinely `'javascript'`). - `condition` row now declares `language: 'expression'`, `helpText: 'CEL predicate — the hook runs only when TRUE'` (triage's wording, matching the sibling rows' phrasing). - Pinned in `packages/spec/src/system/metadata-form-declared-rows.pin.test.ts` (no existing row-language assertion was found, so the pin was added beside that file's existing form-row pins): asserts `hook.condition` declares `type: 'code'` / `language: 'expression'`, with a control against `field.visibleWhen` (a sibling predicate row already correct) so the assertion is shown to actually discriminate. - `en.metadata-forms.generated.ts` regenerated (`node scripts/check-i18n-bundles.mjs --write`) to pick up the new helpText. `zh-CN` / `ja-JP` / `es-ES` kept their existing translated values under the tool's merge behaviour (now stale relative to the new English source — the tool does not auto-translate). - Changeset: `@objectstack/spec` patch. ## Why this landing site Matches the constraint's expected surface exactly: `hook.form.ts`, the pin, the regenerated translation bundles, and the changeset. No other producer or consumer needed a change. ## Tests - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-declared-rows.pin.test.ts src/system/metadata-form-zod-reconciliation.test.ts src/data/hook.test.ts src/data/hook-body.test.ts` — 173 passed. - `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2 src/apps/translations` — 399 passed (21 files), including `metadata-forms-vocabulary.test.ts` and `hook-execution-panel-echo-decisions.test.ts`. - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated` — all 15 generated artifacts up to date. - `pnpm check:i18n` — all 9 packages in sync, no undeclared authoring keys. - `pnpm --filter @objectstack/spec typecheck` — clean. - Gate derivation: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` → 85 derived families, all run and reconciled (`--ran`): 84 green, 1 honestly NOT-MEASURED — `pnpm check:dual-build-cjs-loads` exits its own `PREREQUISITE NOT MET` (3) because this worktree never ran a full `pnpm build` across every package in the monorepo (studio, client-react, several connectors/plugins/services have no `dist/`); that whole-workspace build is disproportionate to this one-row change and is CI's own "Build Core" job to run. Nothing about this diff is implicated in that gate. ## Acceptance notes None — no out-of-scope findings surfaced while making this change. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8255a51 commit 48efe91

4 files changed

Lines changed: 47 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`hook.form.ts`'s `condition` row now declares `language: 'expression'`, matching the CEL predicate `HookSchema.condition` actually is.
6+
7+
Clause-②: no
8+
9+
The row previously declared `language: 'javascript'` — the same declared language as a real script row (`body.source`) — over a field that is `EvaluatedExpressionInputSchema`, a CEL predicate. A consumer keyed on the row's declared language could not tell the predicate apart from a script. The `helpText` moves from "Optional formula — skip the hook when this evaluates to false" to "CEL predicate — the hook runs only when TRUE", matching the phrasing every sibling predicate row (`field.form.ts` / `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the formula `expression` row) already uses.
10+
11+
No key is added, removed, narrowed or widened, and no parse verdict changes — `type: 'code'` and `language` are already-declared form-DSL vocabulary. `metadata-form-declared-rows.pin.test.ts` pins the new value, with a control against a sibling predicate row.

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
903903
},
904904
condition: {
905905
label: "Condition",
906-
helpText: "Optional formula — skip the hook when this evaluates to false"
906+
helpText: "CEL predicate — the hook runs only when TRUE"
907907
},
908908
retryPolicy: {
909909
label: "Retry Policy",

‎packages/spec/src/data/hook.form.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const hookForm = defineForm({
7474
{ label: 'System (elevated)', value: 'system' },
7575
{ label: 'User (triggering user)', value: 'user' },
7676
] },
77-
{ field: 'condition', type: 'code', language: 'javascript', colSpan: 2, helpText: 'Optional formula — skip the hook when this evaluates to false' },
77+
{ field: 'condition', type: 'code', language: 'expression', colSpan: 2, helpText: 'CEL predicate — the hook runs only when TRUE' },
7878
{
7979
field: 'retryPolicy',
8080
type: 'composite',

‎packages/spec/src/system/metadata-form-declared-rows.pin.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type FormEntry = {
4848
field?: string;
4949
widget?: string;
5050
type?: string;
51+
language?: string;
5152
helpText?: string;
5253
visibleWhen?: string | { dialect?: string; source?: string };
5354
fields?: FormEntry[];
@@ -168,3 +169,36 @@ describe('declared keys that now have a form row (#19085)', () => {
168169
expect(row.helpText).toContain('"name"');
169170
});
170171
});
172+
173+
/**
174+
* **A `type: 'code'` row's `language` must name the slot it actually edits**
175+
* (#20439).
176+
*
177+
* `hook.form.ts`'s `condition` row declared `language: 'javascript'` over
178+
* `HookSchema.condition`, which is `EvaluatedExpressionInputSchema` — a CEL
179+
* predicate, not a script. Every sibling predicate row (`field.form.ts` /
180+
* `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the
181+
* formula `expression` row) already declares `language: 'expression'`; a
182+
* consumer keyed on the declared language (objectui#10963's `CodeWidget`
183+
* fix) cannot otherwise tell this row apart from a real script row
184+
* (`body.source`, `action.source`, both genuinely `'javascript'`).
185+
*/
186+
describe('hook.condition declares the CEL predicate language it edits (#20439)', () => {
187+
it('hook.condition is a `code` row declaring language: expression, not javascript', () => {
188+
const [row, ...extra] = rowFor('hook', 'condition');
189+
expect(row, '`condition` is declared by HookSchema; the hook form must offer it').toBeDefined();
190+
expect(extra, 'one row only').toEqual([]);
191+
expect(row.type).toBe('code');
192+
expect(row.language).toBe('expression');
193+
});
194+
195+
it('CONTROL: a sibling predicate row (field.visibleWhen) also declares expression', () => {
196+
// Proves the assertion above actually discriminates: run the identical
197+
// probe against a row known to already be correct, so a helper that
198+
// stopped reading `language` at all would fail here rather than passing
199+
// hook.condition vacuously.
200+
const [row] = rowFor('field', 'visibleWhen');
201+
expect(row.type).toBe('code');
202+
expect(row.language).toBe('expression');
203+
});
204+
});

0 commit comments

Comments
 (0)