dsh-contract-suite 0.1.0-alpha.7 → 0.1.0-alpha.9

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-zh.md CHANGED
@@ -33,6 +33,82 @@ dsh plugin --profile <profile> add dsh-contract-suite
33
33
 
34
34
  *工作流:本地输入 → 插件 → 本地输出;不需要调用模型。*
35
35
 
36
+ ## 一句话
37
+
38
+ 一次装齐合同法务两件套,并核对两个成员包的版本是否可解析。
39
+
40
+ ## 适合 / 不适合
41
+
42
+ - **适合**:要在一个 DSH profile 里同时装合同录入与条款审查;需要把两个成员锁在同一组版本;装完先确认成员行一个不少。
43
+ - **不适合**:只想装其中一件(直接装对应成员包更轻);想让 suite 自己审合同(suite 没有工具面,审查由成员包完成);需要 OCR、外部 CLM 或 e-sign 对接。
44
+
45
+ ## 输入
46
+
47
+ - 安装动作本身不读业务单据。
48
+ - 自带样例读取 `samples/quickstart/input/profile.json`(合成 profile dump,< 1 KB)、本包 `package.json` 与 `cordis.patch.yml`;不读其他目录、不读客户合同。
49
+ - 成员包各自的输入(合同文本、条款清单、playbook)见成员仓库 README;本 suite 不改变它们的输入输出。
50
+
51
+ ## 输出
52
+
53
+ - 终端给出成员清单、固定版本、解析来源;退出码 `0` 表示 2/2 可解析。
54
+ - `node samples/quickstart/run.mjs --json` 输出金标 JSON,文件在 `samples/quickstart/expected/result.json`。
55
+ - 不写任何文件、不调用模型、不联网、不上传;不改动原合同。
56
+
57
+ ## 示例
58
+
59
+ 把这句话粘进 DSH:
60
+
61
+ > 帮我装好合同法务两件套(合同录入、条款审查),确认两个成员包都能解析,并列出各自版本。
62
+
63
+ 装好后在包目录跑自带样例(离线,不装依赖也能跑):
64
+
65
+ ```sh
66
+ node samples/quickstart/run.mjs
67
+ ```
68
+
69
+ 真实输出:
70
+
71
+ ```text
72
+ dsh-contract-suite quickstart (offline)
73
+ suite: dsh-contract-suite 0.1.0-alpha.8
74
+ profile: contract-demo (synthetic, 2/2 rows enabled)
75
+ members: 2/2 resolved
76
+ dsh-legal-intake 0.1.0-alpha.10 [manifest pin] profile=enabled
77
+ dsh-legal-clause 0.1.0-alpha.12 [manifest pin] profile=enabled
78
+ network: false (no fetch, no registry call)
79
+ golden: samples/quickstart/expected/result.json matches
80
+ quickstart ok
81
+ ```
82
+
83
+ ![dsh-contract-suite quickstart 真实运行输出](docs/assets/quickstart.png)
84
+
85
+ 截图就是上面这条命令的真实输出;金标与截图记录见 `screenshots.json`。
86
+
87
+ ## 权限
88
+
89
+ 读:`samples/quickstart/input/profile.json`、`cordis.patch.yml`、`package.json`;不读其他目录
90
+ 写:无(R0,只打印到终端;不写 `$DSH_HOME`、不改原文件)
91
+ 联网:否
92
+ 外部系统:无
93
+
94
+ ## FAQ
95
+
96
+ 1. **装不上或装完少一个成员?** 先跑 `node samples/quickstart/run.mjs`。如果两处成员列表不一致,会报 `E_SUITE_PATCH_MISMATCH`;按 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 对齐后重装。
97
+ 2. **结果不对,版本和套件声明的不一样?** 套件固定成员版本;单独升级成员会报 `E_SUITE_VERSION_MISMATCH`。删掉该 profile 下的 `node_modules` 后重装 suite。
98
+ 3. **能直接审我的合同吗?** suite 本身不处理合同,只负责一次装齐成员。合同录入与条款审查各自用成员包的工具;自带样例只用合成 profile,不读真实合同。
99
+ 4. **卸载 suite 会把成员也删掉吗?** 不会。卸载只移除 suite 行,成员包保留;不需要时再逐个卸载。
100
+
101
+ ## 版本
102
+
103
+ 当前版本:`0.1.0-alpha.8`(与 `package.json` 一致)。
104
+
105
+ 本地验证(2026-11-02):
106
+
107
+ ```sh
108
+ node --test test/suite.test.mjs # 8 项通过,0 失败
109
+ node samples/quickstart/run.mjs --check # golden matches
110
+ ```
111
+
36
112
  ## 安装
