dsh-d365-kit 0.34.1 → 0.34.3

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +318 -308
  2. package/CHANGELOG.zh-CN.md +317 -307
  3. package/INSTALL.md +94 -94
  4. package/INSTALL.zh-CN.md +93 -93
  5. package/LICENSE +21 -21
  6. package/README.md +336 -327
  7. package/README.zh-CN.md +336 -327
  8. package/client.js +1818 -1818
  9. package/cordis.patch.yml +44 -44
  10. package/docs/en/architecture.md +172 -172
  11. package/docs/en/checks.md +184 -184
  12. package/docs/en/installation.md +151 -151
  13. package/docs/en/permissions.md +161 -161
  14. package/docs/en/scenarios.md +202 -202
  15. package/docs/en/tools.md +665 -665
  16. package/docs/en/troubleshooting.md +131 -131
  17. package/docs/zh/architecture.md +172 -172
  18. package/docs/zh/checks.md +184 -184
  19. package/docs/zh/installation.md +151 -151
  20. package/docs/zh/permissions.md +161 -161
  21. package/docs/zh/scenarios.md +202 -202
  22. package/docs/zh/tools.md +665 -665
  23. package/docs/zh/troubleshooting.md +131 -131
  24. package/examples/prompts.en.md +206 -206
  25. package/examples/prompts.zh-CN.md +206 -206
  26. package/examples/waivers.example.json +20 -20
  27. package/icon.svg +6 -6
  28. package/index.js +139 -139
  29. package/lib/auth.js +181 -181
  30. package/lib/client.js +496 -496
  31. package/lib/code-check.js +549 -549
  32. package/lib/config.js +323 -323
  33. package/lib/contract-history.js +49 -49
  34. package/lib/contract.js +583 -583
  35. package/lib/csharp-check.js +493 -493
  36. package/lib/data-migrate.js +1318 -1318
  37. package/lib/data-policy.js +78 -78
  38. package/lib/error-codes.js +80 -80
  39. package/lib/gitlab.js +161 -161
  40. package/lib/health.js +205 -205
  41. package/lib/migrate.js +732 -732
  42. package/lib/preflight.js +116 -116
  43. package/lib/refs.js +191 -191
  44. package/lib/resources.js +320 -320
  45. package/lib/retention-defaults.js +17 -17
  46. package/lib/retention.js +384 -384
  47. package/lib/runtime.js +232 -232
  48. package/lib/settings-http.js +512 -512
  49. package/lib/settings-store.js +371 -371
  50. package/lib/snapshot.js +149 -149
  51. package/lib/tool-code-check.js +133 -133
  52. package/lib/tool-configure.js +169 -169
  53. package/lib/tool-contract.js +376 -376
  54. package/lib/tool-data-sync.js +559 -559
  55. package/lib/tool-data-write.js +203 -203
  56. package/lib/tool-data.js +282 -282
  57. package/lib/tool-diff.js +484 -484
  58. package/lib/tool-environments.js +106 -106
  59. package/lib/tool-flows.js +540 -540
  60. package/lib/tool-health.js +127 -127
  61. package/lib/tool-layers.js +497 -497
  62. package/lib/tool-metadata.js +417 -417
  63. package/lib/tool-migrate.js +102 -102
  64. package/lib/tool-refs.js +206 -206
  65. package/lib/tool-reports.js +299 -299
  66. package/lib/tool-sync.js +430 -430
  67. package/lib/tool-write.js +563 -563
  68. package/lib/waivers.js +83 -83
  69. package/lib/write-actions.js +1374 -1374
  70. package/lib/write-policy.js +199 -199
  71. package/lib/write-store.js +167 -167
  72. package/locale/en.json +68 -68
  73. package/locale/zh.json +68 -68
  74. package/package.json +2 -2
  75. package/skills/d365-troubleshooting/SKILL.md +115 -115
