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
@@ -3,6 +3,12 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { analyzeSources } from "./source-analysis.mjs";
6
+ import {
7
+ readStaticFile,
8
+ readStaticDirectory,
9
+ inputError,
10
+ lowerInputLimits,
11
+ } from "./static-input.mjs";
6
12
  import {
7
13
  collectPackageSources,
8
14
  clientEntry as declaredClientEntry,
@@ -14,12 +20,19 @@ const cmp = (a, b) =>
14
20
  : String(a ?? "") > String(b ?? "")
15
21
  ? 1
16
22
  : 0;
17
- const pkgJson = (dir) => {
23
+ const pkgJson = (dir, maxBytes, check) => {
24
+ let readBytes = 0;
18
25
  try {
19
- const p = JSON.parse(fs.readFileSync(join(dir, "package.json"), "utf8"));
20
- return p && typeof p === "object" && !Array.isArray(p) ? p : null;
21
- } catch {
22
- return null;
26
+ const bytes = readStaticFile(join(dir, "package.json"), maxBytes, check);
27
+ readBytes = bytes.length;
28
+ const p = JSON.parse(bytes.toString("utf8"));
29
+ if (!p || typeof p !== "object" || Array.isArray(p))
30
+ throw new Error("Invalid package object");
31
+ return { meta: p, bytes: bytes.length };
32
+ } catch (error) {
33
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return {};
34
+ if (error.code === "STATIC_INVENTORY_BUDGET") throw error;
35
+ return { error, bytes: readBytes };
23
36
  }
24
37
  };
25
38
  const sortCalls = (calls) =>
@@ -45,7 +58,7 @@ function clientHalfDeliverable(pkgDir, j) {
45
58
  platform: "web",
46
59
  ok: false,
47
60
  declaredEntry: null,
48
- why: '声明了 dsh.client 却没有 exports["./client"]',
61
+ why: '声明了 dsh.client,但 exports["./client"] 不是字符串或含字符串 default 的对象',
49
62
  };
50
63
  try {
51
64
  const root = fs.realpathSync(pkgDir),
@@ -96,14 +109,37 @@ export function extractWithProvenance(nm, options = {}) {
96
109
  // 找出「社区插件」:package.json 里有 dsh 清单、且非 @deepseek-ai 官方
97
110
  const inventoryDiagnostics = [];
98
111
  const targets = [];
99
- for (const entry of fs.readdirSync(nm).sort()) {
112
+ const inventory = lowerInputLimits(
113
+ {
114
+ entries: 50000,
115
+ bytes: 32 * 1024 * 1024,
116
+ manifestBytes: 65536,
117
+ timeMs: 5000,
118
+ },
119
+ options.inventoryLimits,
120
+ );
121
+ let inventoryEntries = 0,
122
+ inventoryBytes = 0;
123
+ const started = Date.now();
124
+ const checkInventory = () => {
125
+ if (Date.now() - started > inventory.timeMs)
126
+ throw inputError("STATIC_INVENTORY_BUDGET", "插件清单超过时间预算");
127
+ };
128
+ const listInventory = (dir) => {
129
+ const names = readStaticDirectory(
130
+ dir,
131
+ inventory.entries - inventoryEntries,
132
+ checkInventory,
133
+ );
134
+ inventoryEntries += names.length;
135
+ return names;
136
+ };
137
+ for (const entry of listInventory(nm)) {
138
+ checkInventory();
100
139
  const names = entry.startsWith("@")
101
140
  ? (() => {
102
141
  try {
103
- return fs
104
- .readdirSync(join(nm, entry))
105
- .sort()
106
- .map((s) => join(entry, s));
142
+ return listInventory(join(nm, entry)).map((s) => join(entry, s));
107
143
  } catch (error) {
108
144
  throw new Error(
109
145
  `无法读取插件 scope ${entry}:${error.code || error.message}`,
@@ -112,12 +148,20 @@ export function extractWithProvenance(nm, options = {}) {
112
148
  })()
113
149
  : [entry];
114
150
  for (const base of names) {
151
+ checkInventory();
115
152
  const full = join(nm, base);
116
- const j = pkgJson(full);
117
- if (!j && fs.existsSync(join(full, "package.json")))
153
+ const metadata = pkgJson(
154
+ full,
155
+ Math.min(inventory.manifestBytes, inventory.bytes - inventoryBytes),
156
+ checkInventory,
157
+ );
158
+ inventoryBytes += metadata.bytes || 0;
159
+ const j = metadata.meta;
160
+ if (metadata.error)
118
161
  inventoryDiagnostics.push({
119
162
  kind: "invalid-package-metadata",
120
163
  file: base,
164
+ code: metadata.error.code || "STATIC_INPUT_INVALID",
121
165
  });
122
166
  if (
123
167
  !j ||
@@ -52,6 +52,12 @@ export function nextSteps(finding) {
52
52
  "在报告给出的源码位置核对服务读取及必需 inject 的作用域,并确认 absent 观测绑定同一 profile、DSH 版本与采样时间。",
53
53
  "核对该服务的实际提供者及 loader 状态,补充相关功能操作结果;服务缺失的推断影响不等于业务失败已实测。",
54
54
  ];
55
+ case "api-argument-invalid":
56
+ case "api-option-unsupported":
57
+ return [
58
+ "按源码位置核对选项类型、枚举、实际传值和精确目标版本,查阅 evidence 中固定源码及归档摘要;方法存在或能力守卫不证明参数有效。",
59
+ "在独立目标会话核验草稿保留、替换或追加;本项仅是静态参数风险,不证明业务失败,也不自动修改或提交草稿。",
60
+ ];
55
61
  case "host-method-missing":
56
62
  return [
57
63
  "在报告给出的文件与行号核对实际方法调用、Context 绑定及能力守卫,检查是否存在报告尚未识别的兜底。",
@@ -1,5 +1,6 @@
1
1
  /** Bounded manual-review sidecar storage. Never mutates reports or baseline state. */
2
2
  import fs from "node:fs/promises";
3
+ import { acquireStateLock, stateDirectory } from "./state-lock.mjs";
3
4
  import { constants } from "node:fs";
4
5
  import path from "node:path";
5
6
  import os from "node:os";
@@ -105,16 +106,13 @@ function storage(context, options) {
105
106
  `reviews.${context.targetKey}.json`,
106
107
  );
107
108
  }
108
- async function safeDirectory(dir, { create = false } = {}) {
109
- if (create) await fs.mkdir(dir, { recursive: true, mode: 0o700 });
110
- const stat = await fs.lstat(dir);
111
- if (
112
- !stat.isDirectory() ||
113
- stat.isSymbolicLink() ||
114
- (typeof process.getuid === "function" && stat.uid !== process.getuid())
115
- )
116
- throw fault("Unsafe review directory", "REVIEW_STORAGE");
117
- if (create && stat.mode & 0o077) await fs.chmod(dir, 0o700);
109
+ async function safeDirectory(dir, options = {}) {
110
+ try {
111
+ stateDirectory(dir, options);
112
+ } catch (error) {
113
+ if (error.code === "ENOENT") throw error;
114
+ throw fault(error.message, "REVIEW_STORAGE");
115
+ }
118
116
  }
119
117
  async function load(file) {
120
118
  try {
@@ -185,18 +183,11 @@ export async function saveReview(context, input, options = {}) {
185
183
  let lockHandle = null;
186
184
  try {
187
185
  try {
188
- lockHandle = await fs.open(
189
- lock,
190
- constants.O_CREAT |
191
- constants.O_EXCL |
192
- constants.O_WRONLY |
193
- constants.O_NOFOLLOW,
194
- 0o600,
195
- );
186
+ lockHandle = acquireStateLock(lock);
196
187
  } catch (error) {
197
188
  if (error.code === "EEXIST")
198
189
  throw fault(
199
- `人工处置写入锁仍存在或上次保存已中断:${lock}。刷新不会清除此锁;确认没有保存任务后,仅清理该.lock及同目录遗留.reviews-*.tmp,保留原始reviews JSON。`,
190
+ `人工处置写入锁仍存在或上次保存已中断:${lock}。刷新不会清除此锁;使用对应源码仓库的 bin/recover-lock.mjs 检查并显式恢复已确认退出的持有者;保留原始 JSON 及临时文件。`,
200
191
  "REVIEW_BUSY",
201
192
  );
202
193
  throw error;
@@ -237,9 +228,6 @@ export async function saveReview(context, input, options = {}) {
237
228
  return combineReviews(context, records, { revision: next.revision });
238
229
  } finally {
239
230
  await fs.rm(temp, { force: true }).catch(() => {});
240
- if (lockHandle) {
241
- await lockHandle.close();
242
- await fs.unlink(lock).catch(() => {});
243
- }
231
+ lockHandle?.release();
244
232
  }
245
233
  }
@@ -1,13 +1,14 @@
1
1
  /** Shared CLI/panel share view. Never serialize raw inputs, logs, evidence or baseline. */
2
2
  import { TOOL_VERSION } from "./version.mjs";
3
3
  import { nextSteps } from "./finding-guidance.mjs";
4
+ import { terminalText } from "./contracts.mjs";
4
5
 
5
6
  function safe(value) {
6
7
  const raw = String(value ?? "未提供");
7
8
  // Hide the whole field: token-by-token replacement leaks the tail of a path
8
9
  // containing spaces and can turn a multi-line log into apparently safe prose.
9
10
  if (
10
- /[\x00-\x1f\x7f\u202a-\u202e\u2066-\u2069]/.test(raw) ||
11
+ terminalText(raw) !== raw ||
11
12
  /(?:[a-z]:[\\/]|\\\\|~[\\/]|(?:^|[\s("'`=::,;])\/|\.\.[\\/]|\/[^\s/]+\/)/i.test(
12
13
  raw,
13
14
  ) ||
@@ -0,0 +1,232 @@
1
+ /** Bounded, non-evaluating subset of DSH's ordered bundle patch composition.
2
+ * Unsupported expressions/aliases and cross-bundle targets limit coverage.
3
+ * Only insert rows (and known group descendants) introduce module roots.
4
+ */
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { parseDocument, isAlias, isMap, isSeq, isPair } from "yaml";
8
+ import { inputError } from "./static-input.mjs";
9
+
10
+ export function patchMounts(files, { root, read, check, note, limits }) {
11
+ const rows = [],
12
+ index = new Map(),
13
+ bases = new WeakMap(),
14
+ assertionNames = new WeakMap();
15
+ let nodes = 0,
16
+ complete = true;
17
+ const uncertain = (file, message) => {
18
+ complete = false;
19
+ note("patch-resolution-uncertain", file, message);
20
+ };
21
+ const invalid = (message) => {
22
+ throw new Error(message);
23
+ };
24
+ const inspect = (root) => {
25
+ const queue = [[root, 0]];
26
+ while (queue.length) {
27
+ check();
28
+ const [node, depth] = queue.pop();
29
+ if (!node || typeof node !== "object") continue;
30
+ if (++nodes > limits.yamlNodes || depth > limits.yamlDepth)
31
+ throw inputError(
32
+ "STATIC_INPUT_BUDGET",
33
+ "Patch node/depth budget exceeded",
34
+ );
35
+ // Zero alias expansion: anchors alone are harmless, aliases are explicit
36
+ // unsupported input. No cyclic graph or expansion reaches composition.
37
+ if (isAlias(node))
38
+ invalid("Patch aliases are unsupported (expansion budget 0)");
39
+ if (isMap(node) || isSeq(node))
40
+ for (const item of node.items) queue.push([item, depth + 1]);
41
+ else if (isPair(node)) {
42
+ queue.push([node.key, depth + 1], [node.value, depth + 1]);
43
+ }
44
+ }
45
+ };
46
+ const rowList = (entries, base) => {
47
+ if (!Array.isArray(entries))
48
+ invalid("Patch insert/group config must be a literal row list");
49
+ for (const row of entries) {
50
+ if (!(row instanceof Map)) invalid("Patch row must be a literal mapping");
51
+ for (const key of ["group", "disabled"])
52
+ if (
53
+ row.has(key) &&
54
+ row.get(key) !== null &&
55
+ typeof row.get(key) !== "boolean"
56
+ )
57
+ invalid("Patch row flags must be literal booleans");
58
+ if (typeof row.get("name") !== "string" || !row.get("name"))
59
+ invalid("Patch row name is unavailable");
60
+ if (row.has("id") && typeof row.get("id") !== "string")
61
+ invalid("Patch row id must be a string");
62
+ bases.set(row, base);
63
+ const name = row.get("name");
64
+ assertionNames.set(
65
+ row,
66
+ path.isAbsolute(name) || name.startsWith("./") || name.startsWith("../")
67
+ ? pathToFileURL(path.resolve(root, base, name)).href
68
+ : name,
69
+ );
70
+ const id = row.get("id");
71
+ if (id) {
72
+ if (index.has(id)) invalid("Duplicate patch row id is ambiguous");
73
+ index.set(id, row);
74
+ }
75
+ if (row.get("group") && Array.isArray(row.get("config")))
76
+ rowList(row.get("config"), base);
77
+ }
78
+ return entries;
79
+ };
80
+ for (let i = 0; i < files.length; i++) {
81
+ const file = files[i];
82
+ try {
83
+ check();
84
+ if (i >= limits.yamlFiles)
85
+ throw inputError(
86
+ "STATIC_INPUT_BUDGET",
87
+ "Patch file count budget exceeded",
88
+ );
89
+ const doc = parseDocument(read(file), {
90
+ uniqueKeys: true,
91
+ schema: "core",
92
+ });
93
+ check();
94
+ if (doc.errors.length || doc.warnings.length)
95
+ invalid("Invalid or unsupported patch YAML");
96
+ inspect(doc.contents);
97
+ const patches = doc.toJS({ mapAsMap: true, maxAliasCount: 0 });
98
+ if (!Array.isArray(patches)) invalid("Patch must be a top-level list");
99
+ for (const patch of patches) {
100
+ check();
101
+ if (!(patch instanceof Map))
102
+ invalid("Patch operation must be a mapping");
103
+ const id = patch.get("id"),
104
+ insert = patch.get("insert");
105
+ if (patch.has("insert")) {
106
+ const target = id ? index.get(id) : null;
107
+ if (id && (!target || !target.get("group"))) {
108
+ uncertain(file, "Insert target is unavailable or not a group");
109
+ continue;
110
+ }
111
+ const added = rowList(insert, path.dirname(file));
112
+ if (target) {
113
+ if (!Array.isArray(target.get("config"))) target.set("config", []);
114
+ target.get("config").push(...added);
115
+ } else rows.push(...added);
116
+ continue;
117
+ }
118
+ const target = id && index.get(id);
119
+ if (!target) {
120
+ uncertain(
121
+ file,
122
+ "Override target is unavailable; name is an assertion, not a mount",
123
+ );
124
+ continue;
125
+ }
126
+ if (
127
+ patch.get("name") &&
128
+ patch.get("name") !== assertionNames.get(target)
129
+ ) {
130
+ uncertain(file, "Override name assertion did not match");
131
+ continue;
132
+ }
133
+ for (const [key, value] of patch) {
134
+ if (["id", "name"].includes(key)) continue;
135
+ if (
136
+ ["group", "disabled"].includes(key) &&
137
+ value !== null &&
138
+ typeof value !== "boolean"
139
+ )
140
+ invalid("Dynamic patch flags cannot establish mounts");
141
+ target.set(key, value);
142
+ }
143
+ // Like the host, config replacements do not reindex new child ids.
144
+ // Relative paths in such replacements use the profile tree, not the
145
+ // patch file's base, so they must not be guessed package-relative.
146
+ }
147
+ } catch (error) {
148
+ complete = false;
149
+ note(
150
+ error.code === "STATIC_INPUT_BUDGET"
151
+ ? "scan-budget-exceeded"
152
+ : "patch-unreadable",
153
+ file,
154
+ error.message,
155
+ );
156
+ return { mounts: [], complete: false };
157
+ }
158
+ }
159
+ const mounts = [];
160
+ const walk = (entries, disabled = false) => {
161
+ for (const row of entries) {
162
+ check();
163
+ if (!(row instanceof Map)) {
164
+ uncertain(null, "Group replacement contains an invalid row");
165
+ continue;
166
+ }
167
+ const name = row.get("name"),
168
+ group = row.get("group") === true;
169
+ if (
170
+ ["group", "disabled"].some(
171
+ (key) =>
172
+ row.has(key) &&
173
+ row.get(key) !== null &&
174
+ typeof row.get(key) !== "boolean",
175
+ )
176
+ ) {
177
+ uncertain(null, "Replacement row flags are not literal booleans");
178
+ continue;
179
+ }
180
+ const blocked = disabled || row.get("disabled") === true;
181
+ if (!group && blocked) continue;
182
+ if (typeof name !== "string") {
183
+ uncertain(null, "Group replacement name is unavailable");
184
+ continue;
185
+ }
186
+ if (name === "cordis:group" && group) {
187
+ const children = row.get("config");
188
+ if (!Array.isArray(children))
189
+ uncertain(null, "Group config is not a literal row list");
190
+ else walk(children, blocked);
191
+ } else if (name.startsWith("cordis:")) {
192
+ uncertain(null, "Builtin subtree semantics are not established");
193
+ } else {
194
+ if (path.isAbsolute(name)) mounts.push(name);
195
+ else if (name.startsWith("file://")) {
196
+ try {
197
+ const url = new URL(name);
198
+ if (url.search || url.hash)
199
+ throw Error("URL modifiers are not established");
200
+ mounts.push(fileURLToPath(url));
201
+ } catch {
202
+ uncertain(null, "File URL mount is not established");
203
+ }
204
+ } else if (name.startsWith("./") || name.startsWith("../")) {
205
+ if (!bases.has(row))
206
+ uncertain(
207
+ null,
208
+ "Relative replacement row depends on the profile base",
209
+ );
210
+ else mounts.push("./" + path.join(bases.get(row), name));
211
+ } else if (name.startsWith("."))
212
+ uncertain(null, "Relative mount depends on the profile base");
213
+ else mounts.push(name);
214
+ if (group)
215
+ uncertain(null, "Custom group descendants are not established");
216
+ }
217
+ }
218
+ };
219
+ try {
220
+ walk(rows);
221
+ } catch (error) {
222
+ note(
223
+ error.code === "STATIC_INPUT_BUDGET"
224
+ ? "scan-budget-exceeded"
225
+ : "patch-unreadable",
226
+ null,
227
+ error.message,
228
+ );
229
+ return { mounts: [], complete: false };
230
+ }
231
+ return { mounts, complete };
232
+ }
@@ -2,13 +2,40 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { parentPort, workerData } from "node:worker_threads";
5
+ import {
6
+ readStaticDirectory,
7
+ readStaticFile,
8
+ lowerInputLimits,
9
+ } from "./static-input.mjs";
10
+ const limits = lowerInputLimits(
11
+ {
12
+ entries: 50000,
13
+ files: 50000,
14
+ directories: 10000,
15
+ bytes: 32 * 1024 * 1024,
16
+ timeMs: 5000,
17
+ },
18
+ workerData.limits,
19
+ );
20
+ const started = Date.now();
21
+ const check = () => {
22
+ if (Date.now() - started > limits.timeMs) throw Error("缓存身份时间预算超限");
23
+ };
24
+ let entries = 0,
25
+ bytesRead = 0;
26
+ const list = (dir) => {
27
+ const names = readStaticDirectory(dir, limits.entries - entries, check);
28
+ entries += names.length;
29
+ return names;
30
+ };
5
31
  const hash = createHash("sha256");
6
32
  const add = (value) => hash.update(JSON.stringify(value) + "\n");
7
- let remaining = 50000,
8
- directories = 10000;
33
+ let remaining = limits.files,
34
+ directories = limits.directories;
9
35
  const services = new Set(),
10
36
  manifests = [];
11
37
  function metadata(file) {
38
+ check();
12
39
  if (--remaining < 0) throw Error("缓存身份文件预算超限,等待后续刷新");
13
40
  const stat = fs.statSync(file);
14
41
  add([
@@ -23,32 +50,29 @@ function metadata(file) {
23
50
  return stat;
24
51
  }
25
52
  function smallFile(file, limit = 1024 * 1024) {
26
- let fd;
27
53
  try {
28
54
  metadata(file);
29
- // Open nonblocking before inspecting the same descriptor. A swapped
30
- // symlink or FIFO cannot turn the following bounded read into a host hang.
31
- fd = fs.openSync(
32
- file,
33
- fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0),
34
- );
35
- const stat = fs.fstatSync(fd);
36
- if (!stat.isFile() || stat.size > limit)
37
- throw Error("缓存身份输入不是预算内普通文件");
38
- const buffer = Buffer.alloc(limit + 1),
39
- length = fs.readSync(fd, buffer, 0, buffer.length, 0);
40
- if (length > limit) throw Error("缓存身份输入超过读取预算");
41
- const bytes = buffer.subarray(0, length);
55
+ let bytes;
56
+ try {
57
+ bytes = readStaticFile(
58
+ file,
59
+ Math.min(limit, limits.bytes - bytesRead),
60
+ check,
61
+ );
62
+ } catch (error) {
63
+ if (error.code === "STATIC_INPUT_TYPE")
64
+ throw Error("缓存身份输入不是预算内普通文件");
65
+ throw error;
66
+ }
67
+ bytesRead += bytes.length;
42
68
  add(bytes.toString("base64"));
43
69
  return bytes;
44
70
  } catch (error) {
45
- if (error.code === "ENOENT" || error.code === "ENOTDIR") {
71
+ if (["ENOENT", "ENOTDIR"].includes(error.code)) {
46
72
  add([file, "missing"]);
47
73
  return null;
48
74
  }
49
75
  throw error;
50
- } finally {
51
- if (fd !== undefined) fs.closeSync(fd);
52
76
  }
53
77
  }
54
78
 
@@ -66,7 +90,7 @@ function walk(root) {
66
90
  )
67
91
  continue;
68
92
  seen.add(real);
69
- for (const entry of fs.readdirSync(dir).sort()) {
93
+ for (const entry of list(dir)) {
70
94
  const file = path.join(dir, entry);
71
95
  let stat;
72
96
  try {
@@ -86,12 +110,9 @@ try {
86
110
  const { nodeModules, files = [], context = {} } = workerData;
87
111
  add(context);
88
112
  metadata(nodeModules);
89
- for (const name of fs.readdirSync(nodeModules).sort()) {
113
+ for (const name of list(nodeModules)) {
90
114
  const bases = name.startsWith("@")
91
- ? fs
92
- .readdirSync(path.join(nodeModules, name))
93
- .sort()
94
- .map((n) => path.join(name, n))
115
+ ? list(path.join(nodeModules, name)).map((n) => path.join(name, n))
95
116
  : [name];
96
117
  for (const base of bases) {
97
118
  const root = path.join(nodeModules, base);
@@ -99,7 +120,7 @@ try {
99
120
  try {
100
121
  bytes = smallFile(path.join(root, "package.json"), 65536);
101
122
  } catch (error) {
102
- if (remaining < 0) throw error;
123
+ if (remaining < 0 || error.code === "STATIC_INPUT_BUDGET") throw error;
103
124
  add([root, "unreadable-manifest", error.code || error.message]);
104
125
  continue;
105
126
  }
@@ -1,11 +1,12 @@
1
- import { runWorkerTask } from "./worker-analysis.mjs";
1
+ import { runReportWorkerTask } from "./worker-analysis.mjs";
2
2
  /** Bounded metadata walk runs off the host thread and never imports plugin code. */
3
- export function reportIdentity(inputs) {
4
- return runWorkerTask(
3
+ export function reportIdentity(inputs, { signal } = {}) {
4
+ return runReportWorkerTask(
5
5
  new URL("./report-identity-worker.mjs", import.meta.url),
6
6
  inputs,
7
7
  {
8
8
  timeoutMs: 5000,
9
+ signal,
9
10
  resourceLimits: {
10
11
  maxOldGenerationSizeMb: 128,
11
12
  maxYoungGenerationSizeMb: 16,