37
113
 
38
114
  安装并验证:
@@ -72,33 +148,18 @@ suite 只负责安装这些成员,本身没有工具面;版本由 `dependenc
72
148
 
73
149
  | 成员 | 版本 |
74
150
  |---|---:|
75
- | `dsh-legal-intake` | `0.1.0-alpha.6` |
76
- | `dsh-legal-clause` | `0.1.0-alpha.8` |
151
+ | `dsh-legal-intake` | `0.1.0-alpha.10` |
152
+ | `dsh-legal-clause` | `0.1.0-alpha.12` |
77
153
 
78
- ## 快速开始
154
+ ## 安装后自检
79
155
 
80
- ```sh
81
- dsh plugin --profile <profile> add dsh-contract-suite
82
- dsh --profile <profile> --dump-config | grep dsh-contract-suite
83
- ```
84
-
85
- ![Terminal demo: real dsh-contract-suite install output](docs/assets/dsh-contract-suite-install.gif)
86
-
87
- *临时 profile 中真实 `dsh plugin add dsh-finops-suite` 输出;路径为临时目录。*
88
-
89
- ## 示例
90
-
91
- ### fixture 支撑的示例
92
-
93
- 命令:
156
+ 装完先跑自带样例,确认两个成员都能解析:
94
157
 
95
158
  ```sh
