dsh-crate-web 0.1.0 → 0.1.2-preview.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -290
  3. package/README.zh-CN.md +2 -303
  4. package/THIRD-PARTY-LICENSE.txt +21 -0
  5. package/cordis.patch.yml +1 -6
  6. package/core/dsh_pack/_version.py +1 -0
  7. package/core/dsh_pack/bundle.py +74 -0
  8. package/core/dsh_pack/cli.py +81 -18
  9. package/core/dsh_pack/desktop_verify.py +304 -0
  10. package/core/dsh_pack/diagnostics.py +1170 -0
  11. package/core/dsh_pack/errors.py +16 -1
  12. package/core/dsh_pack/export.py +86 -103
  13. package/core/dsh_pack/export_plan.py +96 -0
  14. package/core/dsh_pack/identity.py +25 -0
  15. package/core/dsh_pack/importer.py +1598 -1347
  16. package/core/dsh_pack/network.py +5 -4
  17. package/core/dsh_pack/pack.py +14 -2
  18. package/core/dsh_pack/portability.py +392 -0
  19. package/core/dsh_pack/preflight.py +63 -16
  20. package/core/dsh_pack/troubleshooting.py +94 -0
  21. package/core/dsh_pack/user_cli.py +16 -0
  22. package/core/dsh_pack/validation.py +64 -4
  23. package/core/dsh_pack/verify.py +1485 -1336
  24. package/core/dsh_pack/windows_verify.py +177 -0
  25. package/desktop-patch-helper.mjs +140 -0
  26. package/lib/client.js +292 -102
  27. package/lib/index.d.ts +7 -1
  28. package/lib/index.js +1121 -229
  29. package/package.json +13 -5
  30. package/restart-helper.mjs +121 -121
  31. package/runtime/python/LICENSE.txt +676 -676
  32. package/runtime/python/__install__.json +81 -81
  33. package/runtime/python/python314._pth +6 -6
  34. package/skills/dsh-crate-troubleshooting/SKILL.md +172 -0
  35. package/skills/dsh-crate-troubleshooting/compatibility.json +11 -0
  36. package/skills/dsh-crate-troubleshooting/reference/diagnostics.generated.md +96 -0
  37. package/skills/dsh-crate-troubleshooting/reference/repair-boundaries.md +60 -0
  38. package/skills/dsh-crate-troubleshooting/reference/stages.generated.md +22 -0
  39. package/verify-desktop.mjs +314 -0
