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/lib/snapshot.js CHANGED
@@ -1,149 +1,149 @@
1
- /**
2
- * Snapshot storage.
3
- *
4
- * A snapshot is a directory that becomes visible only when its `manifest.json`
5
- * exists: resource files are written first and the manifest last, so an
6
- * interrupted sync leaves an orphan directory that no reader ever picks up.
7
- *
8
- * The manifest is also what makes a snapshot trustworthy: it records, per
9
- * resource, whether the pull was complete, truncated by the row cap, or failed.
10
- * Without that, "the environment does not have this" and "we never fetched it"
11
- * would look identical in a diff.
12
- */
13
- import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
14
- import { join, resolve, sep } from 'node:path'
15
- import { dataDirectory } from './config.js'
16
-
17
- /** Root directory holding snapshots, diffs and query artifacts. */
18
- export function dataRoot(config) {
19
- return join(dataDirectory(config), '.d365')
20
- }
21
-
22
- export function snapshotsRoot(config) {
23
- return join(dataRoot(config), 'snapshots')
24
- }
25
-
26
- export function diffsRoot(config) {
27
- return join(dataRoot(config), 'diffs')
28
- }
29
-
30
- /** Sortable, filename-safe snapshot id: `20261102T071005Z`. */
31
- export function snapshotId(date = new Date()) {
32
- return date.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')
33
- }
34
-
35
- export function envSnapshotDir(config, envKey, id) {
36
- return join(snapshotsRoot(config), envKey, id)
37
- }
38
-
39
- /**
40
- * Ensure the data root exists and carries a `.gitignore`, so a metadata dump
41
- * of a customer org never gets committed by accident.
42
- */
43
- export async function ensureDataRoot(config) {
44
- const root = dataRoot(config)
45
- await mkdir(root, { recursive: true })
46
- await writeFile(
47
- join(root, '.gitignore'),
48
- '# D365 snapshots and query artifacts may contain customer metadata.\n*\n',
49
- { flag: 'wx' },
50
- ).catch(() => {})
51
- return root
52
- }
53
-
54
- /**
55
- * Write one snapshot: resource files first, manifest last.
56
- * @param files - array of `{ resource, payload }`.
57
- * @returns the snapshot directory.
58
- */
59
- export async function writeSnapshot(config, envKey, id, manifest, files) {
60
- const dir = envSnapshotDir(config, envKey, id)
61
- await mkdir(dir, { recursive: true })
62
- for (const file of files) {
63
- // Compact JSON: a 200k-row resource is ~35% smaller without indentation,
64
- // and nothing reads these files by eye.
65
- await writeFile(join(dir, `${file.resource}.json`), `${JSON.stringify(file.payload)}\n`, 'utf8')
66
- }
67
- await writeFile(join(dir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
68
- return dir
69
- }
70
-
71
- /**
72
- * List snapshots of one environment, newest first. Only directories with a
73
- * manifest count — the rest are interrupted writes.
74
- * @returns `[{ id, dir, manifest }]`.
75
- */
76
- export async function listSnapshots(config, envKey) {
77
- const dir = join(snapshotsRoot(config), envKey)
78
- let names
79
- try {
80
- names = await readdir(dir, { withFileTypes: true })
81
- } catch {
82
- return []
83
- }
84
-
85
- const snapshots = []
86
- for (const entry of names) {
87
- if (!entry.isDirectory()) continue
88
- const manifestPath = join(dir, entry.name, 'manifest.json')
89
- try {
90
- const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
91
- snapshots.push({ id: entry.name, dir: join(dir, entry.name), manifest })
92
- } catch {
93
- continue
94
- }
95
- }
96
- return snapshots.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0))
97
- }
98
-
99
- /** Read a manifest, or null when the snapshot does not exist. */
100
- export async function readManifest(config, envKey, id) {
101
- try {
102
- const text = await readFile(join(envSnapshotDir(config, envKey, id), 'manifest.json'), 'utf8')
103
- return JSON.parse(text)
104
- } catch {
105
- return null
106
- }
107
- }
108
-
109
- /** Read one resource's rows from a snapshot, or null when it was not synced. */
110
- export async function readResource(config, envKey, id, resource) {
111
- try {
112
- const text = await readFile(join(envSnapshotDir(config, envKey, id), `${resource}.json`), 'utf8')
113
- return JSON.parse(text)
114
- } catch {
115
- return null
116
- }
117
- }
118
-
119
- /**
120
- * Delete the oldest snapshots beyond `keep`, inside one environment only.
121
- * Refuses any path that resolves outside the environment's snapshot directory.
122
- * @returns the deleted ids.
123
- */
124
- export async function pruneSnapshots(config, envKey, keep) {
125
- if (!Number.isFinite(keep) || keep < 1) return []
126
- const snapshots = await listSnapshots(config, envKey)
127
- if (snapshots.length <= keep) return []
128
-
129
- const base = resolve(join(snapshotsRoot(config), envKey))
130
- const deleted = []
131
- for (const snapshot of snapshots.slice(keep)) {
132
- const target = resolve(snapshot.dir)
133
- if (target !== base && target.startsWith(base + sep)) {
134
- await rm(target, { recursive: true, force: true })
135
- deleted.push(snapshot.id)
136
- }
137
- }
138
- return deleted
139
- }
140
-
141
- /** Write a diff report and return its workspace-relative path. */
142
- export async function writeDiffReport(config, name, markdown) {
143
- const root = diffsRoot(config)
144
- await mkdir(root, { recursive: true })
145
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
146
- const file = join(root, `${stamp}-${name.replace(/[^a-zA-Z0-9._-]+/g, '-')}.md`)
147
- await writeFile(file, `${markdown}\n`, 'utf8')
148
- return file.replace(/\\/g, '/')
149
- }
1
+ /**
2
+ * Snapshot storage.
3
+ *
4
+ * A snapshot is a directory that becomes visible only when its `manifest.json`
5
+ * exists: resource files are written first and the manifest last, so an
6
+ * interrupted sync leaves an orphan directory that no reader ever picks up.
7
+ *
8
+ * The manifest is also what makes a snapshot trustworthy: it records, per
9
+ * resource, whether the pull was complete, truncated by the row cap, or failed.
10
+ * Without that, "the environment does not have this" and "we never fetched it"
11
+ * would look identical in a diff.
12
+ */
13
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
14
+ import { join, resolve, sep } from 'node:path'
15
+ import { dataDirectory } from './config.js'
16
+
17
+ /** Root directory holding snapshots, diffs and query artifacts. */
18
+ export function dataRoot(config) {
19
+ return join(dataDirectory(config), '.d365')
20
+ }
21
+
22
+ export function snapshotsRoot(config) {
23
+ return join(dataRoot(config), 'snapshots')
24
+ }
25
+
26
+ export function diffsRoot(config) {
27
+ return join(dataRoot(config), 'diffs')
28
+ }
29
+
30
+ /** Sortable, filename-safe snapshot id: `20261102T071005Z`. */
31
+ export function snapshotId(date = new Date()) {
32
+ return date.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')
33
+ }
34
+
35
+ export function envSnapshotDir(config, envKey, id) {
36
+ return join(snapshotsRoot(config), envKey, id)
37
+ }
38
+
39
+ /**
40
+ * Ensure the data root exists and carries a `.gitignore`, so a metadata dump
41
+ * of a customer org never gets committed by accident.
42
+ */
43
+ export async function ensureDataRoot(config) {
44
+ const root = dataRoot(config)
45
+ await mkdir(root, { recursive: true })
46
+ await writeFile(
47
+ join(root, '.gitignore'),
48
+ '# D365 snapshots and query artifacts may contain customer metadata.\n*\n',
49
+ { flag: 'wx' },
50
+ ).catch(() => {})
51
+ return root
52
+ }
53
+
54
+ /**
55
+ * Write one snapshot: resource files first, manifest last.
56
+ * @param files - array of `{ resource, payload }`.
57
+ * @returns the snapshot directory.
58
+ */
59
+ export async function writeSnapshot(config, envKey, id, manifest, files) {
60
+ const dir = envSnapshotDir(config, envKey, id)
61
+ await mkdir(dir, { recursive: true })
62
+ for (const file of files) {
63
+ // Compact JSON: a 200k-row resource is ~35% smaller without indentation,
64
+ // and nothing reads these files by eye.
65
+ await writeFile(join(dir, `${file.resource}.json`), `${JSON.stringify(file.payload)}\n`, 'utf8')
66
+ }
67
+ await writeFile(join(dir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
68
+ return dir
69
+ }
70
+
71
+ /**
72
+ * List snapshots of one environment, newest first. Only directories with a
73
+ * manifest count — the rest are interrupted writes.
74
+ * @returns `[{ id, dir, manifest }]`.
75
+ */
76
+ export async function listSnapshots(config, envKey) {
77
+ const dir = join(snapshotsRoot(config), envKey)
78
+ let names
79
+ try {
80
+ names = await readdir(dir, { withFileTypes: true })
81
+ } catch {
82
+ return []
83
+ }
84
+
85
+ const snapshots = []
86
+ for (const entry of names) {
87
+ if (!entry.isDirectory()) continue
88
+ const manifestPath = join(dir, entry.name, 'manifest.json')
89
+ try {
90
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
91
+ snapshots.push({ id: entry.name, dir: join(dir, entry.name), manifest })
92
+ } catch {
93
+ continue
94
+ }
95
+ }
96
+ return snapshots.sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : 0))
97
+ }
98
+
99
+ /** Read a manifest, or null when the snapshot does not exist. */
100
+ export async function readManifest(config, envKey, id) {
101
+ try {
102
+ const text = await readFile(join(envSnapshotDir(config, envKey, id), 'manifest.json'), 'utf8')
103
+ return JSON.parse(text)
104
+ } catch {
105
+ return null
106
+ }
107
+ }
108
+
109
+ /** Read one resource's rows from a snapshot, or null when it was not synced. */
110
+ export async function readResource(config, envKey, id, resource) {
111
+ try {
112
+ const text = await readFile(join(envSnapshotDir(config, envKey, id), `${resource}.json`), 'utf8')
113
+ return JSON.parse(text)
114
+ } catch {
115
+ return null
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Delete the oldest snapshots beyond `keep`, inside one environment only.
121
+ * Refuses any path that resolves outside the environment's snapshot directory.
122
+ * @returns the deleted ids.
123
+ */
124
+ export async function pruneSnapshots(config, envKey, keep) {
125
+ if (!Number.isFinite(keep) || keep < 1) return []
126
+ const snapshots = await listSnapshots(config, envKey)
127
+ if (snapshots.length <= keep) return []
128
+
129
+ const base = resolve(join(snapshotsRoot(config), envKey))
130
+ const deleted = []
131
+ for (const snapshot of snapshots.slice(keep)) {
132
+ const target = resolve(snapshot.dir)
133
+ if (target !== base && target.startsWith(base + sep)) {
134
+ await rm(target, { recursive: true, force: true })
135
+ deleted.push(snapshot.id)
136
+ }
137
+ }
138
+ return deleted
139
+ }
140
+
141
+ /** Write a diff report and return its workspace-relative path. */
142
+ export async function writeDiffReport(config, name, markdown) {
143
+ const root = diffsRoot(config)
144
+ await mkdir(root, { recursive: true })
145
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
146
+ const file = join(root, `${stamp}-${name.replace(/[^a-zA-Z0-9._-]+/g, '-')}.md`)
147
+ await writeFile(file, `${markdown}\n`, 'utf8')
148
+ return file.replace(/\\/g, '/')
149
+ }
@@ -1,133 +1,133 @@
1
- /**
2
- * `d365_code_check` — what does this merge request break in a target environment?
3
- *
4
- * Only the files the MR touches are analyzed, and only their added/removed
5
- * lines: the question is "will this change run elsewhere", not "is this
6
- * repository clean".
7
- */
8
- import { loadConfig } from './config.js'
9
- import { runCodeCheck } from './code-check.js'
10
-
11
- export const definition = {
12
- name: 'd365_code_check',
13
- description:
14
- '检查一次 GitLab MR 的改动在目标环境会不会跑不起来:JS/TS 映射到 Web 资源与表单,核对改动行读取的字段/实体是否还在表单上;'
15
- + 'C# 映射到插件程序集,核对程序集版本、插件类型、步骤、step 镜像与过滤属性。无法静态判定的进人工确认清单。只读。',
16
- parameters: {
17
- type: 'object',
18
- additionalProperties: false,
19
- required: ['mergeRequest'],
20
- properties: {
21
- mergeRequest: {
22
- type: 'string',
23
- description: 'MR 链接(https://gitlab…/group/project/-/merge_requests/42),或 IID(42 / !42)。',
24
- },
25
- project: {
26
- type: 'string',
27
- description: 'GitLab 项目路径 group/project;省略时用 MR 链接里的项目,再省略用设置页的默认项目。',
28
- },
29
- targets: {
30
- type: 'array',
31
- items: { type: 'string' },
32
- description: '要对照检查的环境 key 列表;省略则全部环境。',
33
- },
34
- },
35
- },
36
- output: {
37
- schema: {
38
- type: 'object',
39
- additionalProperties: false,
40
- required: ['text'],
41
- properties: {
42
- text: { type: 'string' },
43
- },
44
- },
45
- render(_args, value) {
46
- return [{ type: 'text', text: value?.text ?? '' }]
47
- },
48
- },
49
- }
50
-
51
- function renderReport(report, reportPath) {
52
- const lines = []
53
- lines.push(`代码改动检查:MR !${report.iid}${report.title ? ` · ${report.title}` : ''}`)
54
- lines.push(`项目:${report.project}`)
55
- if (report.sourceBranch) lines.push(`分支:${report.sourceBranch} → ${report.targetBranch}`)
56
- lines.push(`目标环境:${report.targets.join('、')}`)
57
- lines.push('')
58
-
59
- lines.push('| 改动文件 | 类型 | 增/删行 | 检查到的引用 |')
60
- lines.push('| --- | --- | --- | --- |')
61
- for (const file of report.files) {
62
- const refs = file.references
63
- ? `字段 ${file.references.attributes.length} · 实体 ${file.references.entities.length} · GUID ${file.references.guids.length}`
64
- : file.facts
65
- ? `类 ${file.facts.classes.length} · 镜像 ${file.facts.images.length} · 字面量 ${file.facts.literals.length}`
66
- : '—'
67
- lines.push(`| ${file.path} | ${file.kind} | +${file.addedLines ?? 0}/-${file.removedLines ?? 0} | ${refs} |`)
68
- }
69
-
70
- for (const file of report.files) {
71
- if (!file.targets?.length) continue
72
- lines.push('')
73
- lines.push(`### ${file.path}`)
74
- for (const target of file.targets) {
75
- if (file.kind === 'csharp') {
76
- const types = target.types?.length ?? 0
77
- const steps = (target.types ?? []).reduce((sum, item) => sum + (item.steps?.length ?? 0), 0)
78
- const mapped = target.assembly
79
- ? `程序集 ${target.assembly}(${target.version ?? '版本未知'}${target.certain ? '' : ',推断待确认'}),类型 ${types},步骤 ${steps}`
80
- : '未匹配到程序集'
81
- lines.push(`- ${target.target}:${mapped}`)
82
- continue
83
- }
84
- const forms = target.forms?.length ?? 0
85
- const mapped = target.resourceName
86
- ? `→ ${target.resourceName}${target.certain ? '' : '(推断,待确认)'},${forms} 张表单引用`
87
- : '→ 未找到对应 Web 资源'
88
- lines.push(`- ${target.target}:${mapped}`)
89
- }
90
- }
91
-
92
- const blockers = report.findings.filter((item) => item.severity === 'blocker')
93
- const warnings = report.findings.filter((item) => item.severity === 'warning')
94
-
95
- lines.push('')
96
- lines.push(`### 阻断项 ${blockers.length}`)
97
- if (!blockers.length) lines.push('没有发现必然失败的改动。')
98
- for (const item of blockers) {
99
- lines.push(`- [${item.target}] ${item.file}${item.line ? `:${item.line}` : ''} — ${item.message}`)
100
- }
101
-
102
- lines.push('')
103
- lines.push(`### 警告 ${warnings.length}`)
104
- if (!warnings.length) lines.push('没有警告。')
105
- for (const item of warnings) {
106
- lines.push(`- [${item.target}] ${item.file}${item.line ? `:${item.line}` : ''} — ${item.message}`)
107
- }
108
-
109
- lines.push('')
110
- lines.push(`### 人工确认 ${report.human.length}`)
111
- if (!report.human.length) lines.push('没有需要人工确认的条目。')
112
- for (const item of report.human) {
113
- lines.push(`- ${item.id} ${item.object}:${item.question}`)
114
- lines.push(` 证据:${item.evidence}`)
115
- lines.push(` 确认:${item.howToCheck}`)
116
- lines.push(` 影响:${item.impact}`)
117
- }
118
-
119
- lines.push('')
120
- lines.push(`报告:${reportPath}`)
121
- return lines.join('\n')
122
- }
123
-
124
- export function register(ctx, state) {
125
- return ctx.tools.register({
126
- ...definition,
127
- async execute(args, exec) {
128
- const config = await loadConfig(state.rawConfig)
129
- const { report, reportPath } = await runCodeCheck(config, args ?? {}, exec?.signal)
130
- return { text: renderReport(report, reportPath) }
131
- },
132
- })
133
- }
1
+ /**
2
+ * `d365_code_check` — what does this merge request break in a target environment?
3
+ *
4
+ * Only the files the MR touches are analyzed, and only their added/removed
5
+ * lines: the question is "will this change run elsewhere", not "is this
6
+ * repository clean".
7
+ */
8
+ import { loadConfig } from './config.js'
9
+ import { runCodeCheck } from './code-check.js'
10
+
11
+ export const definition = {
12
+ name: 'd365_code_check',
13
+ description:
14
+ '检查一次 GitLab MR 的改动在目标环境会不会跑不起来:JS/TS 映射到 Web 资源与表单,核对改动行读取的字段/实体是否还在表单上;'
15
+ + 'C# 映射到插件程序集,核对程序集版本、插件类型、步骤、step 镜像与过滤属性。无法静态判定的进人工确认清单。只读。',
16
+ parameters: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ required: ['mergeRequest'],
20
+ properties: {
21
+ mergeRequest: {
22
+ type: 'string',
23
+ description: 'MR 链接(https://gitlab…/group/project/-/merge_requests/42),或 IID(42 / !42)。',
24
+ },
25
+ project: {
26
+ type: 'string',
27
+ description: 'GitLab 项目路径 group/project;省略时用 MR 链接里的项目,再省略用设置页的默认项目。',
28
+ },
29
+ targets: {
30
+ type: 'array',
31
+ items: { type: 'string' },
32
+ description: '要对照检查的环境 key 列表;省略则全部环境。',
33
+ },
34
+ },
35
+ },
36
+ output: {
37
+ schema: {
38
+ type: 'object',
39
+ additionalProperties: false,
40
+ required: ['text'],
41
+ properties: {
42
+ text: { type: 'string' },
43
+ },
44
+ },
45
+ render(_args, value) {
46
+ return [{ type: 'text', text: value?.text ?? '' }]
47
+ },
48
+ },
49
+ }
50
+
51
+ function renderReport(report, reportPath) {
52
+ const lines = []
53
+ lines.push(`代码改动检查:MR !${report.iid}${report.title ? ` · ${report.title}` : ''}`)
54
+ lines.push(`项目:${report.project}`)
55
+ if (report.sourceBranch) lines.push(`分支:${report.sourceBranch} → ${report.targetBranch}`)
56
+ lines.push(`目标环境:${report.targets.join('、')}`)
57
+ lines.push('')
58
+
59
+ lines.push('| 改动文件 | 类型 | 增/删行 | 检查到的引用 |')
60
+ lines.push('| --- | --- | --- | --- |')
61
+ for (const file of report.files) {
62
+ const refs = file.references
63
+ ? `字段 ${file.references.attributes.length} · 实体 ${file.references.entities.length} · GUID ${file.references.guids.length}`
64
+ : file.facts
65
+ ? `类 ${file.facts.classes.length} · 镜像 ${file.facts.images.length} · 字面量 ${file.facts.literals.length}`
66
+ : '—'
67
+ lines.push(`| ${file.path} | ${file.kind} | +${file.addedLines ?? 0}/-${file.removedLines ?? 0} | ${refs} |`)
68
+ }
69
+
70
+ for (const file of report.files) {
71
+ if (!file.targets?.length) continue
72
+ lines.push('')
73
+ lines.push(`### ${file.path}`)
74
+ for (const target of file.targets) {
75
+ if (file.kind === 'csharp') {
76
+ const types = target.types?.length ?? 0
77
+ const steps = (target.types ?? []).reduce((sum, item) => sum + (item.steps?.length ?? 0), 0)
78
+ const mapped = target.assembly
79
+ ? `程序集 ${target.assembly}(${target.version ?? '版本未知'}${target.certain ? '' : ',推断待确认'}),类型 ${types},步骤 ${steps}`
80
+ : '未匹配到程序集'
81
+ lines.push(`- ${target.target}:${mapped}`)
82
+ continue
83
+ }
84
+ const forms = target.forms?.length ?? 0
85
+ const mapped = target.resourceName
86
+ ? `→ ${target.resourceName}${target.certain ? '' : '(推断,待确认)'},${forms} 张表单引用`
87
+ : '→ 未找到对应 Web 资源'
88
+ lines.push(`- ${target.target}:${mapped}`)
89
+ }
90
+ }
91
+
92
+ const blockers = report.findings.filter((item) => item.severity === 'blocker')
93
+ const warnings = report.findings.filter((item) => item.severity === 'warning')
94
+
95
+ lines.push('')
96
+ lines.push(`### 阻断项 ${blockers.length}`)
97
+ if (!blockers.length) lines.push('没有发现必然失败的改动。')
98
+ for (const item of blockers) {
99
+ lines.push(`- [${item.target}] ${item.file}${item.line ? `:${item.line}` : ''} — ${item.message}`)
100
+ }
101
+
102
+ lines.push('')
103
+ lines.push(`### 警告 ${warnings.length}`)
104
+ if (!warnings.length) lines.push('没有警告。')
105
+ for (const item of warnings) {
106
+ lines.push(`- [${item.target}] ${item.file}${item.line ? `:${item.line}` : ''} — ${item.message}`)
107
+ }
108
+
109
+ lines.push('')
110
+ lines.push(`### 人工确认 ${report.human.length}`)
111
+ if (!report.human.length) lines.push('没有需要人工确认的条目。')
112
+ for (const item of report.human) {
113
+ lines.push(`- ${item.id} ${item.object}:${item.question}`)
114
+ lines.push(` 证据:${item.evidence}`)
115
+ lines.push(` 确认:${item.howToCheck}`)
116
+ lines.push(` 影响:${item.impact}`)
117
+ }
118
+
119
+ lines.push('')
120
+ lines.push(`报告:${reportPath}`)
121
+ return lines.join('\n')
122
+ }
123
+
124
+ export function register(ctx, state) {
125
+ return ctx.tools.register({
126
+ ...definition,
127
+ async execute(args, exec) {
128
+ const config = await loadConfig(state.rawConfig)
129
+ const { report, reportPath } = await runCodeCheck(config, args ?? {}, exec?.signal)
130
+ return { text: renderReport(report, reportPath) }
131
+ },
132
+ })
133
+ }