96
- dsh plugin --profile <profile> add dsh-contract-suite
159
+ node samples/quickstart/run.mjs
97
160
  ```
98
161
 
99
- 这是 fixture 支撑的验收行,不是客户运行;没有编造输出。
100
-
101
- ![Real install output](docs/assets/dsh-contract-suite-install.gif)
162
+ 成员包各自的工具面见上表链接的成员仓库;suite 本身不提供工具。
102
163
 
103
164
  ## 边界
104
165
 
package/README.md CHANGED
@@ -33,6 +33,82 @@ dsh plugin --profile <profile> add dsh-contract-suite
33
33
 
34
34
  *Workflow: local input → plugin → local output. No model call is required.*
35
35
 
36
+ ## One line
37
+
38
+ Install the two contract packages in one command and verify every member version resolves.
39
+
40
+ ## Best for / not for
41
+
42
+ - **Best for:** installing dsh-legal-intake and dsh-legal-clause into one DSH profile; keeping both members on one pinned version set; confirming no member row is missing right after install.
43
+ - **Not for:** installing only one package (install that member directly); expecting the suite itself to review contracts (it has no tool surface — the members do); OCR, external CLM, or e-sign integration.
44
+
45
+ ## Input
46
+
47
+ - The install itself reads no business documents.
48
+ - The bundled sample reads `samples/quickstart/input/profile.json` (synthetic profile dump, < 1 KB), plus this package's `package.json` and `cordis.patch.yml`. It reads no other directory and no customer contract.
49
+ - Each member's own input (contract text, clause lists, playbook) is documented in that member's README; the suite does not change it.
50
+
51
+ ## Output
52
+
53
+ - The terminal prints the member list, pinned version, and where each version was resolved from; exit code `0` means 2/2 resolved.
54
+ - `node samples/quickstart/run.mjs --json` prints the golden JSON stored at `samples/quickstart/expected/result.json`.
55
+ - No files are written, no model is called, no network is used, nothing is uploaded, and source contracts are untouched.
56
+
57
+ ## Example
58
+
59
+ Paste this into DSH:
60
+
61
+ > 帮我装好合同法务两件套(合同录入、条款审查),确认两个成员包都能解析,并列出各自版本。
62
+
63
+ Then run the bundled sample from the package directory (offline, no dependency install needed):
64
+
65
+ ```sh
66
+ node samples/quickstart/run.mjs
67
+ ```
68
+
69
+ Real output:
70
+
71
+ ```text
72
+ dsh-contract-suite quickstart (offline)
73
+ suite: dsh-contract-suite 0.1.0-alpha.8
74
+ profile: contract-demo (synthetic, 2/2 rows enabled)
75
+ members: 2/2 resolved
76
+ dsh-legal-intake 0.1.0-alpha.10 [manifest pin] profile=enabled
77
+ dsh-legal-clause 0.1.0-alpha.12 [manifest pin] profile=enabled
78
+ network: false (no fetch, no registry call)
79
+ golden: samples/quickstart/expected/result.json matches
80
+ quickstart ok
81
+ ```
82
+
83
+ ![dsh-contract-suite quickstart real terminal output](docs/assets/quickstart.png)
84
+
85
+ The screenshot is the real output of the command above; the golden file and screenshot metadata are in `screenshots.json`.
86
+
87
+ ## Permissions
88
+
89
+ Read: `samples/quickstart/input/profile.json`, `cordis.patch.yml`, `package.json`; no other directory
90
+ Write: none (R0 — terminal output only; no `$DSH_HOME` writes, source files untouched)
91
+ Network: no
92
+ External systems: none
93
+
94
+ ## FAQ
95
+
96
+ 1. **The install fails or a member is missing.** Run `node samples/quickstart/run.mjs`. If the two member lists disagree it raises `E_SUITE_PATCH_MISMATCH`; align them as described in [TROUBLESHOOTING.md](TROUBLESHOOTING.md), then reinstall.
97
+ 2. **The version does not match the suite declaration.** The suite pins exact member versions; upgrading one member alone raises `E_SUITE_VERSION_MISMATCH`. Remove `node_modules` in that profile and reinstall the suite.
98
+ 3. **Can the suite review my contracts?** No — the suite only installs members. Use the member packages for intake and clause review; the bundled sample uses a synthetic profile and reads no real contract.
99
+ 4. **Does uninstalling the suite remove the members?** No. Removing the suite removes only its row; member packages stay installed until you remove them.
100
+
101
+ ## Version
102
+
103
+ Current version: `0.1.0-alpha.8` (matches `package.json`).
104
+
105
+ Local verification (2026-11-02):
106
+
107
+ ```sh
108
+ node --test test/suite.test.mjs # 8 passed, 0 failed
109
+ node samples/quickstart/run.mjs --check # golden matches
110
+ ```
111
+
36
112
  ## Install
37
113
 
38
114
  Install and verify:
@@ -72,33 +148,18 @@ The suite only installs these members; it has no tool surface of its own. Versio
72
148
 
73
149
  | Member | Version |
74
150
  |---|---:|
75
- | `dsh-legal-intake` | `0.1.0-alpha.6` |
76
- | `dsh-legal-clause` | `0.1.0-alpha.8` |
77
-
78
- ## Quick start
151
+ | `dsh-legal-intake` | `0.1.0-alpha.10` |
152
+ | `dsh-legal-clause` | `0.1.0-alpha.12` |
79
153
 
80
- ```sh
81
- dsh plugin --profile <profile> add dsh-contract-suite
82
- dsh --profile <profile> --dump-config | grep dsh-contract-suite
83
- ```
84
-
85
- ![Terminal demo: real dsh-contract-suite install output](docs/assets/dsh-contract-suite-install.gif)
154
+ ## Post-install self-check
86
155
 
87
- *Real `dsh plugin add dsh-finops-suite` output in a temporary profile; the path is temporary.*
88
-
89
- ## Examples
90
-
91
- ### Fixture-backed example
92
-
93
- Command:
156
+ Run the bundled sample after install to confirm both members resolve:
94
157
 
95
158
  ```sh
