dsh-lite-memory 0.18.25 → 0.18.27

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.
@@ -83,9 +83,10 @@
83
83
  **范围路由**:模型给的 `scope` 是提案,不是命令。
84
84
 
85
85
  - 显式 `scope="global"` 表示「用户在这一次对话里要求存全局」,只有用户确实这么说才传;模型自己觉得通用不是理由。显式全局不再因为缺少 `evidenceRef` 被降级。
86
- - 仍然会拦住的两类:设备事实没有条件(`machine` / `targetOs` / `runtime` / `shell` 至少一个),以及正文或依据里出现本机路径、回环地址、端口的「通用流程」——后者换台机器/换个项目就不成立,留在项目层。
86
+ - 仍然会拦住的两类:设备事实没有条件(`machine` / `targetOs` / `runtime` / `shell` 至少一个),以及正文或依据里写死本机路径、回环地址、端口却没声明适用环境的「通用流程」——后者换台机器/换个项目就不成立,留在项目层。
87
+ - 声明了上述环境条件的本机流程(例如本机特有的命令包装)可以进全局层:条件不成立的机器上不会拿出来用;命令名/工具名本身不算本机痕迹。
87
88
  - `memoryClass="project_fact"` 不因写了 `global` 就变全局:项目事实换个项目本来就不成立。
88
- - 按类别推导(没有显式 `scope`)保持更严:`portable_procedure` 没有 `evidenceRef` 就落项目层。
89
+ - 按类别推导(没有显式 `scope`)保持更严:`portable_procedure` 没有 `evidenceRef`、也没有环境条件时落项目层。
89
90
  - 每次降级都要能解释:只在降级时返回 `scopeNote` 一行,说明为什么没进全局、怎么改才进得去;正常落全局或落项目不带这行。
90
91
 
91
92
  **限额与腾位**:每层活跃条目软上限 200、硬上限 1000;单条正文 4096 字节;单层总字节 1 MiB。到硬上限时把最久没被读的活跃条目移进归档,并在结果里写明「已归档「标题」腾位」,不拒绝新记忆;单条正文超限只报错、不截断。
@@ -0,0 +1,5 @@
1
+ # 0.18.26
2
+
3
+ - 只在本机成立、但跨项目复用的命令/流程(例如本机 npm 包装命令):声明 machine 或 runtime 条件后可以进全局层,换台机器条件不成立就不会拿出来用;仍然写死本机路径/端口又没声明环境的,继续留在项目层。
4
+ - 显式存全局的降级说明补了一句怎么改:补机器条件后可以存全局,不用把规则改写成一个不存在的通用说法。
5
+ - CI 稳定性:召回投影的耗时上限可以在测试里放宽(CI 冷启动偶尔超过 150ms,逻辑断言不该被「这轮先不注入」判失败);旧库正文比对按行尾归一(Windows 检出把 LF 转成 CRLF)。Windows 与 Ubuntu 六格矩阵全绿。
@@ -0,0 +1,5 @@
1
+ # 0.18.27
2
+
3
+ - 可信上下文补上宿主真实平台(`darwin → macos`,`win32 → windows`):条件写 `targetOs` 的规则从「永远待确认」变成按平台裁决,macOS 上写的本机流程在 macOS 上直接生效。
4
+ - 此前全局层只能收「声明了 machine/runtime 条件」的流程,但条件无从比对;这次把能自动核验的那一维接上,条件不再只是摆设。
5
+ - 召回夹具改用与平台无关的条件,避免 CI 与开发机行为不一致。
package/index.js CHANGED
@@ -804,7 +804,7 @@ const SAVE_PARAMS = {
804
804
  enum: ['distinct'],
805
805
  description: '明确表示「这确实是另一条」。返回 needsResolution 时,只有确认不是同一件事才带上它再存一次',
806
806
  },
