@noob-stupid/dsh-plugin-console 0.5.33 → 0.5.35

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.
@@ -0,0 +1,241 @@
1
+ // L1 · domain —— compat-verdict.js(适配门结论的**判据版本戳** + 过期结论的读取代发重算;0.5.34 加法)
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
+ //
4
+ // ── 真机背景(2026-10-01,用户实测)────────────────────────────────────────────────
5
+ // `~/.dsh/plugin-console/compat-pending.json` 里 `dshmarket` 的记录仍是
6
+ // `check:"fail"` + `checkNote:"源码仍引用 0.1.2 起已删除的 dsh-settings API(…)——实际不兼容…"`
7
+ // —— 那是 **0.5.33 收紧判据之前**的误报结论(真机复核:现在 0 命中)。面板照旧把它当事实展示
8
+ // (「实际不兼容,启用会让整个服务启动崩溃」+「待适配」永久钉住),**持续误导用户**。
9
+ //
10
+ // ── 本模块只做两件事(**不放宽任何判据**)──────────────────────────────────────────
11
+ // ① 给结论打**判据版本戳**:记录里的 `scanVerdict.version` 标明"这条结论是哪一版判据算的"
12
+ // (戳来自 settings-api-scan.js 的 `SCAN_CRITERIA_VERSION`,判据改一次就要改它)。
13
+ // ② **读取代发**:读清单时若记录的戳落后于当前判据 → 用**当前判据只读重算**
14
+ // · 结论变了 → 回写(`check` / `checkNote` / `scanVerdict`,含 previous + rescinded + 来源证据)
15
+ // · 结论没变 → **零写盘**(幂等;同一条记录不会每次读都写盘)
16
+ // · 包解析不到 → 不猜:如实标注"无法复核"(保留旧结论,绝不假装成当前判据的结论)
17
+ // `fail` 永远不会因为"戳旧"就被改成 `pass` —— 只有**真的用当前判据重算**出非 fail 才算撤回;
18
+ // 真不兼容的(重算仍 fail)一个字都不动。
19
+ //
20
+ // 行数预算:独立模块,留在 600 行架构硬顶内。
21
+
22
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
23
+ import { dirname, join } from 'node:path'
24
+ import { dshHome } from '../infra/paths.js'
25
+ import { SCAN_CRITERIA_VERSION } from './settings-api-scan.js'
26
+
27
+ /**
28
+ * 进程内重算备忘(纯性能,不影响判据):"结论未变 ⇒ 不写盘"意味着记录上的判据戳**一直是旧的**,
29
+ * 于是每次 `GET /state`(面板每隔几秒轮询一次)都会对同一批包重跑源码扫描(单包上限 120 文件 / 400KB)。
30
+ * 键含包路径 + 版本 + package.json 的 mtime:包一变(升级/改源码)就重算,包没变就复用。
31
+ */
32
+ const VERDICT_MEMO = new Map()
33
+ const MEMO_LIMIT = 200
34
+
35
+ /** 判据版本戳(当前判据的版本;判据变了就跟着 settings-api-scan.js 一起变)。 */
36
+ const SCANNER_VERSION = SCAN_CRITERIA_VERSION
37
+
38
+ /** 适配门清单文件(原在 compat.js;0.5.34 搬到本模块,供"读取代发"一处收口)。 */
39
+ const compatPendingFile = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
40
+
41
+ /**
42
+ * 「这条禁用记录是我们自己写进去的」判据(纯函数)。
43
+ * 只有这类记录才允许在**误报被撤回**时给用户一键解锁入口 —— 用户手写的禁用块不在此列。
44
+ */
45
+ const SELF_INFLICTED_SOURCES = new Set([
46
+ 'preflight-disabled-before-upgrade', // 框架升级前的预扫(dshmarket 就是这条)
47
+ 'boot-quarantine', // 启动失败隔离
48
+ 'auto-preflight', // 指纹变化自动预检
49
+ ])
50
+ function isSelfInflictedSource(source) {
51
+ return typeof source === 'string' && SELF_INFLICTED_SOURCES.has(source)
52
+ }
53
+
54
+ /** 记录上的判据戳(缺失 = 旧时代结论:必须重算)。 */
55
+ function stampOf(record) {
56
+ const s = record?.scanVerdict
57
+ if (s === null || typeof s !== 'object') return { version: null, decision: null, reason: null, at: null, rescinded: false, evidence: null }
58
+ return {
59
+ version: typeof s.version === 'string' ? s.version : null,
60
+ decision: typeof s.decision === 'string' ? s.decision : null,
61
+ reason: typeof s.reason === 'string' ? s.reason : null,
62
+ at: Number.isFinite(s.at) ? s.at : null,
63
+ rescinded: s.rescinded === true,
64
+ evidence: typeof s.evidence === 'string' ? s.evidence : null,
65
+ }
66
+ }
67
+
68
+ /** 这条记录的结论是不是"旧判据算的"(纯函数)。 */
69
+ function isStaleVerdict(record, version = SCANNER_VERSION) {
70
+ return stampOf(record).version !== version
71
+ }
72
+
73
+ /**
74
+ * 纯判据:给一组记录算出「哪些结论过期、用当前判据重算后是什么」(IO 全由 `recompute` 注入)。
75
+ * `recompute(record)` → `{ decision, reason }`;解析不到包时返回 `{ unresolved: '原因' }`。
76
+ * 返回每条的 `{ rowId, status, decision, reason, storedDecision, storedNote, rescinded, evidence }`,
77
+ * status ∈ fresh(戳已是最新,无需重算)/ updated(结论变了 → 调用方回写)/ unchanged(结论没变 → **零写盘**)
78
+ * / unresolved(复核不了 → 如实标注,保留旧结论)。
79
+ */
80
+ function planVerdictRefresh(records, { version = SCANNER_VERSION, recompute } = {}) {
81
+ const out = []
82
+ for (const rec of (Array.isArray(records) ? records : [])) {
83
+ if (rec === null || typeof rec !== 'object' || typeof rec.rowId !== 'string' || rec.rowId === '') continue
84
+ const storedDecision = typeof rec.check === 'string' && rec.check !== '' ? rec.check : null
85
+ const storedNote = typeof rec.checkNote === 'string' ? rec.checkNote : null
86
+ const stamp = stampOf(rec)
87
+ if (stamp.version === version) {
88
+ out.push({ rowId: rec.rowId, status: 'fresh', decision: storedDecision, reason: stamp.reason ?? storedNote, storedDecision, storedNote, rescinded: stamp.rescinded, evidence: stamp.evidence, version })
89
+ continue
90
+ }
91
+ // 没给重算器(或明确要求只读)→ 不猜:标成"过期、未复核"
92
+ if (typeof recompute !== 'function') {
93
+ out.push({ rowId: rec.rowId, status: 'stale', decision: storedDecision, reason: storedNote, storedDecision, storedNote, rescinded: false, evidence: null, version: null })
94
+ continue
95
+ }
96
+ let next = null
97
+ try {
98
+ next = recompute(rec)
99
+ } catch (error) {
100
+ next = { unresolved: String(error?.message ?? error) }
101
+ }
102
+ if (next === null || next === undefined) next = { unresolved: '重算没有返回结论' }
103
+ if (typeof next.unresolved === 'string' && next.unresolved !== '') {
104
+ out.push({ rowId: rec.rowId, status: 'unresolved', decision: storedDecision, reason: storedNote, storedDecision, storedNote, rescinded: false, evidence: null, version: null, unresolved: next.unresolved })
105
+ continue
106
+ }
107
+ const decision = typeof next.decision === 'string' && next.decision !== '' ? next.decision : 'unknown'
108
+ const reason = typeof next.reason === 'string' ? next.reason : null
109
+ out.push({
110
+ rowId: rec.rowId,
111
+ status: decision === storedDecision ? 'unchanged' : 'updated',
112
+ decision,
113
+ reason,
114
+ storedDecision,
115
+ storedNote,
116
+ // 「旧判据判 fail,当前判据不再 fail」= 误报被撤回(这是唯一允许解除"待适配"的情形)
117
+ rescinded: storedDecision === 'fail' && decision !== 'fail',
118
+ evidence: isSelfInflictedSource(rec.source) ? String(rec.source) : null,
119
+ version,
120
+ })
121
+ }
122
+ return out
123
+ }
124
+
125
+ /**
126
+ * 读取代发(IO 版):把 `planVerdictRefresh` 的结论落到记录上,**结论变了才写盘**(写后读回核实)。
127
+ * `deps`:`{ resolvePkg, readPkg, check, writeText, readText, now }` —— 全部可注入(离线测试)。
128
+ * 返回 `{ version, entries, updated, unchanged, unresolved, fresh, wrote, writeError, file }`。
129
+ */
130
+ function refreshCompatPendingVerdicts({ pending, baseDir, file = null, deps = {} } = {}) {
131
+ const {
132
+ resolvePkg, readPkg, check, now = () => Date.now(), stat = statSync,
133
+ readText = (p) => readFileSync(p, 'utf8'), writeText = (p, data) => writeFileSync(p, data, 'utf8'),
134
+ memo = VERDICT_MEMO,
135
+ } = deps
136
+ const target = file ?? compatPendingFile()
137
+ const records = Array.isArray(pending?.pending) ? pending.pending : []
138
+ const fwVer = typeof pending?.frameworkVersion === 'string' ? pending.frameworkVersion : null
139
+ const recompute = (rec) => {
140
+ if (typeof rec.moduleName !== 'string' || rec.moduleName === '') return { unresolved: '记录里没有 moduleName(无法定位已装包)' }
141
+ if (typeof resolvePkg !== 'function' || typeof readPkg !== 'function' || typeof check !== 'function') return { unresolved: '读取代发缺少解析器(内部接线问题)' }
142
+ let pkgPath = null
143
+ try { pkgPath = resolvePkg(rec.moduleName, baseDir) } catch (error) { return { unresolved: '解析包失败:' + String(error?.message ?? error) } }
144
+ if (pkgPath === null || pkgPath === undefined) return { unresolved: `本机解析不到 ${rec.moduleName}` }
145
+ let pkg = null
146
+ try { pkg = readPkg(pkgPath) } catch (error) { return { unresolved: '读取包信息失败:' + String(error?.message ?? error) } }
147
+ if (pkg === null || pkg === undefined) return { unresolved: '读取包信息失败' }
148
+ const mtimeMs = (() => { try { return stat(pkgPath).mtimeMs } catch { return 0 } })()
149
+ const key = `${rec.rowId}|${pkgPath}|${pkg.version ?? ''}|${mtimeMs}|${SCANNER_VERSION}`
150
+ if (memo instanceof Map && memo.has(key)) return memo.get(key)
151
+ const verdict = check(pkg, fwVer ?? '?', dirname(pkgPath))
152
+ const out = { decision: verdict?.decision ?? 'unknown', reason: verdict?.reason ?? null }
153
+ if (memo instanceof Map) {
154
+ if (memo.size >= MEMO_LIMIT) memo.clear()
155
+ memo.set(key, out)
156
+ }
157
+ return out
158
+ }
159
+ const entries = planVerdictRefresh(records, { recompute })
160
+ const byRow = new Map(entries.map((e) => [e.rowId, e]))
161
+ const updated = []
162
+ const at = now()
163
+ for (const rec of records) {
164
+ const e = byRow.get(rec?.rowId)
165
+ if (e === undefined || e.status !== 'updated') continue
166
+ const previous = { decision: e.storedDecision, note: e.storedNote }
167
+ rec.check = e.decision
168
+ rec.checkNote = e.reason
169
+ rec.scanVerdict = { version: SCANNER_VERSION, decision: e.decision, reason: e.reason, at, previous, rescinded: e.rescinded, evidence: e.evidence }
170
+ rec.verdictUpdatedAt = at
171
+ updated.push({ rowId: e.rowId, from: e.storedDecision, to: e.decision, rescinded: e.rescinded, evidence: e.evidence })
172
+ }
173
+ const result = {
174
+ version: SCANNER_VERSION,
175
+ file: target,
176
+ entries,
177
+ updated,
178
+ unchanged: entries.filter((e) => e.status === 'unchanged').map((e) => e.rowId),
179
+ unresolved: entries.filter((e) => e.status === 'unresolved').map((e) => ({ rowId: e.rowId, reason: e.unresolved })),
180
+ fresh: entries.filter((e) => e.status === 'fresh').map((e) => e.rowId),
181
+ wrote: false,
182
+ writeError: null,
183
+ }
184
+ if (updated.length === 0) return result // ② 结论未变(或无可重算)→ **零写盘**
185
+ const payload = { ...pending, pending: records, updatedAt: new Date(at).toISOString() }
186
+ try {
187
+ mkdirSync(dirname(target), { recursive: true })
188
+ writeText(target, JSON.stringify(payload, null, 2), 'utf8')
189
+ } catch (error) {
190
+ result.writeError = '回写适配门清单失败:' + String(error?.message ?? error)
191
+ return result
192
+ }
193
+ // 读回核实:只有真的落盘且内容一致才算写成功(绝不谎报"已更正")
194
+ try {
195
+ const back = JSON.parse(readText(target))
196
+ const rec = (back?.pending ?? []).find((p) => p.rowId === updated[0].rowId)
197
+ const ok = rec !== undefined && rec.check === updated[0].to && stampOf(rec).version === SCANNER_VERSION
198
+ if (!ok) result.writeError = '回写后读回内容与期望不一致(结论没真正落盘)'
199
+ } catch (error) {
200
+ result.writeError = '回写后读回失败:' + String(error?.message ?? error)
201
+ }
202
+ result.wrote = result.writeError === null
203
+ return result
204
+ }
205
+
206
+ /**
207
+ * 一条记录对外的"结论视图"(面板/门控摘要用):当前判据下的结论 + 是否过期 + 是否已撤回。
208
+ * `entry` = `refreshCompatPendingVerdicts()` 的 `entries` 里对应那条(可省 → 只看记录上的判据戳)。
209
+ */
210
+ function verdictView(record, entry = null) {
211
+ const stamp = stampOf(record)
212
+ const status = entry?.status ?? (stamp.version === SCANNER_VERSION ? 'fresh' : 'stale')
213
+ const evidence = entry?.evidence ?? stamp.evidence ?? (isSelfInflictedSource(record?.source) ? String(record.source) : null)
214
+ if (status === 'updated') {
215
+ return { decision: entry.decision, reason: entry.reason, version: entry.version, stale: false, rescinded: entry.rescinded === true, evidence, unresolved: null, status, previous: { decision: entry.storedDecision, note: entry.storedNote } }
216
+ }
217
+ if (status === 'fresh') {
218
+ return { decision: stamp.decision ?? (typeof record?.check === 'string' ? record.check : null), reason: stamp.reason ?? (typeof record?.checkNote === 'string' ? record.checkNote : null), version: stamp.version, stale: false, rescinded: stamp.rescinded, evidence, unresolved: null, status }
219
+ }
220
+ if (status === 'unchanged') {
221
+ // 结论与旧判据一致(已用当前判据复核过,但**不写盘**):结论可信,判据戳仍是旧的
222
+ return { decision: entry.decision, reason: entry.reason, version: entry.version, stale: true, rescinded: false, evidence, unresolved: null, status }
223
+ }
224
+ if (status === 'unresolved') {
225
+ return { decision: entry.decision, reason: entry.reason, version: null, stale: true, rescinded: false, evidence: null, unresolved: entry.unresolved, status }
226
+ }
227
+ // 'stale':本轮没有重算(没给基准/只读)→ 如实标"旧判据、未复核"
228
+ return { decision: stamp.decision ?? (typeof record?.check === 'string' ? record.check : null), reason: stamp.reason ?? (typeof record?.checkNote === 'string' ? record.checkNote : null), version: stamp.version, stale: true, rescinded: stamp.rescinded, evidence, unresolved: null, status: 'stale' }
229
+ }
230
+
231
+ export {
232
+ SCANNER_VERSION,
233
+ SELF_INFLICTED_SOURCES,
234
+ compatPendingFile,
235
+ isSelfInflictedSource,
236
+ isStaleVerdict,
237
+ planVerdictRefresh,
238
+ refreshCompatPendingVerdicts,
239
+ stampOf,
240
+ verdictView,
241
+ }
@@ -13,6 +13,8 @@ import { listEntries } from './runtime.js'
13
13
  import { fetchJsonUrl } from '../infra/http.js'
