@xrmforge/devkit 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,255 +1,357 @@
1
- # XrmForge - AI Agent Instructions
2
-
3
- This file helps AI coding assistants write optimal Dynamics 365 form scripts.
4
-
5
- ## Packages
6
-
7
- - `@xrmforge/typegen` - Generates typed declarations from Dataverse metadata
8
- - `@xrmforge/helpers` - Browser-safe runtime: select(), parseLookup(), typedForm(), Xrm constants, Action executors
9
- - `@xrmforge/testing` - Type-safe form mocks: createFormMock(), fireOnChange()
10
- - `@xrmforge/devkit` - esbuild IIFE bundles via xrmforge build
11
- - `@xrmforge/eslint-plugin` - D365-specific ESLint rules
12
-
13
- ## Generated Types (generated/ directory)
14
-
15
- Run `xrmforge generate` to create:
16
- - `generated/forms/{entity}.ts` - Form interface + Fields/Tabs/Sections/Subgrids enums
17
- - `generated/optionsets/{entity}.ts` - OptionSet const enums
18
- - `generated/entities/{entity}.ts` - Entity interface
19
- - `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
20
- - `generated/entity-names.ts` - EntityNames const enum
21
- - `generated/index.ts` - Barrel file with `export * from` re-exports
22
-
23
- ## Rules: MANDATORY (every violation is a bug)
24
-
25
- 1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
26
- ```typescript
27
- import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
28
- form.getAttribute(Fields.Name) // CORRECT
29
- form.getAttribute("name") // BUG - raw string
30
- ```
31
-
32
- 2. **OptionSet Enum** for ALL value comparisons. Never magic numbers.
33
- ```typescript
34
- import { StatusCode } from '../generated/optionsets/invoice.js';
35
- if (status === StatusCode.Active) // CORRECT
36
- if (status === 0) // BUG - magic number
37
- ```
38
-
39
- 3. **FormContext Cast** to generated form interface in every onLoad:
40
- ```typescript
41
- import type { AccountMainForm } from '../generated/forms/account.js';
42
- const form = ctx.getFormContext() as AccountMainForm;
43
- ```
44
-
45
- 4. **EntityNames Enum** in ALL Xrm.WebApi calls:
46
- ```typescript
47
- import { EntityNames } from '../generated/entity-names.js';
48
- Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
49
- ```
50
-
51
- 5. **parseLookup()** from @xrmforge/helpers for ALL lookup value access:
52
- ```typescript
53
- import { parseLookup } from '@xrmforge/helpers';
54
- const customer = parseLookup(form.getAttribute(Fields.CustomerId));
55
- ```
56
-
57
- 6. **select()** from @xrmforge/helpers for ALL $select queries:
58
- ```typescript
59
- import { select } from '@xrmforge/helpers';
60
- Xrm.WebApi.retrieveRecord(EntityNames.Account, id, select(Fields.Name, Fields.Revenue))
61
- ```
62
-
63
- 7. **wrapHandler()** around EVERY exported async event handler:
64
- ```typescript
65
- import { createLogger } from '../shared/logger';
66
- import { wrapHandler } from '../shared/error-handler';
67
- const logger = createLogger('Namespace.Entity');
68
- export const onLoad = wrapHandler('Namespace.Entity.onLoad', logger, async (ctx) => {
69
- // handler code
70
- });
71
- ```
72
-
73
- 8. **createFormMock()** from @xrmforge/testing for ALL form tests:
74
- ```typescript
75
- import { createFormMock, fireOnChange, setupXrmMock } from '@xrmforge/testing';
76
- ```
77
-
78
- 9. **Module exports** (not window/global assignments). esbuild globalName handles namespacing.
79
-
80
- 10. **Structured Logger** instead of console.* (except in logger.ts itself):
81
- ```typescript
82
- import { createLogger } from '../shared/logger';
83
- const logger = createLogger('Namespace.Entity');
84
- logger.info('Form loaded', { recordId });
85
- ```
86
-
87
- ## Rules: NEVER (every occurrence is a bug)
88
-
89
- - Never `getAttribute("raw_string")` when Fields enum exists
90
- - Never magic numbers for OptionSet values (use OptionSet enums)
91
- - Never `Xrm.Page` (deprecated since D365 v9.0)
92
- - Never synchronous XMLHttpRequest
93
- - Never `eval()`
94
- - Never `window.X = ...` (use module exports)
95
- - Never `console.log/warn/error` in form scripts (use shared logger)
96
- - Never export async handlers without wrapHandler()
97
- - Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames)
98
- - Never `"?$select=name,revenue"` as raw string (use select() from @xrmforge/helpers)
99
- - Never `.getValue()[0].id.replace(...)` for lookups (use parseLookup() from @xrmforge/helpers)
100
-
101
- ## Mandatory Shared Utilities
102
-
103
- Every XrmForge project MUST have these in `src/shared/`:
104
-
105
- ### logger.ts
106
- ```typescript
107
- export interface Logger { debug(msg: string, data?: unknown): void; info(...); warn(...); error(...); }
108
- export function createLogger(namespace: string): Logger;
109
- // Only file allowed to use console.*
110
- ```
111
-
112
- ### error-handler.ts
113
- ```typescript
114
- export function wrapHandler<T>(name: string, logger: Logger, handler: T): T;
115
- // Catches sync+async errors, shows form notification, never rethrows
116
- ```
117
-
118
- ### constants.ts
119
- ```typescript
120
- export const NOTIFICATION_IDS = { ... } as const;
121
- export const MESSAGES = { ... } as const;
122
- ```
123
-
124
- ## Before/After Examples
125
-
126
- ### Field Access
127
- ```typescript
128
- // BEFORE: formContext.getAttribute("name").getValue()
129
- // AFTER:
130
- import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
131
- import type { AccountMainForm } from '../generated/forms/account.js';
132
- const form = ctx.getFormContext() as AccountMainForm;
133
- form.getAttribute(Fields.AccountName).getValue(); // StringAttribute, typed
134
- ```
135
-
136
- ### OptionSet Comparison
137
- ```typescript
138
- // BEFORE: if (status.getValue() === 595300002) { ... }
139
- // AFTER:
140
- import { StatusCode } from '../generated/optionsets/invoice.js';
141
- if (status.getValue() === StatusCode.Gebucht) { ... }
142
- ```
143
-
144
- ### Testing
145
- ```typescript
146
- import { createFormMock } from '@xrmforge/testing';
147
- const mock = createFormMock<AccountMainForm>({
148
- name: 'Test', statuscode: 0
149
- });
150
- onLoad(mock.executionContext);
151
- expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
152
- ```
153
-
154
- ## File Structure
155
-
156
- ```
157
- src/forms/{entity}-form.ts - Form scripts (one per entity)
158
- src/shared/{name}.ts - Shared utilities
159
- generated/ - Generated types (do not edit manually)
160
- tests/forms/{entity}.test.ts - Tests
161
- xrmforge.config.json - Build config
162
- ```
163
-
164
- ## Pattern Recognition: Legacy to XrmForge
165
-
166
- When you see these patterns in legacy code, apply the XrmForge replacement:
167
-
168
- | Legacy Pattern | XrmForge Replacement |
169
- |---|---|
170
- | `getAttribute("name")` | `getAttribute(Fields.Name)` |
171
- | `getControl("name")` | `getControl(Fields.Name)` |
172
- | `getValue() === 595300000` | `getValue() === OptionSets.StatusCode.Active` |
173
- | `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
174
- | `"?$select=name,revenue"` | `select(Fields.Name, Fields.Revenue)` (from @xrmforge/helpers) |
175
- | `value[0].id.replace("{","")...` | `parseLookup(form.getAttribute(Fields.X))` (from @xrmforge/helpers) |
176
- | `Xrm.Page.getAttribute(...)` | `formContext.getAttribute(...)` |
177
- | `var formContext` (global) | `const form = ctx.getFormContext()` (parameter) |
178
- | `function form_OnLoad(ctx)` | `export function onLoad(ctx: Xrm.Events.EventContext)` |
179
- | `.then(success, error)` | `async/await with try/catch` |
180
-
181
- ### Creating OptionSet Enums from Legacy Magic Numbers
182
-
183
- When you find magic numbers like `getValue() === 105710002` in legacy code:
184
- 1. Search the file for ALL numeric comparisons with getValue()
185
- 2. Create a const enum in generated/optionsets/ with descriptive names
186
- 3. Import and use the enum instead of the number
187
-
188
- Example:
189
- ```typescript
190
- // generated/optionsets/invoice.ts
191
- export const enum InvoiceStatusCode {
192
- Neu = 1,
193
- Versendet = 105710000,
194
- Abgeschlossen = 105710001,
195
- Gebucht = 105710002,
196
- }
197
-
198
- // In the form script:
199
- import { InvoiceStatusCode } from '../../generated/optionsets/invoice.js';
200
- if (status.getValue() === InvoiceStatusCode.Gebucht) { ... }
201
- ```
202
-
203
- ## Testing with Global Xrm Mock
204
-
205
- Use `setupXrmMock()` from @xrmforge/testing to mock the global Xrm namespace:
206
- ```typescript
207
- import { createFormMock, setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
208
-
209
- beforeEach(() => setupXrmMock());
210
- afterEach(() => teardownXrmMock());
211
-
212
- // Override specific WebApi methods:
213
- setupXrmMock({
214
- webApiOverrides: {
215
- retrieveMultipleRecords: async () => ({ entities: [{ name: 'Test' }] }),
216
- },
217
- });
218
- ```
219
-
220
- ## Build
221
-
222
- ```bash
223
- npx xrmforge build # IIFE bundles for D365
224
- npx xrmforge build --watch # Watch mode (~10ms rebuilds)
225
- ```
226
-
227
- ## @types/xrm Pitfalls (known issues)
228
-
229
- When creating manual typings without `xrmforge generate`:
230
-
231
- 1. **Form Interface:** Do NOT use `interface extends Xrm.FormContext` (getAttribute overload conflicts).
232
- Use `Omit` pattern instead:
233
- ```typescript
234
- interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
235
- getAttribute(name: Fields.AccountName): Xrm.Attributes.StringAttribute;
236
- getAttribute(name: string): Xrm.Attributes.Attribute;
237
- // ...
238
- }
239
- ```
240
-
241
- 2. **AlertDialogResponse** does NOT exist in @types/xrm. Use `Xrm.Async.PromiseLike<void>`.
242
-
243
- 3. **ConfirmDialogResponse** does NOT exist. Use `Xrm.Navigation.ConfirmResult`.
244
-
245
- 4. **setNotification()** requires 2 arguments: (message, uniqueId).
246
-
247
- 5. **openFile()** requires `fileSize` property in FileDetails.
248
-
249
- 6. **const enum in .d.ts files** cannot be imported at runtime by test frameworks.
250
- Since v0.8.0, XrmForge generates `.ts` files, so this is no longer an issue.
251
- For manual typings, use regular `enum` in `.ts` files (not `.d.ts`).
252
-
253
- ## Full Migration Guide
254
-
255
- See: https://www.npmjs.com/package/@xrmforge/typegen (MIGRATION.md)
1
+ # XrmForge - AI Agent Instructions
2
+
3
+ This file helps AI coding assistants write optimal Dynamics 365 form scripts.
4
+
5
+ ## Packages
6
+
7
+ - `@xrmforge/typegen` - Generates typed declarations from Dataverse metadata
8
+ - `@xrmforge/helpers` - Browser-safe runtime: select(), parseLookup(), typedForm(), Xrm constants, Action executors
9
+ - `@xrmforge/testing` - Type-safe form mocks: createFormMock(), fireOnChange()
10
+ - `@xrmforge/devkit` - esbuild IIFE bundles via xrmforge build
11
+ - `@xrmforge/eslint-plugin` - D365-specific ESLint rules
12
+
13
+ ## Generated Types (generated/ directory)
14
+
15
+ Run `xrmforge generate` to create:
16
+ - `generated/forms/{entity}.ts` - Form interface + Fields/Tabs/Sections/Subgrids enums
17
+ - `generated/optionsets/{entity}.ts` - OptionSet const enums
18
+ - `generated/entities/{entity}.ts` - Entity interface
19
+ - `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
20
+ - `generated/entity-names.ts` - EntityNames const enum
21
+ - `generated/index.ts` - Barrel file with `export * from` re-exports
22
+
23
+ ## Rules: MANDATORY (every violation is a bug)
24
+
25
+ 1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
26
+ ```typescript
27
+ import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
28
+ form.getAttribute(Fields.Name) // CORRECT
29
+ form.getAttribute("name") // BUG - raw string
30
+ ```
31
+
32
+ 2. **OptionSet Enum** for ALL value comparisons. Never magic numbers.
33
+ ```typescript
34
+ import { StatusCode } from '../generated/optionsets/invoice.js';
35
+ if (status === StatusCode.Active) // CORRECT
36
+ if (status === 0) // BUG - magic number
37
+ ```
38
+
39
+ 3. **FormContext Cast** to generated form interface in every onLoad:
40
+ ```typescript
41
+ import type { AccountMainForm } from '../generated/forms/account.js';
42
+ const form = ctx.getFormContext() as AccountMainForm;
43
+ ```
44
+
45
+ 4. **EntityNames Enum** in ALL Xrm.WebApi calls:
46
+ ```typescript
47
+ import { EntityNames } from '../generated/entity-names.js';
48
+ Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
49
+ ```
50
+
51
+ 5. **Lookup helpers** from @xrmforge/helpers for ALL lookup value access:
52
+ ```typescript
53
+ import { formLookup, formLookupId, parseLookup } from '@xrmforge/helpers';
54
+ // Form lookups (getAttribute on FormContext):
55
+ const customer = formLookup(form.getAttribute(Fields.CustomerId));
56
+ const customerId = formLookupId(form.getAttribute(Fields.CustomerId));
57
+ // Web API response lookups (_fieldname_value + OData annotations):
58
+ const parent = parseLookup(apiResponse, 'parentaccountid');
59
+ ```
60
+
61
+ 6. **select()** from @xrmforge/helpers for ALL $select queries:
62
+ ```typescript
63
+ import { select } from '@xrmforge/helpers';
64
+ Xrm.WebApi.retrieveRecord(EntityNames.Account, id, select(Fields.Name, Fields.Revenue))
65
+ ```
66
+
67
+ 7. **wrapHandler()** around EVERY exported async event handler:
68
+ ```typescript
69
+ import { createLogger } from '../shared/logger';
70
+ import { wrapHandler } from '../shared/error-handler';
71
+ const logger = createLogger('Namespace.Entity');
72
+ export const onLoad = wrapHandler('Namespace.Entity.onLoad', logger, async (ctx) => {
73
+ // handler code
74
+ });
75
+ ```
76
+
77
+ 8. **createFormMock()** from @xrmforge/testing for ALL form tests:
78
+ ```typescript
79
+ import { createFormMock, fireOnChange, setupXrmMock } from '@xrmforge/testing';
80
+ ```
81
+
82
+ 9. **Module exports** (not window/global assignments). esbuild globalName handles namespacing.
83
+
84
+ 10. **Structured Logger** instead of console.* (except in logger.ts itself):
85
+ ```typescript
86
+ import { createLogger } from '../shared/logger';
87
+ const logger = createLogger('Namespace.Entity');
88
+ logger.info('Form loaded', { recordId });
89
+ ```
90
+
91
+ ## Rules: NEVER (every occurrence is a bug)
92
+
93
+ - Never `getAttribute("raw_string")` when Fields enum exists
94
+ - Never magic numbers for OptionSet values (use OptionSet enums)
95
+ - Never `Xrm.Page` (deprecated since D365 v9.0)
96
+ - Never synchronous XMLHttpRequest
97
+ - Never `eval()`
98
+ - Never `window.X = ...` (use module exports)
99
+ - Never `console.log/warn/error` in form scripts (use shared logger)
100
+ - Never export async handlers without wrapHandler()
101
+ - Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames)
102
+ - Never `"?$select=name,revenue"` as raw string (use select() from @xrmforge/helpers)
103
+ - Never `.getValue()[0].id.replace(...)` for lookups (use formLookup/formLookupId from @xrmforge/helpers)
104
+ - Never `import ... from '@xrmforge/typegen'` in browser code. @xrmforge/typegen is a Node.js CLI tool. Use `@xrmforge/helpers` for browser-safe runtime functions (select, parseLookup, formLookup, createUnboundAction, etc.)
105
+
106
+ ## Mandatory Shared Utilities
107
+
108
+ Every XrmForge project MUST have these in `src/shared/`:
109
+
110
+ ### logger.ts
111
+ ```typescript
112
+ export interface Logger { debug(msg: string, data?: unknown): void; info(...); warn(...); error(...); }
113
+ export function createLogger(namespace: string): Logger;
114
+ // Only file allowed to use console.*
115
+ ```
116
+
117
+ ### error-handler.ts
118
+ ```typescript
119
+ export function wrapHandler<T>(name: string, logger: Logger, handler: T): T;
120
+ // Catches sync+async errors, shows form notification, never rethrows
121
+ ```
122
+
123
+ ### constants.ts
124
+ ```typescript
125
+ export const NOTIFICATION_IDS = { ... } as const;
126
+ export const MESSAGES = { ... } as const;
127
+ ```
128
+
129
+ ## Before/After Examples
130
+
131
+ ### Field Access
132
+ ```typescript
133
+ // BEFORE: formContext.getAttribute("name").getValue()
134
+ // AFTER:
135
+ import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
136
+ import type { AccountMainForm } from '../generated/forms/account.js';
137
+ const form = ctx.getFormContext() as AccountMainForm;
138
+ form.getAttribute(Fields.AccountName).getValue(); // StringAttribute, typed
139
+ ```
140
+
141
+ ### OptionSet Comparison
142
+ ```typescript
143
+ // BEFORE: if (status.getValue() === 595300002) { ... }
144
+ // AFTER:
145
+ import { StatusCode } from '../generated/optionsets/invoice.js';
146
+ if (status.getValue() === StatusCode.Gebucht) { ... }
147
+ ```
148
+
149
+ ### Testing
150
+ ```typescript
151
+ import { createFormMock } from '@xrmforge/testing';
152
+ const mock = createFormMock<AccountMainForm>({
153
+ name: 'Test', statuscode: 0
154
+ });
155
+ onLoad(mock.asEventContext());
156
+ expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
157
+ ```
158
+
159
+ ## File Structure
160
+
161
+ ```
162
+ src/forms/{entity}-form.ts - Form scripts (one per entity)
163
+ src/shared/{name}.ts - Shared utilities
164
+ generated/ - Generated types (do not edit manually)
165
+ tests/forms/{entity}.test.ts - Tests
166
+ xrmforge.config.json - Build config
167
+ ```
168
+
169
+ ## Pattern Recognition: Legacy to XrmForge
170
+
171
+ When you see these patterns in legacy code, apply the XrmForge replacement:
172
+
173
+ | Legacy Pattern | XrmForge Replacement |
174
+ |---|---|
175
+ | `getAttribute("name")` | `getAttribute(Fields.Name)` |
176
+ | `getControl("name")` | `getControl(Fields.Name)` |
177
+ | `getValue() === 595300000` | `getValue() === OptionSets.StatusCode.Active` |
178
+ | `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
179
+ | `"?$select=name,revenue"` | `select(Fields.Name, Fields.Revenue)` (from @xrmforge/helpers) |
180
+ | `value[0].id.replace("{","")...` | `parseLookup(form.getAttribute(Fields.X))` (from @xrmforge/helpers) |
181
+ | `Xrm.Page.getAttribute(...)` | `formContext.getAttribute(...)` |
182
+ | `var formContext` (global) | `const form = ctx.getFormContext()` (parameter) |
183
+ | `function form_OnLoad(ctx)` | `export function onLoad(ctx: Xrm.Events.EventContext)` |
184
+ | `.then(success, error)` | `async/await with try/catch` |
185
+
186
+ ### Creating OptionSet Enums from Legacy Magic Numbers
187
+
188
+ When you find magic numbers like `getValue() === 105710002` in legacy code:
189
+ 1. Search the file for ALL numeric comparisons with getValue()
190
+ 2. Create a const enum in generated/optionsets/ with descriptive names
191
+ 3. Import and use the enum instead of the number
192
+
193
+ Example:
194
+ ```typescript
195
+ // generated/optionsets/invoice.ts
196
+ export const enum InvoiceStatusCode {
197
+ Neu = 1,
198
+ Versendet = 105710000,
199
+ Abgeschlossen = 105710001,
200
+ Gebucht = 105710002,
201
+ }
202
+
203
+ // In the form script:
204
+ import { InvoiceStatusCode } from '../../generated/optionsets/invoice.js';
205
+ if (status.getValue() === InvoiceStatusCode.Gebucht) { ... }
206
+ ```
207
+
208
+ ## Testing with Global Xrm Mock
209
+
210
+ Use `setupXrmMock()` from @xrmforge/testing to mock the global Xrm namespace:
211
+ ```typescript
212
+ import { createFormMock, setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
213
+
214
+ beforeEach(() => setupXrmMock());
215
+ afterEach(() => teardownXrmMock());
216
+
217
+ // Override specific WebApi methods:
218
+ setupXrmMock({
219
+ webApiOverrides: {
220
+ retrieveMultipleRecords: async () => ({ entities: [{ name: 'Test' }] }),
221
+ },
222
+ });
223
+ ```
224
+
225
+ ## Build
226
+
227
+ ```bash
228
+ npx xrmforge build # IIFE bundles for D365
229
+ npx xrmforge build --watch # Watch mode (~10ms rebuilds)
230
+ ```
231
+
232
+ ## @types/xrm Pitfalls (known issues)
233
+
234
+ When creating manual typings without `xrmforge generate`:
235
+
236
+ 1. **Form Interface:** Do NOT use `interface extends Xrm.FormContext` (getAttribute overload conflicts).
237
+ Use `Omit` pattern instead:
238
+ ```typescript
239
+ interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
240
+ getAttribute(name: Fields.AccountName): Xrm.Attributes.StringAttribute;
241
+ getAttribute(name: string): Xrm.Attributes.Attribute;
242
+ // ...
243
+ }
244
+ ```
245
+
246
+ 2. **AlertDialogResponse** does NOT exist in @types/xrm. Use `Xrm.Async.PromiseLike<void>`.
247
+
248
+ 3. **ConfirmDialogResponse** does NOT exist. Use `Xrm.Navigation.ConfirmResult`.
249
+
250
+ 4. **setNotification()** requires 2 arguments: (message, uniqueId).
251
+
252
+ 5. **openFile()** requires `fileSize` property in FileDetails.
253
+
254
+ 6. **const enum in .d.ts files** cannot be imported at runtime by test frameworks.
255
+ Since v0.8.0, XrmForge generates `.ts` files, so this is no longer an issue.
256
+ For manual typings, use regular `enum` in `.ts` files (not `.d.ts`).
257
+
258
+ ## Self-Check (MANDATORY before Tests)
259
+
260
+ After converting ALL scripts, run these checks. Fix every violation before proceeding to tests.
261
+ Document results in SESSION-GEDAECHTNIS.md (violation count per category).
262
+
263
+ ### Pattern Compliance (all must be 0, or documented exception)
264
+
265
+ ```bash
266
+ # 1. Raw field strings in getAttribute/getControl (must use Fields Enum)
267
+ grep -rn "getAttribute('" src/forms/ --include="*.ts" | grep -v "Fields\."
268
+ grep -rn "getControl('" src/forms/ --include="*.ts" | grep -v "Fields\."
269
+
270
+ # 2. Magic numbers in OptionSet comparisons (must use OptionSet Enum)
271
+ grep -rn "getValue() ===" src/ --include="*.ts" | grep -E "[0-9]{3,}"
272
+
273
+ # 3. Direct _value access instead of parseLookup (in Web API responses)
274
+ grep -rn "_value\b" src/ --include="*.ts" | grep -v "generated/" | grep -v "parseLookup" | grep -v "getValue"
275
+
276
+ # 4. Raw entity names in WebApi calls (must use EntityNames)
277
+ grep -rn "retrieveRecord\|retrieveMultipleRecords\|deleteRecord\|createRecord\|updateRecord" src/ --include="*.ts" | grep "'[a-z]" | grep -v "EntityNames"
278
+
279
+ # 5. Missing select() - no raw "$select=" strings anywhere in src/
280
+ grep -rn '\$select' src/ --include="*.ts" | grep -v "select(" | grep -v "generated/"
281
+
282
+ # 6. Missing FormContext Cast in onLoad (must have "as <Generated>Form")
283
+ grep -rn "getFormContext()" src/forms/ --include="*.ts" | grep -v " as "
284
+
285
+ # 7. Exported handlers without wrapHandler
286
+ grep -rn "^export const\|^export async function\|^export function" src/forms/ --include="*.ts" | grep -v "wrapHandler"
287
+
288
+ # 8. Entity-level FieldsEnums not used (generated/fields/ should be imported)
289
+ echo "Fields imports from generated/fields/:"
290
+ grep -rn "from.*generated/fields/" src/ --include="*.ts" | wc -l
291
+ ```
292
+
293
+ ### Code Quality (all must be 0)
294
+
295
+ ```bash
296
+ # console.* outside logger.ts
297
+ grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"
298
+
299
+ # Xrm.Page (deprecated since D365 v9.0)
300
+ grep -rn "Xrm\.Page" src/ --include="*.ts"
301
+
302
+ # var declarations
303
+ grep -rnE "^\s*var " src/ --include="*.ts"
304
+
305
+ # eval()
306
+ grep -rn "\beval(" src/ --include="*.ts"
307
+
308
+ # XMLHttpRequest
309
+ grep -rn "XMLHttpRequest" src/ --include="*.ts"
310
+
311
+ # as any without eslint-disable comment explaining why
312
+ grep -rn "as any" src/ --include="*.ts" | grep -v "eslint-disable"
313
+ ```
314
+
315
+ ### Documentation (all must pass)
316
+
317
+ ```bash
318
+ # Files without JSDoc header (first line must be /**)
319
+ for f in src/forms/*.ts src/shared/*.ts; do
320
+ head -1 "$f" | grep -q "^/\*\*" || echo "No header: $f"
321
+ done
322
+
323
+ # Exported functions without JSDoc
324
+ grep -rn -B1 "^export " src/ --include="*.ts" | grep -E "^[^*]*export" | grep -v "/\*\*"
325
+ ```
326
+
327
+ ### Test Completeness
328
+
329
+ ```bash
330
+ # Every form script needs a test file
331
+ for f in src/forms/*.ts; do
332
+ base=$(basename "$f" .ts)
333
+ test -f "tests/forms/${base}.test.ts" || echo "No test: $f"
334
+ done
335
+
336
+ # Every test file must use setupXrmMock
337
+ for f in tests/**/*.test.ts; do
338
+ grep -q "setupXrmMock" "$f" || echo "No setupXrmMock: $f"
339
+ done
340
+
341
+ # Every test file needs at least 2 test cases
342
+ for f in tests/**/*.test.ts; do
343
+ count=$(grep -c "it(" "$f" 2>/dev/null || echo 0)
344
+ [ "$count" -lt 2 ] && echo "Only $count tests: $f"
345
+ done
346
+ ```
347
+
348
+ ### Exceptions
349
+
350
+ Some checks have legitimate exceptions:
351
+ - **Raw field strings in helpers**: Generic helper functions that accept `fieldName: string` parameters cannot use Fields Enums. Document these.
352
+ - **System entities not in EntityNames**: Entities not in the Solution (e.g. `annotation`, `transactioncurrency`, `systemuser`) may use string literals. Document which ones.
353
+ - **as any for Grid.refresh()**: `@types/xrm` does not type `Grid.refresh()`. Requires eslint-disable with explanation.
354
+
355
+ ## Full Migration Guide
356
+
357
+ See: https://www.npmjs.com/package/@xrmforge/typegen (MIGRATION.md)