Skip to content

Commit 121ccea

Browse files
os-zhuangclaude
andauthored
docs(skills): hook 示例改用 defineHook(),不再教裸 : Hook 字面量 (#4274) (#4283)
* docs(skills): hook examples author with defineHook(), not bare `: Hook` literals (#4274) #4273 shipped defineHook() and pointed docs + error prescriptions at it, but skills/objectstack-data — the first thing an AI author reads — still taught the bare-literal form in 18 places across SKILL.md, rules/hooks.md, rules/validation.md, and references/data-hooks.md. Mechanical rewrite: `const x: Hook = {…}` → `const x = defineHook({…})` (imports switched to the value import), plus the factory-over-bare-literal note in rules/hooks.md and references/data-hooks.md, phrased identically to the schema.mdx note from #4273. Every rewritten block was scanned for unknown top-level keys — HookSchema is strict, so a stray key inside a defineHook() example would teach a crash — none found; the os:check gate covers the tagged blocks (198 examples green). Closes #4274 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: empty changeset — docs-only PR, releases nothing Same convention as #4279: the changeset gate wants an explicit statement of release intent, and skills/ ships in no npm package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ac6c0be commit 121ccea

5 files changed

Lines changed: 62 additions & 46 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
---
3+
4+
docs-only: skills/objectstack-data hook examples author with `defineHook()`
5+
instead of bare `: Hook` literals, closing the #4269 authoring-validation loop
6+
on the skill surface (#4274). Releases nothing.

skills/objectstack-data/SKILL.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,9 @@ Implement business logic at data operation lifecycle points:
396396

397397
<!-- os:check -->
398398
```typescript
399-
import { Hook, HookContext } from '@objectstack/spec/data';
399+
import { defineHook, HookContext } from '@objectstack/spec/data';
400400

401-
const accountHook: Hook = {
401+
export default defineHook({
402402
name: 'account_defaults',
403403
object: 'account',
404404
events: ['beforeInsert'],
@@ -408,9 +408,7 @@ const accountHook: Hook = {
408408
}
409409
ctx.input.created_at = new Date().toISOString();
410410
},
411-
};
412-
413-
export default accountHook;
411+
});
414412
```
415413

416414
The `handler` above is the inline (in-process) form. The **preferred**,
@@ -433,7 +431,7 @@ Mirror these CRM-style patterns when designing enterprise metadata objects:
433431
| Capability gating | `src/objects/*.object.ts` | Use `enable` flags (`trackHistory`, `apiMethods`, `files`, `feeds`, `activities`) per object |
434432
| Index + validation pairing | `src/objects/*.object.ts` | Keep `indexes[]` aligned to common filters and enforce invariants with `validations[]` |
435433
| Relationship constraints | `src/objects/*.object.ts` | Use `lookup` + `lookupFilters` (`[{ field, operator, value }]`) for constrained child selection |
436-
| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (a `Hook` object registered via `defineStack({ hooks })`) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). |
434+
| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (authored with `defineHook()`, registered via `defineStack({ hooks })` or the `*.hook.ts` convention scan) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). |
437435
| State transitions | `src/objects/*.object.ts` | Prefer explicit `state_machine` validation rules (one per state field) — there is **no** separate `stateMachines` map |
438436

439437
For metadata authoring, keep expressions in CEL (`P\`...\``, `F\`...\``,

skills/objectstack-data/references/data-hooks.md

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,18 @@ ObjectStack provides **8 lifecycle events** organized by operation type:
7878

7979
## Hook Definition Schema
8080

81-
Every hook must conform to the `HookSchema`:
81+
Every hook must conform to the `HookSchema`. Author it with `defineHook()`
82+
preferred over a bare `: Hook` literal (the same rule as `defineDatasource`):
83+
it validates when the module is imported, so constraint-level mistakes a bare
84+
annotation can't catch — a non-`snake_case` `name`, a misspelled key routed
85+
through a spread — fail while you author instead of at deploy, and the export
86+
carries defaults already materialized (#4269).
8287

8388
```typescript
8489
import { P } from '@objectstack/spec';
85-
import { Hook, HookContext } from '@objectstack/spec/data';
90+
import { defineHook, HookContext } from '@objectstack/spec/data';
8691

87-
const myHook: Hook = {
92+
const myHook = defineHook({
8893
// Required: Unique identifier (snake_case)
8994
name: 'my_validation_hook',
9095

@@ -122,7 +127,7 @@ const myHook: Hook = {
122127
maxRetries: 3,
123128
backoffMs: 1000,
124129
},
125-
};
130+
});
126131
```
127132

128133
### Key Properties Explained
@@ -420,9 +425,9 @@ is **not** in the partial patch) and flip that position to `filled`:
420425

421426
<!-- os:check -->
422427
```typescript
423-
import type { Hook } from '@objectstack/spec/data';
428+
import { defineHook } from '@objectstack/spec/data';
424429

425-
const fillPositionOnHire: Hook = {
430+
const fillPositionOnHire = defineHook({
426431
name: 'fill_position_on_hire',
427432
object: 'candidate',
428433
events: ['afterUpdate'],
@@ -441,7 +446,7 @@ const fillPositionOnHire: Hook = {
441446
capabilities: ['api.read', 'api.write', 'log'],
442447
},
443448
onError: 'log',
444-
};
449+
});
445450

446451
export default fillPositionOnHire;
447452
```
@@ -675,7 +680,7 @@ handler: async (ctx: HookContext) => {
675680
### 1. Setting Default Values
676681

677682
```typescript
678-
const setAccountDefaults: Hook = {
683+
const setAccountDefaults = defineHook({
679684
name: 'account_defaults',
680685
object: 'account',
681686
events: ['beforeInsert'],
@@ -693,13 +698,13 @@ const setAccountDefaults: Hook = {
693698
ctx.input.owner_id = ctx.session.userId;
694699
}
695700
},
696-
};
701+
});
697702
```
698703

699704
### 2. Data Validation
700705

701706
```typescript
702-
const validateAccount: Hook = {
707+
const validateAccount = defineHook({
703708
name: 'account_validation',
704709
object: 'account',
705710
events: ['beforeInsert', 'beforeUpdate'],
@@ -719,13 +724,13 @@ const validateAccount: Hook = {
719724
throw new Error('Annual revenue cannot be negative');
720725
}
721726
},
722-
};
727+
});
723728
```
724729

725730
### 3. Preventing Deletion
726731

727732
```typescript
728-
const protectStrategicAccounts: Hook = {
733+
const protectStrategicAccounts = defineHook({
729734
name: 'protect_strategic_accounts',
730735
object: 'account',
731736
events: ['beforeDelete'],
@@ -747,13 +752,13 @@ const protectStrategicAccounts: Hook = {
747752
throw new Error(`Cannot delete account with ${oppCount} active opportunities`);
748753
}
749754
},
750-
};
755+
});
751756
```
752757

753758
### 4. Data Enrichment
754759

755760
```typescript
756-
const enrichLeadScore: Hook = {
761+
const enrichLeadScore = defineHook({
757762
name: 'lead_scoring',
758763
object: 'lead',
759764
events: ['beforeInsert', 'beforeUpdate'],
@@ -782,13 +787,13 @@ const enrichLeadScore: Hook = {
782787

783788
ctx.input.score = score;
784789
},
785-
};
790+
});
786791
```
787792

788793
### 5. Triggering Workflows
789794

790795
```typescript
791-
const notifyOnStatusChange: Hook = {
796+
const notifyOnStatusChange = defineHook({
792797
name: 'notify_status_change',
793798
object: 'opportunity',
794799
events: ['afterUpdate'],
@@ -810,13 +815,13 @@ const notifyOnStatusChange: Hook = {
810815
// });
811816
}
812817
},
813-
};
818+
});
814819
```
815820

816821
### 6. Creating Related Records
817822

818823
```typescript
819-
const createAuditTrail: Hook = {
824+
const createAuditTrail = defineHook({
820825
name: 'audit_trail',
821826
object: ['account', 'contact', 'opportunity'],
822827
events: ['afterInsert', 'afterUpdate', 'afterDelete'],
@@ -836,13 +841,13 @@ const createAuditTrail: Hook = {
836841
} : undefined,
837842
});
838843
},
839-
};
844+
});
840845
```
841846

842847
### 7. External API Integration
843848

844849
```typescript
845-
const syncToExternalCRM: Hook = {
850+
const syncToExternalCRM = defineHook({
846851
name: 'sync_external_crm',
847852
object: 'account',
848853
events: ['afterInsert', 'afterUpdate'],
@@ -867,13 +872,13 @@ const syncToExternalCRM: Hook = {
867872
console.error('Failed to sync to external CRM', error);
868873
}
869874
},
870-
};
875+
});
871876
```
872877

873878
### 8. Multi-Object Logic
874879

875880
```typescript
876-
const cascadeAccountUpdate: Hook = {
881+
const cascadeAccountUpdate = defineHook({
877882
name: 'cascade_account_updates',
878883
object: 'account',
879884
events: ['afterUpdate'],
@@ -887,13 +892,13 @@ const cascadeAccountUpdate: Hook = {
887892
);
888893
}
889894
},
890-
};
895+
});
891896
```
892897

893898
### 9. Conditional Execution
894899

895900
```typescript
896-
const highValueAccountAlert: Hook = {
901+
const highValueAccountAlert = defineHook({
897902
name: 'high_value_alert',
898903
object: 'account',
899904
events: ['afterInsert'],
@@ -904,7 +909,7 @@ const highValueAccountAlert: Hook = {
904909
console.log(`🚨 High-value account created: ${ctx.result.name}`);
905910
// Send alert to sales leadership
906911
},
907-
};
912+
});
908913
```
909914

910915
### 10. Data Masking (Read Operations)
@@ -916,7 +921,7 @@ const highValueAccountAlert: Hook = {
916921
> subscription covers both `find` and `findOne`.
917922
918923
```typescript
919-
const maskSensitiveData: Hook = {
924+
const maskSensitiveData = defineHook({
920925
name: 'mask_pii',
921926
object: ['contact', 'lead'],
922927
events: ['afterFind'], // fires for findOne too — no separate afterFindOne
@@ -942,7 +947,7 @@ const maskSensitiveData: Hook = {
942947
}
943948
}
944949
},
945-
};
950+
});
946951
```
947952

948953
---
@@ -1013,16 +1018,16 @@ export const onEnable = async (ctx: { ql: ObjectQL }) => {
10131018

10141019
```typescript
10151020
// src/objects/account.hook.ts
1016-
import { Hook, HookContext } from '@objectstack/spec/data';
1021+
import { defineHook, HookContext } from '@objectstack/spec/data';
10171022

1018-
const accountHook: Hook = {
1023+
const accountHook = defineHook({
10191024
name: 'account_logic',
10201025
object: 'account',
10211026
events: ['beforeInsert', 'beforeUpdate'],
10221027
handler: async (ctx: HookContext) => {
10231028
// Validation logic
10241029
},
1025-
};
1030+
});
10261031

10271032
export default accountHook;
10281033

@@ -1250,7 +1255,7 @@ const validators = [
12501255
validateWebsite,
12511256
];
12521257

1253-
const composedHook: Hook = {
1258+
const composedHook = defineHook({
12541259
name: 'validation_suite',
12551260
object: 'account',
12561261
events: ['beforeInsert', 'beforeUpdate'],
@@ -1259,13 +1264,13 @@ const composedHook: Hook = {
12591264
await validator(ctx);
12601265
}
12611266
},
1262-
};
1267+
});
12631268
```
12641269

12651270
### Conditional Hook Execution
12661271

12671272
```typescript
1268-
const conditionalHook: Hook = {
1273+
const conditionalHook = defineHook({
12691274
name: 'enterprise_only',
12701275
object: 'account',
12711276
events: ['afterInsert'],
@@ -1277,7 +1282,7 @@ const conditionalHook: Hook = {
12771282

12781283
// Enterprise-specific logic
12791284
},
1280-
};
1285+
});
12811286
```
12821287

12831288
---

skills/objectstack-data/rules/hooks.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ The canonical reference includes:
2828

2929
```typescript
3030
import { P } from '@objectstack/spec';
31-
import { Hook, HookContext } from '@objectstack/spec/data';
31+
import { defineHook, HookContext } from '@objectstack/spec/data';
3232

33-
const hook: Hook = {
33+
const hook = defineHook({
3434
name: 'my_hook', // Required: unique identifier
3535
object: 'account', // Required: target object(s)
3636
events: ['beforeInsert'], // Required: lifecycle events
@@ -40,9 +40,16 @@ const hook: Hook = {
4040
priority: 100, // Optional: execution order
4141
async: false, // Optional: background execution (after* only)
4242
condition: P`record.status == 'active'`, // Optional: conditional execution (CEL)
43-
};
43+
});
4444
```
4545

46+
Prefer `defineHook()` over a bare `: Hook` literal (the same rule as
47+
`defineDatasource`): it validates when the module is imported, so
48+
constraint-level mistakes a bare annotation can't catch — a non-`snake_case`
49+
`name`, a misspelled key routed through a spread — fail while you author
50+
instead of at deploy, and the export carries defaults already materialized
51+
(#4269).
52+
4653
### Logic: `body` (preferred) or `handler` (deprecated)
4754

4855
A hook's logic comes from **either** an inline `handler` function **or** a

skills/objectstack-data/rules/validation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,9 @@ Calling an external API, hitting another object, or running arbitrary code is
235235
lifecycle hook and throw on failure — the typed, supported extension point:
236236

237237
```typescript
238-
import { Hook, HookContext } from '@objectstack/spec/data';
238+
import { defineHook, HookContext } from '@objectstack/spec/data';
239239

240-
const taxIdCheck: Hook = {
240+
const taxIdCheck = defineHook({
241241
name: 'tax_id_external_check',
242242
object: 'account',
243243
events: ['beforeInsert', 'beforeUpdate'],
@@ -246,7 +246,7 @@ const taxIdCheck: Hook = {
246246
throw new Error('Invalid tax ID');
247247
}
248248
},
249-
};
249+
});
250250
```
251251

252252
## Validation Properties

0 commit comments

Comments
 (0)