14
14
  import { dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
15
15
  import { semverRangeMatch, semverRangeMatchLoose } from '../infra/semver.js'
16
+ // 0.5.34 加法:适配门结论的**判据版本戳** + 过期结论的读取代发重算(见该模块头注释)。
17
+ import { SCANNER_VERSION, compatPendingFile, isSelfInflictedSource, planVerdictRefresh, refreshCompatPendingVerdicts, verdictView } from './compat-verdict.js'
16
18
  // 0.5.33 改错:已删除 API 的源码扫描搬进独立 domain 模块(判据收紧为"真的从 dsh-settings 绑定进来")。
17
19
  // 这三个名字继续由本模块导出,对外契约不变。
18
20
  import { REMOVED_SETTINGS_SYMBOLS, referencesRemovedSymbol, scanSettingsApiUsage } from './settings-api-scan.js'
@@ -82,8 +84,6 @@ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
82
84
  // 支持 0.1.x 系列(0.1.0 / 0.1.1 等);0.2 / 1.0 等破坏性大版本才标记不支持
83
85
  const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.\d+/u
84
86
 
85
- const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
86
-
87
87
  /**
88
88
  * 启用前冒烟检查(服务永不崩机制):在独立子进程中动态 import 插件主模块,
89
89
  * 捕获 loader 会遇到的 import/resolution 错误(SyntaxError、缺失导出、模块缺失)。
@@ -126,7 +126,49 @@ function probePluginImport(moduleName, profileDir) {
126
126
  })
127
127
  }
128
128
 
129
- /** 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 null。 */
129
+ /** 适配门清单文件(0.5.34 起由 compat-verdict.js 定义,供"读取代发"一处收口)。 */
130
+ const COMPAT_PENDING_FILE = compatPendingFile
131
+
132
+ /**
133
+ * 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 `{ pending: null, ... }`。
134
+ *
135
+ * 0.5.34 加法(**读取代发**):给 `baseDir`(当前 profile 目录)时,对**判据戳落后**的记录
136
+ * 用当前判据只读重算:结论变了才回写(幂等:结论不变**零写盘**),解析不到包就如实标注"无法复核"。
137
+ * 真不兼容的(重算仍 fail)一个字都不动 —— 这条只解决"过期结论仍当事实展示",不放宽任何判据。
138
+ * 返回 `{ pending, refresh, verdicts }`:`verdicts` = rowId → 结论视图(面板/门控摘要用)。
139
+ */
140
+ function readCompatPendingWithVerdicts({ baseDir = null, deps = {} } = {}) {
141
+ let raw = null
142
+ try { raw = JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return { pending: null, refresh: null, verdicts: new Map() } }
143
+ if (raw === null || typeof raw !== 'object') return { pending: null, refresh: null, verdicts: new Map() }
144
+ let refresh = null
145
+ if (baseDir !== null && baseDir !== undefined && Array.isArray(raw.pending) && raw.pending.length > 0) {
146
+ try {
147
+ refresh = refreshCompatPendingVerdicts({
148
+ pending: raw,
149
+ baseDir,
150
+ deps: {
151
+ resolvePkg: (name, base) => resolvePackageJson(name, base),
152
+ readPkg: (p) => JSON.parse(readFileSync(p, 'utf8')),
153
+ check: checkPluginFrameworkCompat,
154
+ ...deps,
155
+ },
156
+ })
157
+ } catch (error) {
158
+ // 复核本身出问题也必须如实回报(不静默):结论按记录原样展示,附上失败原因
159
+ refresh = { version: SCANNER_VERSION, entries: [], updated: [], unchanged: [], unresolved: [], fresh: [], wrote: false, writeError: '读取代发重算失败:' + String(error?.message ?? error), file: COMPAT_PENDING_FILE() }
160
+ }
161
+ }
162
+ const entries = Array.isArray(refresh?.entries) ? refresh.entries : planVerdictRefresh(raw.pending)
163
+ const verdicts = new Map()
164
+ for (const rec of raw.pending) {
165
+ if (rec === null || typeof rec !== 'object' || typeof rec.rowId !== 'string') continue
166
+ verdicts.set(rec.rowId, verdictView(rec, entries.find((e) => e.rowId === rec.rowId) ?? null))
167
+ }
168
+ return { pending: raw, refresh, verdicts }
169
+ }
170
+
171
+ /** 读取清单(原契约不变:只读、不做复核)。需要"读取代发"的路径用 readCompatPendingWithVerdicts。 */
130
172
  function readCompatPending() {
131
173
  try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
132
174
  }
@@ -283,15 +325,18 @@ async function detectCompat(baseUrl) {
283
325
  }
284
326
 
285
327
  /** 待适配行的「现在是否已适配」检测(只提示,不自动解锁——用户定案:我点才开)。
286
- * 返回 Map<rowId, {version, check, note}>:仅当版本已变化且源码扫描不再 fail 时才算可解锁。 */
328
+ * 返回 Map<rowId, {version, check, note, basis}>:可解锁有两种**互不放松**的来路 ——
329
+ * ① `version-changed`:插件真的更新过(版本 ≠ 记录版本)且源码扫描不再 fail(原判据,不变);
330
+ * ② `verdict-rescinded`(0.5.34 加法):记录里的 `fail` 是**旧判据的误报**,当前判据重算后不再 fail。
331
+ * 真不兼容(重算仍 fail)**一个都不放行**;②也不需要"版本变化"——否则用户会被永久钉在「待适配」。 */
287
332
  function detectAdoptablePending(ports) {
288
333
  const out = new Map()
289
- const pending = readCompatPending()
334
+ let profileDir = null
335
+ try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
336
+ const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
290
337
  if (pending === null) return out
291
338
  const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : null
292
339
  if (fwVer === null) return out
293
- let profileDir = null
294
- try { profileDir = dirname(findPatchPath(ports)) } catch { return out }
295
340
  for (const p of (pending.pending ?? []).filter((x) => (x.status ?? 'pending') === 'pending')) {
296
341
  try {
297
342
  const pkgPath = resolvePackageJson(p.moduleName, profileDir)
@@ -300,9 +345,18 @@ function detectAdoptablePending(ports) {
300
345
  const version = typeof pkg.version === 'string' ? pkg.version : null
301
346
  const changed = version !== null && version !== p.version
302
347
  const check = checkPluginFrameworkCompat(pkg, fwVer, dirname(pkgPath))
303
- if (changed && check.decision !== 'fail') {
304
- out.set(p.rowId, { version, check: check.decision, note: check.reason ?? null })
305
- }
348
+ if (check.decision === 'fail') continue
349
+ const view = verdicts.get(p.rowId) ?? null
350
+ const rescinded = view?.rescinded === true
351
+ if (!changed && !rescinded) continue
352
+ out.set(p.rowId, {
353
+ version,
354
+ check: check.decision,
355
+ note: check.reason ?? null,
356
+ basis: changed ? 'version-changed' : 'verdict-rescinded',
357
+ previousVerdict: view?.storedDecision ?? null,
358
+ evidence: view?.evidence ?? null,
359
+ })
306
360
  } catch {}
307
361
  }
308
362
  return out
@@ -317,8 +371,11 @@ function detectAdoptablePending(ports) {
317
371
  *
318
372
  * pending —— 当前被拦下/待解锁的行(含原因、来源、是否已通过源码扫描可解锁)
319
373
  * adopted —— 曾经被拦、现已适配解锁的行(让用户能核对"到底动过哪些行")
374
+ *
375
+ * 0.5.34 加法:每行多带 `verdict`(当前判据下的结论视图:`stale` / `rescinded` / `unresolved`)——
376
+ * 旧判据的结论不许再被当成事实展示;**复核不了**(包解析不到)的行在 `note` 上如实标注。
320
377
  */
321
- function gatingSummary(ports, compatPending, ctxEntries = null) {
378
+ function gatingSummary(ports, compatPending, ctxEntries = null, verdicts = null) {
322
379
  const rec = compatPending ?? { frameworkVersion: null, upgradeFrom: null, pending: [] }
323
380
  const all = Array.isArray(rec.pending) ? rec.pending : []
324
381
  let info = new Map()
@@ -329,11 +386,14 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
329
386
  for (const e of Array.isArray(ctxEntries) ? ctxEntries : []) {
330
387
  if (typeof e?.rowId === 'string' && e.rowId !== '') byRow.set(e.rowId, e)
331
388
  }
389
+ const byVerdict = verdicts instanceof Map ? verdicts : new Map()
332
390
  const row = (p) => {
333
391
  const meta = info.get(p.rowId) ?? null
334
392
  const entry = byRow.get(p.rowId) ?? null
335
393
  const moduleName = p.moduleName ?? meta?.moduleName ?? entry?.moduleName ?? null
336
394
  const name = moduleName ?? p.rowId ?? '(未知行)'
395
+ const verdict = byVerdict.get(p.rowId) ?? verdictView(p)
396
+ const note = p.checkNote ?? null
337
397
  return {
338
398
  rowId: p.rowId ?? null,
339
399
  name,
@@ -342,12 +402,16 @@ function gatingSummary(ports, compatPending, ctxEntries = null) {
342
402
  version: p.version ?? meta?.version ?? entry?.version ?? null,
343
403
  status: p.status ?? 'pending',
344
404
  check: p.check ?? null,
345
- note: p.checkNote ?? null,
405
+ // 复核不了的行:**如实标注**"旧判据结论、未复核"(不许继续当事实;也不猜新结论)
406
+ note: verdict.unresolved === null || verdict.unresolved === undefined
407
+ ? note
408
+ : `${note ?? ''}(旧判据结论,未能复核:${verdict.unresolved})`,
346
409
  source: p.source ?? null,
347
410
  adoptedAt: p.adoptedAt ?? null,
348
411
  forcedAt: p.forcedAt ?? null,
349
412
  adoptedBy: p.adoptedBy ?? null,
350
413
  enabled: entry === null ? null : entry.enabled === true,
414
+ verdict,
351
415
  }
352
416
  }
353
417
  const isPending = (p) => (p.status ?? 'pending') === 'pending'
@@ -391,9 +455,10 @@ function rowIdModuleMap(ports) {
391
455
  /**
392
456
  * 框架升级适配门:安装/更新完成后自动校验兼容性,
393
457
  * 通过(版本已变化 + 未在声明/依赖层面明确不兼容)则移除补丁禁用块并解锁启用。
458
+ * 0.5.34:走**读取代发**(`profileDir` 已知)—— 顺手把过期判据的结论复核/回写,避免旧误报继续挂在这里。
394
459
  */
395
460
  async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patchPath, ports }) {
396
- const pending = readCompatPending()
461
+ const { pending } = readCompatPendingWithVerdicts({ baseDir: profileDir })
397
462
  if (!pending || !Array.isArray(pending.pending)) return { ran: false }
398
463
  const interested = new Set([packageName, ...(syncedNames ?? [])].filter((n) => n !== null))
399
464
  const targets = pending.pending.filter((p) => (p.status ?? 'pending') === 'pending' && interested.has(p.moduleName))
@@ -438,7 +503,7 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
438
503
  /** AI 赋能框架适配预检:查 registry 最新版声明 + 兼容门清单,给出权威说明。 */
439
504
  async function frameworkCompatReportFor(source, ports, profileDir) {
440
505
  const fwVer = currentFrameworkVersion(ports)
441
- const pending = readCompatPending()
506
+ const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir })
442
507
  const norm = String(source ?? '').trim()
443
508
  const isNpm = norm.startsWith('@') || !norm.includes('/')
444
509
  const installed = (() => {
@@ -448,7 +513,10 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
448
513
  return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
449
514
  } catch { return null }
450
515
  })()
451
- const pendHits = (pending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending' && p.moduleName === norm)
516
+ // 命中行带上当前判据的结论视图(`fail` 仍然是 fail;过期/复核不了会如实标注)
517
+ const pendHits = (pending?.pending ?? [])
518
+ .filter((p) => (p.status ?? 'pending') === 'pending' && p.moduleName === norm)
519
+ .map((p) => ({ ...p, verdict: verdicts.get(p.rowId) ?? verdictView(p) }))
452
520
  if (!isNpm) {
453
521
  return {
454
522
  kind: 'repo', frameworkVersion: fwVer, packageName: null, latest: null, check: null,
@@ -487,4 +555,4 @@ async function frameworkCompatReportFor(source, ports, profileDir) {
487
555
  summary: parts.length > 0 ? parts.join(';') : `框架 ${fwVer},无待适配记录`,
488
556
  }
489
557
  }
490
- export { gatingSummary, COMPAT_PENDING_FILE, COMPAT_GATE_FILE, COMPAT_ACK_FILE, COMPAT_GATE_DEFAULTS, NOTICE_MODES, DEFAULT_NOTICE_MODE, REMOVED_SETTINGS_SYMBOLS, FIBER_STATE, FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, referencesRemovedSymbol, scanSettingsApiUsage, isFrameworkOwnedPackage, checkPluginFrameworkCompat, probePluginImport, readCompatPending, writeCompatPending, readCompatGate, writeCompatGate, readCompatAck, writeCompatAck, resolveNoticeState, frameworkCheckPromptText, CONSOLE_VERSION, detectCompat, detectAdoptablePending, rowIdModuleMap, frameworkCompatReportFor, maybeAutoAdaptCompat }
558
+ export { gatingSummary, COMPAT_PENDING_FILE, COMPAT_GATE_FILE, COMPAT_ACK_FILE, COMPAT_GATE_DEFAULTS, NOTICE_MODES, DEFAULT_NOTICE_MODE, REMOVED_SETTINGS_SYMBOLS, FIBER_STATE, FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, referencesRemovedSymbol, scanSettingsApiUsage, isFrameworkOwnedPackage, checkPluginFrameworkCompat, probePluginImport, readCompatPending, readCompatPendingWithVerdicts, writeCompatPending, verdictView, SCANNER_VERSION, readCompatGate, writeCompatGate, readCompatAck, writeCompatAck, resolveNoticeState, frameworkCheckPromptText, CONSOLE_VERSION, detectCompat, detectAdoptablePending, rowIdModuleMap, frameworkCompatReportFor, maybeAutoAdaptCompat }