package/cordis.patch.yml CHANGED
@@ -1,44 +1,44 @@
1
- # dsh-d365-kit (host plugin).
2
- #
3
- # Configuration
4
- # ------------
5
- # `configFile` is read on every tool call, so environment definitions can be
6
- # edited without reinstalling the bundle. When it is unset (this shipped shape),
7
- # the plugin falls back to, in order:
8
- # 1. the D365_CONFIG_FILE environment variable
9
- # 2. <USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json
10
- # (when that directory exists)
11
- # 3. <cwd>\d365.config.json
12
- #
13
- # Credentials are never written here: name the environment variables instead.
14
- # Resolution order per value is
15
- # 1. the inline value (tenantId / clientId / clientSecret)
16
- # 2. the value saved in d365.secrets.json by the settings page
17
- # 3. the variable named by <kind>Env
18
- # 4. D365_<ENV>_<KIND> e.g. D365_DEV_CLIENT_SECRET
19
- # 5. D365_<KIND> e.g. D365_CLIENT_SECRET
20
- - insert:
21
- - id: dsh-d365-kit
22
- name: 'dsh-d365-kit'
23
- config:
24
- # configFile: 'D:\path\to\d365.config.json'
25
- defaultEnvironment: dev
26
- timeoutMs: 60000
27
- maxRecords: 200
28
- # Snapshots are large (~125 MB per full pull), so keep only the last two
29
- # per environment: enough for `@prev` comparisons, bounded on disk.
30
- snapshotKeep: 2
31
- environments: {}
32
- # environments:
33
- # dev:
34
- # url: 'https://contoso-dev.crm5.dynamics.com'
35
- # label: Dev
36
- # cloud: public # public | usgovernment | dod | china
37
- # tenantIdEnv: D365_TENANT_ID
38
- # clientIdEnv: D365_DEV_CLIENT_ID
39
- # clientSecretEnv: D365_DEV_CLIENT_SECRET
40
- # uat:
41
- # url: 'https://contoso-uat.crm5.dynamics.com'
42
- # label: UAT
43
- # clientIdEnv: D365_UAT_CLIENT_ID
44
- # clientSecretEnv: D365_UAT_CLIENT_SECRET
1
+ # dsh-d365-kit (host plugin).
2
+ #
3
+ # Configuration
4
+ # ------------
5
+ # `configFile` is read on every tool call, so environment definitions can be
6
+ # edited without reinstalling the bundle. When it is unset (this shipped shape),
7
+ # the plugin falls back to, in order:
8
+ # 1. the D365_CONFIG_FILE environment variable
9
+ # 2. <USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json
10
+ # (when that directory exists)
11
+ # 3. <cwd>\d365.config.json
12
+ #
13
+ # Credentials are never written here: name the environment variables instead.
14
+ # Resolution order per value is
15
+ # 1. the inline value (tenantId / clientId / clientSecret)
16
+ # 2. the value saved in d365.secrets.json by the settings page
17
+ # 3. the variable named by <kind>Env
18
+ # 4. D365_<ENV>_<KIND> e.g. D365_DEV_CLIENT_SECRET
19
+ # 5. D365_<KIND> e.g. D365_CLIENT_SECRET
20
+ - insert:
21
+ - id: dsh-d365-kit
22
+ name: 'dsh-d365-kit'
23
+ config:
24
+ # configFile: 'D:\path\to\d365.config.json'
25
+ defaultEnvironment: dev
26
+ timeoutMs: 60000
27
+ maxRecords: 200
28
+ # Snapshots are large (~125 MB per full pull), so keep only the last two
29
+ # per environment: enough for `@prev` comparisons, bounded on disk.
30
+ snapshotKeep: 2
31
+ environments: {}
32
+ # environments:
33
+ # dev:
34
+ # url: 'https://contoso-dev.crm5.dynamics.com'
35
+ # label: Dev
36
+ # cloud: public # public | usgovernment | dod | china
37
+ # tenantIdEnv: D365_TENANT_ID
38
+ # clientIdEnv: D365_DEV_CLIENT_ID
39
+ # clientSecretEnv: D365_DEV_CLIENT_SECRET
40
+ # uat:
41
+ # url: 'https://contoso-uat.crm5.dynamics.com'
42
+ # label: UAT
43
+ # clientIdEnv: D365_UAT_CLIENT_ID
44
+ # clientSecretEnv: D365_UAT_CLIENT_SECRET
@@ -1,172 +1,172 @@
1
- # Architecture and Development
2
-
3
- **English** | [简体中文](../zh/architecture.md)
4
-
5
- ## Layout
6
-
7
- One DSH bundle in two parts:
8
-
9
- ```
10
- index.js # Host side: registers the 19 tools, mounts the settings page HTTP routes
11
- client.js # Web side: settings page (six tabs, no build step, a browser module directly)
12
- lib/*.js # All Host-side logic (41 modules)
13
- ```
14
-
15
- Zero runtime dependencies: only Node's built-in `fetch` / `fs` / `path`. No TypeScript, no bundler, no `node_modules`.
16
-
17
- `cordis.patch.yml` declares the bundle and its default config (`configFile`, `timeoutMs`, `snapshotKeep`, `environments`);
18
- `package.json`'s `dsh.bundle.patch` points at it, and `dsh.client` declares the Web-side entry.
19
-
20
- ## Module responsibilities
21
-
22
- ```
23
- lib/
24
- client.js Dataverse Web API client: URL building, paging, retries, permission gate, tokens
25
- auth.js Entra ID client-credentials flow; tokens cached per credential fingerprint
26
- config.js Config load/normalize; configFile fallback chain; secret resolution
27
- resources.js Query definitions and field mappings for 19 metadata resource types
28
- runtime.js makeClient(env, config), org info, version
29
- snapshot.js Snapshot writing, manifest, snapshot directory
30
- retention.js Retention policy, disk stats, cleanup, compaction
31
- write-policy.js Capability table (CAPABILITIES) and per-environment authorization
32
- write-actions.js Planner and executor for all writes (the only code that changes an environment)
33
- write-store.js Plan and change-record read/write, plan state machine
34
- preflight.js Pre-execution idle check (msdyn_solutionhistories)
35
- settings-store.js Settings page data layer (environments, permissions, general params, GitLab)
36
- settings-http.js Settings page routes (exact paths on the auth channel)
37
- contract.js Contract fact reading, comparison, rendering
38
- migrate.js Migration assessment (layer lookup per component)
39
- code-check.js MR check (JS/TS)
40
- csharp-check.js MR check (C#)
41
- refs.js Reverse index (forms/views/flows/steps)
42
- health.js Environment health
43
- gitlab.js GitLab API (MR diff, connectivity)
44
- waivers.js Contract waivers
45
- contract-history.js Drift trend
46
- data-migrate.js Business-record migration (plan, lookup resolution, writes, run log)
47
- tool-*.js 19 tools, one file per tool (or per related group)
48
- ```
49
-
50
- ## Data flow
51
-
52
- ```
53
- agent calls d365_* ──> tool-*.js ──> lib capability modules ──> client.js ──> Dataverse Web API
54
- │ │
55
- └────────── summary + file paths <─────────────────────┘
56
- │
57
- .d365/{snapshots,diffs,reports,query,changes}
58
- ```
59
-
60
- Design principles:
61
-
62
- 1. **Tools return a summary and a path; the body lands in a file** — do not stuff tens of MB of JSON into the conversation;
63
- 2. **Read-only first** — only `d365_write execute` and `d365_data_write` change an environment, and both require human approval;
64
- 3. **Decisions live in one place** — what can be seen and what can be changed are both decided at the request entry point in `lib/client.js`, so new tools cannot bypass them;
65
- 4. **Live beats snapshot** — "how is it now" questions such as drift, migration, and health read live data; snapshots are only for comparison across time or environments;
66
- 5. **Rules are data** — contract rules, retention policy, and the capability table are all table-driven; adding a rule does not change the flow.
67
-
68
- ## The path of one request
69
-
70
- Take `d365_metadata resource=attributes` as an example:
71
-
72
- ```
73
- tool-metadata.js
74
- → pickEnvironments(args) select environments
75
- → loadConfig(state.rawConfig) read config (done at every tool entry)
76
- → makeClient(env, config) build the client
77
- → resources.js query definitions build path/params
78
- → client.getPagedWithSelectFallback paging + column-name fallback
79
- → permission gate (metadata entity sets → metadata.read; other GETs → data.read; writes → data.write)
80
- → getAccessToken (cached per credential fingerprint)
81
- → fetch; 429/5xx retried per method
82
- → cross-environment alignment + summary
83
- → more than 12 rows writes .d365/query/*.json
84
- ```
85
-
86
- ## Adding a tool
87
-
88
- ```js
89
- // lib/tool-demo.js
90
- import { pickEnvironments } from './tool-metadata.js'; // or roll your own
91
-
92
- export default function registerDemo(ctx, state) {
93
- const disposer = ctx.tools.register({
94
- name: 'd365_demo',
95
- description: 'One sentence on what question it answers.',
96
- parameters: { // must be a standard JSON Schema
97
- type: 'object',
98
- additionalProperties: false,
99
- required: ['entity'],
100
- properties: {
101
- entity: { type: 'string', description: 'Entity logical name' },
102
- environment: { type: 'string', description: 'Environment key' },
103
- },
104
- },
105
- output: {
106
- schema: { // likewise a standard JSON Schema
107
- type: 'object',
108
- additionalProperties: false,
109
- required: ['text'],
110
- properties: { text: { type: 'string' } },
111
- },
112
- },
113
- async execute(args, exec) {
114
- const config = await loadConfig(state.rawConfig); // note: rawConfig
115
- ...
116
- return { text };
117
- },
118
- });
119
- ctx.effect(() => disposer);
120
- }
121
- ```
122
-
123
- Then import it in `index.js`, call `registerDemo(ctx, state)`, and add the disposer to the list.
124
-
125
- DSH's hard requirements (all of these have bitten us):
126
-
127
- | Requirement | Symptom |
128
- | --- | --- |
129
- | Both `parameters` and `output.schema` are standard JSON Schema (root `type: 'object'`, `required` is an array) | `schema must be a JSON Schema of 'type: "object"', got 'type: null'` |
130
- | Output strictly matches `output.schema`: no extra fields, no `type: ['string','null']` | `value.x is not a declared property` / `type arrays are not supported` |
131
- | Config comes from `state.rawConfig` (`index.js` passes `{ rawConfig }`) | Environments mysteriously missing, lists empty |
132
- | Host-side changes require a plugin reload | Tool behavior is still the old one |
133
-
134
- ## Client (settings page)
135
-
136
- `client.js` is a browser module: `window.__ModuleLoader__.load({ id: 'dsh-d365-kit', factory })`.
137
-
138
- - The six tabs' panels are **mounted at the same time**, with inactive ones hidden by `display:none` — switching tabs does not lose edit state;
139
- - Copy ships a built-in Chinese/English dictionary; `t()` computes on every call from `localeService.getLocale().active`, independent of whether `locale.register` succeeded;
140
- - Language switches re-render by subscribing to locale changes;
141
- - Every write operation carries `x-d365-meta-write: 1` (the Host uses it to reject requests that do not come from the settings page).
142
-
143
- **The module id must match the package name**, otherwise the loader runs it as two modules, reports `duplicate factory registration`, and the whole settings page disappears.
144
-
145
- ## Local verification
146
-
147
- Most logic can be tested without an environment: stub `globalThis.fetch` and `ctx.tools` and call the module functions directly.
148
-
149
- ```js
150
- globalThis.fetch = async (url) => new Response(JSON.stringify({ value: [] }), { status: 200 });
151
- ```
152
-
153
- Note: `new Response('', { status: 204 })` throws `Invalid response status code 204` in Node; for 204 use a `null` body.
154
-
155
- Minimal set for verifying against a real environment:
156
-
157
- ```
158
- d365_envs check=true
159
- d365_metadata resource=entities top=2
160
- d365_layers component=account componentType=Entity
161
- d365_health environments=["<env>"]
162
- ```
163
-
164
- ## Known pitfalls (for the next person to change the code)
165
-
166
- - Metadata endpoints do not accept `$top` / `$orderby`; the client degrades progressively. When writing a new query, prefer `getPagedWithSelectFallback`;
167
- - `getPagedWithSelectFallback` returns `{ page, selectDropped, ... }`; the rows are in `result.page.records`;
168
- - `odataString()` only escapes quotes and **does not add them**; write your own `'...'` in filters;
169
- - `client.getOne()` appends `$top=1`, which returns 400 for `solutions` / `solutioncomponents`; use `requestJson` and page manually instead;
170
- - Virtual/audit tables such as the layer table and `msdyn_solutionhistories` behave differently from normal tables; verify with a small sample first;
171
- - Do not rewrite files containing non-ASCII with PowerShell 5.1 `Get-Content | Set-Content` (it re-encodes them as ANSI); use an editing tool;
172
- - Writing `package.json` with `Set-Content -Encoding utf8` adds a BOM, and `JSON.parse` fails outright.
1
+ # Architecture and Development
2
+
3
+ **English** | [简体中文](../zh/architecture.md)
4
+
5
+ ## Layout
6
+
7
+ One DSH bundle in two parts:
8
+
9
+ ```
10
+ index.js # Host side: registers the 19 tools, mounts the settings page HTTP routes
11
+ client.js # Web side: settings page (six tabs, no build step, a browser module directly)
12
+ lib/*.js # All Host-side logic (41 modules)
13
+ ```
14
+
15
+ Zero runtime dependencies: only Node's built-in `fetch` / `fs` / `path`. No TypeScript, no bundler, no `node_modules`.
16
+
17
+ `cordis.patch.yml` declares the bundle and its default config (`configFile`, `timeoutMs`, `snapshotKeep`, `environments`);
18
+ `package.json`'s `dsh.bundle.patch` points at it, and `dsh.client` declares the Web-side entry.
19
+
20
+ ## Module responsibilities
21
+
22
+ ```
23
+ lib/
24
+ client.js Dataverse Web API client: URL building, paging, retries, permission gate, tokens
25
+ auth.js Entra ID client-credentials flow; tokens cached per credential fingerprint
26
+ config.js Config load/normalize; configFile fallback chain; secret resolution
27
+ resources.js Query definitions and field mappings for 19 metadata resource types
28
+ runtime.js makeClient(env, config), org info, version
29
+ snapshot.js Snapshot writing, manifest, snapshot directory
30
+ retention.js Retention policy, disk stats, cleanup, compaction
31
+ write-policy.js Capability table (CAPABILITIES) and per-environment authorization
32
+ write-actions.js Planner and executor for all writes (the only code that changes an environment)
33
+ write-store.js Plan and change-record read/write, plan state machine
34
+ preflight.js Pre-execution idle check (msdyn_solutionhistories)
35
+ settings-store.js Settings page data layer (environments, permissions, general params, GitLab)
36
+ settings-http.js Settings page routes (exact paths on the auth channel)
37
+ contract.js Contract fact reading, comparison, rendering
38
+ migrate.js Migration assessment (layer lookup per component)
39
+ code-check.js MR check (JS/TS)
40
+ csharp-check.js MR check (C#)
41
+ refs.js Reverse index (forms/views/flows/steps)
42
+ health.js Environment health
43
+ gitlab.js GitLab API (MR diff, connectivity)
44
+ waivers.js Contract waivers
45
+ contract-history.js Drift trend
46
+ data-migrate.js Business-record migration (plan, lookup resolution, writes, run log)
47
+ tool-*.js 19 tools, one file per tool (or per related group)
48
+ ```
49
+
50
+ ## Data flow
51
+
52
+ ```
53
+ agent calls d365_* ──> tool-*.js ──> lib capability modules ──> client.js ──> Dataverse Web API
54
+ │ │
55
+ └────────── summary + file paths <─────────────────────┘
56
+ │
57
+ .d365/{snapshots,diffs,reports,query,changes}
58
+ ```
59
+
60
+ Design principles:
61
+
62
+ 1. **Tools return a summary and a path; the body lands in a file** — do not stuff tens of MB of JSON into the conversation;
63
+ 2. **Read-only first** — only `d365_write execute` and `d365_data_write` change an environment, and both require human approval;
64
+ 3. **Decisions live in one place** — what can be seen and what can be changed are both decided at the request entry point in `lib/client.js`, so new tools cannot bypass them;
65
+ 4. **Live beats snapshot** — "how is it now" questions such as drift, migration, and health read live data; snapshots are only for comparison across time or environments;
66
+ 5. **Rules are data** — contract rules, retention policy, and the capability table are all table-driven; adding a rule does not change the flow.
67
+
68
+ ## The path of one request
69
+
70
+ Take `d365_metadata resource=attributes` as an example:
71
+
72
+ ```
73
+ tool-metadata.js
74
+ → pickEnvironments(args) select environments
75
+ → loadConfig(state.rawConfig) read config (done at every tool entry)
76
+ → makeClient(env, config) build the client
77
+ → resources.js query definitions build path/params
78
+ → client.getPagedWithSelectFallback paging + column-name fallback
79
+ → permission gate (metadata entity sets → metadata.read; other GETs → data.read; writes → data.write)
80
+ → getAccessToken (cached per credential fingerprint)
81
+ → fetch; 429/5xx retried per method
82
+ → cross-environment alignment + summary
83
+ → more than 12 rows writes .d365/query/*.json
84
+ ```
85
+
86
+ ## Adding a tool
87
+
88
+ ```js
89
+ // lib/tool-demo.js
90
+ import { pickEnvironments } from './tool-metadata.js'; // or roll your own
91
+
92
+ export default function registerDemo(ctx, state) {
93
+ const disposer = ctx.tools.register({
94
+ name: 'd365_demo',
95
+ description: 'One sentence on what question it answers.',
96
+ parameters: { // must be a standard JSON Schema
97
+ type: 'object',
98
+ additionalProperties: false,
99
+ required: ['entity'],
100
+ properties: {
101
+ entity: { type: 'string', description: 'Entity logical name' },
102
+ environment: { type: 'string', description: 'Environment key' },
103
+ },
104
+ },
105
+ output: {
106
+ schema: { // likewise a standard JSON Schema
107
+ type: 'object',
108
+ additionalProperties: false,
109
+ required: ['text'],
110
+ properties: { text: { type: 'string' } },
111
+ },
112
+ },
113
+ async execute(args, exec) {
114
+ const config = await loadConfig(state.rawConfig); // note: rawConfig
115
+ ...
116
+ return { text };
117
+ },
118
+ });
119
+ ctx.effect(() => disposer);
120
+ }
121
+ ```
122
+
123
+ Then import it in `index.js`, call `registerDemo(ctx, state)`, and add the disposer to the list.
124
+
125
+ DSH's hard requirements (all of these have bitten us):
126
+
127
+ | Requirement | Symptom |
128
+ | --- | --- |
129
+ | Both `parameters` and `output.schema` are standard JSON Schema (root `type: 'object'`, `required` is an array) | `schema must be a JSON Schema of 'type: "object"', got 'type: null'` |
130
+ | Output strictly matches `output.schema`: no extra fields, no `type: ['string','null']` | `value.x is not a declared property` / `type arrays are not supported` |
131
+ | Config comes from `state.rawConfig` (`index.js` passes `{ rawConfig }`) | Environments mysteriously missing, lists empty |
132
+ | Host-side changes require a plugin reload | Tool behavior is still the old one |
133
+
134
+ ## Client (settings page)
135
+
136
+ `client.js` is a browser module: `window.__ModuleLoader__.load({ id: 'dsh-d365-kit', factory })`.
137
+
138
+ - The six tabs' panels are **mounted at the same time**, with inactive ones hidden by `display:none` — switching tabs does not lose edit state;
139
+ - Copy ships a built-in Chinese/English dictionary; `t()` computes on every call from `localeService.getLocale().active`, independent of whether `locale.register` succeeded;
140
+ - Language switches re-render by subscribing to locale changes;
141
+ - Every write operation carries `x-d365-meta-write: 1` (the Host uses it to reject requests that do not come from the settings page).
142
+
143
+ **The module id must match the package name**, otherwise the loader runs it as two modules, reports `duplicate factory registration`, and the whole settings page disappears.
144
+
145
+ ## Local verification
146
+
147
+ Most logic can be tested without an environment: stub `globalThis.fetch` and `ctx.tools` and call the module functions directly.
148
+
149
+ ```js
150
+ globalThis.fetch = async (url) => new Response(JSON.stringify({ value: [] }), { status: 200 });
151
+ ```
152
+
153
+ Note: `new Response('', { status: 204 })` throws `Invalid response status code 204` in Node; for 204 use a `null` body.
154
+
155
+ Minimal set for verifying against a real environment:
156
+
157
+ ```
158
+ d365_envs check=true
159
+ d365_metadata resource=entities top=2
160
+ d365_layers component=account componentType=Entity
161
+ d365_health environments=["<env>"]
162
+ ```
163
+
164
+ ## Known pitfalls (for the next person to change the code)
165
+
166
+ - Metadata endpoints do not accept `$top` / `$orderby`; the client degrades progressively. When writing a new query, prefer `getPagedWithSelectFallback`;
167
+ - `getPagedWithSelectFallback` returns `{ page, selectDropped, ... }`; the rows are in `result.page.records`;
168
+ - `odataString()` only escapes quotes and **does not add them**; write your own `'...'` in filters;
169
+ - `client.getOne()` appends `$top=1`, which returns 400 for `solutions` / `solutioncomponents`; use `requestJson` and page manually instead;
170
+ - Virtual/audit tables such as the layer table and `msdyn_solutionhistories` behave differently from normal tables; verify with a small sample first;
171
+ - Do not rewrite files containing non-ASCII with PowerShell 5.1 `Get-Content | Set-Content` (it re-encodes them as ANSI); use an editing tool;
172
+ - Writing `package.json` with `Set-Content -Encoding utf8` adds a BOM, and `JSON.parse` fails outright.