dsh-d365-kit 0.0.0-stage → 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 (79) hide show
  1. package/CHANGELOG.md +318 -0
  2. package/CHANGELOG.zh-CN.md +317 -0
  3. package/INSTALL.md +94 -0
  4. package/INSTALL.zh-CN.md +93 -0
  5. package/LICENSE +21 -0
  6. package/README.md +335 -2
  7. package/README.zh-CN.md +336 -0
  8. package/client.js +1818 -0
  9. package/cordis.patch.yml +44 -0
  10. package/docs/en/architecture.md +172 -0
  11. package/docs/en/checks.md +184 -0
  12. package/docs/en/installation.md +151 -0
  13. package/docs/en/permissions.md +161 -0
  14. package/docs/en/scenarios.md +202 -0
  15. package/docs/en/tools.md +665 -0
  16. package/docs/en/troubleshooting.md +131 -0
  17. package/docs/zh/architecture.md +172 -0
  18. package/docs/zh/checks.md +184 -0
  19. package/docs/zh/installation.md +151 -0
  20. package/docs/zh/permissions.md +161 -0
  21. package/docs/zh/scenarios.md +202 -0
  22. package/docs/zh/tools.md +665 -0
  23. package/docs/zh/troubleshooting.md +131 -0
  24. package/examples/prompts.en.md +206 -0
  25. package/examples/prompts.zh-CN.md +206 -0
  26. package/examples/waivers.example.json +20 -0
  27. package/icon.svg +6 -0
  28. package/index.js +142 -0
  29. package/lib/auth.js +181 -0
  30. package/lib/client.js +496 -0
  31. package/lib/code-check.js +549 -0
  32. package/lib/config.js +323 -0
  33. package/lib/contract-history.js +49 -0
  34. package/lib/contract.js +583 -0
  35. package/lib/csharp-check.js +493 -0
  36. package/lib/data-migrate.js +1318 -0
  37. package/lib/data-policy.js +78 -0
  38. package/lib/error-codes.js +80 -0
  39. package/lib/errors.js +81 -0
  40. package/lib/gitlab.js +161 -0
  41. package/lib/health.js +205 -0
  42. package/lib/migrate.js +732 -0
  43. package/lib/preflight.js +116 -0
  44. package/lib/refs.js +191 -0
  45. package/lib/resources.js +320 -0
  46. package/lib/retention-defaults.js +17 -0
  47. package/lib/retention.js +384 -0
  48. package/lib/runtime.js +232 -0
  49. package/lib/settings-http.js +512 -0
  50. package/lib/settings-store.js +371 -0
  51. package/lib/skill.js +76 -0
  52. package/lib/snapshot.js +149 -0
  53. package/lib/tool-code-check.js +133 -0
  54. package/lib/tool-configure.js +169 -0
  55. package/lib/tool-contract.js +376 -0
  56. package/lib/tool-data-sync.js +559 -0
  57. package/lib/tool-data-write.js +203 -0
  58. package/lib/tool-data.js +282 -0
  59. package/lib/tool-diff.js +484 -0
  60. package/lib/tool-environments.js +106 -0
  61. package/lib/tool-flows.js +540 -0
  62. package/lib/tool-health.js +127 -0
  63. package/lib/tool-invoke.js +284 -0
  64. package/lib/tool-layers.js +497 -0
  65. package/lib/tool-metadata.js +417 -0
  66. package/lib/tool-migrate.js +102 -0
  67. package/lib/tool-refs.js +206 -0
  68. package/lib/tool-reports.js +299 -0
  69. package/lib/tool-sync.js +430 -0
  70. package/lib/tool-trace.js +420 -0
  71. package/lib/tool-write.js +563 -0
  72. package/lib/waivers.js +83 -0
  73. package/lib/write-actions.js +1374 -0
  74. package/lib/write-policy.js +199 -0
  75. package/lib/write-store.js +167 -0
  76. package/locale/en.json +68 -0
  77. package/locale/zh.json +68 -0
  78. package/package.json +59 -4
  79. package/skills/d365-troubleshooting/SKILL.md +115 -0