96
- dsh plugin --profile <profile> add dsh-contract-suite
159
+ node samples/quickstart/run.mjs
97
160
  ```
98
161
 
99
- This is a fixture-backed acceptance row, not a customer run; no output is invented.
100
-
101
- ![Real install output](docs/assets/dsh-contract-suite-install.gif)
162
+ Each member's tool surface lives in its own repository; the suite ships no tools.
102
163
 
103
164
  ## Limits
104
165
 
package/SUITE.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  | Member | Pinned version |
8
8
  |---|---:|
9
- | `dsh-legal-intake` | `0.1.0-alpha.9` |
10
- | `dsh-legal-clause` | `0.1.0-alpha.11` |
9
+ | `dsh-legal-intake` | `0.1.0-alpha.10` |
10
+ | `dsh-legal-clause` | `0.1.0-alpha.12` |
11
11
 
12
12
  ## Install
13
13
 
@@ -0,0 +1,30 @@
1
+ # 故障排查 — dsh-contract-suite
2
+
3
+ 每条按「症状 / 原因 / 动作」写。报错原文的格式是 `<一句人话>(E_XXX)|下一步:<一个动作>`;把 `E_XXX` 和那一行一起发到 issue 即可定位。不要把真实合同、条款清单或客户数据贴进 issue。
4
+
5
+ ## 1. 装完少一个成员(E_SUITE_PATCH_MISMATCH)
6
+
7
+ - **症状**:`cordis.patch.yml 与 dependencies 不一致(dependencies 有但 patch 没有:dsh-legal-clause)(E_SUITE_PATCH_MISMATCH)|下一步:让两处成员列表完全一致后重新打包发布`
8
+ - **原因**:升级时只改了 `package.json` 的 `dependencies`,忘了在 `cordis.patch.yml` 增加对应的 `id` / `name` 行;或者删成员时只删了一处。
9
+ - **动作**:在本包根目录跑 `node samples/quickstart/run.mjs`,按报错里列出的差异把 `cordis.patch.yml` 与 `dependencies` 对齐,重新打包后再安装。
10
+
11
+ ## 2. 套件清单读不出来(E_SUITE_MANIFEST_MISSING)
12
+
13
+ - **症状**:`找不到套件清单 package.json(E_SUITE_MANIFEST_MISSING)|下一步:在套件包根目录运行命令,或重新安装套件`
14
+ - **原因**:命令跑在了错误目录;tarball 解压不完整,`cordis.patch.yml` 或 `samples/` 没进包;`package.json` 被手工改坏成非法 JSON。
15
+ - **动作**:回到解压后的套件根目录(能看到 `package.json` 和 `cordis.patch.yml`)再跑;仍失败就删掉该 profile 下的套件目录,重新执行 `dsh plugin --profile <profile> add dsh-contract-suite`。
16
+
17
+ ## 3. 成员版本对不上(E_SUITE_VERSION_MISMATCH)
18
+
19
+ - **症状**:`成员 dsh-legal-intake 已安装 0.1.0-alpha.11,套件固定 0.1.0-alpha.10(E_SUITE_VERSION_MISMATCH)|下一步:删除 node_modules 后重新安装套件,不要单独升级成员包`
20
+ - **原因**:在同一个 profile 里单独升级过某个成员,或 `node_modules` 里残留了旧版本;suite 承诺的是固定的一组成员版本。
21
+ - **动作**:删除该 profile 下的 `node_modules`,重新安装 suite;如果确实需要新成员版本,等套件发布新的固定版本,不要只升成员。
22
+
23
+ ## 还不行
24
+
25
+ ```sh
26
+ node samples/quickstart/run.mjs --check # 自带样例必须通过
27
+ node --test test/suite.test.mjs # 8 项测试
28
+ ```
29
+
30
+ 仍失败时请在 <https://github.com/PerryLink/dsh-contract-suite/issues> 附上:包版本、Node 版本、最小合成输入、完整报错那一行。
@@ -0,0 +1,10 @@
1
+ $ node samples/quickstart/run.mjs
2
+ dsh-contract-suite quickstart (offline)
3
+ suite: dsh-contract-suite 0.1.0-alpha.8
4
+ profile: contract-demo (synthetic, 2/2 rows enabled)
5
+ members: 2/2 resolved
6
+ dsh-legal-intake 0.1.0-alpha.10 [manifest pin] profile=enabled
7
+ dsh-legal-clause 0.1.0-alpha.12 [manifest pin] profile=enabled
8
+ network: false (no fetch, no registry call)
9
+ golden: samples/quickstart/expected/result.json matches
10
+ quickstart ok
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-contract-suite",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.9",
4
4
  "description": "One-command install for dsh-legal-intake and dsh-legal-clause.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -8,14 +8,34 @@
8
8
  "manifestVersion": 1,
9
9
  "bundle": {
10
10
  "patch": "./cordis.patch.yml"
11
+ },
12
+ "demo": {
13
+ "sample": "samples/quickstart/input/profile.json",
14
+ "prompt": "帮我装好合同法务两件套(合同录入、条款审查),确认两个成员包都能解析,并列出各自版本。",
15
+ "expect": "samples/quickstart/expected/result.json",
16
+ "screenshot": "docs/assets/quickstart.png",
17
+ "reads": [
18
+ "samples/quickstart/input/profile.json(合成 profile dump)与本包 cordis.patch.yml、package.json"
19
+ ],
20
+ "writes": [],
21
+ "network": false,
22
+ "external": []
11
23
  }
12
24
  },
25
+ "scripts": {
26
+ "quickstart": "node samples/quickstart/run.mjs",
27
+ "test": "node --test test/suite.test.mjs",
28
+ "gate": "node scripts/gate.mjs",
29
+ "check": "node scripts/check.mjs"
30
+ },
13
31
  "engines": {
14
32
  "dsh": ">=0.2.1-rc.3 <0.3.0",
15
33
  "node": ">=24.9.0 <25"
16
34
  },
17
35
  "files": [
18
36
  "cordis.patch.yml",
37
+ "src",
38
+ "samples",
19
39
  "README.md",
20
40
  "LICENSE",
21
41
  "README-zh.md",
@@ -29,6 +49,8 @@
29
49
  "CONTRIBUTING.md",
30
50
  "CHANGELOG.md",
31
51
  "SUITE.md",
52
+ "TROUBLESHOOTING.md",
53
+ "screenshots.json",
32
54
  "icon.svg"
33
55
  ],
34
56
  "keywords": [
@@ -52,8 +74,8 @@
52
74
  "access": "public"
53
75
  },
54
76
  "dependencies": {
55
- "dsh-legal-intake": "0.1.0-alpha.9",
56
- "dsh-legal-clause": "0.1.0-alpha.11"
77
+ "dsh-legal-intake": "0.1.0-alpha.10",
78
+ "dsh-legal-clause": "0.1.0-alpha.12"
57
79
  },
58
80
  "icon": "icon.svg"
59
81
  }
@@ -0,0 +1,32 @@
1
+ {
2
+ "suite": "dsh-contract-suite",
3
+ "version": "0.1.0-alpha.8",
4
+ "sample": "samples/quickstart/input/profile.json",
5
+ "network": false,
6
+ "profile": {
7
+ "name": "contract-demo",
8
+ "kind": "synthetic",
9
+ "rows": 2,
10
+ "enabled": 2,
11
+ "missing": 0
12
+ },
13
+ "members": [
14
+ {
15
+ "id": "dsh-legal-intake",
16
+ "version": "0.1.0-alpha.10",
17
+ "pin": "0.1.0-alpha.10",
18
+ "profile_row": "enabled"
19
+ },
20
+ {
21
+ "id": "dsh-legal-clause",
22
+ "version": "0.1.0-alpha.12",
23
+ "pin": "0.1.0-alpha.12",
24
+ "profile_row": "enabled"
25
+ }
26
+ ],
27
+ "summary": {
28
+ "members": 2,
29
+ "resolved": 2,
30
+ "ok": true
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "_note": "Synthetic DSH profile dump for the dsh-contract-suite quickstart. No user, customer or production data.",
3
+ "profile": "contract-demo",
4
+ "kind": "synthetic",
5
+ "created_at": "2026-11-02T00:00:00Z",
6
+ "rows": [
7
+ { "id": "dsh-legal-intake", "version": "0.1.0-alpha.10", "enabled": true },
8
+ { "id": "dsh-legal-clause", "version": "0.1.0-alpha.12", "enabled": true }
9
+ ]
10
+ }
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-contract-suite quickstart — offline demonstration of what a one-command
4
+ * install gives you: every member named in `cordis.patch.yml` resolves, and the
5
+ * synthetic profile dump has one enabled row per member at the pinned version.
6
+ *
7
+ * node samples/quickstart/run.mjs # human-readable + golden check
8
+ * node samples/quickstart/run.mjs --json # golden result on stdout
9
+ * node samples/quickstart/run.mjs --check # same as default, explicit
10
+ *
11
+ * The golden file `expected/result.json` is the stdout of `--json`, so it is a
12
+ * real run, not hand-written. This script never touches the network and writes
13
+ * no files.
14
+ */
15
+ import assert from 'node:assert/strict'
16
+ import { readFileSync } from 'node:fs'
17
+ import path from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { inspectMembers, readProfile, SuiteError } from '../../src/members.mjs'
20
+
21
+ const here = path.dirname(fileURLToPath(import.meta.url))
22
+ const root = path.resolve(here, '..', '..')
23
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
24
+ const demo = pkg.dsh?.demo || {}
25
+ const sampleRef = demo.sample || 'samples/quickstart/input/profile.json'
26
+ const expectedRef = demo.expect || 'samples/quickstart/expected/result.json'
27
+ const mode = process.argv.includes('--json') ? 'json' : process.argv.includes('--check') ? 'check' : 'summary'
28
+
29
+ function buildResult() {
30
+ const suite = inspectMembers(root)
31
+ const profile = readProfile(root, sampleRef)
32
+ const rows = new Map(profile.rows.map((row) => [row.id, row]))
33
+ const missing = []
34
+ const members = suite.members.map((member) => {
35
+ const row = rows.get(member.id)
36
+ if (!row) {
37
+ missing.push(member.id)
38
+ return { id: member.id, version: member.version, pin: member.pin, profile_row: 'missing' }
39
+ }
40
+ if (row.version !== member.pin) {
41
+ throw new SuiteError(
42
+ 'E_SUITE_VERSION_MISMATCH',
43
+ `profile 中 ${member.id} 的版本 ${String(row.version)} 与套件固定 ${member.pin} 不一致`,
44
+ '重新生成 profile dump,或删除该行后重新安装套件',
45
+ )
46
+ }
47
+ return {
48
+ id: member.id,
49
+ version: member.version,
50
+ pin: member.pin,
51
+ profile_row: row.enabled === false ? 'disabled' : 'enabled',
52
+ }
53
+ })
54
+ const enabled = members.filter((member) => member.profile_row === 'enabled').length
55
+ return {
56
+ suite: suite.name,
57
+ version: suite.version,
58
+ sample: sampleRef,
59
+ network: false,
60
+ profile: {
61
+ name: String(profile.profile ?? 'unknown'),
62
+ kind: String(profile.kind ?? 'synthetic'),
63
+ rows: members.length,
64
+ enabled,
65
+ missing: missing.length,
66
+ },
67
+ members,
68
+ summary: { members: members.length, resolved: members.length, ok: missing.length === 0 },
69
+ }
70
+ }
71
+
72
+ function readExpected() {
73
+ return JSON.parse(readFileSync(path.join(root, expectedRef), 'utf8'))
74
+ }
75
+
76
+ function printSummary(result) {
77
+ console.log(`${result.suite} quickstart (offline)`)
78
+ console.log(`suite: ${result.suite} ${result.version}`)
79
+ console.log(
80
+ `profile: ${result.profile.name} (${result.profile.kind}, ${result.profile.enabled}/${result.profile.rows} rows enabled)`,
81
+ )
82
+ console.log(`members: ${result.summary.resolved}/${result.summary.members} resolved`)
83
+ for (const member of result.members) {
84
+ const source = member.profile_row === 'enabled' ? '[manifest pin]' : `[${member.profile_row}]`
85
+ console.log(
86
+ ` ${member.id.padEnd(20)} ${member.version.padEnd(16)} ${source.padEnd(17)} profile=${member.profile_row}`,
87
+ )
88
+ }
89
+ console.log(`network: false (no fetch, no registry call)`)
90
+ }
91
+
92
+ function main() {
93
+ const result = buildResult()
94
+ if (mode === 'json') {
95
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
96
+ if (!result.summary.ok) process.exitCode = 1
97
+ return
98
+ }
99
+ printSummary(result)
100
+ if (!result.summary.ok) {
101
+ console.error(`quickstart failed: profile is missing member rows (${result.profile.missing})`)
102
+ process.exitCode = 1
103
+ return
104
+ }
105
+ try {
106
+ assert.deepStrictEqual(result, readExpected())
107
+ console.log(`golden: ${expectedRef} matches`)
108
+ console.log('quickstart ok')
109
+ } catch {
110
+ console.error(`quickstart failed: output does not match ${expectedRef}`)
111
+ console.error('run `node samples/quickstart/run.mjs --json` and diff it against the golden file')
112
+ process.exitCode = 1
113
+ }
114
+ }
115
+
116
+ try {
117
+ main()
118
+ } catch (error) {
119
+ if (error instanceof SuiteError) console.error(`quickstart failed: ${error.message}`)
120
+ else console.error(`quickstart failed: ${error instanceof Error ? error.message : String(error)}`)
121
+ process.exitCode = 1
122
+ }
@@ -0,0 +1,15 @@
1
+ [
2
+ {
3
+ "path": "docs/assets/quickstart.png",
4
+ "kind": "quickstart",
5
+ "alt": "dsh-contract-suite quickstart terminal output",
6
+ "caption": "Real command: node samples/quickstart/run.mjs — dsh-contract-suite 0.1.0-alpha.8, synthetic profile, 2/2 members resolved (dsh-legal-intake 0.1.0-alpha.10, dsh-legal-clause 0.1.0-alpha.12), offline, golden matches. Rendered from the captured terminal output.",
7
+ "command": "node samples/quickstart/run.mjs",
8
+ "run_at": "2026-11-02T14:59:53Z",
9
+ "output_sha256": "e6862e344bfdb23ba5b7db4ca23cc17abafd5151936b2e598bf5e78c4f3babd8",
10
+ "output_path": "samples/quickstart/expected/result.json",
11
+ "log": "docs/assets/quickstart.log",
12
+ "log_sha256": "ed7846f6f0e80882cb09998d448f5b4e683472b2c79faa9aa4f20ef9dcb0e65c",
13
+ "image_sha256": "cd666a1219ee4b8058895749480b5e572eb1fc921b02c2fee0390d20d87a432c"
14
+ }
15
+ ]
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Offline suite-member inspector.
3
+ *
4
+ * A suite is install-only: it has no tools of its own. This module is the one
5
+ * piece of runtime logic, shared by the quickstart sample and the release gate.
6
+ * It reads `package.json` + `cordis.patch.yml`, then checks every member named in
7
+ * the patch. When a member is already installed under `node_modules/`, its real
8
+ * version is used and compared with the suite pin; otherwise the manifest pin is
9
+ * reported. No network access, no writes.
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs'
12
+ import path from 'node:path'
13
+
14
+ export class SuiteError extends Error {
15
+ constructor(code, message, next) {
16
+ super(`${message}(${code})|下一步:${next}`)
17
+ this.name = 'SuiteError'
18
+ this.code = code
19
+ this.next = next
20
+ }
21
+ }
22
+
23
+ function readJson(filePath, label) {
24
+ let text
25
+ try {
26
+ text = readFileSync(filePath, 'utf8')
27
+ } catch {
28
+ throw new SuiteError(
29
+ 'E_SUITE_MANIFEST_MISSING',
30
+ `读不到${label}(${path.basename(filePath)})`,
31
+ '确认套件 tarball 完整,或在包根目录重新安装套件',
32
+ )
33
+ }
34
+ try {
35
+ return JSON.parse(text)
36
+ } catch {
37
+ throw new SuiteError(
38
+ 'E_SUITE_MANIFEST_MISSING',
39
+ `${label}不是合法 JSON,套件读不出成员清单`,
40
+ '重新安装套件,或用 npm 重新下载 tarball',
41
+ )
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Parse the small `- insert:` shape used by cordis.patch.yml. Every member must
47
+ * carry both `id:` and `name:` and the two must match; anything else means the
48
+ * patch no longer describes the dependency set.
49
+ */
50
+ function parsePatch(text) {
51
+ const members = []
52
+ let current = null
53
+ for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
54
+ const id = line.match(/^\s*-\s+id:\s*([^\s#]+)\s*$/)
55
+ if (id) {
56
+ current = { id: id[1], name: null }
57
+ members.push(current)
58
+ continue
59
+ }
60
+ const name = line.match(/^\s+name:\s*([^\s#]+)\s*$/)
61
+ if (name && current) current.name = name[1]
62
+ }
63
+ return members
64
+ }
65
+
66
+ export function readManifest(rootDir) {
67
+ const pkgPath = path.join(rootDir, 'package.json')
68
+ if (!existsSync(pkgPath)) {
69
+ throw new SuiteError(
70
+ 'E_SUITE_MANIFEST_MISSING',
71
+ '找不到套件清单 package.json',
72
+ '在套件包根目录运行命令,或重新安装套件',
73
+ )
74
+ }
75
+ const pkg = readJson(pkgPath, '套件清单')
76
+ const patchRef = pkg.dsh?.bundle?.patch
77
+ if (!patchRef) {
78
+ throw new SuiteError(
79
+ 'E_SUITE_MANIFEST_MISSING',
80
+ 'package.json 缺少 dsh.bundle.patch,套件不知道要装哪些成员',
81
+ '恢复 dsh.bundle.patch 字段后重新打包发布',
82
+ )
83
+ }
84
+ const patchPath = path.resolve(rootDir, patchRef)
85
+ if (!existsSync(patchPath)) {
86
+ throw new SuiteError(
87
+ 'E_SUITE_MANIFEST_MISSING',
88
+ `找不到成员清单 ${patchRef}`,
89
+ '确认发布物包含 cordis.patch.yml,或重新安装套件',
90
+ )
91
+ }
92
+ const members = parsePatch(readFileSync(patchPath, 'utf8'))
93
+ if (members.length === 0) {
94
+ throw new SuiteError(
95
+ 'E_SUITE_PATCH_MISMATCH',
96
+ 'cordis.patch.yml 里没有任何成员',
97
+ '按 package.json 的 dependencies 补回成员行后重新发布',
98
+ )
99
+ }
100
+ const seen = new Set()
101
+ for (const member of members) {
102
+ if (seen.has(member.id)) {
103
+ throw new SuiteError(
104
+ 'E_SUITE_PATCH_MISMATCH',
105
+ `cordis.patch.yml 重复列出成员 ${member.id}`,
106
+ '每个成员只保留一条 id/name 行',
107
+ )
108
+ }
109
+ seen.add(member.id)
110
+ if (member.name !== member.id) {
111
+ throw new SuiteError(
112
+ 'E_SUITE_PATCH_MISMATCH',
113
+ `cordis.patch.yml 的 ${member.id} 缺少匹配的 name 行`,
114
+ '让 id 与 name 两行保持同名后重新发布',
115
+ )
116
+ }
117
+ }
118
+ const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {}
119
+ const depIds = Object.keys(deps)
120
+ const missingInPatch = depIds.filter((id) => !seen.has(id))
121
+ const missingInDeps = [...seen].filter((id) => !(id in deps))
122
+ if (missingInPatch.length > 0 || missingInDeps.length > 0) {
123
+ const pieces = []
124
+ if (missingInPatch.length > 0) pieces.push(`dependencies 有但 patch 没有:${missingInPatch.join(', ')}`)
125
+ if (missingInDeps.length > 0) pieces.push(`patch 有但 dependencies 没有:${missingInDeps.join(', ')}`)
126
+ throw new SuiteError(
127
+ 'E_SUITE_PATCH_MISMATCH',
128
+ `cordis.patch.yml 与 dependencies 不一致(${pieces.join(';')})`,
129
+ '让两处成员列表完全一致后重新打包发布',
130
+ )
131
+ }
132
+ return {
133
+ name: pkg.name,
134
+ version: pkg.version,
135
+ patch: patchRef,
136
+ members: members.map(({ id }) => ({ id, version: deps[id] })),
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Resolve every member. A member installed at exactly the pinned version is
142
+ * reported from `node_modules`; otherwise the dependency pin is the answer.
143
+ * Any installed-but-different version is a hard failure because a suite promises
144
+ * one fixed member set.
145
+ */
146
+ export function inspectMembers(rootDir = process.cwd()) {
147
+ const manifest = readManifest(rootDir)
148
+ const members = manifest.members.map(({ id, version: pin }) => {
149
+ const installedPkg = path.join(rootDir, 'node_modules', id, 'package.json')
150
+ if (!existsSync(installedPkg)) {
151
+ return { id, version: pin, pin, source: 'manifest' }
152
+ }
153
+ const installed = readJson(installedPkg, `已安装成员 ${id} 的清单`)
154
+ if (installed.name !== id) {
155
+ throw new SuiteError(
156
+ 'E_SUITE_VERSION_MISMATCH',
157
+ `node_modules 里的 ${id} 实际是 ${String(installed.name)}`,
158
+ '删除 node_modules 后重新安装套件,避免同名目录被占用',
159
+ )
160
+ }
161
+ if (installed.version !== pin) {
162
+ throw new SuiteError(
163
+ 'E_SUITE_VERSION_MISMATCH',
164
+ `成员 ${id} 已安装 ${String(installed.version)},套件固定 ${pin}`,
165
+ '删除 node_modules 后重新安装套件,不要单独升级成员包',
166
+ )
167
+ }
168
+ return { id, version: installed.version, pin, source: 'node_modules' }
169
+ })
170
+ return { name: manifest.name, version: manifest.version, members }
171
+ }
172
+
173
+ export function readProfile(rootDir, sampleRef) {
174
+ const samplePath = path.resolve(rootDir, sampleRef)
175
+ if (!existsSync(samplePath)) {
176
+ throw new SuiteError(
177
+ 'E_SUITE_MANIFEST_MISSING',
178
+ `找不到 quickstart 输入样例 ${sampleRef}`,
179
+ '确认发布物包含 samples/quickstart/input/,或重新安装套件',
180
+ )
181
+ }
182
+ const profile = readJson(samplePath, 'quickstart 样例')
183
+ if (!Array.isArray(profile.rows)) {
184
+ throw new SuiteError(
185
+ 'E_SUITE_MANIFEST_MISSING',
186
+ 'quickstart 样例缺少 rows 数组,读不出安装后的成员行',
187
+ '恢复 samples/quickstart/input/profile.json 后重跑',
188
+ )
189
+ }
190
+ return profile
191
+ }