dsh-plugin-compat-check 0.1.6 → 0.1.7

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 (46) hide show
  1. package/README.md +17 -30
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/index.js +1295 -550
  4. package/kernel/lib/api-semantics.mjs +223 -0
  5. package/kernel/lib/author-ci-budget.mjs +25 -0
  6. package/kernel/lib/author-ci-output.mjs +22 -0
  7. package/kernel/lib/author-ci-policy.mjs +6 -1
  8. package/kernel/lib/baseline.mjs +109 -38
  9. package/kernel/lib/catalog.mjs +11 -3
  10. package/kernel/lib/contracts.mjs +16 -1
  11. package/kernel/lib/execution-environment.mjs +52 -0
  12. package/kernel/lib/extract.mjs +57 -13
  13. package/kernel/lib/finding-guidance.mjs +6 -0
  14. package/kernel/lib/finding-review-store.mjs +11 -23
  15. package/kernel/lib/issue-report.mjs +2 -1
  16. package/kernel/lib/patch-mounts.mjs +232 -0
  17. package/kernel/lib/report-identity-worker.mjs +47 -26
  18. package/kernel/lib/report-identity.mjs +4 -3
  19. package/kernel/lib/report-service.mjs +118 -38
  20. package/kernel/lib/report.mjs +13 -17
  21. package/kernel/lib/source-analysis.mjs +258 -42
  22. package/kernel/lib/source-graph.mjs +353 -75
  23. package/kernel/lib/state-lock.mjs +206 -0
  24. package/kernel/lib/state-store.mjs +41 -30
  25. package/kernel/lib/static-input.mjs +111 -0
  26. package/kernel/lib/syntax.mjs +242 -20
  27. package/kernel/lib/upgrade-contract.mjs +1 -0
  28. package/kernel/lib/upgrade-input.mjs +9 -4
  29. package/kernel/lib/upgrade-preview.mjs +11 -15
  30. package/kernel/lib/upgrade-worker.mjs +10 -1
  31. package/kernel/lib/verdict.mjs +28 -5
  32. package/kernel/lib/version.mjs +2 -34
  33. package/kernel/lib/worker-analysis.mjs +74 -3
  34. package/kernel/rules/README.md +30 -208
  35. package/kernel/rules/dsh-0.2.1-rc.3-review.json +63 -0
  36. package/kernel/rules/dsh-api-catalog-0.2.1-rc.3.json +743 -0
  37. package/kernel/rules/severity.json +77 -0
  38. package/lib/client.js +4668 -3423
  39. package/lib/finding-guidance.js +6 -0
  40. package/lib/local-request.js +44 -0
  41. package/lib/report-contract.js +16 -1
  42. package/lib/upgrade-contract.js +1 -0
  43. package/package.json +3 -2
  44. package/scripts/prepublish-check.mjs +1 -22
  45. package/scripts/stage-contract.mjs +7 -0
  46. package/skill/SKILL.md +13 -12
@@ -2,6 +2,7 @@
2
2
  export const SUPPORTED_PREVIEW_VERSIONS = Object.freeze([
3
3
  "0.2.0-rc.2",
4
4
  "0.2.1-rc.2",
5
+ "0.2.1-rc.3",
5
6
  ]);
6
7
  const record = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
7
8
  const text = (x) => typeof x === "string" && x.length > 0;
@@ -4,6 +4,7 @@ import os from "node:os";
4
4
  import { runWorkerTask } from "./worker-analysis.mjs";
5
5
  import path from "node:path";
6
6
  import { createHash } from "node:crypto";
7
+ import { readStaticDirectory } from "./static-input.mjs";
7
8
 
8
9
  export const sha256 = (bytes) =>
9
10
  createHash("sha256").update(bytes).digest("hex");