807
- scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto 按记忆类别判定(默认);只有用户这一次明确说「以后所有项目都这样」才用 global,模型自己判断通用不是理由;明确说只在这个项目用 project。本机路径/端口和项目事实即使写 global 也会留在项目层,降级原因见 scopeNote' },
807
+ scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto 按记忆类别判定(默认);只有用户这一次明确说「以后所有项目都这样」才用 global,模型自己判断通用不是理由;明确说只在这个项目用 project。本机路径/端口和项目事实即使写 global 也会留在项目层,降级原因见 scopeNote;只在本机成立的命令/流程声明 machine 或 runtime 条件后可进全局层' },
808
808
  memoryClass: { type: 'string', enum: ['personal_preference', 'project_fact', 'environment_fact', 'portable_procedure'], description: '归属提案:个人偏好 / 项目事实 / 设备事实 / 可搬流程。服务端据此判定存哪层;设备事实必须同时给设备条件' },
809
809
  durability: { type: 'string', enum: ['durable', 'task'], description: '一般认为 durable;只是一次性任务参数用 task,服务端会拒绝落盘' },
810
810
  evidenceRef: { type: 'string', description: '归属判定的依据(哪条消息、哪个文件、哪次观察);用户没说范围时用来判断能不能存' },
@@ -1015,7 +1015,8 @@ function statusLabel(status) {
1015
1015
  */
1016
1016
  export const RECALL_MODES = ['adaptive', 'tool_only', 'off'];
1017
1017
  const RECALL_QUERY_CHARS = 400;
1018
- const RECALL_DEADLINE_MS = 150;
1018
+ /** 单轮投影的耗时上限:慢机器上宁可这轮不注入,也不把回合拖住。测试可临时放宽。 */
1019
+ let recallDeadlineMs = 150;
1019
1020
  const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read 带标题取正文,别凭标题猜):';
1020
1021
 
1021
1022
  /** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
@@ -1992,7 +1993,7 @@ export function apply(ctx, config) {
1992
1993
  /** 显式要了全局却被硬冲突挡回项目层时说清原因:否则模型只能看到 scope=project,无从纠。 */
1993
1994
  const scopeNote = (() => {
1994
1995
  if (args.scope !== 'global' || scope.key === 'global') return '';
1995
- if (saved.reasonCode === 'machine_bound_scope') return '按本机痕迹留在项目层:正文或依据里写了本机路径/端口,改成别的项目仍成立的通用规则再存全局';
1996
+ if (saved.reasonCode === 'machine_bound_scope') return '按本机痕迹留在项目层:正文或依据里写了本机路径/端口,改成别的项目仍成立的通用规则再存全局;只在本机适用的流程可补 machine 条件后存全局';
1996
1997
  if (saved.reasonCode === 'model_global_project_fact') return '按项目事实留在项目层:换成别的项目就不成立;确实是通用流程就改 memoryClass';
1997
1998
  return '';
1998
1999
  })();
@@ -2706,7 +2707,7 @@ export function apply(ctx, config) {
2706
2707
  return decision;
2707
2708
  }
2708
2709
  /** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
2709
- if (signal?.aborted || Date.now() - startedAt > RECALL_DEADLINE_MS) {
2710
+ if (signal?.aborted || Date.now() - startedAt > recallDeadlineMs) {
2710
2711
  rollbackBudget();
2711
2712
  if (snapshot.cards.length) clearRecall(scope, sessionId);
2712
2713
  clearPrepared(agent);
@@ -3044,6 +3045,8 @@ export function apply(ctx, config) {
3044
3045
  mode: recallMode,
3045
3046
  preparedOf: (agent) => prepared.get(agent) || null,
3046
3047
  windowOf: (agent) => windowOf(agent),
3048
+ /** 测试专用:CI 冷启动偶尔超过 150ms,逻辑测试不该被耗时上限判失败。 */
3049
+ setDeadlineMs: (ms) => { if (Number.isFinite(ms) && ms > 0) recallDeadlineMs = ms; },
3047
3050
  };