@@ -0,0 +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
@@ -0,0 +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.
@@ -0,0 +1,184 @@
1
+ # Check Rules
2
+
3
+ **English** | [简体中文](../zh/checks.md)
4
+
5
+ Three check tools answer different questions:
6
+
7
+ | Tool | Question | Data source |
8
+ | --- | --- | --- |
9
+ | `d365_migrate` | What happens when this solution is imported into the target environment | Live read, layer lookup per component |
10
+ | `d365_contract` | Do downstream environments still honor upstream registrations and references | Live read (drift reflects the present) |
11
+ | `d365_code_check` | Will the code changes in this MR run in the target environment | GitLab MR + live environment read |
12
+
13
+ Shared convention: **high confidence blocks outright, medium confidence drops to a warning, anything static analysis cannot decide goes to manual confirmation** — better to have a human take one more look than to let a single false positive turn the tool into noise.
14
+
15
+ ---
16
+
17
+ ## Migration assessment (d365_migrate assess)
18
+
19
+ Component-by-component comparison produces these findings:
20
+
21
+ | Rule | Criterion | Level |
22
+ | --- | --- | --- |
23
+ | `component-missing` | The component does not exist in the target environment | info |
24
+ | `solution-absent` | The target does not have this solution (first import) | warning |
25
+ | `version-not-newer` | The target version is not lower than the source version (the import does not overwrite it with a newer one) | warning |
26
+ | `managed-mismatch` | Managed state differs between source and target | warning |
27
+ | `source-unmanaged` | The source is unmanaged, so the import leaves a layer that cannot be removed by uninstalling | warning |
28
+ | `option-values-lost` | Option values the target has and the source lacks — deleted by the import | **critical** |
29
+ | `option-label-drift` | The same value carries a different label | warning |
30
+ | `shadowed` | Another solution owns the top layer of this component in the target, so its definition is invalidated after import | listed in summary |
31
+
32
+ ### Component types and layers
33
+
34
+ Only six types can be looked up by name in the layer table:
35
+
36
+ | `msdyn_solutioncomponentname` | componenttype |
37
+ | --- | --- |
38
+ | `Entity` | 1 |
39
+ | `Attribute` | 2 |
40
+ | `OptionSet` | 9 |
41
+ | `EntityRelationship` | 10 |
42
+ | `Workflow` | 29 |
43
+ | `WebResource` | 61 |
44
+
45
+ The remaining types (36 reports, 50 chart/security types, 70 app types…) have no matching name in the layer table, so they only get an **existence check** and are marked "not assessed" in the report.
46
+
47
+ The keys used to identify a component across environments come from the layer record's `msdyn_componentjson`: Entity → `logicalname`, Attribute → `logicalname` + parent table, OptionSet → `name`, EntityRelationship → `schemaname`, Workflow → `name` + `category`, WebResource → `name`.
48
+
49
+ ### Why it is slow
50
+
51
+ `msdyn_componentlayer` is a virtual entity: it can only be queried one component at a time by "component id + component type" and cannot be fetched in bulk per solution; a single component takes about 2 seconds. Hence `maxComponents` (default 150) and `concurrency` (default 4, max 8), and only `full: true` assesses everything.
52
+
53
+ ### Reading order for the report
54
+
55
+ 1. Start with **critical**: are any option values lost;
56
+ 2. Then **shadowing**: whose definition gets invalidated by the import, and is it one of your own managed patches;
57
+ 3. Finally **missing pieces and versions**: a component missing from the target usually means the release pipeline skipped a step;
58
+ 4. Landing the conclusion: if the change can join a managed solution and ride along with upgrades, do not ship a separate small unmanaged solution — layers stack by import time, and the next patch that comes in covers it again.
59
+
60
+ ---
61
+
62
+ ## Contract drift (d365_contract)
63
+
64
+ Treat upstream plugin registrations and visual component references as a **contract**, and check whether the downstream still satisfies it.
65
+
66
+ | Rule | Criterion | Level |
67
+ | --- | --- | --- |
68
+ | `assembly-missing` | Upstream has the assembly, downstream does not | Blocker |
69
+ | `assembly-version-older` | Downstream version is lower than upstream | Warning |
70
+ | `plugin-type-missing` | A type registered upstream is missing downstream | Blocker |
71
+ | `plugin-step-missing` | The type exists but the step for the "message + stage + mode" combination is missing | Blocker |
72
+ | `form-library-missing` | A web resource referenced by a downstream form does not exist | Blocker |
73
+ | `sitemap-missing` | An upstream custom sitemap is missing downstream | Warning |
74
+ | `handler-missing` | The function named by a form handler cannot be found in the project scripts | Warning |
75
+ | `option-value-missing` | Downstream is missing an upstream option value (or has a value upstream lacks) | Warning |
76
+
77
+ Details:
78
+
79
+ - Only **unmanaged** assemblies are compared by default; managed assemblies come from Microsoft or from a managed solution and cannot be changed locally;
80
+ - Steps compare by "type + message + stage + mode": async and sync are two different contracts;
81
+ - Option value comparison requires passing `optionSets` explicitly (each one is read from `GlobalOptionSetDefinitions`); without it, nothing is compared;
82
+ - Content that exists downstream but **not** upstream is never a blocker; when an option set has extra values the only hint is "confirm whether it has been back-ported".
83
+
84
+ ### Handler check
85
+
86
+ With `handlers: true`, the project scripts referenced by forms are downloaded and checked for the existence of `functionName`.
87
+
88
+ - Only **project scripts** are judged. The prefixes are inferred from the source environment's unmanaged solutions — the publisher prefix that most of them use, plus `new_` — so a fresh install works against any organization without configuration; set `handlerPrefixes` to pin the list exactly. Partner and sample solutions are deliberately left out: their bundles are minified, so their internal names are mangled and every handler would look missing (on one real environment: 94 findings instead of 14);
89
+ - The handler's own library is checked first; if the function is not found, the search widens to the other project libraries the form loads (form-level shared scope really exists: one library provides the function, another binds it);
90
+ - Platform-injected handlers (`absoluteurl_onchange`, `Main_Form_onload`, etc.) come from system scripts and are not judged;
91
+ - Downloading is capped (400 scripts per environment); anything beyond that is treated as "content unreadable, cannot confirm" rather than falsely reported as a blocker.
92
+
93
+ The first version judged against "every script library a form loads" and produced 306 warnings; narrowed to "project libraries, own library first" it produces 14, all of them cases where the script was read and the function genuinely is not there. **That number is the design goal: a small actionable set, not an unreadable pile.**
94
+
95
+ ### Waivers and trends
96
+
97
+ ```json
98
+ { "action": "waive", "rule": "form-library-missing", "env": "sit",
99
+ "object": "/_static/_common/scripts/CommandbarActions.js", "reason": "this environment does not use SLA dialogs", "days": 30 }
100
+ ```
101
+
102
+ - Stored in `.d365/waivers.json`; `env` accepts `*`, `object` accepts `*` and the prefix form `x*`;
103
+ - An expiry is mandatory (default 30 days); once expired the item reappears in the report automatically;
104
+ - The report shows "N waived", and waived items never enter the blocker list;
105
+ - Every drift run appends an entry to `.d365/reports/contract-history.json` (60 kept), and the next run over the same `source → targets` reports "new / gone".
106
+
107
+ ### Why live data
108
+
109
+ Plugin registration scale: one environment has about 376 assemblies, 6,400 types, and 5,900 types with steps (about 94,000 step records), read in tens of seconds. Drift computed from a snapshot a few days old produces false conclusions, so no snapshots are used here.
110
+
111
+ ---
112
+
113
+ ## Code check (d365_code_check)
114
+
115
+ Reads the MR once and analyzes only the files it touches. Requires a read-only GitLab token (`read_api`); the token's user must be at least **Reporter**.
116
+
117
+ ### JS / TS
118
+
119
+ Chain: changed file → match a web resource in the environment by **file name** (when paths differ, take the closest same-name candidate and mark it "inferred, pending confirmation" in the report) → find the forms that reference that resource → compare the changed lines one by one.
120
+
121
+ | Check | Verdict |
122
+ | --- | --- |
123
+ | A field read by an added line is on **no** form that loads the library | one blocker |
124
+ | The field is only on some forms (the script may never reach that branch on those forms) | warning, listing the missing forms (at most 3) |
125
+ | A referenced entity does not exist (form `datafieldname`, `Xrm.WebApi` entity set) | blocker |
126
+ | A deleted function is still referenced by a form handler | blocker |
127
+ | A deleted file is still referenced by a form | blocker (not reported when no form references it) |
128
+ | `columns.get('x')` | manual confirmation (view/subgrid column — not being a form field is normal) |
129
+ | A prefixed string subscript `["contoso_x"]` | manual confirmation (cannot tell statically whether it is a field or a plain key) |
130
+ | A newly introduced hard-coded GUID, option value, or environment URL | warning or manual confirmation |
131
+ | Configuration data, field-level security, the running user's privileges | manual confirmation (cannot be decided statically) |
132
+
133
+ Access patterns covered: `getAttribute`, `getControl`, `attributes.get`, `formContext.data.entity.attributes.get`, `Xrm.Page.getAttribute`, `attributes[...]`, and prefixed string subscripts.
134
+ Relying on a single `getAttribute` regex misses a lot of compiled output (measured: a 103 KB compiled TS script hit 0 times), so access is split into three groups: definitely a field, definitely a column, needs human judgment.
135
+
136
+ ### C# / project files
137
+
138
+ Infer the assembly name from the **innermost project directory** in the path (an `<AssemblyName>` in the `.csproj` wins when present) → **exact match** against `pluginassemblies` in the environment → look up `plugintypes`, `sdkmessageprocessingsteps`, `sdkmessageprocessingstepimages`.
139
+
140
+ | Check | Verdict |
141
+ | --- | --- |
142
+ | The assembly does not exist in the target environment (shared projects such as `.Processes` fall here) | warning + manual confirmation of which assembly the file ultimately ships in |
143
+ | The source version differs from the environment assembly version | warning |
144
+ | An `IPlugin` class has no matching plugin type in the target environment | blocker |
145
+ | The type exists but has no steps at all (changes will never fire) | warning |
146
+ | Code reads `PreEntityImages["X"]` / `PostEntityImages["X"]` but the step has no such image registered | blocker (Create steps do not check pre-images) |
147
+ | A step's `filteringattributes` references a field that does not exist on the entity | blocker |
148
+ | An enumerated `contoso_*` literal is neither a table nor a global option set in the target | manual confirmation |
149
+ | Two or more steps on the same `message\|stage` | warning (execution order is undefined when ranks collide) |
150
+ | Code uses `secureConfig` / a constructor with two string parameters | manual confirmation (the Web API does not return secure configuration content) |
151
+ | APIs the sandbox may forbid (`System.IO`, `System.Net`, `Thread.Sleep`…) | warning |
152
+
153
+ Assembly names are matched exactly, never approximated — a wrong guess checks types and steps against the wrong assembly, which is worse than not checking at all.
154
+
155
+ ### Report structure
156
+
157
+ ```
158
+ Changed file table
159
+ Per-file impact (which resource/assembly it maps to, which forms/types)
160
+ Blockers (must fix)
161
+ Warnings (should fix, or register a waiver)
162
+ Manual confirmation (HC-01…, each entry states why a machine cannot decide)
163
+ ```
164
+
165
+ ### Known limits
166
+
167
+ - Only the MR's added and removed lines are analyzed; no other repository files are read (no import resolution, no call-chain following);
168
+ - Path → web resource name is inferred from the file name, and marked "inferred" when the repository path and the resource name disagree;
169
+ - C# filtering attributes, images, and types are judged from registration data only; secure/unsecure configuration content and implicit ordering beyond rank still need manual confirmation;
170
+ - The running user's privileges and the actual shape of the data are not judged.
171
+
172
+ ---
173
+
174
+ ## Division of labor between the three checks
175
+
176
+ ```
177
+ Code change ──d365_code_check──> can this change ship (one-off, MR-level)
178
+ Upstream vs downstream ──d365_contract──> is the contract between environments broken (ongoing, version-level)
179
+ Solution ──d365_migrate──> will this import shadow someone else (one-off, release-level)
180
+ Who overrode it ──d365_layers──> why this environment looks the way it does
181
+ Who uses it ──d365_refs──> the impact of changing or deleting it
182
+ ```
183
+
184
+ They do not replace each other: `d365_layers` tells you who overrode it, `d365_refs` tells you who references it, and `d365_code_check` tells you whether the lines you changed land on something that does not exist.
@@ -0,0 +1,151 @@
1
+ # Installation & Configuration
2
+
3
+ **English** | [简体中文](../zh/installation.md)
4
+
5
+ ## Prerequisites
6
+
7
+ | Item | Requirement |
8
+ | --- | --- |
9
+ | DSH | Desktop edition, able to open the Web GUI (the settings page is a Web client plugin) |
10
+ | Network | Able to reach the environment domain (`*.crm*.dynamics.com` or the matching region) and the sign-in endpoint |
11
+ | Application user | An app registration in Entra ID plus an application user created in Dataverse, giving you `ClientId` / `ClientSecret` / `TenantId` |
12
+ | Permissions | Read metadata, read solutions (`prvReadSolution`), read custom APIs (`prvReadCustomAPI`), read the layer table (`prvReadmsdyn_componentlayer`) |
13
+
14
+ A read-only application user is enough; for the write capabilities, messages such as `CreateAttribute` require the matching permission on that user in the environment (system customizer, or the matching privileges ticked in a custom role).
15
+
16
+ > Once the application user is created in one environment, create it again in **every** target environment (the same ClientId can be reused).
17
+
18
+ ## Installation
19
+
20
+ ### Option 1: install from npm (shortest)
21
+
22
+ ```bash
23
+ plugin_manager install_bundle target="dsh-d365-kit@0.34.3"
24
+ ```
25
+
26
+ Works once the package is published to the registry pnpm is configured against — public npm or an internal mirror. The package has no scope, so no scope mapping is needed. To install a specific version pin it as `dsh-d365-kit@<version>`.
27
+
28
+ > **GitHub alone is not a registry.** A repository on GitHub is reachable directly through the `git+https://...` spec (option 2), and plain `npm install` can also take `github:<user>/<repo>#<tag>`; but `install_bundle target="dsh-d365-kit"` only resolves after the package is published to a registry. The repo ships `.github/workflows/publish.yml`: once either the `NPM_TRUSTED_PUBLISHING` variable or the `NPM_TOKEN` secret exists, every push to `main` with a new `package.json` version is published for you.
29
+
30
+ ### Option 2: install from Git (no registry needed)
31
+
32
+ ```bash
33
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
34
+ ```
35
+
36
+ DSH pulls the package from git with pnpm. After installing, check:
37
+
38
+ ```
39
+ plugin_manager list_plugins
40
+ ```
41
+
42
+ Success means `include:dsh-d365-kit` is present with `enabled=true` and `fiberPhase=active`.
43
+
44
+ ### Option 3: install from a local directory
45
+
46
+ Put the whole repository directory anywhere on this machine:
47
+
48
+ ```bash
49
+ plugin_manager install_bundle target="D:\path\to\dsh-d365-kit"
50
+ ```
51
+
52
+ This is a `link:` install: code changes take effect immediately (the Host side still needs a reload), and upgrading means running `git pull` yourself.
53
+
54
+ ### Option 4: zip
55
+
56
+ Unzip it, then install it as in option 3.
57
+
58
+ ### A restart is required after installing
59
+
60
+ Host-side tools work as soon as the plugin is installed; the **settings page** belongs to the Web client plugin, and DSH caches the client plugin module graph, so DSH must be restarted before the page is loaded.
61
+
62
+ After restarting, open Settings and you should see "D365 Toolkit". If it is missing, run `d365_envs` first to confirm the Host side has loaded, then check the entry name in `plugin_manager list_plugins`.
63
+
64
+ ## First-time configuration
65
+
66
+ 1. Settings → D365 Toolkit → **Environments** → Add environment:
67
+
68
+ | Field | Description |
69
+ | --- | --- |
70
+ | key | Short name the tools use, e.g. `dev`, `sit`. **Once it goes into a plan it is recorded in the audit log, so pick a stable name** |
71
+ | URL | Environment URL, e.g. `https://contoso-dev.crm5.dynamics.com` |
72
+ | Cloud | `public` / `usgovernment` / `dod` / `china`; determines the sign-in endpoint |
73
+ | Tenant ID | Entra tenant GUID |
74
+ | ClientId | Application (client) ID of the app registration |
75
+ | Client secret | Secret value generated in the app registration |
76
+
77
+ 2. Click "Test connection"; an organization name and version number coming back means the credentials are correct.
78
+ 3. **Save**. The secret goes into `d365.secrets.json`, everything else into `d365.config.json`.
79
+ 4. The **Permissions** tab lists capabilities per environment: "Read metadata" is on by default (turn it off and tables, fields and views become unreadable); tick "Read business data" to read records (off by default).
80
+ 5. To write, tick the matching capabilities and fill in "Default solution for changes" (the unique name of an **existing unmanaged** solution).
81
+
82
+ ## Where the configuration lives
83
+
84
+ The environment list and tool parameters live in `configFile`:
85
+
86
+ ```
87
+ <workspace>/d365.config.json # environment list, default environment, tool parameters, permissions
88
+ <workspace>/d365.secrets.json # holds only clientSecret and gitlabToken (plaintext)
89
+ ```
90
+
91
+ `configFile` is resolved in this order:
92
+
93
+ 1. `configFile` in `cordis.patch.yml` (an absolute path, machine-specific configuration from install time)
94
+ 2. The `D365_CONFIG_FILE` environment variable
95
+ 3. `<USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json` (when that directory exists)
96
+ 4. `<current working directory>\d365.config.json`
97
+
98
+ The repository's `cordis.patch.yml` carries no `configFile`, so the default falls through to item 3; when the workspace is elsewhere, set it with the environment variable or the patch.
99
+ `d365.secrets.json` always sits in the same directory as `configFile`.
100
+
101
+ **Do not commit these two files to Git**: the repository's `.gitignore` already covers them and `.d365/`.
102
+
103
+ ## Credential resolution order
104
+
105
+ Each value (`tenantId` / `clientId` / `clientSecret`) is taken in this order:
106
+
107
+ 1. The inline value in the environment configuration;
108
+ 2. The `clientSecret` the settings page writes into `d365.secrets.json` (an overlay);
109
+ 3. The environment variable name given by `<kind>Env` in that environment's configuration, e.g. `"clientSecretEnv": "D365_DEV_SECRET"`;
110
+ 4. The derived name `D365_<ENV>_<KIND>`, e.g. `D365_dev_CLIENT_SECRET` (the key as-is, hyphens included);
111
+ 5. The shared names `D365_TENANT_ID` / `D365_CLIENT_ID` / `D365_CLIENT_SECRET`.
112
+
113
+ Production is better served by environment variables; the settings page is enough for local debugging.
114
+
115
+ Tokens are cached in process memory and keyed by a fingerprint of "URL + tenant + ClientId + secret" — changed credentials are fetched immediately, with no restart.
116
+
117
+ ## Verification
118
+
119
+ ```
120
+ d365_envs check=true
121
+ d365_metadata resource=entities top=3
122
+ ```
123
+
124
+ If both return content, it works. The first `d365_sync` pulls the full metadata (about 125 MB, three minutes on a real environment); if you would rather not use the disk, skip it and query on demand with `d365_metadata`.
125
+
126
+ ## Upgrade
127
+
128
+ ```bash
129
+ # installed from Git
130
+ plugin_manager remove_bundle target=dsh-d365-kit
131
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
132
+ ```
133
+
134
+ After changing the version number you must **remove first, then install**: link/pnpm returns `already-installed` when it believes the package exists, and does not replace the code.
135
+
136
+ Restart DSH after upgrading (the settings page code has to re-enter the module graph).
137
+
138
+ ## Uninstall
139
+
140
+ ```bash
141
+ plugin_manager remove_bundle target=dsh-d365-kit
142
+ ```
143
+
144
+ Local data (`d365.config.json`, `d365.secrets.json`, `.d365/`) is not deleted; clean it up manually when needed.
145
+
146
+ ## Sharing with a team
147
+
148
+ - Everyone installs a copy and fills in their own `d365.config.json` and secrets;
149
+ - **Permissions are granted per machine**: ticks on someone else's machine do not affect yours, and vice versa;
150
+ - Reports and snapshots live in each person's workspace and are not shared;
151
+ - To give the whole team the same environment definitions, put the `environments` from `d365.config.json` into an internal document — each person still fills in their own secrets.