@@ -52,7 +53,7 @@ export function captureUpgradeInputOwned(nm, overrides = {}, root) {
52
53
  const before = fs.lstatSync(dir);
53
54
  if (!before.isDirectory() || fs.realpathSync(dir) !== dir)
54
55
  throw fail("目录身份变化");
55
- const names = fs.readdirSync(dir).sort();
56
+ const names = readStaticDirectory(dir, limits.entries - entries, check);
56
57
  entries += names.length;
57
58
  check();
58
59
  records.push(["directory", dir, stamp(before), names]);
@@ -61,7 +62,7 @@ export function captureUpgradeInputOwned(nm, overrides = {}, root) {
61
62
  const verifyDir = (dir, { names, before }) => {
62
63
  if (
63
64
  !same(stamp(before), stamp(fs.lstatSync(dir))) ||
64
- !same(names, fs.readdirSync(dir).sort()) ||
65
+ !same(names, readStaticDirectory(dir, names.length, check)) ||
65
66
  fs.realpathSync(dir) !== dir
66
67
  )
67
68
  throw fail("目录在读取时变化");
@@ -250,8 +251,12 @@ export function captureUpgradeInputOwned(nm, overrides = {}, root) {
250
251
  if (first.signature !== second.signature)
251
252
  throw fail("两次观察的目录、文件字节或链接身份不一致");
252
253
  const freeze = (dir) => {
253
- for (const e of fs.readdirSync(dir, { withFileTypes: true }))
254
- if (e.isDirectory()) freeze(path.join(dir, e.name));
254
+ for (const name of readStaticDirectory(dir, limits.entries, () => {
255
+ if (Date.now() - started > limits.timeMs) throw fail("捕获预算超限");
256
+ })) {
257
+ const child = path.join(dir, name);
258
+ if (fs.lstatSync(child).isDirectory()) freeze(child);
259
+ }
255
260
  fs.chmodSync(dir, 0o500);
256
261
  };
257
262
  freeze(root);
@@ -1,16 +1,11 @@
1
1
  /** Offline version prediction: no installation, runtime probe, cache or baseline. */
2
- import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
4
  import { loadCatalog } from "./catalog.mjs";
6
5
  import { TOOL_VERSION } from "./version.mjs";
7
- import {
8
- captureUpgradeInput,
9
- sha256,
10
- upgradeCleanupError,
11
- } from "./upgrade-input.mjs";
6
+ import { captureUpgradeInput, upgradeCleanupError } from "./upgrade-input.mjs";
12
7
  import { runWorkerTask } from "./worker-analysis.mjs";
13
- import { assertReportBody } from "./contracts.mjs";
8
+ import { assertReportBody, terminalText } from "./contracts.mjs";
14
9
  import {
15
10
  SUPPORTED_PREVIEW_VERSIONS,
16
11
  assertUpgradePreview,
@@ -114,13 +109,10 @@ export async function runUpgradePreview(input = {}, options = {}) {
114
109
  };
115
110
  const current = reviewedCatalogFor(currentVersion),
116
111
  target = reviewedCatalogFor(targetVersion);
117
- const rulesFile = path.join(ROOT, "rules", "severity.json"),
118
- rulesBytes = fs.readFileSync(rulesFile);
119
- const rules = JSON.parse(rulesBytes);
120
112
  if (typeof input.nm !== "string" || !input.nm)
121
113
  throw new Error("升级预检需要 node_modules 或 profile 路径");
122
114
  snapshot = await captureUpgradeInput(input.nm, captureLimits, { signal });
123
- const reports = await runWorkerTask(
115
+ const { rulesIdentity, ...reports } = await runWorkerTask(
124
116
  new URL("./upgrade-worker.mjs", import.meta.url),
125
117
  {
126
118
  nm: snapshot.nm,
@@ -135,7 +127,7 @@ export async function runUpgradePreview(input = {}, options = {}) {
135
127
  catalog: target.catalog,
136
128
  evidence: target.evidence,
137
129
  },
138
- rules,
130
+ rulesFile: path.join(ROOT, "rules", "severity.json"),
139
131
  },
140
132
  { timeoutMs, signal },
141
133
  );
@@ -175,7 +167,7 @@ export async function runUpgradePreview(input = {}, options = {}) {
175
167
  supportedVersions: [...SUPPORTED_PREVIEW_VERSIONS],
176
168
  catalogs: { current: current.identity, target: target.identity },
177
169
  artifact: snapshot.artifact,
178
- rules: { file: "rules/severity.json", sha256: sha256(rulesBytes) },
170
+ rules: rulesIdentity,
179
171
  ...reports,
180
172
  changes,
181
173
  summary: {
@@ -213,7 +205,9 @@ export function renderUpgradePreview(result) {
213
205
  ...lines,
214
206
  `无法判定:${result.error}`,
215
207
  ...result.limitations.map((x) => `限制:${x}`),
216
- ].join("\n");
208
+ ]
209
+ .map(terminalText)
210
+ .join("\n");
217
211
  lines.push(
218
212
  `新增 ${result.summary.added};变重 ${result.summary.worsened};消失 ${result.summary.resolved};减轻 ${result.summary.improved};不变 ${result.summary.unchanged}`,
219
213
  );
@@ -252,5 +246,7 @@ export function renderUpgradePreview(result) {
252
246
  for (const note of p.coverage.limitations)
253
247
  lines.push(` 覆盖限制:${note}`);
254
248
  }
255
- return [...lines, ...result.limitations.map((x) => `限制:${x}`)].join("\n");
249
+ return [...lines, ...result.limitations.map((x) => `限制:${x}`)]
250
+ .map(terminalText)
251
+ .join("\n");
256
252
  }
@@ -4,8 +4,13 @@ import { buildVerdicts } from "./verdict.mjs";
4
4
  import { sealReportBody } from "./contracts.mjs";
5
5
  import { TOOL_VERSION } from "./version.mjs";
6
6
 
7
+ import { readStaticFile } from "./static-input.mjs";
8
+ import { sha256 } from "./upgrade-input.mjs";
9
+
7
10
  try {
8
- const { nm, current, target, rules, artifact } = workerData;
11
+ const { nm, current, target, rulesFile, artifact } = workerData;
12
+ const rulesBytes = readStaticFile(rulesFile, 8 * 1024 * 1024);
13
+ const rules = JSON.parse(rulesBytes);
9
14
  const catalog = {
10
15
  host: { ...current.catalog.host, ...target.catalog.host },
11
16
  client: { ...current.catalog.client, ...target.catalog.client },
@@ -41,6 +46,10 @@ try {
41
46
  parentPort.postMessage({
42
47
  ok: true,
43
48
  value: {
49
+ rulesIdentity: {
50
+ file: "rules/severity.json",
51
+ sha256: sha256(rulesBytes),
52
+ },
44
53
  currentReport: reportFor(current),
45
54
  targetReport: reportFor(target),
46
55
  },
@@ -1,9 +1,12 @@
1
1
  /** Pure evidence-driven compatibility verdicts. Unknown is not absence. */
2
2
  import { normalizeObservation } from "./observation.mjs";
3
3
  import { declaredSupport } from "./declarations.mjs";
4
+ import { checkApiSemantics } from "./api-semantics.mjs";
4
5
  export const IMPACT_BASIS = { MEASURED: "measured", INFERRED: "inferred" };
5
6
  export const CERTAINTY = { CERTAIN: "certain", SUSPECTED: "suspected" };
6
7
  export const CATEGORY = {
8
+ API_ARGUMENT_INVALID: "api-argument-invalid",
9
+ API_OPTION_UNSUPPORTED: "api-option-unsupported",
7
10
  HOST_SERVICE_MISSING: "host-service-missing",
8
11
  HOST_METHOD_MISSING: "host-method-missing",
9
12
  UNPROVIDED_DEPENDENCY: "unprovided-dependency",
@@ -190,7 +193,10 @@ export function buildVerdicts({
190
193
  else if (row.state !== "ACTIVE")
191
194
  limitations.push(`条目 ${row.id ?? name} 尚未稳定:${row.state}`);
192
195
  }
193
- if (p.clientHalf?.declared && p.clientHalf.ok === false)
196
+ if (p.clientHalf?.declared && p.clientHalf.ok === false) {
197
+ // Invalid declarations have no graph root to emit a source diagnostic.
198
+ if (p.clientHalf.declaredEntry == null)
199
+ limitations.push("客户端入口声明无法解析,无法确认客户端调用覆盖");
194
200
  add({
195
201
  category: CATEGORY.CLIENT_ENTRY_MISSING,
196
202
  certainty: "certain",
@@ -204,6 +210,7 @@ export function buildVerdicts({
204
210
  ruleSource: "static",
205
211
  ruleRef: "plugins.client.half",
206
212
  });
213
+ }
207
214
  const self = new Set(p.providedServices || []);
208
215
  for (const svc of p.services || []) {
209
216
  const allSites = (p.callSites || []).filter((s) => s.service === svc);
@@ -263,6 +270,7 @@ export function buildVerdicts({
263
270
  }
264
271
  if (
265
272
  (self.has(svc) || providers.has(svc)) &&
273
+ observed.status !== "absent" &&
266
274
  !(observed.status === "present" && observed.complete)
267
275
  ) {
268
276
  limitations.push(`${svc} 仅有提供声明,没有完整的运行时方法证据`);
@@ -432,6 +440,16 @@ export function buildVerdicts({
432
440
  (s) => methods.includes(s.method) || protectedCall(s),
433
441
  ).length;
434
442
  }
443
+ const semantic = checkApiSemantics(p, rules, dshVersion);
444
+ limitations.push(...semantic.limitations);
445
+ const semanticFindings = new Set(semantic.findings);
446
+ for (const finding of semantic.findings) {
447
+ add(finding);
448
+ account(finding.service, "finding", {
449
+ method: finding.method,
450
+ reason: "客户端静态参数契约风险;未实测业务影响",
451
+ });
452
+ }
435
453
  const declaration = declarationOf(p, dshVersion, {
436
454
  exemptions: compatibilityExemptions,
437
455
  });
@@ -494,8 +512,13 @@ export function buildVerdicts({
494
512
  : "package.peerDependencies",
495
513
  });
496
514
  }
497
- if (declaration?.claimsCompatible && findings.length) {
498
- const certain = findings.every((f) => f.certainty === "certain");
515
+ const declarationConflicts = findings.filter(
516
+ (f) => !semanticFindings.has(f),
517
+ );
518
+ if (declaration?.claimsCompatible && declarationConflicts.length) {
519
+ const certain = declarationConflicts.every(
520
+ (f) => f.certainty === "certain",
521
+ );
499
522
  add({
500
523
  category: CATEGORY.DECLARATION_MISMATCH,
501
524
  certainty: certain ? "certain" : "suspected",
@@ -504,13 +527,13 @@ export function buildVerdicts({
504
527
  method: null,
505
528
  location: null,
506
529
  severity: "feature-dead",
507
- impact: `声明兼容 DSH ${dshVersion},但发现 ${findings.length} 项${certain ? "有证据的冲突" : "待核实风险"}`,
530
+ impact: `声明兼容 DSH ${dshVersion},但发现 ${declarationConflicts.length} 项${certain ? "有证据的冲突" : "待核实风险"}`,
508
531
  basis: certain
509
532
  ? "源码/运行时证据与兼容声明冲突"
510
533
  : "原始发现含疑似证据,不能称为实测不符",
511
534
  evidence: {
512
535
  declared: declaration.raw,
513
- measuredFindings: findings.length,
536
+ measuredFindings: declarationConflicts.length,
514
537
  },
515
538
  ruleSource: "declaration",
516
539
  ruleRef: null,
@@ -1,37 +1,5 @@
1
- /**
2
- * 工具版本 —— **单一真源**。
3
- *
4
- * ## 为什么必须有这个文件
5
- *
6
- * docs/Plan.md §1 约定的是「单仓库、**单版本**、一份 Node 内核、三个入口」。
7
- * 而实测下来这条承诺**从来没兑现过**:
8
- *
9
- * | 产物 | 版本 |
10
- * |---|---|
11
- * | `plugin/package.json` | `0.0.1` |
12
- * | CLI(`bin/compat-check.mjs`) | **没有** |
13
- * | `dsh-plugin-compat-check.sh` | **没有** |
14
- * | 报告 JSON | **没有** |
15
- *
16
- * 后果不是"不好看",而是**结论无法追溯**:用户拿着一条「`dsh-keep-awake` 部分可用」
17
- * 来问"凭什么",我第一句必须问"你用的是哪一版工具" —— 而那个问题当时**无解**。
18
- * 对同一个插件,规则表覆盖变了、提取器修了假阳性之后结论都会变。
19
- *
20
- * ## 为什么用 `.mjs` 而不是读 `package.json`
21
- *
22
- * 读者分四种形态存在:仓库检出、npm 装好的包(内核在 `kernel/`)、
23
- * `--probe-boot` 起的临时 profile、以及 `.sh` 编排脚本。
24
- * 它们的相对路径各不相同,**读 package.json 会在某一种形态下失败**。
25
- * 一个常量没有这个问题,而且在打包时会被 `prepack` 一并带进包里。
26
- *
27
- * `plugin/package.json` 的 `version` 仍是 npm 需要的**字面量** ——
28
- * 那一处由 `test/version.test.mjs` 钉住,对不上就报红并告诉你改哪里。
29
- */
30
-
31
- /** 语义化版本。改动它会同时影响三个入口的 `--version` 与报告 JSON 的 `toolVersion`。 */
32
- export const TOOL_VERSION = '0.1.6';
33
-
34
- /** 人类可读的一行,给 `--version` 用 */
1
+ /** Shared version source; package manifests are checked against this literal. */
2
+ export const TOOL_VERSION = '0.1.7';
35
3
  export function versionLine() {
36
4
  return `dsh-plugin-compat-check ${TOOL_VERSION}`;
37
5
  }
@@ -1,3 +1,69 @@
1
+ // Shared by all ordinary analysis/identity jobs imported by this kernel instance.
2
+ const MAX_WORKERS = 2,
3
+ MAX_PENDING = 32;
4
+ let activeWorkers = 0;
5
+ const pendingWorkers = [];
6
+ const workerError = (code, message) =>
7
+ Object.assign(new Error(message), { code });
8
+ export function runReportWorkerTask(url, data, options = {}) {
9
+ const { signal, timeoutMs = 10000 } = options;
10
+ if (
11
+ signal &&
12
+ (typeof signal.aborted !== "boolean" ||
13
+ typeof signal.addEventListener !== "function" ||
14
+ typeof signal.removeEventListener !== "function")
15
+ )
16
+ return Promise.reject(new Error("Invalid worker signal"));
17
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647)
18
+ return Promise.reject(new Error("Invalid worker resource budget / 预算"));
19
+ if (signal?.aborted)
20
+ return Promise.reject(workerError("DSC_WORKER_ABORTED", "静态分析已取消"));
21
+ if (activeWorkers >= MAX_WORKERS && pendingWorkers.length >= MAX_PENDING)
22
+ return Promise.reject(
23
+ workerError("DSC_WORKER_CAPACITY", "工作线程排队容量已满,请稍后重试"),
24
+ );
25
+ return new Promise((resolve, reject) => {
26
+ let queued = false,
27
+ timer;
28
+ const cleanup = () => {
29
+ clearTimeout(timer);
30
+ signal?.removeEventListener("abort", cancel);
31
+ };
32
+ const remove = () => {
33
+ if (!queued) return false;
34
+ queued = false;
35
+ pendingWorkers.splice(pendingWorkers.indexOf(start), 1);
36
+ cleanup();
37
+ return true;
38
+ };
39
+ const cancel = () => {
40
+ if (remove())
41
+ reject(workerError("DSC_WORKER_ABORTED", "静态分析排队已取消"));
42
+ };
43
+ const start = () => {
44
+ queued = false;
45
+ cleanup();
46
+ activeWorkers++;
47
+ runWorkerTask(url, data, options)
48
+ .then(resolve, reject)
49
+ .finally(() => {
50
+ activeWorkers--;
51
+ pendingWorkers.shift()?.();
52
+ });
53
+ };
54
+ if (activeWorkers < MAX_WORKERS) start();
55
+ else {
56
+ queued = true;
57
+ pendingWorkers.push(start);
58
+ signal?.addEventListener("abort", cancel, { once: true });
59
+ timer = setTimeout(() => {
60
+ if (remove())
61
+ reject(workerError("DSC_WORKER_TIMEOUT", "静态分析排队超过时间预算"));
62
+ }, timeoutMs);
63
+ if (signal?.aborted) cancel();
64
+ }
65
+ });
66
+ }
1
67
  import { Worker } from "node:worker_threads";
2
68
 
3
69
  /** Supervise trusted jobs; settle only after reclaiming their worker thread. */
@@ -88,10 +154,15 @@ export function runWorkerTask(
88
154
  });
89
155
  }
90
156
 
91
- export function extractInWorker(nm, options = {}, timeoutMs = 10000) {
92
- return runWorkerTask(
157
+ export function extractInWorker(
158
+ nm,
159
+ options = {},
160
+ timeoutMs = 10000,
161
+ { signal } = {},
162
+ ) {
163
+ return runReportWorkerTask(
93
164
  new URL("./extract-worker.mjs", import.meta.url),
94
165
  { nm, options },
95
- { timeoutMs },
166
+ { timeoutMs, signal },
96
167
  );
97
168
  }
@@ -1,226 +1,48 @@
1
- # 严重度规则表与维护
1
+ # 规则与固定源码参考
2
2
 
3
- 适用于0.1.6;公开版本以registry和Plan发布记录为准。当前契约见 [Plan](../docs/Plan.md),验证见 [自动统计](../docs/current-state.json)。后半保留2026-10-11旧提取器的依据,历史表不是当前覆盖率或运行时真值。
3
+ 本文维护**0.1.7**的规则。契约见[Plan](../docs/Plan.md),维护见[流程](../docs/maintaining.md),限定证据/实际发布状态见[current-state](../docs/current-state.json)。
4
4
 
5
- ## 文件与运行时消费者
5
+ ## 文件与消费者
6
6
 
7
7
  | 文件 | 用途 |
8
8
  |---|---|
9
- | severity.json | 方法严重度/影响文案;引擎读取services下的方法规则,并与代码内默认档合用 |
10
- | dsh-api-catalog.json / dsh-api-catalog-0.2.1-rc.2.json | 原0.2.0-rc.2(host91/client8)与新rc.2(host98/client8)契约快照;普通体检对DSH≥0.2.1-rc.2选新目录,其余/未知选旧目录,CLI显式覆盖优先;升级预检只接受这两个精确版本并拒绝未知版本,未来环境未认证 |
11
- | usage-inventory.json | 带日期/版本/profile及完整性标记的AST库存快照;由coverage显式刷新,不保证与后续提取器重扫同数 |
9
+ | severity.json | services下的方法严重度/影响与端侧约束;apiSemantics下的精确客户端参数、弃用和生命周期参考 |
10
+ | dsh-api-catalog.json | 固定0.2.0-rc.2目录 |
11
+ | dsh-api-catalog-0.2.1-rc.2.json / dsh-api-catalog-0.2.1-rc.3.json | 两个独立精确版本目录;不能因方法名相同而互相认证 |
12
+ | dsh-0.2.1-rc.3-review.json | 固定来源、摘要、签名与语义审查 |
13
+ | usage-inventory.json | 带日期、版本、profile和完整性标记的历史AST库存,不是当前覆盖率 |
12
14
 
13
- 严重度由 `lib/verdict.mjs` 的确定性逻辑、缺失证据、方法规则及默认档共同决定,不由LLM决定。方法规则的severity/impact参与判定与输出,layer控制端别:方法自身优先、服务layer回退。summary/basis/evidence/knownGoneIn/needsReview等用于解释与维护;knownExtractionArtifacts是历史反例登记,不是自动过滤黑名单。
15
+ 普通检查对DSH≥0.2.1-rc.3选rc.3参考,≥0.2.1-rc.2选rc.2,其余/未知选旧目录,显式覆盖优先。登记字节与认证集合以`lib/contracts.mjs.CATALOG_REFERENCES`为准;升级预检接受三个登记精确版本,拒绝未知/范围。回退和自定义目录不认证未来宿主。
14
16
 
15
- 0.1.3按DSH目标分流声明:从0.2.1-rc.1起自有engines.dsh优先,缺失才回退全部官方peer;旧目标保留旧行为。非法/空范围、预发布和peer workspace别名均有回归;engines中的workspace不获放行。独立compatibility.json仅按插件/运行时精确版本豁免,非法字段不能靠豁免绕过;无效记录忽略并保留覆盖限制。
17
+ ## 判定与解释
16
18
 
17
- ## 当前证据与严重度
18
-
19
- | 情形 | 处理 |
19
+ | 证据 | 处理 |
20
20
  |---|---|
21
- | 可映射loader条目FAILED/PENDING | fatal,影响依据measured;禁用、LOADING、无法映射不冒充同一事实 |
22
- | 宿主硬依赖且明确absent | 可判fatal;仅catalog缺项不冒充运行时缺失 |
23
- | 非硬依赖服务明确absent,或有足够方法证据 | 按规则/默认档给feature-dead或degraded;用户影响通常是inferred |
24
- | 官方声明冲突或非法字段 | declared-incompatible,degraded及推断影响;不冒充实测加载失败;有效精确豁免可消除范围冲突,但保留限制且不证明业务成功 |
25
- | 未采样、读取失败、访问器未知、源码不完整 | 保留覆盖限制;不以省略键认定不存在 |
26
-
27
- 方法规则中的degraded应有具体降级/回退依据;peer声明冲突的degraded是引擎针对声明风险的独立默认,不代表观察到回退。方法缺失但没有语义规则时会明确“影响面未知”。声明provide只是候选提供方,不证明已激活,不能遮住完整的present方法观测。
28
-
29
- 宿主、客户端及shared调用点必须分开。客户端方法不能拿宿主probe判缺失;同名服务也不保证同一语义。对源码归属不确定的Context/别名保留suspected。certainty与impactBasis分别表达证据强度和影响是否实测。
30
-
31
- 0.1.4补强:必需inject未观测保持degraded/suspected,仅提示激活前提未确认;同profile明确absent才推断激活障碍,optional/scoped依赖与方法缺失分别处理,不借业务答案调严重度。`--observe-services`另记服务预测身份/摘要,不覆盖静态FN,也不改变生产certainty或impactBasis。
32
-
33
- 仓库独立业务队列将手审的功能调用区间内API风险与真实市场操作、侧栏HTTP/编辑器及有限模型回复结果对照,保持业务、loader标签及预测隔离;不把整个插件的其它发现挪给本功能。正常/自然失败与HTTP503/控件缺失故障组分别计分,限定样本不能改变生产报告中的certainty或impactBasis,也不是新增严重度规则的充分证据。复现见[使用指南](../docs/guide.md#工程与真实集成)。
34
-
35
- ## 如何更新
36
-
37
- ASAR读取器已修正数据起点为8+headerSize,并校验头长、边界、完整读取、UTF-8/JSON及归档SHA256。2026-10-18与Electron原生ASAR读取逐字节及首尾交叉校验后生成独立rc.2目录,旧目录未改。新host增加8项、移除invariants;subagents移除start/startContinuable/drainContinuableChildren/drainContinuableDescendants,增加startActivation/drainChildren/drainDescendants/waitForChildren;其它方法差异和源摘要保留在提交`10eaeba`的`docs/current-state.json`历史记录;当前认证字节见`lib/contracts.mjs`的`CATALOG_REFERENCES`,新候选按[维护流程](../docs/maintaining.md#api目录候选审查)核对。client方法集合未变。旧方法规则保留其原调用语义;不把新方法当同义重命名,也不只凭目录变化新增严重度,未知影响继续回退。
38
-
39
- 先读调用点和目标版本的真实描述符/官方源码,再修改规则,添加行为回归并审查快照差异。原needsReview已在2026-10-15逐条复核,决定与证据保留于severity.json的needsReview.reviewed/evidence及 [Plan](../docs/Plan.md)。
21
+ | 可靠映射且启用的FAILED/PENDING loader | fatal、影响measured;禁用/LOADING/无法映射不冒充同一事实 |
22
+ | 必需inject及合格同目标观测明确absent | 可推断激活障碍;仅目录缺项或未采样不证明缺失 |
23
+ | 非硬依赖明确absent或有充分方法缺失证据 | 按方法规则/默认档给feature-dead或degraded,影响通常inferred |
24
+ | 声明范围冲突或非法字段 | degraded/推断;有效精确豁免仅消除范围冲突,不放行非法字段或证明业务通过 |
25
+ | 未采样、访问器未知、不可读或源码不完整 | 保留覆盖限制,不用省略键认定不存在 |
26
+ | 固定客户端参数类型/枚举不符 | 非致命静态风险;未知值弃权,弃用/生命周期提醒仅作覆盖说明 |
40
27
 
41
- `tools/coverage.mjs` 从AST调用点按host/client/shared分端计数,规则命中与默认影响、类别发现分列。**knownExtractionArtifacts只保留历史反例,不过滤现在的真实调用**;纯客户端规则不用于宿主同名方法。probe须为现代格式并绑定本目标/版本/时间,不能借别的profile的面。写库存要求显式或观测到的合法semver版本,非法版本退出3且不覆盖旧文件。
28
+ 方法自身layer优先、服务layer回退,客户端规则不能套宿主同名方法。提供方声明只是候选,不能掩盖明确absent;present服务上的方法缺失另判。certainty与impactBasis分别描述证据和影响,弱Context/别名保留suspected。没有语义依据的方法缺失明确“影响面未知”,不猜替代API或修复版本。
42
29
 
43
- ```bash
44
- node tools/coverage.mjs --nm <node_modules目录> --dsh-version <已核验版本> --json
45
- node tools/coverage.mjs --nm <node_modules目录> --probe <同目标的现代probe> --write --json
46
- # 默认拒绝将不完整扫描写成库存;审查diagnostics后方可显式接受部分样本
47
- node tools/coverage.mjs --nm <node_modules目录> --dsh-version <已核验版本> --write --allow-partial --json
48
- ```
49
-
50
- 仓库保存的库存是2026-10-15的desktop21插件样本,DSH0.2.0-rc.2,静态catalogue模式,不借用规则复核的私有web探针。扫描受默认字节预算和缺失CSS入口限制,明确为partial:50个有调用服务、125个分端方法对,其中88个有对应规则(host69/79、client19/46)。这些是本次读到的部分集合,不是完整覆盖率或准确率;同名方法不同端分别计数,不能直接和下面旧口径61%比较。sourceDigest记录规范化AST分析摘要,不承诺所有包字节的完整性。历史Python参考不是AST真值。
30
+ DSH≥0.2.1-rc.1优先engines.dsh,缺失回退官方peer,旧目标保留旧策略。独立compatibility.json仅按插件/DSH精确版本豁免;非法字段和无效豁免保留风险/限制。
51
31
 
52
- 2026-10-15规则复核:attachments.readImage由官方实现和真实描述符互证,保留仅该读取路径的影响;sessions.open保留明确的客户端旧回退语义;删除sessions.getSnapshot、settings.get及sessions.map的无依据影响条目;sessions.refresh有守卫且无足够语义证据,不新增规则。客户端运行时未采样,不宣称对应API已被删除。
32
+ summary/basis/evidence/knownGoneIn/needsReview是解释与维护资料。knownExtractionArtifacts只保留历史反例,不自动过滤当前真实调用。degraded方法规则须有降级/回退依据;声明与静态参数风险的degraded不冒充实测回退。
53
33
 
54
- ## 历史依据:覆盖率、方法与反例(2026-10-11,旧提取器)
34
+ ## 更新与度量
55
35
 
56
- > 以下表格、旧命令、完整性判断和“故意没填”均是2026-10-11旧阶段记录,不是现行操作规程;当前证据与更新命令以上方为准。
36
+ 先核对调用点、端侧、目标版本及真实描述符/官方源码,再改规则并加行为回归;不能仅凭目录变化新增严重度。新版本目录、语义基线及来源验证按[维护流程](../docs/maintaining.md#api目录候选审查)分别审查,保留旧基线。
57
37
 
58
- > ⚠️ **这张表由 `node tools/coverage.mjs` 生成,是一台机器的一个 profile 的快照,不是生态普查。**
59
- > 它以前是**手工算的**,于是必然过期。**一张会过期却没人知道的覆盖率表比没有表更糟:它看起来像证据。**
60
-
61
- ```bash
62
- node tools/coverage.mjs # 给人看(**不给面,数字偏大**,见下)
63
- node tools/coverage.mjs --probe <探针报告> # 接近真实用法,推荐
64
- node tools/coverage.mjs --probe <报告> --write # 顺带刷新 rules/usage-inventory.json
38
+ ```sh
39
+ npm run check:catalogs
40
+ node tools/coverage.mjs --nm <目录> --dsh-version <精确版本> --json
41
+ node tools/coverage.mjs --nm <目录> --probe <同目标的现代probe> --write --json
42
+ # 仅审查diagnostics后才显式接受部分库存
43
+ node tools/coverage.mjs --nm <目录> --dsh-version <精确版本> --write --allow-partial --json
65
44
  ```
66
45
 
67
- **口径**(必须固定,否则数字之间不可比):
68
-
69
- - **"有调用点的服务"**:`perPlugin[*].calls` 的键的并集 —— **真的被调用过**的服务。
70
- 它**不是** `report.services`(后者含只被声明过的项,拿它当分母会把覆盖率算低)。
71
- - **排除已知提取产物**,且**按方法粒度**:`knownExtractionArtifacts` 里逐条写了"不要为这些补规则"。
72
- 按服务粒度排会把 `sessions`/`jobs` 这些**真服务**一起排掉。
73
- - **`--probe` 很要紧**:不给运行时面时,方法级结论一律降为「疑似」而**不进 findings**,
74
- 于是"落到默认档"这个数字既不是真实的缺口、也不是用户会看到的东西。
75
- 实测差别:同一个 profile,不给面 7、给面 **3**。
76
-
77
- ### 当时值(2026-10-11,desktop profile,DSH 0.2.0-rc.2,**带运行时面**)
78
-
79
- | 口径 | 值 |
80
- |---|---|
81
- | 插件 | 21 |
82
- | 有调用点的服务 | **49**(原始 50,排掉 1 个纯产物服务) |
83
- | 表中覆盖的服务 | **36 / 49 = 73%** |
84
- | (服务,方法) 对 | **161**(排掉 4 个产物对) |
85
- | 表中有方法级规则的对 | **99 / 161 = 61%** |
86
- | 表中服务总数 / 其中有方法级子表 | 42 / 37 |
87
- | **引擎实测:发现总数 / 规则表命中 / 落到默认档** | **4 / 1 / 3** |
88
-
89
- **最后一行才是唯一真正有意义的口径** —— 前几行都只是它的分解。
90
-
91
- ### 「落到默认档」的那 3 条**不该补规则**
92
-
93
- 它们全是 `unprovided-dependency`(**插件间依赖**,`method` 为 null),而且
94
- `evidence.inHostCatalog === false` —— 也就是说**它们根本不是 DSH API**。
95
- 类别级判定已经给出了准确的影响面("依赖的服务由其它插件提供,当前 profile 里没有提供方")。
96
- 给它们补"服务级规则"等于把"插件之间的依赖关系"伪装成"与 DSH 版本不兼容"。
97
-
98
- ### 「未覆盖的 (服务,方法) 对」里剩下的都是什么
99
-
100
- 按服务是否在**官方宿主目录**里分开看(这是"归不归规则表管"的分界):
101
-
102
- | 类别 | 条数 | 该不该补规则 |
103
- |---|---|---|
104
- | 服务在官方宿主目录里 | **1**(`sessions.has`) | **不该** —— 实测它是 `ws.sessions`(别人的 Map 字段)造的产物,已进 `knownExtractionArtifacts` |
105
- | 服务**不在**目录里 | 74 | **不该** —— 它们是**插件自有服务**(`betterSidebar`/`betterLocale`/`configForms`/`sidebarRightTabs`…)或已知产物(`parser.*`/`ruleNames.*`/集合方法名) |
106
-
107
- **所以规则表在"DSH API"这个范围内是完整的**:凡是真实存在、且被真实调用的宿主 API,
108
- 要么有规则,要么已确证是提取产物。**剩下没规则的,都不是 DSH 的 API。**
109
-
110
- ## 历史补充:新增条目的证据分列
111
-
112
- ### 一、有官方 API 目录直接依据(27 条)
113
-
114
- **方法在目录里声明,且描述写明了它干什么(`basis: catalog`,24 条)**
115
-
116
- | 服务 | 方法 |
117
- |---|---|
118
- | `skills` | `registerProvider` |
119
- | `subagents` | `list`、`getProvider`、`startContinuable`、`interrupt`、`listDescendants`、`drainContinuableChildren` |
120
- | `sessionQuery` | `listSessions`、`readSession`、`observeSession` |
121
- | `sessionTitle` | `rename` |
122
- | `typert` | `register` |
123
- | `agentPresets` | `resolve`、`mount` |
124
- | `agentTeams` | `tryMembership` |
125
- | `sessions`(客户端侧) | `scope`、`search` |
126
- | `locale` | `getLocale`、`addLanguage` |
127
- | `theme` | `register` |
128
- | `layout` | `selectPanel` |
129
- | `uiWorkspace` | `openSession` |
130
- | `workspaces` | `create`、`delete` |
131
-
132
- **目录不声明方法,但官方 description 写明了该方法(`basis: catalog-description`,3 条)**
133
-
134
- | 服务 | 方法 | 依据原文(节选) |
135
- |---|---|---|
136
- | `credentials` | `resolve` | "an empty stored value is absent everywhere — `resolve` skips it" |
137
- | `sessionPersistence` | `open`、`list` | "a created session is observable through `stat`/`list`/`open`" |
138
-
139
- ### 二、从真实调用点推断(`basis: callsite`,7 条)
140
-
141
- 这 7 条都给了 `文件:行`,请按 `evidence` 去读一眼再采信。
142
-
143
- | 服务.方法 | 依据 |
144
- |---|---|
145
- | `llm.listProviders` | `@liustack/modlens/dsh/index.js:397`(`typeof` 探测不成立就整体跳过提供方名解析 → 退回显示上游 id) |
146
- | `settings.installSection` | `@nonamelego/dsh-catppuccin/lib/index.js:734`,且其注释写明该通道对应 DSH ≤ 0.1.6-alpha.2,0.1.7-alpha.1 起移除 |
147
- | `storageDomain.open` | `@michengai/dsh-archive-manager/lib/projcache.js:102`。**注意**:目录声明的是 `closeAll`/`get`,没有 `open`,但 description 写的是 "Opens declared domains over routed backends"。方法名与目录对不上,所以只给 `callsite` |
148
- | `sessions.liveEntryFor` | `@michengai/dsh-archive-manager/lib/workspace.js:932` |
149
- | `sessions.detachEntered` | 同上一行 `:933`;与官方 `enter()` description 里"返回 detach disposer"那一半互为印证 |
150
- | `sessions.using` | `@michengai/dsh-archive-manager/lib/client.js`(`using` 不可用即退回 `binding`) |
151
- | `timer.timeout` | `dsh-recall-plugin/lib/client.js:1430`(不可用即用原生 `setTimeout`) |
152
-
153
- ## 历史补充:故意没填的服务与原因
154
-
155
- 未进入本表的 18 个实际被用到的服务。**这一节和填了的那些一样重要**——乱填一个影响面,等于毁掉本工具唯一的资产。
156
-
157
- ### A. 第三方插件自己的服务,不是 DSH API(8 个)
158
-
159
- `betterLocale`、`companionQq`、`pet`、`remote`、`sidebarRight`、`sidebarRightTabs`、`pluginNavigation`、`webUiSettings`
160
-
161
- 判定依据是**取得方式**:它们全部由客户端 `ctx.get("x")` / `ctx.inject(["x"], …)` 软取,而提供方是另一个插件:
162
-
163
- - `companionQq` —— dsh-meme 的源码注释直接写明"消费 dsh-companion 提供的 `companionQq` 服务(sendImage/isOnline)"。
164
- - `webUiSettings` —— remote-web-ui 的注释写明"由 dsh-web-settings 这个包在 `apply()` 期间发布"。
165
- - `pluginNavigation` —— dsh-mnemon 在自己的客户端 `inner.effect(() => seats.configuration.provide(...))` 里挂上去。
166
- - `sidebarRight` / `sidebarRightTabs` —— dsh-better-sidebar 客户端软取/inject 的同族服务。
167
-
168
- 对这类服务,正确的结论就是引擎现在给的那句默认话术:「该服务由另一个插件提供,当前 profile 里没有提供方」。**写进本表反而会说谎**——本表的语义是"DSH 的某个 API 没了",而这些名字根本不在 DSH 的 API 面上。
169
-
170
- ### B. 由社区启动器提供,官方 shell 上没有(1 个)
171
-
172
- `desktopProfiles` —— @nonamelego/dsh-catppuccin 的类型注释写得很清楚:"`desktopProfiles.current.name` when the community launcher exposes that service; **undefined on the official shell**"。它不是官方 API。
173
-
174
- ### C. 版本相关的旧/新客户端 seam,0.2.0-rc.2 都不在目录里(2 个)
175
-
176
- `settingsScope`、`configForms` —— catppuccin 的注释把版本线说得很明白:`settingsScope` 属于 DSH ≤ 0.1.6-alpha.2 的旧 seam,`configForms` 是 ≥ 0.1.7-alpha.1 的新 seam,而 0.2.0-rc.2 的客户端目录里两个都没有。缺它属于"版本演进",不属于"插件用了不存在的 API",而且插件都做了 `typeof` 探测回退。**记录在案,不填。**
177
-
178
- ### D. 提取误报,不是 API(3 个)
179
-
180
- `parser`(40 个"方法")、`ruleNames`、`topoIndex` —— 来自 `dsh-better-sidebar/lib/client-mermaid.js` 里 vendored 的 markdown 解析器上下文。已在 `severity.json` 的 `knownExtractionArtifacts` 里逐条登记。
181
-
182
- ### E. 判不准,所以不填(3 个)
183
-
184
- | 服务 | 为什么判不准 |
185
- |---|---|
186
- | `conversation` | 只出现在客户端代码(dsh-meme / better-sidebar / git-graph / recall)里,宿主与客户端两个官方目录都没有。既可能是尚未公开的客户端服务,也可能是误报——分不出来 |
187
- | `webStartup` | remote-web-ui 软取它读 `host`/`port`;两个目录都没有。可能是启动器/宿主不公开的面,无法判断缺失意味着什么 |
188
- | `styles` | dsh-recall-plugin 用 `ctx.get("styles")` 并**自带 `document` 注入兜底**(回退路径明确),但同名标识符也出现在 `dsh-better-sidebar/lib/client-mermaid.js` 的 vendored 代码里——无法确认是哪一边 |
189
-
190
- ### F. 提取器已正确处理,无需规则(1 个)
191
-
192
- `betterSidebar` —— 它是插件自己 `ctx.provide()` 出来的服务,**提取器已正确记入 `providedServices`**(同类的还有 dshmarket 的 `market`),因此不会产生假缺口。`pet` / `agentTeams` / `companionQq` / `betterLocale` 这些**没有**被 `ctx.provide()` 捕获的(见 `docs/Plan.md` §2.2 的提供方证据约束),就落进了上面 A 组。
193
-
194
- ## 历史补充:已知提取误报
195
-
196
- `severity.json` 里有一个 `knownExtractionArtifacts` 段,逐条登记了"看起来是 API 缺口、其实是正则产物"的 `服务.方法`,并给出证据。**这一节存在的意义是防止后来者(包括未来的我)出于好心把假阳性升格成"有影响面的确定结论"。**
197
-
198
- 最值得记住的两个(当时新查出来的,不只是继承 `usage-inventory.json` 的已知缺陷):
199
-
200
- 1. **alias 正则漏了词边界**:`lib/extract.mjs` 里 `ctx.get("x")` 的别名规则构造的正则是 `别名\.方法(`,**没有 `\b`**。于是源码里的 `subagents.listDescendants(` 会同时命中 `agents.listDescendants`——因为 `subagents` 里含子串 `agents.`。desktop profile 上正好 2 处。改前它一直以"影响面未知"的形式出现在结论里。
201
- 2. **同文件内别名撞名**:dsh-better-sidebar 的 `lib/client.js` 里同时有 `const service = ctx.get("jobs")`(1 处)和 `const service = ctx.get("betterSidebar")`(6 处)。别名规则对文件内**每一次**匹配都按每个别名目标各记一次,于是文件里所有 `service.X()`——**包括 JSDoc 注释里的**——都被算到 `jobs` 头上。`jobs.getTabs`/`closeTab`/`registerTab` 等 13 个"方法"就是这么来的(`lib/client.js:977` 那处原文是注释里的一句 `service.getTabs()`)。
202
-
203
- 其余登记项:`settings.isTrusted`/`reportNonstrict`/`useStrictBehavior`(KaTeX 的 `parser.settings`)、`settings.settingsGet`/`externalDisable`(better-sidebar 自有的 `SidebarSettingsFace`)、`locale.startsWith`(字符串方法)、`tools.filter/join/push` 与 `sessions.map/filter/slice/...`(数组方法)。
204
-
205
- ## 历史待复核登记(2026-10-11;现已按上文处理)
206
-
207
- 改这些条目会改变输出,按当时的维护约定应先摊开差异再决定,故仅登记证据。以下保留当时的疑问;当前处理决定在needsReview.reviewed中:
208
-
209
- - `sessions.open` / `sessions.getSnapshot`:两个目录里都没有;客户端真正的跳转入口是 `uiWorkspace.openSession`。
210
- - `sessions.refresh`:两个目录里都没有(客户端目录里的是 `refreshProjections`)。
211
- - `settings.get`:0.2.0-rc.2 的宿主目录里 `settings` 没有 `get`,可能和 `settingsGet` 一样来自插件自有的设置面。
212
- - `attachments.readImage`:目录里是 `readImageRequest`,方法名对不上,需要一次运行时探针定音。
213
-
214
- ## 当前提取边界
215
-
216
- 旧inventory来源于正则提取器;当前JSON已由有版本和完整性标记的AST样本替换,旧数值只保留在本页历史表。knownExtractionArtifacts也保留历史反例用途,并非当前引擎的黑名单。新的规则刷新必须记录DSH版本、目标profile、probe身份与时间,并逐条复核源码归属。
217
-
218
- 真解析器已经落地;“疑似比例明显下降”不作为验收指标,因为该比例还受运行时采样覆盖影响。当前已支持有界跨函数/包内模块值流,未知动态行为仍有诊断;不声称完整分析,边界见 [现行契约](../docs/Plan.md)。
219
-
220
- ## 当前维护原则
221
-
222
- 直接改 `severity.json` 并说明依据。特别注意三类容易判错的:
46
+ coverage按AST调用点分host/client/shared统计规则命中及默认影响;不借异profile探针。不完整扫描默认拒绝写库存,写入须合法精确DSH版本,坏版本不覆盖旧文件。文件树/AST摘要不是原子快照或包签名。
223
47
 
224
- - **同名不同义**:`settings.register` 在客户端设置页和宿主设置文档里不是一回事;`sessions`/`jobs`/`timer` 两侧同名但 API 不同。别混。
225
- - **降级 ≠ 无害**:`logger`相关规则按日志/诊断影响描述,不外推全部业务功能均不受影响;但 `jobs.onJobsChanged` 判 `degraded` 是因为它**有明确的轮询回退**,而不是因为它不重要。
226
- - **判不准就不要填**:落到默认档 + 明确的"影响面未知",比编一个看起来合理的影响面好得多。新疑问写入needsReview并保留来源和待核验范围,不把历史“故意没填”当永久结论。
48
+ 库存是2026-10-15、DSH0.2.0-rc.2的desktop历史部分样本;规则复核、旧覆盖率和方法决定可在`39d9f1a:rules/README.md`及severity.json的reviewed/evidence追溯。它们不代表当前生态准确率。真实业务标签、静态/service-only预测和驱动错误独立计分,不借业务答案调严重度;[复现](../docs/maintaining.md#真实插件回归)。