3048
3051
  return () => {
3049
3052
  delete globalThis.__liteMemoryRecall;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.25",
3
+ "version": "0.18.27",
4
4
  "description": "On-demand memory for the DeepSeek Harness: nothing resident, at most three small cards on a real hit, bodies read on demand.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/store.js CHANGED
@@ -2520,7 +2520,8 @@ function machineBoundText(source) {
2520
2520
 
2521
2521
  /**
2522
2522
  * 适用范围判定:内容/依据的普适性优先,模型声明的 scope 只是提案。
2523
- * 模型说 global、但正文写死了本机路径或依据是本机文件时,先留在项目层(或问一次条件)。
2523
+ * 写死了本机路径/端口又没有声明适用环境时留在项目层;声明了机器/系统条件的
2524
+ * 本机流程可以进全局层——换台机器条件不成立,自然不会拿出来用。
2524
2525
  */
2525
2526
  export function routeScope(draft, options = {}) {
2526
2527
  const source = draft || {};
@@ -2532,6 +2533,16 @@ export function routeScope(draft, options = {}) {
2532
2533
  const evidence = typeof source.evidenceRef === 'string' ? source.evidenceRef.trim() : '';
2533
2534
  /** 依据本身是本机文件/路径的,不能算通用流程;正文里带本机痕迹同理。 */
2534
2535
  const machineBound = machineBoundText(source) || (evidence ? machineBoundText({ title: evidence }) : false);
2536
+ /** 声明了机器/系统条件,本机痕迹就被这条条件收住:规则仍可搬到所有满足条件的机器上。 */
2537
+ const envScoped = (() => {
2538
+ try {
2539
+ return (view.normalizeConditions(source.conditions) || []).some((item) => ENVIRONMENT_KEYS.includes(item.key));
2540
+ } catch {
2541
+ return false;
2542
+ }
2543
+ })();
2544
+ /** 真正挡回项目层的只有「本机痕迹 + 没有声明适用环境」;只写命令名不算本机痕迹。 */
2545
+ const machineConflict = machineBound && !envScoped;
2535
2546
  const base = { chosenScope: '', reasonCode: '', requiredConditions: [], needsScope: false, explicit };
2536
2547
  /** 通用层拿不出普适证据时:有项目就留在项目层,没有就问一次运行环境。 */
2537
2548
  const fallbackToProject = (reasonCode) => {
@@ -2553,7 +2564,7 @@ export function routeScope(draft, options = {}) {
2553
2564
  * 没显式 scope 时才用 memoryClass 提案,缺可搬依据的流程留在项目层。
2554
2565
  */
2555
2566
  if (explicit === 'global' && !humanConfirmed) {
2556
- if (machineBound) return fallbackToProject('machine_bound_scope');
2567
+ if (machineConflict) return fallbackToProject('machine_bound_scope');
2557
2568
  if (cls === 'project_fact') return fallbackToProject('model_global_project_fact');
2558
2569
  }
2559
2570
  if (explicit === 'project' && !projectReady) {
@@ -2578,11 +2589,13 @@ export function routeScope(draft, options = {}) {
2578
2589
  }
2579
2590
  if (cls === 'personal_preference') {
2580
2591
  /** 偏好也分「我的习惯」和「这台机器上的习惯」:后者绑机器,不进通用层。 */
2581
- if (machineBound) return fallbackToProject('machine_bound_preference');
2592
+ if (machineConflict) return fallbackToProject('machine_bound_preference');
2582
2593
  return { ...base, chosenScope: 'global', reasonCode: 'class_personal_preference' };
2583
2594
  }
2584
2595
  if (cls === 'portable_procedure') {
2585
- if (machineBound) return fallbackToProject('machine_bound_procedure');
2596
+ if (machineConflict) return fallbackToProject('machine_bound_procedure');
2597
+ /** 带适用环境条件的本机流程:条件成立时才用,换台机器自然不生效。 */
2598
+ if (envScoped) return { ...base, chosenScope: 'global', reasonCode: 'machine_scoped_procedure' };
2586
2599
  /** 说能搬去别的项目,就要有能搬的依据;没有就先留在本项目。 */
2587
2600
  if (!evidence) return fallbackToProject('unbound_evidence');
2588
2601
  return { ...base, chosenScope: 'global', reasonCode: 'class_portable_procedure' };
@@ -460,6 +460,8 @@ const pass = (text, id) => async () => ({
460
460
  messages: [{ role: 'user', id, source: { kind: 'user' }, content: [{ type: 'text', text }] }],
461
461
  });
462
462
  const recall = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
463
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
464
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
463
465
  assert.ok(recall, 'recall context registered');
464
466
  assert.equal(recall.order, 60);
465
467
  assert.equal(recall.interpolate, false);
@@ -545,6 +547,8 @@ const evoEnter = async () => ({
545
547
  });
546
548
  await preStep({ agent: evoAgent, signal: evoSig, turn: '21' }, evoEnter);
547
549
  const evoRecall = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
550
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
551
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
548
552
  assert.match(evoRecall.text({ agent: evoAgent, signal: evoSig }), /开场三句内给结论/);
549
553
  assert.equal(evoRecall.text({ agent: evoAgent, signal: evoSig }).includes('README'), false);
550
554
 
@@ -33,6 +33,8 @@ function boot() {
33
33
  };
34
34
  apply(ctx, { mode: 'adaptive' });
35
35
  const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
36
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
37
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
36
38
  const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
37
39
  const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
38
40
  return { exec, preStep, text: (agent, signal) => provider.text({ agent, signal }), debug: globalThis.__liteMemoryRecall };
@@ -35,6 +35,8 @@ function boot(mode = 'adaptive') {
35
35
  apply(ctx, { mode });
36
36
  const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
37
37
  assert.ok(provider, 'recall provider registered');
38
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
39
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
38
40
  const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
39
41
  const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
40
42
  const text = (agent, signal) => provider.text({ agent, signal });
@@ -39,6 +39,8 @@ function boot(mode = 'adaptive') {
39
39
  apply(ctx, { mode });
40
40
  const debug = globalThis.__liteMemoryRecall;
41
41
  const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
42
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
43
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
42
44
  assert.ok(provider, 'recall provider registered');
43
45
  assert.equal(provider.order, 60);
44
46
  assert.equal(provider.interpolate, false);
@@ -111,8 +113,8 @@ const linuxOnly = save(scopeA, {
111
113
  tags: ['插件', '构建'],
112
114
  when: '',
113
115
  applicability: 'conditional',
114
- conditions: [{ key: 'targetOs', value: 'linux' }],
115
- summary: '只在 Linux CI 上跑构建检查',
116
+ conditions: [{ key: 'machine', value: '别的机器' }],
117
+ summary: '只在另一台机器上跑构建检查',
116
118
  });
117
119
  const unresolved = save(scopeA, { title: '发布前要检查什么', tags: ['发布', '检查'], when: '发布前', applicability: 'unresolved' });
118
120
  const globalHot = save(global, { title: '插件改动后先热刷新', tags: ['插件', '热刷新'], summary: '全局规矩:先热刷新再谈重启' });
@@ -266,10 +268,10 @@ assert.notEqual(bootA.provider.text({ agent: agentB, signal: signalB }), prepare
266
268
  const cwdC = mkdir('c');
267
269
  const scopeC = store.scopeOf(cwdC);
268
270
  save(scopeC, {
269
- title: '只在 Linux 上跑构建检查',
271
+ title: '只在另一台机器上跑构建检查',
270
272
  tags: ['构建'],
271
273
  applicability: 'conditional',
272
- conditions: [{ key: 'targetOs', value: 'linux' }],
274
+ conditions: [{ key: 'machine', value: '别的机器' }],
273
275
  });
274
276
  save(scopeC, { title: '发布前检查清单', tags: ['发布'], when: '发布前', applicability: 'unresolved' });
275
277
  const agent = makeAgent('agent-cond', cwdC);
@@ -33,6 +33,8 @@ function boot(mode = 'adaptive') {
33
33
  };
34
34
  apply(ctx, { mode });
35
35
  const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
36
+ /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
37
+ globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
36
38
  assert.ok(provider, 'recall provider registered');
37
39
  const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
38
40
  const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
@@ -23,6 +23,11 @@ const test = (name, fn) => {
23
23
  };
24
24
 
25
25
  const dirs = [];
26
+ /** Windows 会把同一个临时目录报成 8.3 短名或长名;按目录的文件 ID 判断是不是同一处。 */
27
+ const pathId = (target) => {
28
+ const stat = fs.statSync(target, { bigint: true });
29
+ return `${stat.dev}:${stat.ino}`;
30
+ };
26
31
  const fresh = (label) => {
27
32
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), `lm-state-${label}-`));
28
33
  dirs.push(dir);
@@ -218,9 +223,7 @@ test('S01 仓库根与子目录共用一份记忆', () => {
218
223
  const atSub = scopeOf(sub);
219
224
  assert.equal(atSub.key, atRoot.key);
220
225
  assert.equal(atSub.dir, atRoot.dir);
221
- /* Windows reports the temp root as an 8.3 short name on one side and the long
222
- name on the other; compare what both of them resolve to. */
223
- assert.equal(fs.realpathSync(resolveRoot(sub).root), fs.realpathSync(repo));
226
+ assert.equal(pathId(resolveRoot(sub).root), pathId(repo));
224
227
  });
225
228
 
226
229
  test('S02 符号链接、非 Git 目录、嵌套仓库各归各的', () => {
@@ -251,7 +254,7 @@ test('S03 配置的项目根只覆盖自己的子树,不吞别的目录', () =
251
254
  fs.mkdirSync(path.dirname(config), { recursive: true });
252
255
  fs.writeFileSync(config, JSON.stringify({ projectRoot: root }));
253
256
  try {
254
- assert.equal(resolveRoot(sub).root, fs.realpathSync(root));
257
+ assert.equal(pathId(resolveRoot(sub).root), pathId(root));
255
258
  assert.equal(resolveRoot(sub).source, 'configured');
256
259
  assert.equal(scopeOf(sub).key, scopeOf(root).key);
257
260
  assert.notEqual(resolveRoot(outside).source, 'configured');
@@ -327,7 +330,7 @@ test('S10 旧库迁移:正文、归档、旧标题一起搬进状态', () => {
327
330
  const loaded = load(scope).state;
328
331
  assert.equal(loaded.entries.length, 4);
329
332
  assert.equal(loaded.migration.fromVersion, 1);
330
- assert.equal(bodyOf(scope, 'mem_111111111111'), '第一步跑测试\n第二步改版本号');
333
+ assert.equal(bodyOf(scope, 'mem_111111111111').replace(/\r\n/g, '\n'), '第一步跑测试\n第二步改版本号');
331
334
  const archived = loaded.entries.find((entry) => entry.id === 'mem_444444444444');
332
335
  assert.equal(archived.status, 'archived');
333
336
  assert.equal(archived.current.body, '归档正文');
@@ -1089,6 +1089,24 @@ test('归属路由:本机痕迹的「通用流程」留在项目层,干净
1089
1089
  assert.equal(evidencePath.chosenScope, 'project', '依据本身是本机文件不算普适证据');
1090
1090
  assert.equal(evidencePath.reasonCode, 'machine_bound_procedure');
1091
1091
 
1092
+ const scopedProcedure = store.routeScope({
1093
+ title: 'npm 操作走 dsh-npm', summary: '本机 npm 命令用包装脚本',
1094
+ body: '本机 npm 一律用 dsh-npm;裸 npm 报 ENEEDAUTH 不是缺凭据',
1095
+ memoryClass: 'portable_procedure',
1096
+ conditions: [{ key: 'machine', value: 'macOS 桌面机' }],
1097
+ }, { project });
1098
+ assert.equal(scopedProcedure.chosenScope, 'global', '声明了机器条件的本机流程可进全局层');
1099
+ assert.equal(scopedProcedure.reasonCode, 'machine_scoped_procedure');
1100
+
1101
+ const scopedExplicit = store.routeScope({
1102
+ title: 'npm 操作走 dsh-npm', summary: '本机 npm 命令用包装脚本',
1103
+ body: '本机 npm 一律用 dsh-npm;裸 npm 报 ENEEDAUTH 不是缺凭据',
1104
+ memoryClass: 'portable_procedure', scope: 'global',
1105
+ conditions: [{ key: 'machine', value: 'macOS 桌面机' }],
1106
+ }, { project });
1107
+ assert.equal(scopedExplicit.chosenScope, 'global');
1108
+ assert.equal(scopedExplicit.reasonCode, 'explicit');
1109
+
1092
1110
  const unconfirmed = store.routeScope({
1093
1111
  title: '部署脚本', summary: '本机部署脚本', body: 'bash /tmp/deploy.sh',
1094
1112
  memoryClass: 'portable_procedure', scope: 'global',
@@ -70,7 +70,11 @@ test('条件裁决:不把不知道当不存在,也不把未解决当普遍
70
70
  const record = { id: 'mem_000000000001', applicability: 'conditional', conditions: [{ key: 'targetOs', value: 'macOS' }], when: '' };
71
71
  assert.equal(view.evaluate(record, ctxOf(null, { targetOs: 'macos' })).state, 'match');
72
72
  assert.equal(view.evaluate(record, ctxOf(null, { targetOs: 'linux' })).state, 'mismatch');
73
- assert.equal(view.evaluate(record, ctxOf(null, {})).state, 'unknown');
73
+ assert.equal(ctxOf(null, {}).targetOs, view.hostTargetOs());
74
+ const hostRecord = { ...record, conditions: [{ key: 'targetOs', value: view.hostTargetOs() || 'macos' }] };
75
+ assert.equal(view.evaluate(hostRecord, ctxOf(null, {})).state, 'match');
76
+ const machineOnly = { ...record, conditions: [{ key: 'machine', value: '这台机器' }] };
77
+ assert.equal(view.evaluate(machineOnly, ctxOf(null, {})).state, 'unknown');
74
78
  const unresolved = { ...record, applicability: 'unresolved', conditions: [] };
75
79
  assert.equal(view.statusOf(unresolved, ctxOf(null, {})), 'needs_context');
76
80
  assert.equal(view.canAutoRecall(unresolved, ctxOf(null, {})), false);
package/view.js CHANGED
@@ -152,7 +152,8 @@ export function validateApplicability(applicability, conditions, selection, when
152
152
  export function trustedContext(scope, options = {}) {
153
153
  const cwd = typeof options.cwd === 'string' && options.cwd ? options.cwd : (scope && scope.cwd) || '';
154
154
  return {
155
- targetOs: options.targetOs,
155
+ /** 宿主的真实平台是可信上下文:条件里写 targetOs 的规则本职就是按平台生效。 */
156
+ targetOs: options.targetOs === undefined ? hostTargetOs() : options.targetOs,
156
157
  shell: options.shell,
157
158
  runtime: options.runtime,
158
159
  machine: options.machine,
@@ -166,6 +167,15 @@ export function trustedContext(scope, options = {}) {
166
167
  };
167
168
  }
168
169
 
170
+ /** 只报归一化后的平台名;未知平台不硬猜,留给条件判 unknown。 */
171
+ export function hostTargetOs() {
172
+ const platform = typeof process !== 'undefined' && process ? process.platform : '';
173
+ if (platform === 'darwin') return 'macos';
174
+ if (platform === 'win32') return 'windows';
175
+ if (platform === 'linux') return 'linux';
176
+ return '';
177
+ }
178
+
169
179
  function sameText(left, right) {
170
180
  return plain(left, PATH_CHARS).toLowerCase() === plain(right, PATH_CHARS).toLowerCase();
171
181
  }