dsh-plugin-compat-check 0.1.3 → 0.1.5
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.
- package/README.md +36 -20
- package/THIRD_PARTY_NOTICES.md +139 -0
- package/index.js +288 -55
- package/kernel/lib/author-ci-output.mjs +188 -0
- package/kernel/lib/author-ci-policy.mjs +285 -0
- package/kernel/lib/catalog.mjs +38 -0
- package/kernel/lib/contracts.mjs +88 -0
- package/kernel/lib/dependency-graph.mjs +650 -0
- package/kernel/lib/extract.mjs +14 -0
- package/kernel/lib/finding-guidance.mjs +72 -0
- package/kernel/lib/finding-review-store.mjs +245 -0
- package/kernel/lib/finding-review.mjs +338 -0
- package/kernel/lib/functional-health-contract.mjs +337 -0
- package/kernel/lib/issue-report.mjs +217 -0
- package/kernel/lib/observation.mjs +19 -9
- package/kernel/lib/owned-process.mjs +287 -0
- package/kernel/lib/report-identity-worker.mjs +153 -0
- package/kernel/lib/report-identity.mjs +15 -0
- package/kernel/lib/report-service.mjs +196 -11
- package/kernel/lib/report.mjs +76 -11
- package/kernel/lib/source-analysis.mjs +24 -6
- package/kernel/lib/upgrade-capture-worker.mjs +20 -0
- package/kernel/lib/upgrade-cleanup-worker.mjs +28 -0
- package/kernel/lib/upgrade-contract.mjs +471 -0
- package/kernel/lib/upgrade-input.mjs +309 -0
- package/kernel/lib/upgrade-preview.mjs +256 -0
- package/kernel/lib/upgrade-worker.mjs +50 -0
- package/kernel/lib/verdict.mjs +8 -1
- package/kernel/lib/version.mjs +1 -1
- package/kernel/lib/worker-analysis.mjs +16 -1
- package/kernel/lib/workflow-panels.mjs +706 -0
- package/kernel/rules/README.md +8 -6
- package/lib/client.js +2974 -69
- package/lib/dependency-graph.js +651 -0
- package/lib/finding-guidance.js +73 -0
- package/lib/finding-review.js +339 -0
- package/lib/functional-health-contract.js +338 -0
- package/lib/report-contract.js +88 -0
- package/lib/upgrade-contract.js +472 -0
- package/lib/workflow-panels.js +707 -0
- package/package.json +5 -3
- package/scripts/prepublish-check.mjs +28 -13
- package/scripts/stage-contract.mjs +91 -29
- package/scripts/stage-kernel.mjs +4 -3
- package/skill/SKILL.md +92 -39
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Generated from lib/finding-guidance.mjs; run node plugin/scripts/stage-contract.mjs
|
|
2
|
+
/** Presentation-only follow-up checks. Never changes a finding or performs work. */
|
|
3
|
+
export function nextSteps(finding) {
|
|
4
|
+
if (!finding || typeof finding !== "object") return [];
|
|
5
|
+
switch (finding.category) {
|
|
6
|
+
case "client-entry-missing":
|
|
7
|
+
return [
|
|
8
|
+
'检查该插件 package.json 的 dsh.client 与 exports["./client"] 客户端入口声明,并在实际安装制品中核对入口文件是否存在、可读且已随包交付。',
|
|
9
|
+
"把已装插件的精确版本与制品文件清单对照;如需作者处理,提供缺失入口的包内相对位置,不猜测修复版本。",
|
|
10
|
+
"补充同一目标的浏览器加载结果,并实际操作客户端功能、核验实际功能结果;静态入口发现不等于业务失败已实测。",
|
|
11
|
+
];
|
|
12
|
+
case "declared-incompatible":
|
|
13
|
+
return [
|
|
14
|
+
"复核已装插件 package.json 的 engines.dsh 与官方 peerDependencies、字段格式及目标 DSH 版本;按报告所用声明策略核对生效字段。",
|
|
15
|
+
"只读核对目标 profile 的 compatibility.json 是否有该插件名、插件精确版本与目标 DSH 精确版本的记录;未观测或未授予不等于可自行添加豁免。",
|
|
16
|
+
"补充该目标的实际加载与功能结果;声明冲突不证明已经实际加载失败,豁免也不证明业务通过。",
|
|
17
|
+
];
|
|
18
|
+
case "host-entry-inactive": {
|
|
19
|
+
const state = finding.evidence?.state;
|
|
20
|
+
const step =
|
|
21
|
+
state === "FAILED"
|
|
22
|
+
? "定位该包对应的 FAILED loader 条目,先在本机查看失败原因与同次启动日志,核对导入、初始化失败发生在哪一步。"
|
|
23
|
+
: state === "PENDING"
|
|
24
|
+
? "定位该包对应的 PENDING loader 条目,核对顶层必需 inject 与各依赖服务的提供者是否在同一宿主实际激活。"
|
|
25
|
+
: "在同一目标宿主重新读取该包映射的 loader 条目状态,核对条目身份、启用状态与采样时间,不从说明文本猜测状态。";
|
|
26
|
+
return [
|
|
27
|
+
step,
|
|
28
|
+
"补充同一 profile、DSH 版本和时间的服务观测及 loader 状态;LOADING、禁用或无法映射的条目不能代替 FAILED 或 PENDING 证据。",
|
|
29
|
+
"服务或条目状态恢复后仍需实际操作对应功能并核验结果;激活成功不能代替业务验收。",
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
case "unprovided-dependency": {
|
|
33
|
+
const absent = finding.evidence?.status === "absent";
|
|
34
|
+
const steps = [
|
|
35
|
+
absent
|
|
36
|
+
? "核对本次明确 absent 的服务观测是否属于同一 profile、DSH 版本与依赖作用域,并查看该服务的提供者是否实际激活。"
|
|
37
|
+
: "该依赖仍待确认:补充同一 profile、DSH 版本与有效采样时间的服务观测;省略键或未采样不能确认服务缺失,也不能确认插件挂起。",
|
|
38
|
+
finding.evidence?.hardInject === true
|
|
39
|
+
? "在报告给出的源码位置检查顶层必需 inject 与激活前提,再对照实际 loader 状态;依赖声明本身不能证明已经挂起。"
|
|
40
|
+
: "在报告给出的源码位置区分可选或子作用域依赖与顶层硬依赖;不推断整个插件挂起,实际操作相关功能核验影响。",
|
|
41
|
+
];
|
|
42
|
+
if (finding.evidence?.status === "unreadable")
|
|
43
|
+
steps.push(
|
|
44
|
+
"先在本机核对采样读取失败原因;只补安全的结构观测,不执行 getter 来消除未知。",
|
|
45
|
+
);
|
|
46
|
+
steps.push(
|
|
47
|
+
"核对服务提供者的包声明与实际运行状态;提供声明只是线索,不自动安装依赖或认定某包能提供该服务。",
|
|
48
|
+
);
|
|
49
|
+
return steps;
|
|
50
|
+
}
|
|
51
|
+
case "host-service-missing":
|
|
52
|
+
return [
|
|
53
|
+
"在报告给出的源码位置核对服务读取及必需 inject 的作用域,并确认 absent 观测绑定同一 profile、DSH 版本与采样时间。",
|
|
54
|
+
"核对该服务的实际提供者及 loader 状态,补充相关功能操作结果;服务缺失的推断影响不等于业务失败已实测。",
|
|
55
|
+
];
|
|
56
|
+
case "host-method-missing":
|
|
57
|
+
return [
|
|
58
|
+
"在报告给出的文件与行号核对实际方法调用、Context 绑定及能力守卫,检查是否存在报告尚未识别的兜底。",
|
|
59
|
+
"补充同一目标服务的完整方法结构观测,区分目录线索、未采样方法与明确缺失;实际操作对应功能核验推断影响。",
|
|
60
|
+
"需要替代 API 时先查该目标版本的源码或官方契约并保留出处;本报告不猜测替代方法或修复版本。",
|
|
61
|
+
];
|
|
62
|
+
case "declaration-mismatch":
|
|
63
|
+
return [
|
|
64
|
+
"对照已装插件的兼容声明、精确目标版本及报告中的具体 API 发现,分别核对声明范围与运行时证据。",
|
|
65
|
+
"在已有源码位置检查调用与守卫,并补充同一目标的服务观测和实际功能结果;声明放行不证明业务通过。",
|
|
66
|
+
];
|
|
67
|
+
default:
|
|
68
|
+
return [
|
|
69
|
+
"先核对该发现的类别、依据、已有包内位置和目标版本;缺少位置或状态时保留待确认,不补造证据。",
|
|
70
|
+
"补充同一 profile、DSH 版本与采样时间的相关观测,再实际操作对应功能核验影响;不根据未知类别猜测修复。",
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// Generated from lib/finding-review.mjs; run node plugin/scripts/stage-contract.mjs
|
|
2
|
+
/** Browser-safe manual-review contract. Reviews never modify canonical findings. */
|
|
3
|
+
export const REVIEW_STATUS_LABELS = Object.freeze({
|
|
4
|
+
pending: "待确认",
|
|
5
|
+
reviewed: "已复核",
|
|
6
|
+
"false-positive": "误报",
|
|
7
|
+
fixed: "已修复",
|
|
8
|
+
"needs-review": "待重审",
|
|
9
|
+
});
|
|
10
|
+
export const REVIEW_LIMITS = Object.freeze({
|
|
11
|
+
fileBytes: 1024 * 1024,
|
|
12
|
+
records: 1000,
|
|
13
|
+
note: 4000,
|
|
14
|
+
findings: 5000,
|
|
15
|
+
identityBytes: 128 * 1024,
|
|
16
|
+
});
|
|
17
|
+
const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
18
|
+
const hash = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
|
|
19
|
+
const safe = (x) =>
|
|
20
|
+
typeof x === "string" &&
|
|
21
|
+
!/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(
|
|
22
|
+
x,
|
|
23
|
+
);
|
|
24
|
+
function fail(message) {
|
|
25
|
+
throw Object.assign(new Error(message), { code: "REVIEW_INVALID" });
|
|
26
|
+
}
|
|
27
|
+
export function stableIdentity(value) {
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
let nodes = 0;
|
|
30
|
+
function visit(x, depth = 0) {
|
|
31
|
+
if (++nodes > 50000 || depth > 30) fail("Review identity exceeds limits");
|
|
32
|
+
if (x === null || typeof x === "string" || typeof x === "boolean") return x;
|
|
33
|
+
if (typeof x === "number" && Number.isFinite(x)) return x;
|
|
34
|
+
if (typeof x !== "object" || seen.has(x)) fail("Invalid review identity");
|
|
35
|
+
seen.add(x);
|
|
36
|
+
const out = Array.isArray(x)
|
|
37
|
+
? x.map((v) => visit(v, depth + 1))
|
|
38
|
+
: Object.fromEntries(
|
|
39
|
+
Object.keys(x)
|
|
40
|
+
.sort()
|
|
41
|
+
.filter((k) => x[k] !== undefined)
|
|
42
|
+
.map((k) => [k, visit(x[k], depth + 1)]),
|
|
43
|
+
);
|
|
44
|
+
seen.delete(x);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
const result = JSON.stringify(visit(value));
|
|
48
|
+
if (result.length > REVIEW_LIMITS.identityBytes)
|
|
49
|
+
fail("Review identity exceeds limits");
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
export function validateReviewInput(input) {
|
|
53
|
+
if (
|
|
54
|
+
!object(input) ||
|
|
55
|
+
Object.keys(input).sort().join(",") !== "contextToken,findingId,note,status"
|
|
56
|
+
)
|
|
57
|
+
fail("Invalid review fields");
|
|
58
|
+
if (!hash(input.contextToken) || !hash(input.findingId))
|
|
59
|
+
fail("Invalid review context or finding ID");
|
|
60
|
+
if (
|
|
61
|
+
!Object.hasOwn(REVIEW_STATUS_LABELS, input.status) ||
|
|
62
|
+
input.status === "needs-review"
|
|
63
|
+
)
|
|
64
|
+
fail("Invalid manual review status");
|
|
65
|
+
if (!safe(input.note) || input.note.length > REVIEW_LIMITS.note)
|
|
66
|
+
fail("Invalid review note");
|
|
67
|
+
if (input.status !== "pending" && !input.note.trim())
|
|
68
|
+
fail("Review note is required");
|
|
69
|
+
return {
|
|
70
|
+
contextToken: input.contextToken,
|
|
71
|
+
findingId: input.findingId,
|
|
72
|
+
status: input.status,
|
|
73
|
+
note: input.note.trim(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function validateReviewDocument(doc) {
|
|
77
|
+
if (
|
|
78
|
+
!object(doc) ||
|
|
79
|
+
Object.keys(doc).sort().join(",") !== "records,revision,schemaVersion" ||
|
|
80
|
+
doc.schemaVersion !== 1 ||
|
|
81
|
+
!hash(doc.revision) ||
|
|
82
|
+
!Array.isArray(doc.records) ||
|
|
83
|
+
doc.records.length > REVIEW_LIMITS.records
|
|
84
|
+
)
|
|
85
|
+
fail("Invalid review storage");
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const record of doc.records) {
|
|
88
|
+
if (
|
|
89
|
+
!object(record) ||
|
|
90
|
+
Object.keys(record).sort().join(",") !==
|
|
91
|
+
"findingId,identity,lineage,note,pluginName,pluginVersion,status,updatedAt"
|
|
92
|
+
)
|
|
93
|
+
fail("Invalid review record");
|
|
94
|
+
validateReviewInput({
|
|
95
|
+
contextToken: doc.revision,
|
|
96
|
+
findingId: record.findingId,
|
|
97
|
+
status: record.status,
|
|
98
|
+
note: record.note,
|
|
99
|
+
});
|
|
100
|
+
if (
|
|
101
|
+
!safe(record.pluginName) ||
|
|
102
|
+
!record.pluginName ||
|
|
103
|
+
record.pluginName.length > 500 ||
|
|
104
|
+
!safe(record.pluginVersion) ||
|
|
105
|
+
!record.pluginVersion ||
|
|
106
|
+
record.pluginVersion.length > 200 ||
|
|
107
|
+
typeof record.identity !== "string" ||
|
|
108
|
+
record.identity.length > REVIEW_LIMITS.identityBytes ||
|
|
109
|
+
typeof record.lineage !== "string" ||
|
|
110
|
+
record.lineage.length > 2000 ||
|
|
111
|
+
!Number.isFinite(Date.parse(record.updatedAt)) ||
|
|
112
|
+
seen.has(record.findingId)
|
|
113
|
+
)
|
|
114
|
+
fail("Invalid review record identity");
|
|
115
|
+
const binding = JSON.parse(record.identity);
|
|
116
|
+
if (
|
|
117
|
+
!object(binding) ||
|
|
118
|
+
!object(binding.scope) ||
|
|
119
|
+
!object(binding.finding) ||
|
|
120
|
+
binding.pluginName !== record.pluginName ||
|
|
121
|
+
binding.pluginVersion !== record.pluginVersion ||
|
|
122
|
+
!safe(binding.finding.category) ||
|
|
123
|
+
!binding.finding.category ||
|
|
124
|
+
[
|
|
125
|
+
"profileId",
|
|
126
|
+
"originalPath",
|
|
127
|
+
"mode",
|
|
128
|
+
"toolVersion",
|
|
129
|
+
"dshVersion",
|
|
130
|
+
"rulesIdentity",
|
|
131
|
+
"catalogIdentity",
|
|
132
|
+
"sourceIdentity",
|
|
133
|
+
].some((key) => !safe(binding.scope[key]) || !binding.scope[key]) ||
|
|
134
|
+
stableIdentity(binding) !== record.identity ||
|
|
135
|
+
stableIdentity([
|
|
136
|
+
record.pluginName,
|
|
137
|
+
binding.finding.category,
|
|
138
|
+
binding.finding.service ?? null,
|
|
139
|
+
binding.finding.method ?? null,
|
|
140
|
+
]) !== record.lineage
|
|
141
|
+
)
|
|
142
|
+
fail("Invalid review binding");
|
|
143
|
+
seen.add(record.findingId);
|
|
144
|
+
}
|
|
145
|
+
return doc;
|
|
146
|
+
}
|
|
147
|
+
/** Validate an optional API sidecar without trusting it to be a renderable review. */
|
|
148
|
+
export function validateReviewEnvelope(value, report) {
|
|
149
|
+
try {
|
|
150
|
+
const keys = (x, want) =>
|
|
151
|
+
object(x) && Object.keys(x).sort().join(",") === want;
|
|
152
|
+
const text = (x, max) => safe(x) && x.length > 0 && x.length <= max;
|
|
153
|
+
const note = (x) => safe(x) && x.length <= REVIEW_LIMITS.note;
|
|
154
|
+
const manual = (x) =>
|
|
155
|
+
Object.hasOwn(REVIEW_STATUS_LABELS, x) && x !== "needs-review";
|
|
156
|
+
const record = (r, summary = false) =>
|
|
157
|
+
keys(
|
|
158
|
+
r,
|
|
159
|
+
summary
|
|
160
|
+
? "findingId,note,noteTruncated,pluginName,pluginVersion,status,updatedAt"
|
|
161
|
+
: "findingId,note,pluginName,pluginVersion,status,updatedAt",
|
|
162
|
+
) &&
|
|
163
|
+
hash(r.findingId) &&
|
|
164
|
+
text(r.pluginName, 500) &&
|
|
165
|
+
text(r.pluginVersion, 200) &&
|
|
166
|
+
manual(r.status) &&
|
|
167
|
+
note(r.note) &&
|
|
168
|
+
(r.status === "pending" ||
|
|
169
|
+
r.note.trim().length > 0 ||
|
|
170
|
+
(summary && r.noteTruncated)) &&
|
|
171
|
+
text(r.updatedAt, 40) &&
|
|
172
|
+
Number.isFinite(Date.parse(r.updatedAt)) &&
|
|
173
|
+
(!summary ||
|
|
174
|
+
(typeof r.noteTruncated === "boolean" && r.note.length <= 200));
|
|
175
|
+
if (
|
|
176
|
+
!keys(value, "contextToken,error,history,items,revision,schemaVersion") ||
|
|
177
|
+
value.schemaVersion !== 1 ||
|
|
178
|
+
!Array.isArray(value.items) ||
|
|
179
|
+
value.items.length > REVIEW_LIMITS.findings ||
|
|
180
|
+
!Array.isArray(value.history) ||
|
|
181
|
+
value.history.length > REVIEW_LIMITS.records ||
|
|
182
|
+
!(value.error === null || text(value.error, 4000))
|
|
183
|
+
)
|
|
184
|
+
return false;
|
|
185
|
+
if (value.error && value.contextToken === null && value.revision === null)
|
|
186
|
+
return value.items.length === 0 && value.history.length === 0;
|
|
187
|
+
if (!hash(value.contextToken) || !hash(value.revision)) return false;
|
|
188
|
+
const seenSlots = new Set(),
|
|
189
|
+
seenHistory = new Set();
|
|
190
|
+
for (const item of value.items) {
|
|
191
|
+
if (
|
|
192
|
+
!keys(
|
|
193
|
+
item,
|
|
194
|
+
"findingId,findingIndex,history,historyCount,note,pluginName,pluginVersion,record,reviewReason,status",
|
|
195
|
+
) ||
|
|
196
|
+
!hash(item.findingId) ||
|
|
197
|
+
!text(item.pluginName, 500) ||
|
|
198
|
+
!text(item.pluginVersion, 200) ||
|
|
199
|
+
!Number.isSafeInteger(item.findingIndex) ||
|
|
200
|
+
item.findingIndex < 0 ||
|
|
201
|
+
!Object.hasOwn(REVIEW_STATUS_LABELS, item.status) ||
|
|
202
|
+
!note(item.note) ||
|
|
203
|
+
!Array.isArray(item.history) ||
|
|
204
|
+
item.history.length > 3 ||
|
|
205
|
+
!Number.isSafeInteger(item.historyCount) ||
|
|
206
|
+
item.historyCount < item.history.length ||
|
|
207
|
+
item.historyCount > REVIEW_LIMITS.records ||
|
|
208
|
+
!(item.reviewReason === null || text(item.reviewReason, 4000))
|
|
209
|
+
)
|
|
210
|
+
return false;
|
|
211
|
+
const slot = JSON.stringify([item.pluginName, item.findingIndex]);
|
|
212
|
+
if (seenSlots.has(slot)) return false;
|
|
213
|
+
seenSlots.add(slot);
|
|
214
|
+
if (report) {
|
|
215
|
+
const plugin = report.plugins?.[item.pluginName];
|
|
216
|
+
if (
|
|
217
|
+
!plugin ||
|
|
218
|
+
plugin.version !== item.pluginVersion ||
|
|
219
|
+
!Array.isArray(plugin.findings) ||
|
|
220
|
+
item.findingIndex >= plugin.findings.length
|
|
221
|
+
)
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (item.record !== null) {
|
|
225
|
+
if (
|
|
226
|
+
!record(item.record) ||
|
|
227
|
+
item.record.findingId !== item.findingId ||
|
|
228
|
+
item.record.pluginName !== item.pluginName ||
|
|
229
|
+
item.record.pluginVersion !== item.pluginVersion ||
|
|
230
|
+
item.record.status !== item.status ||
|
|
231
|
+
item.record.note !== item.note
|
|
232
|
+
)
|
|
233
|
+
return false;
|
|
234
|
+
} else if (
|
|
235
|
+
item.note !== "" ||
|
|
236
|
+
!["pending", "needs-review"].includes(item.status)
|
|
237
|
+
)
|
|
238
|
+
return false;
|
|
239
|
+
if (
|
|
240
|
+
item.status === "needs-review" &&
|
|
241
|
+
(!item.historyCount || !item.reviewReason)
|
|
242
|
+
)
|
|
243
|
+
return false;
|
|
244
|
+
if (item.historyCount > 0 && !item.reviewReason) return false;
|
|
245
|
+
const local = new Set();
|
|
246
|
+
for (const previous of item.history) {
|
|
247
|
+
if (
|
|
248
|
+
!record(previous, true) ||
|
|
249
|
+
previous.findingId === item.findingId ||
|
|
250
|
+
previous.pluginName !== item.pluginName ||
|
|
251
|
+
local.has(previous.findingId)
|
|
252
|
+
)
|
|
253
|
+
return false;
|
|
254
|
+
local.add(previous.findingId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const previous of value.history) {
|
|
258
|
+
if (!record(previous) || seenHistory.has(previous.findingId))
|
|
259
|
+
return false;
|
|
260
|
+
seenHistory.add(previous.findingId);
|
|
261
|
+
}
|
|
262
|
+
if (report) {
|
|
263
|
+
let count = 0;
|
|
264
|
+
for (const plugin of Object.values(report.plugins || {})) {
|
|
265
|
+
if (!Array.isArray(plugin.findings)) return false;
|
|
266
|
+
count += plugin.findings.length;
|
|
267
|
+
}
|
|
268
|
+
if (count !== value.items.length) return false;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Exact identity includes evidence; lineage only offers old records as review history. */
|
|
276
|
+
export function combineReviews(
|
|
277
|
+
context,
|
|
278
|
+
records,
|
|
279
|
+
{ revision = "0".repeat(64), error = null } = {},
|
|
280
|
+
) {
|
|
281
|
+
const byId = new Map(),
|
|
282
|
+
byLineage = new Map(),
|
|
283
|
+
currentIds = new Set(context.findings.map((f) => f.findingId));
|
|
284
|
+
const publicRecord = (r) => ({
|
|
285
|
+
findingId: r.findingId,
|
|
286
|
+
pluginName: r.pluginName,
|
|
287
|
+
pluginVersion: r.pluginVersion,
|
|
288
|
+
status: r.status,
|
|
289
|
+
note: r.note,
|
|
290
|
+
updatedAt: r.updatedAt,
|
|
291
|
+
});
|
|
292
|
+
for (const record of records) {
|
|
293
|
+
byId.set(record.findingId, record);
|
|
294
|
+
if (!byLineage.has(record.lineage)) byLineage.set(record.lineage, []);
|
|
295
|
+
byLineage.get(record.lineage).push(record);
|
|
296
|
+
}
|
|
297
|
+
const items = context.findings.map((entry) => {
|
|
298
|
+
const exact = byId.get(entry.findingId);
|
|
299
|
+
const record =
|
|
300
|
+
exact?.identity === entry.identity ? publicRecord(exact) : null;
|
|
301
|
+
const previous = byLineage.get(entry.lineage) || [];
|
|
302
|
+
const historyCount = previous.length - (exact ? 1 : 0);
|
|
303
|
+
// Keep row summaries bounded; the independent history retains each full note once.
|
|
304
|
+
const history = [];
|
|
305
|
+
for (let i = previous.length - 1; i >= 0 && history.length < 3; i--) {
|
|
306
|
+
if (previous[i].findingId === entry.findingId) continue;
|
|
307
|
+
const old = publicRecord(previous[i]);
|
|
308
|
+
history.push({
|
|
309
|
+
...old,
|
|
310
|
+
note: old.note.slice(0, 200),
|
|
311
|
+
noteTruncated: old.note.length > 200,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
findingId: entry.findingId,
|
|
316
|
+
pluginName: entry.pluginName,
|
|
317
|
+
pluginVersion: entry.pluginVersion,
|
|
318
|
+
findingIndex: entry.findingIndex,
|
|
319
|
+
status: record?.status || (historyCount ? "needs-review" : "pending"),
|
|
320
|
+
note: record?.note || "",
|
|
321
|
+
record,
|
|
322
|
+
history,
|
|
323
|
+
historyCount,
|
|
324
|
+
reviewReason: historyCount
|
|
325
|
+
? "插件源码或版本、规则、目录、工具、观察目标或发现证据已变化;旧处置仅供参考,需要重新核实。"
|
|
326
|
+
: null,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
contextToken: context.contextToken,
|
|
332
|
+
revision,
|
|
333
|
+
error,
|
|
334
|
+
items,
|
|
335
|
+
history: records
|
|
336
|
+
.filter((r) => !currentIds.has(r.findingId))
|
|
337
|
+
.map(publicRecord),
|
|
338
|
+
};
|
|
339
|
+
}
|