@@ -0,0 +1,177 @@
1
+ """独立桌面验证器的Win32资源、进程身份和被动TCP表;不依赖测试目录。"""
2
+ import ctypes as c
3
+ import os,socket
4
+
5
+ class ProcessEntry(c.Structure):
6
+ _fields_ = [('size', c.c_ulong), ('usage', c.c_ulong), ('pid', c.c_ulong),
7
+ ('heap', c.c_size_t), ('module', c.c_ulong), ('threads', c.c_ulong),
8
+ ('parent', c.c_ulong), ('priority', c.c_long), ('flags', c.c_ulong), ('image', c.c_wchar * 260)]
9
+
10
+ def process_tree(root_pid):
11
+ kernel = c.WinDLL('kernel32', use_last_error=True)
12
+ kernel.CreateToolhelp32Snapshot.argtypes = [c.c_ulong, c.c_ulong]
13
+ kernel.CreateToolhelp32Snapshot.restype = c.c_void_p
14
+ kernel.Process32FirstW.argtypes = kernel.Process32NextW.argtypes = [c.c_void_p, c.POINTER(ProcessEntry)]
15
+ kernel.CloseHandle.argtypes = [c.c_void_p]
16
+ handle = kernel.CreateToolhelp32Snapshot(2, 0)
17
+ if handle == c.c_void_p(-1).value:
18
+ raise OSError(c.get_last_error(), 'process snapshot')
19
+ rows = []
20
+ try:
21
+ row = ProcessEntry()
22
+ row.size = c.sizeof(row)
23
+ valid = kernel.Process32FirstW(handle, c.byref(row))
24
+ while valid:
25
+ rows.append((row.pid, row.parent))
26
+ valid = kernel.Process32NextW(handle, c.byref(row))
27
+ error = c.get_last_error()
28
+ if error not in (0, 18):
29
+ raise OSError(error, 'process enumeration')
30
+ finally:
31
+ kernel.CloseHandle(handle)
32
+ owned = {root_pid}
33
+ for _ in range(len(rows) + 1):
34
+ added = {pid for pid, parent in rows if parent in owned} - owned
35
+ if not added:
36
+ return owned
37
+ owned.update(added)
38
+ raise RuntimeError('process ancestry did not converge')
39
+
40
+ class BasicJobLimits(c.Structure):
41
+ _fields_ = [('processTime', c.c_int64), ('jobTime', c.c_int64), ('flags', c.c_ulong),
42
+ ('minimumWorkingSet', c.c_size_t), ('maximumWorkingSet', c.c_size_t),
43
+ ('activeProcesses', c.c_ulong), ('affinity', c.c_size_t), ('priority', c.c_ulong), ('scheduling', c.c_ulong)]
44
+
45
+ class JobIo(c.Structure):
46
+ _fields_ = [(name, c.c_uint64) for name in ['readOperations', 'writeOperations', 'otherOperations', 'readBytes', 'writeBytes', 'otherBytes']]
47
+
48
+ class ExtendedJobLimits(c.Structure):
49
+ _fields_ = [('basic', BasicJobLimits), ('io', JobIo), ('processMemory', c.c_size_t),
50
+ ('jobMemory', c.c_size_t), ('peakProcessMemory', c.c_size_t), ('peakJobMemory', c.c_size_t)]
51
+
52
+ class OwnedJob:
53
+ """Only the created controller joins; its future children cannot outlive Core."""
54
+ def __init__(self):
55
+ self.kernel = c.WinDLL('kernel32', use_last_error=True)
56
+ self.kernel.CreateJobObjectW.argtypes = [c.c_void_p, c.c_wchar_p]
57
+ self.kernel.CreateJobObjectW.restype = c.c_void_p
58
+ self.kernel.SetInformationJobObject.argtypes = [c.c_void_p, c.c_int, c.c_void_p, c.c_ulong]
59
+ self.kernel.AssignProcessToJobObject.argtypes = [c.c_void_p, c.c_void_p]
60
+ self.kernel.CloseHandle.argtypes = [c.c_void_p]
61
+ self.handle = self.kernel.CreateJobObjectW(None, None)
62
+ if not self.handle:
63
+ raise OSError(c.get_last_error(), 'CreateJobObjectW')
64
+ limits = ExtendedJobLimits()
65
+ limits.basic.flags = 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, no breakaway.
66
+ if not self.kernel.SetInformationJobObject(self.handle, 9, c.byref(limits), c.sizeof(limits)):
67
+ error = c.get_last_error()
68
+ self.close()
69
+ raise OSError(error, 'SetInformationJobObject')
70
+
71
+ def assign(self, process):
72
+ if not self.kernel.AssignProcessToJobObject(self.handle, int(process._handle)):
73
+ raise OSError(c.get_last_error(), 'AssignProcessToJobObject')
74
+
75
+ def active_processes(self):
76
+ class Accounting(c.Structure):
77
+ _fields_ = [(name, c.c_int64) for name in ['user', 'kernel', 'periodUser', 'periodKernel']] + [
78
+ (name, c.c_ulong) for name in ['pageFaults', 'totalProcesses', 'activeProcesses', 'terminatedProcesses']]
79
+ value = Accounting()
80
+ self.kernel.QueryInformationJobObject.argtypes = [c.c_void_p, c.c_int, c.c_void_p, c.c_ulong, c.c_void_p]
81
+ if not self.kernel.QueryInformationJobObject(self.handle, 1, c.byref(value), c.sizeof(value), None):
82
+ raise OSError(c.get_last_error(), 'QueryInformationJobObject')
83
+ return fields(value)
84
+
85
+ def close(self):
86
+ if self.handle:
87
+ self.kernel.CloseHandle(self.handle)
88
+ self.handle = None
89
+ class Memory(c.Structure):
90
+ _fields_=[('length',c.c_ulong),('load',c.c_ulong)]+[(n,c.c_uint64) for n in ['totalPhysical','availablePhysical','totalPageFile','availablePageFile','totalVirtual','availableVirtual','availableExtendedVirtual']]
91
+ class ProcessMemory(c.Structure):
92
+ _fields_=[('cb',c.c_ulong),('pageFaults',c.c_ulong)]+[(n,c.c_size_t) for n in ['peakWorkingSet','workingSet','peakPagedPool','pagedPool','peakNonpagedPool','nonpagedPool','pageFile','peakPageFile','privateBytes']]
93
+ class Io(c.Structure):
94
+ _fields_=[(n,c.c_uint64) for n in ['readOperations','writeOperations','otherOperations','readBytes','writeBytes','otherBytes']]
95
+ def fields(obj):return {name:getattr(obj,name) for name,_ in obj._fields_}
96
+
97
+ def summarize_resources(rows):
98
+ """区间差分CPU及离散采样峰值;不把Host资源归因于单个Worker。"""
99
+ valid=[r for r in rows if 'cpu100ns' in r.get('system',{})];cpu=[];by={}
100
+ for a,b in zip(valid,valid[1:]):
101
+ previous=a['system']['cpu100ns'];current=b['system']['cpu100ns']
102
+ total=sum(current[k]-previous[k] for k in ['kernelIncludingIdle','user']);idle=current['idle']-previous['idle']
103
+ if total>0:cpu.append(100*(total-idle)/total)
104
+ for row in valid:
105
+ for process in row.get('processes',[]):by.setdefault(process['pid'],[]).append((row,process))
106
+ processes=[]
107
+ for pid,pairs in by.items():
108
+ rates=[];memory=[p['memory'] for r,p in pairs if p.get('memory')]
109
+ for (a,p),(b,q) in zip(pairs,pairs[1:]):
110
+ elapsed=(b['monotonicNs']-a['monotonicNs'])/1e9;delta=sum(q['cpu100ns'].values())-sum(p['cpu100ns'].values())
111
+ if elapsed>0:rates.append(delta/1e7/elapsed/(b['system']['logicalCpus'] or 1)*100)
112
+ last=pairs[-1][1]
113
+ processes.append({'pid':pid,'role':last['role'],'peakSampledWorkingSetBytes':max((m['workingSet'] for m in memory),default=None),
114
+ 'peakSampledPrivateBytes':max((m['privateBytes'] for m in memory),default=None),'cpuPercentOfMachinePeak':max(rates,default=None),
115
+ 'finalCumulativeCpuSeconds':sum(last['cpu100ns'].values())/1e7,'finalCumulativeIo':last.get('io')})
116
+ loads=[r['system']['memory']['load'] for r in valid]
117
+ return {'systemCpuPercentPeak':max(cpu,default=None),'systemMemoryLoadPercentRange':[min(loads),max(loads)] if loads else None,
118
+ 'processes':processes,'interpretation':'离散采样;Host含Worker和其他业务。I/O为Windows进程累计计数,不能等同物理磁盘吞吐;缺样本为null。'}
119
+
120
+ class WindowsCounters:
121
+ def __init__(self):
122
+ self.k=c.WinDLL('kernel32',use_last_error=True);self.ps=c.WinDLL('psapi',use_last_error=True)
123
+ k=self.k
124
+ k.OpenProcess.argtypes=[c.c_ulong,c.c_int,c.c_ulong];k.OpenProcess.restype=c.c_void_p
125
+ k.CloseHandle.argtypes=[c.c_void_p];k.CloseHandle.restype=c.c_int
126
+ k.GetProcessTimes.argtypes=[c.c_void_p]+[c.POINTER(c.c_uint64)]*4;k.GetProcessTimes.restype=c.c_int
127
+ k.GetSystemTimes.argtypes=[c.POINTER(c.c_uint64)]*3;k.GetSystemTimes.restype=c.c_int
128
+ k.GlobalMemoryStatusEx.argtypes=[c.POINTER(Memory)];k.GlobalMemoryStatusEx.restype=c.c_int
129
+ k.GetProcessIoCounters.argtypes=[c.c_void_p,c.POINTER(Io)];k.GetProcessIoCounters.restype=c.c_int
130
+ k.GetExitCodeProcess.argtypes=[c.c_void_p,c.POINTER(c.c_ulong)];k.GetExitCodeProcess.restype=c.c_int
131
+ k.QueryFullProcessImageNameW.argtypes=[c.c_void_p,c.c_ulong,c.c_wchar_p,c.POINTER(c.c_ulong)];k.QueryFullProcessImageNameW.restype=c.c_int
132
+ self.ps.GetProcessMemoryInfo.argtypes=[c.c_void_p,c.POINTER(ProcessMemory),c.c_ulong];self.ps.GetProcessMemoryInfo.restype=c.c_int
133
+ def system(self):
134
+ idle,kernel,user=(c.c_uint64() for _ in range(3));memory=Memory();memory.length=c.sizeof(memory)
135
+ if not self.k.GetSystemTimes(c.byref(idle),c.byref(kernel),c.byref(user)) or not self.k.GlobalMemoryStatusEx(c.byref(memory)):raise OSError(c.get_last_error(),'system counters')
136
+ return {'cpu100ns':{'idle':idle.value,'kernelIncludingIdle':kernel.value,'user':user.value},'memory':fields(memory),'logicalCpus':os.cpu_count()}
137
+ def open(self,pid):
138
+ handle=self.k.OpenProcess(0x1010,False,pid)
139
+ if not handle:raise OSError(c.get_last_error(),'OpenProcess for observation')
140
+ image=c.create_unicode_buffer(32768);length=c.c_ulong(len(image))
141
+ if not self.k.QueryFullProcessImageNameW(handle,0,image,c.byref(length)):
142
+ self.k.CloseHandle(handle);raise OSError(c.get_last_error(),'process image')
143
+ return handle,image.value
144
+ def process(self,pid,handle,role,image):
145
+ creation,end,kernel,user=(c.c_uint64() for _ in range(4));memory=ProcessMemory();memory.cb=c.sizeof(memory);io=Io();code=c.c_ulong()
146
+ if not self.k.GetProcessTimes(handle,*[c.byref(x) for x in [creation,end,kernel,user]]):raise OSError(c.get_last_error(),'process times')
147
+ mem_ok=bool(self.ps.GetProcessMemoryInfo(handle,c.byref(memory),memory.cb));io_ok=bool(self.k.GetProcessIoCounters(handle,c.byref(io)))
148
+ exited=bool(end.value);self.k.GetExitCodeProcess(handle,c.byref(code))
149
+ return {'pid':pid,'role':role,'image':image,'creationFiletime':str(creation.value),'cpu100ns':{'kernel':kernel.value,'user':user.value},
150
+ 'memory':fields(memory) if mem_ok else None,'io':fields(io) if io_ok else None,'exited':exited,'exitCode':code.value if exited else None}
151
+
152
+
153
+ class Row4(c.Structure):
154
+ _fields_=[('state',c.c_ulong),('localAddress',c.c_ubyte*4),('localPort',c.c_ulong),('remoteAddress',c.c_ubyte*4),('remotePort',c.c_ulong),('pid',c.c_ulong)]
155
+ class Row6(c.Structure):
156
+ _fields_=[('localAddress',c.c_ubyte*16),('localScope',c.c_ulong),('localPort',c.c_ulong),('remoteAddress',c.c_ubyte*16),('remoteScope',c.c_ulong),('remotePort',c.c_ulong),('state',c.c_ulong),('pid',c.c_ulong)]
157
+ STATES={1:'CLOSED',2:'LISTEN',3:'SYN_SENT',4:'SYN_RCVD',5:'ESTABLISHED',6:'FIN_WAIT1',7:'FIN_WAIT2',8:'CLOSE_WAIT',9:'CLOSING',10:'LAST_ACK',11:'TIME_WAIT',12:'DELETE_TCB'}
158
+
159
+ def tcp_rows():
160
+ dll=c.WinDLL('iphlpapi',use_last_error=True)
161
+ dll.GetExtendedTcpTable.argtypes=[c.c_void_p,c.POINTER(c.c_ulong),c.c_int,c.c_ulong,c.c_ulong,c.c_ulong];dll.GetExtendedTcpTable.restype=c.c_ulong
162
+ rows=[]
163
+ for family,kind in [(socket.AF_INET,Row4),(socket.AF_INET6,Row6)]:
164
+ size=c.c_ulong();status=dll.GetExtendedTcpTable(None,c.byref(size),False,family,5,0)
165
+ if status not in (0,122):raise OSError(status,'TCP表大小查询')
166
+ buf=c.create_string_buffer(size.value+65536);size.value=len(buf)
167
+ status=dll.GetExtendedTcpTable(buf,c.byref(size),False,family,5,0)
168
+ if status:raise OSError(status,'TCP表读取')
169
+ count=c.c_ulong.from_buffer(buf).value
170
+ if 4+count*c.sizeof(kind)>len(buf):raise ValueError('TCP表结构超出缓冲区')
171
+ for i in range(count):
172
+ item=kind.from_buffer_copy(buf,4+i*c.sizeof(kind))
173
+ rows.append({'family':'IPv4' if family==socket.AF_INET else 'IPv6','localAddress':socket.inet_ntop(family,bytes(item.localAddress)),
174
+ 'localPort':socket.ntohs(item.localPort&65535),'remoteAddress':socket.inet_ntop(family,bytes(item.remoteAddress)),
175
+ 'remotePort':socket.ntohs(item.remotePort&65535),'state':STATES.get(item.state,str(item.state)),'pid':item.pid})
176
+ return rows
177
+
@@ -0,0 +1,140 @@
1
+ /** Read-only adapter: use the installed official parser/composer, never activate entries. */
2
+ import { createHash } from 'node:crypto'
3
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import { dirname, join, resolve } from 'node:path'
6
+ import { pathToFileURL } from 'node:url'
7
+ import { isDeepStrictEqual } from 'node:util'
8
+
9
+ const hash = value => createHash('sha256').update(value).digest('hex')
10
+ const runtime = resolve(process.argv[2])
11
+ const profile = resolve(process.argv[3])
12
+ const anchor = join(runtime, 'package.json')
13
+ const require = createRequire(anchor)
14
+ const { parseDocument, isMap, isScalar, isSeq } = require('yaml')
15
+ const boot = await import(pathToFileURL(join(runtime, 'node_modules/@deepseek-ai/dsh-app-boot/lib/index.js')).href)
16
+
17
+ function rowsById(rows, result = new Map(), depth = 0) {
18
+ if (depth > 64) throw new Error('patch nesting exceeds adapter limit')
19
+ for (const row of rows ?? []) {
20
+ if (!row || typeof row !== 'object') continue
21
+ if (typeof row.id === 'string') result.set(row.id, [...(result.get(row.id) ?? []), row])
22
+ if (Array.isArray(row.config)) rowsById(row.config, result, depth + 1)
23
+ if (Array.isArray(row.insert)) rowsById(row.insert, result, depth + 1)
24
+ }
25
+ return result
26
+ }
27
+
28
+ function hasFilesystemInsert(rows, depth = 0) {
29
+ if (depth > 64) throw new Error('patch nesting exceeds adapter limit')
30
+ return (rows ?? []).some(row => row && typeof row === 'object' &&
31
+ (typeof row.name === 'string' && row.name.startsWith('file:') ||
32
+ row.group && Array.isArray(row.config) && hasFilesystemInsert(row.config, depth + 1)))
33
+ }
34
+
35
+ function main() {
36
+ const path = join(profile, 'cordis.patch.yml')
37
+ if (!existsSync(path)) return { changed: false, reason: 'no-profile-patch' }
38
+ const original = readFileSync(path)
39
+ const source = boot.loadOptionalPatches('dsh-crate', path) ?? []
40
+ // A custom root is deliberately left to its owner; this adapter targets the
41
+ // normal empty-root + ordered Bundle + id-targeted override contract only.
42
+ if ((boot.loadOptionalPatches('dsh-crate', join(profile, 'cordis.yml')) ?? []).length) {
43
+ return { changed: false, reason: 'custom-root-entries' }
44
+ }
45
+ const manifest = boot.readProfileManifest('dsh-crate', profile)
46
+ // Retired official Bundles are not in the runtime's current catalog. Avoid
47
+ // loadProfileDirectory here: it can rewrite a manifest to remove them.
48
+ const catalog = new Set([...boot.DEFAULT_PROFILE_BUNDLES, ...boot.OPTIONAL_BUNDLES,
49
+ ...Object.values(boot.PROFILE_TEMPLATES).flatMap(template => template.bundles)])
50
+ const exemptions = boot.readProfileVersionExemptions(profile)
51
+ const layers = []
52
+ for (const name of manifest.dsh?.profile?.bundles ?? []) {
53
+ if (name.startsWith('@deepseek-ai/') && !catalog.has(name)) return { changed: false, reason: 'uncatalogued-official-bundle' }
54
+ const root = boot.resolveBundleDir('dsh-crate', name, anchor, profile)
55
+ const bundleManifest = boot.readProfileManifest('dsh-crate', root)
56
+ const issue = boot.evaluatePluginCompatibility(bundleManifest, exemptions)
57
+ if (issue && !issue.exempted) return { changed: false, reason: 'incompatible-bundle' }
58
+ const bundle = bundleManifest.dsh?.bundle
59
+ if (!bundle) throw new Error('declared Bundle has no patch')
60
+ layers.push(...boot.bundlePatchPaths(root, bundle).map(file => boot.loadOverlayPatches('dsh-crate', file)))
61
+ }
62
+ const baseWarnings = []
63
+ const base = boot.composeEntries(layers, message => baseWarnings.push(message))
64
+ if (baseWarnings.length) return { changed: false, reason: 'ambiguous-bundle-composition' }
65
+ const declarations = new Map()
66
+ for (const patches of layers) {
67
+ for (const patch of patches) {
68
+ for (const [id, rows] of rowsById(patch.insert ?? [])) {
69
+ declarations.set(id, (declarations.get(id) ?? 0) + rows.length)
70
+ }
71
+ }
72
+ }
73
+ const slots = rowsById(base)
74
+ const occurrences = rowsById(source)
75
+ // The official parser anchors filesystem insert names beside the original
76
+ // file. Preserve these profiles unchanged rather than compare two dialects.
77
+ if (source.some(patch => hasFilesystemInsert(patch.insert))) {
78
+ return { changed: false, reason: 'filesystem-insert-requires-official-parser' }
79
+ }
80
+ const homePatch = boot.loadOptionalPatches('dsh-crate', join(dirname(dirname(profile)), 'cordis.patch.yml')) ?? []
81
+ const homeSlots = rowsById(homePatch)
82
+ const document = parseDocument(original.toString('utf8'), {
83
+ customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: value => value }],
84
+ uniqueKeys: true,
85
+ })
86
+ if (document.errors.length || !isSeq(document.contents)) throw new Error('profile patch cannot be safely represented')
87
+ const changes = []
88
+ for (const item of document.contents.items) {
89
+ if (!isMap(item)) continue
90
+ const idNode = item.get('id', true), nameNode = item.get('name', true)
91
+ if (!isScalar(idNode) || !isScalar(nameNode)) continue
92
+ const id = idNode.value, name = nameNode.value
93
+ if (typeof id !== 'string' || typeof name !== 'string' || !/^@deepseek-ai\/[A-Za-z0-9._-]+$/.test(name)) continue
94
+ const slot = slots.get(id)
95
+ if (slot?.length !== 1 || slot[0].name !== name || declarations.get(id) !== 1 || occurrences.get(id)?.length !== 1 || homeSlots.has(id)) continue
96
+ // Preserve explicit package overrides and any hoisted dependency that can
97
+ // select a different implementation than the installation anchor.
98
+ if (name in (manifest.dependencies ?? {}) || name in (manifest.optionalDependencies ?? {})) continue
99
+ if (existsSync(join(profile, 'node_modules', name, 'package.json')) ||
100
+ existsSync(join(dirname(profile), 'node_modules', name, 'package.json'))) continue
101
+ let official
102
+ try { official = require.resolve(name + '/package.json') } catch { continue }
103
+ if (!realpathSync(official).replaceAll('\\', '/').startsWith(realpathSync(join(runtime, 'node_modules')).replaceAll('\\', '/') + '/')) continue
104
+ try {
105
+ const local = createRequire(join(profile, 'package.json')).resolve(name + '/package.json')
106
+ if (realpathSync(local) !== realpathSync(official)) continue
107
+ } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') continue }
108
+ item.delete('name')
109
+ changes.push({ id, name })
110
+ }
111
+ if (!changes.length) return { changed: false, reason: 'no-equivalent-official-overrides' }
112
+ const content = document.toString()
113
+ // Official loadOptionalPatches treats !!js as inert expression nodes. It
114
+ // does not evaluate them, and this helper never calls Loader/boot/apply.
115
+ const yaml = require('js-yaml')
116
+ const expression = new yaml.Type('tag:yaml.org,2002:js', {
117
+ kind: 'scalar', resolve: value => typeof value === 'string',
118
+ construct: value => ({ __jsExpr: value }),
119
+ })
120
+ const next = yaml.load(content, { schema: yaml.JSON_SCHEMA.extend(expression) })
121
+ const beforeWarnings = [], afterWarnings = []
122
+ const before = boot.composeEntries([...layers, source, homePatch], message => beforeWarnings.push(message))
123
+ const after = boot.composeEntries([...layers, next, homePatch], message => afterWarnings.push(message))
124
+ if (!isDeepStrictEqual(before, after) || !isDeepStrictEqual(beforeWarnings, afterWarnings)) {
125
+ throw new Error('the official Loader composition changed; refusing adaptation')
126
+ }
127
+ return { changed: true, originalSha256: hash(original), runtimeSha256: hash(content),
128
+ loaderTreeSha256: hash(JSON.stringify(before)), removedNames: changes,
129
+ contentBase64: Buffer.from(content).toString('base64') }
130
+ }
131
+
132
+ try { process.stdout.write(JSON.stringify(main()) + '\n') }
133
+ catch (error) {
134
+ // Core redacts the full process diagnostic before exposing it. Do not echo
135
+ // parser snippets, which may contain values from the configuration file.
136
+ const detail = error instanceof Error ? { name: error.name,
137
+ message: error.message.split(/\n|\r/)[0], code: error.code ?? null } : { name: 'Error', message: String(error).split(/\n|\r/)[0] }
138
+ process.stdout.write(JSON.stringify({ error: 'official patch equivalence could not be verified', detail }) + '\n')
139
+ process.exitCode = 1
140
+ }