dsh-premise-trace 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-premise-trace contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,57 @@
1
+ # dsh-premise-trace — premise tracing
2
+
3
+ A bundled skill for DSH: work out why a subject moved from state A to state B, what it moves to next, or — for a single state — which conditions must hold together.
4
+
5
+ ## Questions it answers
6
+
7
+ - Why did it end up like this
8
+ - When does it change, and what comes next
9
+ - What does it depend on (single state)
10
+
11
+ ## How it judges
12
+
13
+ A state holds because several conditions hold together. A condition can change while the state stays put. The state fails only when a condition fails and no fallback can replace it.
14
+
15
+ Four tests, in order:
16
+
17
+ | Test | Question |
18
+ |---|---|
19
+ | Necessary | Remove it — does the state still hold? |
20
+ | Independent | After removal, is what remains "another describable state", or the state ceasing to exist / the environment gone? |
21
+ | Critical | After it collapses, is there a second path to restore the state? |
22
+ | Cascading | Does removing it take several other conditions down with it? |
23
+
24
+ Seven steps: fix the state → list candidates → screen them → split internal/external → recurse → exchange → read the change.
25
+
26
+ ## Three deliverables
27
+
28
+ - **Current state**: one table of holding conditions, each with its verdict; one line on what is shifting and where.
29
+ - **Transition**: a table for A and one for B, plus the cause and process of the shift.
30
+ - **Forecast**: a list of condition movements, plus two or three possible next states (B most likely, plus C and D), each with the condition change that leads to it.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ dsh plugin --profile <profile> add github:taoziliyouchong/dsh-premise-trace
36
+ ```
37
+
38
+ Restart DSH afterwards. The skill appears in the skill catalog as `premise-trace`.
39
+
40
+ This package registers no tools, commands, routes or UI — the only difference is one more entry in the skill catalog.
41
+
42
+ ## Compatibility
43
+
44
+ `engines.dsh` declares `>=0.2.1-0 <0.3.0-0`; not guaranteed beyond 0.3.
45
+
46
+ ## Maintenance
47
+
48
+ ```sh
49
+ npm run check # syntax
50
+ npm run smoke # structure / bundle declaration / registration / skill body
51
+ ```
52
+
53
+ Edit `assets/premise-trace.md` for the method, `index.js` for registration; restart DSH to apply.
54
+
55
+ ## License
56
+
57
+ MIT
package/README.md CHANGED
@@ -1,3 +1,65 @@
1
- # Temporary Holding Version
1
+ # dsh-premise-trace —— 前提追溯
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 作用:一件事正在变化时,回望它怎么变成现在这样、说清现在靠什么成立、判断下一步会变成什么。
4
+
5
+ - **回望过去**:它从哪个状态转过来,中间哪几项变了
6
+ - **看清现状**:这个状态靠什么成立
7
+ - **往后**:下一步会变成什么,还有没有别的落点
8
+
9
+ ## 怎么判
10
+
11
+ 一个状态靠几项成立条件同时成立;条件变了状态未必变;条件失效且没有第二条路,状态才不成立。
12
+
13
+ 四道判定,按顺序过:
14
+
15
+ | 判定 | 问什么 |
16
+ |---|---|
17
+ | 必要 | 撤掉它,状态还成立吗 |
18
+ | 独立 | 撤掉后剩下的是「另一种可描述的状态」,还是状态不存在/环境不存在 |
19
+ | 关键 | 塌了有没有第二条路补位 |
20
+ | 级联 | 撤掉会不会连带几项一起失效 |
21
+
22
+ 七步:定状态 → 列候选 → 筛项 → 分内外 → 递归 → 交换 → 读变化。两态时多看一步补项回炉,直到两张表都不再增项。
23
+
24
+ ## 三种交付
25
+
26
+ - **解释现状**:一张成立条件表,逐条标关键判定;表后一句「哪项现在在变、往哪变」。
27
+ - **解释转变**:A、B 两张表,加一段转化的原因与过程。
28
+ - **判断走向**:异动清单,加两三个可能(B 大概率,另有 C、D),每个跟一句「哪条条件怎么变才走到它」。
29
+
30
+ ## 安装
31
+
32
+ ```sh
33
+ dsh plugin --profile <profile> add github:taoziliyouchong/dsh-premise-trace
34
+ ```
35
+
36
+ 装完重启 DSH。技能名 `premise-trace`,出现在技能目录里;说「用前提追溯分析:……」,或直接问「它靠什么成立」「它下一步会变成什么」即可唤起。
37
+
38
+ 本包没有工具、命令、路由与浏览器界面,装上前后唯一的区别是技能目录多一项。
39
+
40
+ ## 包内容
41
+
42
+ | 文件 | 作用 |
43
+ |---|---|
44
+ | `index.js` | 向技能服务注册 `premise-trace`(bundled,档位 600) |
45
+ | `invariant.js` | 组合用的伴随插件 |
46
+ | `cordis.patch.yml` | 组合声明:`dsh plugin add` 靠这一行把插件接进层栈 |
47
+ | `assets/premise-trace.md` | 方法正文(技能加载时读取全文) |
48
+ | `scripts/smoke.mjs` | 本机冒烟:结构、组合声明、注册、正文可读 |
49
+
50
+ ## 兼容
51
+
52
+ `engines.dsh` 声明 `>=0.2.1-0 <0.3.0-0`;0.3 起不保证。
53
+
54
+ ## 维护
55
+
56
+ ```sh
57
+ npm run check # 语法
58
+ npm run smoke # 结构 / 组合声明 / 注册 / 正文可读
59
+ ```
60
+
61
+ 改方法正文只动 `assets/premise-trace.md`,改注册动 `index.js`;改完重启 DSH 生效。
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,120 @@
1
+ # 前提追溯 · 写法
2
+
3
+ > 本技能由 `dsh-premise-trace` 插件提供(bundled,唯一权威版本;改正文请编辑插件 `assets/premise-trace.md`)。
4
+
5
+ ## 这个技能管什么
6
+
7
+ 解释一个主体为什么从状态 A 转到状态 B,或判断它下一步转到哪;只有一个状态时,解释这个状态靠什么成立。
8
+
9
+ 想起它的时刻:问「它为什么变成现在这样」「它什么时候会变」「它下一步会变成什么」「它靠什么撑着」;要判断一个方案、模式、组织、制度、技术、习惯、价格、共识、人物转变的走向。
10
+
11
+ 先定问法(三档,共用同一套筛项):
12
+
13
+ - **解释现状**——只有一个状态,问它靠什么成立:筛一张表,交一张表。
14
+ - **解释转变**——有 A、B 两个状态:筛两张表,交两段加转化原因。
15
+ - **判断走向**——在单态表或 B 表上逐条看条件异动,再落成两三个可能:交异动清单+B(大概率)、C、D,每个后面跟一句「哪条条件一变就走到它」。
16
+
17
+ ## 根本
18
+
19
+ 一个状态靠几项成立条件撑着——撤掉任何一条就塌;条件变了,状态未必变;变了以后落到哪个状态,不止一种可能。
20
+
21
+ ## 基本点(方向,不是判决表)
22
+
23
+ 1. **必要**——撤了塌不塌:撤掉它状态还成立,它就是背景项(与状态无关的那类),不是条件。
24
+ 2. **独立**——撤了剩下什么:剩下另一种可描述的状态,才是独立条件;剩下「这个状态不存在」是状态定义的一部分;剩下「世界没了」是背景项。后两类不单列。
25
+ 3. **关键**——塌了有没有第二条路补位:有路可重建的是非关键成立条件(冷备),没有的是关键成立条件。关键可以多项,也可以为零:各项都有冷备就是组合失效,写组合,不挑一项;不拿「现在卡在哪一根」顶替——那是瓶颈,天然只有一条。
26
+ 4. **级联**——撤掉会连带让几项一起失效:塌多深看这条。
27
+
28
+ ## 怎么想
29
+
30
+ 1. **定状态**:先定一个能判的状态——主体+性质+档位。不定档,撤项测试没有判据——撤掉一项,是塌还是降一档,说不清。只有一个状态就到此为止;要解释转变,再定第二个状态,两态必须同一个主体、同一套比较方面。先看 B 是怎么来的——自己的条件凑够才在,还是 A 的条件失效、无补位、剩下这个形态;后一种不去找 B 的成立条件,先看 A 哪几条失效、为什么没补位,再看为什么剩下的是这个形态。观察顺序是先看到状态变、再反推条件变;因果只有一条:条件变 → 状态变。
31
+
32
+ - 状态段是让人判「这是哪个态」,不是论证这个态。性质变了是换状态;性质没变、只是强弱变了,才是升降一档。
33
+
34
+ 2. **列候选**:先分题面与答法——题面=这道题在问什么、为什么要问、问给谁听;答法=谁来判、拿什么判、判出来的「我」是谁。再按「这道题要答出来,需要哪几样东西」把两边的功能列全:判定标准、待判的内容、判断的材料、要答案的动机、承当判断的「我」、能回应的关系、可用的行动。宁可多列,热备也先留着,筛的时候再剔。
35
+
36
+ - 两态时只按「两态差在哪」挑,题面会整块消失,两态都在的项在候选阶段就被排除,后面补不回来。
37
+ - 单态时按「这个状态要成立需要哪几样」列,不按「现在缺什么」列——按缺什么列会把候选压成待办清单,撑住状态的项反而漏掉。
38
+ - 清单外的项,这套方法生产不出来:递归只沿关键项钻,交换只查两表不对称,两边都漏的补不回;要补只能换一种切法重列候选,或拿外部清单对照。
39
+
40
+ 3. **筛项**:对每一项,在已定的每个状态各撤一次(只有一个状态就只撤它);两态时两撤同时给出三个结果,不用分两遍走。三档按依赖顺序判,后一档的输入都是前一档的输出:
41
+
42
+ - 一档·必要:撤了不塌 → 与状态无关,不是条件(背景项,或热备:撤了瞬间顶上、状态不中断),不单列。
43
+ - 二档·独立:塌了,看剩下什么——剩下「这个状态不存在」→ 定义项,不单列;剩下「世界没了」→ 背景项,不单列;剩下另一种可描述状态 → 成立条件。
44
+ - 三档·补位:成立条件,撤掉它有没有第二条路——有=冷备,没有=关键。
45
+
46
+ 不先撤判不了塌不塌,不先判成条件判不了冷热备,不先塌判不了关键。逐项定完再写,不挑一条就收。
47
+
48
+ 4. **分内外**:内=主体自身的结构,外=环境给的。外部最容易漏——它一直在,就不觉得是条件。
49
+
50
+ 5. **递归**:把每一条成立条件都当成一个新状态,重跑 1-4 步。拆出来的条件替换被拆的那条:被拆开的条件不再单列,表里写拆出来的;拆不出成立条件的,原项保留。同样过必要、独立、补位、内外四道筛;热备踢出;同一项在多处出现就合并;推到它不再起作用为止。
51
+
52
+ - 递归只沿成立条件往下钻,补不回候选阶段漏掉的项;切法决定它看得见什么——把条件切成「判据在谁手里」,拆出来的是判据源;切成「他为什么需要判据」,才拆得到藏在里面的那条。
53
+
54
+ 6. **交换**:两表互换,两个方向各跑一遍,只用来查漏——对方有、自己没有的项,回去过必要与独立两道判定,撤掉自己就塌才补。单态没有第二张表,跳过此步。
55
+
56
+ - 每项按自己一侧的实测填,判定结果不继承:对方表里是关键,自己这边可能只是冷备,甚至撤了不塌、根本不补。
57
+ - 补入后按本状态补关键判定(有第二路=冷备,没有=关键);补入的项按递归规则当新状态重跑,推到它不再起作用。
58
+ - 回炉递归若又长出新的项,再拿去对方表交换;交换再补、补再回炉,跑到两表都不再增项为止。
59
+ - 交换只查两表不对称的项,两边都漏的项查不出来。
60
+
61
+ 7. **读变化**:哪项先变、变成什么——失效、被替换、减弱、增强;状态跟不跟着变。硬规则:失效且无替代,状态必不成立;变弱或变强,状态可以不动。
62
+
63
+ 顺序不能跳:必要 → 独立 → 补位 → 内外 → 递归 → 交换 → 补项回炉(关键判定+递归)→ 读变化;回炉出新项就再交换,直到两表不再增项。单态跑到递归、读变化为止,跳过交换那一段。
64
+
65
+ ## 筛出来的项怎么归
66
+
67
+ | 类 | 判定 | 写法 |
68
+ |---|---|---|
69
+ | 成立条件 | 撤了塌,且剩下的是另一种可描述的状态 | 单列,一行一项 |
70
+ | 关键成立条件 | 成立条件里,塌后没有第二条路补位 | 单列,标「关键」 |
71
+ | 非关键成立条件(冷备) | 有第二条路,但先塌后重建 | 单列 |
72
+ | 定义项 | 撤掉后「这个状态不存在」——状态语义自带的 | 不单列 |
73
+ | 背景项/热备 | 撤掉后「世界没了」,或撤掉后状态照旧成立;热备撤了瞬间顶上、状态不中断 | 不单列 |
74
+ | 维持项 | 撤了短期不塌、长期走弱 | 不进成立条件表,交付时单独标一句 |
75
+
76
+ - 发现漏项的信号:某条条件要预设另一条才说得通,被预设的那条就是漏项,补进来。
77
+ - 同一项经不同路径出现时并成一条,不重复计。
78
+
79
+ ## 交付
80
+
81
+ 按问法交:
82
+
83
+ - **解释现状(单态)**:一张成立条件表,表后一句「哪项现在在变、往哪变」。表外另标一句维持项——撤了短期不塌、长期走弱的那种:它不进成立条件表,但决定状态能撑多久。
84
+ - **解释转变(两态)**:三段——1. A 的成立条件表;2. B 的成立条件表;3. A→B 转化的原因和过程(单列,排最后;前两段只列成立条件,原因不写进表)。
85
+ - **判断走向**:异动清单+两三个可能。清单一行一项(这项现在什么状态、异动长什么样、冷备能不能顶上);可能写 B(大概率,附一句凭什么最大)、C、D,每个跟一句「哪条条件怎么变才走到它」。B 只有一个,另外至少列一条。
86
+
87
+ - 只列结论项:撤项测试没通过的不出现;一行一项;删掉某行后结论不变,那行就是过程,删。
88
+ - 成立条件表逐条标关键判定:关键项标「关键」,非关键项注明理由(有冷备、或被另一条路重建);关键可以不止一项,有几项标几项;「现在谁在卡着」不作关键理由。
89
+ - 不写没通过筛项的候选,不写层级编号;默认一屏内,超了先合并同类项。
90
+ - 成立条件、原因、方向、快慢直接写,不用比方。
91
+
92
+ ## 要预测时
93
+
94
+ 拿最近的一张表(单态的现状表,或两态的 B 表)逐条盯:
95
+
96
+ 1. 这项现在什么状态:成立、在变弱、在变强、已有替代。
97
+ 2. 异动长什么样:失效、被替换、减弱、增强各是什么迹象。
98
+ 3. 由条件怎么变推可能,不是只推一个:把「哪几项条件怎么变」的组合列出来,每组落到一个可能——
99
+ B 是大概率,另有 C、D。不编:走得通的组合才写,走不通的不写。
100
+ 4. 每个可能写三样:触发(哪几条条件怎么变)、长什么样(主体+性质+档位,能判是哪个状态)、
101
+ 大还是小(按条件现在离失效多远、补位顶不顶得上排,不硬给百分比)。
102
+ 5. B 要写明凭什么最大:哪条条件离失效最近、哪条补位最薄;说不出,B 就不是大概率。
103
+ 6. 非关键项失效:冷备顶上要多久;顶不上就按关键项算。
104
+ 7. 变弱或变强,状态可以不动——不写成已翻。
105
+
106
+ 维持项不在成立条件表里,但预测要带上:它决定状态能撑多久。
107
+
108
+ ## 边界
109
+
110
+ - 对象:靠成立条件成立的状态或状态转变——组织、制度、技术、习惯、模式、价格、共识、人物转变;数学对象与物理常数没有前提。
111
+ - 两态要可比:同一个主体、同一套比较方面;同一状态的来回波动不算转变。单态没有可比对象,但「同一套比较方面」仍要守住,否则撤项测试没有判据。
112
+ - 单态只筛一张表,不硬凑 A、B 两态;它答的是靠什么成立,不答怎么补——补法另找一套。
113
+ - 维持项不进成立条件表:它不是条件,是状态能撑多久的来源。
114
+ - 成立条件与原因分层:成立条件是状态成立所靠的几项;原因是推着成立条件变的东西。两层不混。
115
+ - 看的是成立条件:缺了它这个状态不成立的那几项,不是它此刻需要外面有什么。
116
+ - 关键成立条件相对于状态与时间:同一主体的不同状态,关键项可以不同;同一个项今天可替换、明天可能没第二条路,判关键要按当时算。
117
+ - 剩余形态:A 的条件失效后剩下的形态会自己稳定、长出条件;之后按成立条件分析它,但别把后来长出的条件当成它出现的原因。
118
+ - 内外不是固定的:同一项可以从内变外。
119
+ - 力度:条件在变弱就写变弱,写明多快;没发生的不写成已发生。
120
+ - 分工:追「谁干的」用逆向倒推;讲否定之后的形态变化用扬弃;这里看成立条件怎么变、主体怎么换状态。
@@ -0,0 +1,4 @@
1
+ # dsh-premise-trace:一个插件行,注册一个内置技能 premise-trace(方法正文在 assets/premise-trace.md)。
2
+ - insert:
3
+ - id: dsh-premise-trace
4
+ name: dsh-premise-trace
package/index.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 前提追溯:判断一个主体为什么从状态 A 转到状态 B、下一步转到哪,或解释单个状态靠哪几项成立条件同时成立。
3
+ *
4
+ * 只注册一个内置技能,没有工具、命令、路由与浏览器半边;除了 node:fs 不引任何包。
5
+ */
6
+
7
+ import { readFileSync } from 'node:fs'
8
+
9
+ /**
10
+ * 技能排序档位:与宿主内置 bundled 技能同档。
11
+ * 宿主导出过这个常量(BUNDLED_SKILL_RANK = 600),但那是宿主内部包,装进别人的 profile
12
+ * 不一定解析得到,所以这里写定值,靠 scripts/smoke.mjs 在校验时比对资产文件是否可读。
13
+ */
14
+ const SKILL_RANK = 600
15
+
16
+ /** Cordis 插件名。 */
17
+ export const name = 'dsh-premise-trace'
18
+ /** 需要的服务。 */
19
+ export const inject = ['skills']
20
+
21
+ /** 注册内置技能。 */
22
+ export function apply(ctx) {
23
+ // 单 provider 挂全部技能;本包只有一个
24
+ const skillEntries = [
25
+ {
26
+ name: 'premise-trace',
27
+ description:
28
+ '前提追溯:解释一个主体怎么从状态 A 转到状态 B,或解释单个状态靠什么成立——状态靠哪几项成立条件同时成立,成立条件变强/变弱/被替换/失效后哪项是关键成立条件;不是列它外面需要什么。',
29
+ file: 'premise-trace.md',
30
+ },
31
+ ]
32
+
33
+ ctx.skills.registerProvider(() => ({
34
+ name: 'dsh-premise-trace',
35
+ list: () =>
36
+ Promise.resolve(
37
+ skillEntries.map((s) => ({
38
+ name: s.name,
39
+ description: s.description,
40
+ invocation: { modelInvocable: true, userInvocable: true },
41
+ provider: 'dsh-premise-trace',
42
+ source: 'bundled',
43
+ resourceBase: { kind: 'directory', path: new URL('./assets/', import.meta.url).pathname },
44
+ rank: SKILL_RANK,
45
+ locator: new URL(`./assets/${s.file}`, import.meta.url),
46
+ })),
47
+ ),
48
+ async get(candidate) {
49
+ const s = skillEntries.find((x) => x.name === candidate.name) ?? skillEntries[0]
50
+ const body = readFileSync(new URL(`./assets/${s.file}`, import.meta.url), 'utf8')
51
+ return { ...candidate, content: body }
52
+ },
53
+ }))
54
+ }
package/invariant.js ADDED
@@ -0,0 +1,10 @@
1
+ const PACKAGE_NAME = 'dsh-premise-trace'
2
+
3
+ /** Cordis companion plugin name. */
4
+ export const name = 'dsh-premise-trace-invariant'
5
+ /** Service required before the companion can reserve package ownership. */
6
+ export const inject = ['invariants']
7
+
8
+ const install = () => {}
9
+
10
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "dsh-premise-trace",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "回望一件事怎么变成现在这样,说清它现在靠什么成立,判断它下一步会变成什么。",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./invariant": "./invariant.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "invariant.js",
15
+ "cordis.patch.yml",
16
+ "assets",
17
+ "scripts",
18
+ "README.md",
19
+ "README.en.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": "^22.19.0 || >=24.0.0",
24
+ "dsh": ">=0.2.1-0 <0.3.0-0"
25
+ },
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./cordis.patch.yml"
29
+ }
30
+ },
31
+ "keywords": [
32
+ "deepseek-harness",
33
+ "dsh",
34
+ "dsh-plugin",
35
+ "skill",
36
+ "reasoning",
37
+ "analysis",
38
+ "conditions",
39
+ "causal-analysis"
40
+ ],
41
+ "scripts": {
42
+ "check": "node --check index.js && node --check invariant.js",
43
+ "smoke": "node scripts/smoke.mjs"
44
+ },
45
+ "author": "taoziliyouchong",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/taoziliyouchong/dsh-premise-trace"
50
+ },
51
+ "homepage": "https://github.com/taoziliyouchong/dsh-premise-trace#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/taoziliyouchong/dsh-premise-trace/issues"
54
+ }
55
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 本机冒烟:不起应用、不联网、不碰任何会话数据。
4
+ *
5
+ * 六关:
6
+ * 1. 包结构 —— files 声明的必需文件都在
7
+ * 2. 组合声明 —— dsh.bundle.patch 指向的文件在,里面的 insert id 等于包名
8
+ * 3. 注册 —— 桩 ctx 跑 apply:导出名等于包名,注册 1 个技能 provider、1 个技能
9
+ * 4. 正文可读 —— provider.get() 能读出 assets 里的方法正文,长度与关键小节齐
10
+ * 5. 零外部依赖 —— 运行时只引 node: 内置模块,不引宿主内部包
11
+ * 6. 伴随插件 —— invariant.js 导出名与登记名符合约定
12
+ *
13
+ * 用法:node scripts/smoke.mjs (任一条失败退出码 1)
14
+ */
15
+
16
+ import { readFileSync, existsSync } from 'node:fs'
17
+ import { dirname, join, resolve } from 'node:path'
18
+ import { fileURLToPath, pathToFileURL } from 'node:url'
19
+
20
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
21
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
22
+ const problems = []
23
+
24
+ const ok = (label, extra = '') => console.log('ok ' + label + (extra ? ' ' + extra : ''))
25
+ const fail = (label, why) => {
26
+ problems.push(label + ': ' + why)
27
+ console.log('FAIL ' + label + ' ' + why)
28
+ }
29
+
30
+ /** 1. 包结构 */
31
+ const REQUIRED = [
32
+ 'index.js',
33
+ 'invariant.js',
34
+ 'cordis.patch.yml',
35
+ 'assets/premise-trace.md',
36
+ 'README.md',
37
+ 'README.en.md',
38
+ 'LICENSE',
39
+ 'scripts/smoke.mjs',
40
+ ]
41
+ const missing = REQUIRED.filter((rel) => !existsSync(join(root, rel)))
42
+ if (missing.length > 0) fail('结构', '缺 ' + missing.join('、'))
43
+ else ok('结构', REQUIRED.length + ' 个必需文件齐全')
44
+
45
+ /** 2. 组合声明:dsh plugin add 靠这一条把包接进层栈 */
46
+ const patchRel = pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch
47
+ if (!patchRel) fail('组合声明', 'package.json 没声明 dsh.bundle.patch')
48
+ else if (!existsSync(join(root, patchRel))) fail('组合声明', 'dsh.bundle.patch 指向的文件不存在:' + patchRel)
49
+ else {
50
+ const patch = readFileSync(join(root, patchRel), 'utf8')
51
+ if (!patch.includes('id: ' + pkg.name)) fail('组合声明', patchRel + ' 里没有 id: ' + pkg.name)
52
+ else if (!patch.includes('name: ' + pkg.name)) fail('组合声明', patchRel + ' 里没有 name: ' + pkg.name)
53
+ else ok('组合声明', 'dsh.bundle.patch → ' + patchRel + '(id 与 name 都是 ' + pkg.name + ')')
54
+ }
55
+ if (pkg.files && !pkg.files.includes('assets')) fail('组合声明', 'files 里没有 assets,装出来的包读不到方法正文')
56
+ if (pkg.exports && pkg.exports['./client']) fail('组合声明', '本包没有浏览器半边,exports 不该声明 ./client')
57
+
58
+ /** 3. 注册:桩 ctx 跑 apply */
59
+ const mod = await import(pathToFileURL(join(root, 'index.js')).href)
60
+ if (mod.name !== pkg.name) fail('注册', 'index.js 导出的 name 是 ' + mod.name + ',不等于包名 ' + pkg.name)
61
+ else ok('注册', 'name = ' + mod.name)
62
+ if (!Array.isArray(mod.inject) || !mod.inject.includes('skills')) fail('注册', 'inject 里没有 skills')
63
+
64
+ const providers = []
65
+ const ctx = {
66
+ skills: {
67
+ registerProvider(factory) {
68
+ providers.push(factory())
69
+ },
70
+ },
71
+ }
72
+ try {
73
+ mod.apply(ctx)
74
+ } catch (error) {
75
+ fail('注册', 'apply 抛错:' + error.message)
76
+ }
77
+ if (providers.length !== 1) fail('注册', '应注册 1 个技能 provider,实际 ' + providers.length)
78
+ else {
79
+ const listed = await providers[0].list()
80
+ if (!Array.isArray(listed) || listed.length !== 1) fail('注册', 'provider 应列出 1 个技能,实际 ' + (listed || []).length)
81
+ else {
82
+ const skill = listed[0]
83
+ if (skill.name !== 'premise-trace') fail('注册', '技能名是 ' + skill.name + ',应为 premise-trace')
84
+ else if (skill.source !== 'bundled') fail('注册', '技能 source 是 ' + skill.source + ',应为 bundled')
85
+ else if (typeof skill.rank !== 'number') fail('注册', '技能缺数字档位 rank')
86
+ else if (!skill.invocation || !skill.invocation.modelInvocable) fail('注册', '技能没有对模型开放')
87
+ else ok('注册', '1 个技能:' + skill.name + '(rank ' + skill.rank + ')')
88
+ }
89
+ }
90
+
91
+ /** 4. 正文可读:方法正文必须随包带上、能被 get() 读出 */
92
+ if (providers.length === 1) {
93
+ try {
94
+ const skill = (await providers[0].list())[0]
95
+ const loaded = await providers[0].get({ name: skill.name })
96
+ const body = String(loaded && loaded.content)
97
+ const bytes = Buffer.byteLength(body, 'utf8')
98
+ if (bytes < 10000) fail('正文', '正文只有 ' + bytes + ' 字节,疑似截断')
99
+ else if (!body.includes('关键成立条件')) fail('正文', '正文里找不到「关键成立条件」')
100
+ else if (!body.includes('## 边界')) fail('正文', '正文里缺「## 边界」小节')
101
+ else ok('正文', bytes + ' 字节,关键小节齐')
102
+ } catch (error) {
103
+ fail('正文', 'get() 抛错:' + error.message)
104
+ }
105
+ }
106
+
107
+ /** 5. 零外部依赖:运行时只引 node: 内置模块 */
108
+ for (const rel of ['index.js', 'invariant.js']) {
109
+ const source = readFileSync(join(root, rel), 'utf8')
110
+ const imports = [...source.matchAll(/from\s+'([^']+)'/g)].map((m) => m[1])
111
+ const external = imports.filter((spec) => !spec.startsWith('node:'))
112
+ if (external.length > 0) fail('依赖', rel + ' 引了外部包:' + external.join('、'))
113
+ }
114
+ if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
115
+ fail('依赖', 'package.json 声明了 dependencies,本包应为零依赖:' + Object.keys(pkg.dependencies).join('、'))
116
+ }
117
+ if (!problems.some((p) => p.startsWith('依赖'))) ok('依赖', '运行时只用 node: 内置模块')
118
+
119
+ /** 6. 伴随插件 */
120
+ const companion = await import(pathToFileURL(join(root, 'invariant.js')).href)
121
+ if (companion.name !== pkg.name + '-invariant') fail('伴随插件', '导出的 name 是 ' + companion.name + ',应为 ' + pkg.name + '-invariant')
122
+ else if (!Array.isArray(companion.inject) || !companion.inject.includes('invariants')) fail('伴随插件', 'inject 里没有 invariants')
123
+ else {
124
+ const registered = []
125
+ const invariantCtx = { invariants: { register: (name) => registered.push(name) } }
126
+ await companion.apply(invariantCtx)
127
+ if (registered[0] !== pkg.name) fail('伴随插件', '登记的包名是 ' + registered[0] + ',应为 ' + pkg.name)
128
+ else ok('伴随插件', '登记 ' + pkg.name)
129
+ }
130
+
131
+ console.log('')
132
+ if (problems.length > 0) {
133
+ console.error('冒烟失败 ' + problems.length + ' 项')
134
+ process.exit(1)
135
+ }
136
+ console.log('冒烟通过')
137
+ process.exit(0)