skill-family-harness-node 0.17.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ <!-- release-skill:changelog:start version=0.19.0 locale=en baseline=sha256:c3b3a06b509cc53719c2de75d1edacd22c1711c99c5f7687ad91f350976262e3 -->
4
+ ## [0.19.0] - 2026-09-07
5
+
6
+ Harness 0.19.0 adds runMechanismCliBatch and the explicit --batch CLI mode for bounded, ordered, same-operation batch transport, with canonical-json as the first operation.
7
+
8
+ ### Added
9
+
10
+ - Adds runMechanismCliBatch({input, output, error}) on the existing fixed mechanism CLI, reading one batch request and returning per-item results with inputIndex, exitCode, and the original single-request response.
11
+ - Adds the explicit --batch mode to the official Bundle-projected mechanisms-cli.mjs. Without --batch the old single-request path is unchanged.
12
+
13
+ ### Changed
14
+
15
+ - Batch structure and capacity refusals use TypeError with error.details.kind from the closed set batch-structure-invalid, batch-item-limit, batch-input-limit, and batch-output-limit.
16
+ - A single item's mechanism failure is recorded in its result position and sibling items continue; the whole batch exits 2 when any item failed.
17
+
18
+ ### Upgrade Notes
19
+
20
+ Pin all three Foundation packages to exactly 0.19.0. The fixed capacity policy is 256 items, 16 MiB input bytes, and 32 MiB output bytes; consumers own grouping independent requests and splitting oversized batches. This entry is candidate: re-verify after upgrading. The old single-request CLI keeps its previous contract.
21
+ <!-- release-skill:changelog:end version=0.19.0 locale=en -->
22
+
23
+
24
+ <!-- release-skill:changelog:start version=0.18.0 locale=en baseline=sha256:ca3679574ea53013c830563ae818326062794aec1f486e6953e89fa8937be644 -->
25
+ ## [0.18.0] - 2026-09-05
26
+
27
+ Harness 0.18.0 adds the stable replaceFixedSetAtomic operation for replacing one existing fixed-set directory with one complete staged sibling.
28
+
29
+ ### Added
30
+
31
+ - Adds replaceFixedSetAtomic to the package root and fixed-set-publication subpath. Source and target must be real sibling directories under the same canonical parent.
32
+ - Uses one Darwin RENAME_SWAP or Linux RENAME_EXCHANGE commit. On success, the complete new set occupies the target and the displaced old target remains at sourceRoot.
33
+
34
+ ### Changed
35
+
36
+ - Reports pre-commit, post-commit, publication, verification, commit, and durability state through the existing SFC2004 mechanism-error surface when replacement cannot return a verified success.
37
+
38
+ ### Upgrade Notes
39
+
40
+ Pin all three Foundation packages to exactly 0.18.0. replaceFixedSetAtomic is not idempotent: calling it again with the same paths exchanges the directories back. Never retry blindly after success or a post-commit or indeterminate error. The caller owns cleanup of the displaced target after a verified success.
41
+ <!-- release-skill:changelog:end version=0.18.0 locale=en -->
42
+
43
+
3
44
  <!-- release-skill:changelog:start version=0.17.0 locale=en baseline=sha256:5c685956ff1920dccdd91198687dbc1414bfef474a0d297bbcb7704014c549bd -->
4
45
  ## [0.17.0] - 2026-09-01
5
46
 
@@ -1,5 +1,46 @@
1
1
  # 变更日志
2
2
 
3
+ <!-- release-skill:changelog:start version=0.19.0 locale=zh-CN baseline=sha256:e0a716c69bbc9033af79ab9b1d1b607ba997b5324bb8301dfc8e0b7f4072dab2 -->
4
+ ## [0.19.0] - 2026-09-07
5
+
6
+ Harness 0.19.0 在既有固定机制 CLI 上新增 runMechanismCliBatch 与显式 --batch 模式,提供有界、有序、同操作批量传输,首批操作 canonical-json。
7
+
8
+ ### 新增
9
+
10
+ - 新增 runMechanismCliBatch({input, output, error}):读入一个批量请求,逐项返回 inputIndex、exitCode 与原单请求响应。
11
+ - 官方 Bundle 投影的 mechanisms-cli.mjs 新增显式 --batch 模式。不带 --batch 时旧单请求路径不变。
12
+
13
+ ### 变更
14
+
15
+ - 批量结构与容量拒绝使用 TypeError,error.details.kind 封闭为 batch-structure-invalid、batch-item-limit、batch-input-limit、batch-output-limit。
16
+ - 单项机制失败写入该项结果位置,其余项继续执行;任一机制项失败时整批退出码为 2。
17
+
18
+ ### 升级说明
19
+
20
+ 三个 Foundation 包须一起精确锁定到 0.19.0。固定容量政策为 256 项、16 MiB 输入字节、32 MiB 输出字节;相互独立请求的分组与超限切分由消费者负责。该入口为候选能力,升级后须重新验证。旧单请求 CLI 合同保持不变。
21
+ <!-- release-skill:changelog:end version=0.19.0 locale=zh-CN -->
22
+
23
+
24
+ <!-- release-skill:changelog:start version=0.18.0 locale=zh-CN baseline=sha256:ba40c3c901c8f717b18d131344fe94a470c149ddb1d0f96e5611d38bf1c70d3d -->
25
+ ## [0.18.0] - 2026-09-05
26
+
27
+ Harness 0.18.0 新增稳定的 replaceFixedSetAtomic,用一个完整暂存目录替换一个既有固定集合目录。
28
+
29
+ ### 新增
30
+
31
+ - 包根与 fixed-set-publication 子路径新增 replaceFixedSetAtomic。源目录和目标目录必须是同一规范父目录下的真实兄弟目录。
32
+ - 提交只执行一次 Darwin RENAME_SWAP 或 Linux RENAME_EXCHANGE。成功后,新集合完整位于目标路径,被置换的旧目标留在 sourceRoot。
33
+
34
+ ### 变更
35
+
36
+ - 替换无法返回已验证成功时,沿用 SFC2004 机制错误表面,报告提交前后、发布、验证、提交和持久化状态。
37
+
38
+ ### 升级说明
39
+
40
+ 三个 Foundation 包须一起精确锁定到 0.18.0。replaceFixedSetAtomic 不是幂等操作:相同路径再次调用会把两个目录交换回去。成功后,或收到提交后及不确定错误时,均不得盲目重试。调用方只在确认成功后负责清理被置换的旧目标。
41
+ <!-- release-skill:changelog:end version=0.18.0 locale=zh-CN -->
42
+
43
+
3
44
  <!-- release-skill:changelog:start version=0.17.0 locale=zh-CN baseline=sha256:fc1c5f48463b2d62363952f566011600a8a84b11456519507c8dc03bf58756d5 -->
4
45
  ## [0.17.0] - 2026-09-01
5
46
 
package/README.md CHANGED
@@ -4,22 +4,28 @@
4
4
 
5
5
  # skill-family-harness-node
6
6
 
7
- <!-- release-skill:release-version: 0.17.0 -->
7
+ <!-- release-skill:release-version: 0.19.0 -->
8
8
 
9
9
  The **single default Node implementation** of the Contracts mechanism protocol. This is a thin runtime: it only implements the mechanism protocol, introduces no business semantics, and does not provide a second-language implementation.
10
10
 
11
11
  <!-- release-skill:managed:start id=latest-release -->
12
- **0.17.0** (2026-09-01)
12
+ **0.19.0** (2026-09-07)
13
13
 
14
- Harness 0.17.0 is a lockstep version update with no new mechanism, native source change, or public API.
14
+ Harness 0.19.0 adds runMechanismCliBatch and the explicit --batch CLI mode for bounded, ordered, same-operation batch transport, with canonical-json as the first operation.
15
+
16
+ **Added**
17
+
18
+ - Adds runMechanismCliBatch({input, output, error}) on the existing fixed mechanism CLI, reading one batch request and returning per-item results with inputIndex, exitCode, and the original single-request response.
19
+ - Adds the explicit --batch mode to the official Bundle-projected mechanisms-cli.mjs. Without --batch the old single-request path is unchanged.
15
20
 
16
21
  **Changed**
17
22
 
18
- - Moves the package version to 0.17.0 together with Contracts and Engineering Kit while preserving the existing Harness capability and native prebuild surfaces.
23
+ - Batch structure and capacity refusals use TypeError with error.details.kind from the closed set batch-structure-invalid, batch-item-limit, batch-input-limit, and batch-output-limit.
24
+ - A single item's mechanism failure is recorded in its result position and sibling items continue; the whole batch exits 2 when any item failed.
19
25
 
20
26
  **Upgrade Notes**
21
27
 
22
- Pin all three Foundation packages to exactly 0.17.0. Engineering-baseline validation and structural comparison belong to Contracts and Engineering Kit; no Harness migration is required.
28
+ Pin all three Foundation packages to exactly 0.19.0. The fixed capacity policy is 256 items, 16 MiB input bytes, and 32 MiB output bytes; consumers own grouping independent requests and splitting oversized batches. This entry is candidate: re-verify after upgrading. The old single-request CLI keeps its previous contract.
23
29
  <!-- release-skill:managed:end id=latest-release -->
24
30
 
25
31
  ## Problem It Solves
@@ -32,7 +38,7 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
32
38
 
33
39
  ## Installation and Minimal Example
34
40
 
35
- Version 0.17.0 is a local candidate. Build all three tarballs into one temporary directory and install those exact files for a candidate check:
41
+ Version 0.18.0 is a local candidate. Build all three tarballs into one temporary directory and install those exact files for a candidate check:
36
42
 
37
43
  ```sh
38
44
  pack_dir="$(mktemp -d)"
@@ -40,13 +46,13 @@ pack_dir="$(mktemp -d)"
40
46
  (cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
41
47
  (cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
42
48
  mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
43
- (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.17.0.tgz" "$pack_dir/skill-family-harness-node-0.17.0.tgz" "$pack_dir/skill-family-engineering-kit-0.17.0.tgz")
49
+ (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.18.0.tgz" "$pack_dir/skill-family-harness-node-0.18.0.tgz" "$pack_dir/skill-family-engineering-kit-0.18.0.tgz")
44
50
  ```
45
51
 
46
52
  After publication, use the registry coordinate:
47
53
 
48
54
  ```sh
49
- npm install skill-family-harness-node@0.17.0
55
+ npm install skill-family-harness-node@0.19.0
50
56
  npm info skill-family-harness-node --help
51
57
  ```
52
58
 
@@ -118,6 +124,7 @@ The capability remains **candidate**. Pin all three Foundation packages exactly
118
124
  | `superviseProcess` / `validateTimeoutPolicy` | The single bounded subprocess supervisor. Its 0.11.0 `rawSink` option writes raw stdout/stderr bytes only to a fresh canonical private root, then waits for child/stream close, queued writes, fsync, and handle close. The caller must exclusively control the sink namespace for the entire call; handle protection does not prove stable pathname/root identity. |
119
125
  | `observeFilesystemTree` | Observe a complete bound tree. Default/reject preserves the existing UTF-16 member order; record uses code-point order and records symlink target bytes without following them. |
120
126
  | `observeExecutableIdentity` | Observe a caller-bound executable, its symlink chain, launch bytes, and any script interpreter chain for immediate pre-spawn re-observation. |
127
+ | `createFixedSetPublicationManifest` / `publishFixedSet` / `replaceFixedSetAtomic` | Publish a complete fixed set without replacement, or atomically exchange a complete staged sibling with one existing target. |
121
128
  | `parseRequest` / `processRequest` | Parse `operation-request`, output terminal `operation-result`. |
122
129
  | `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | Consume a Contracts-validated report model, deterministically render neutral Markdown, and verify source/result/report binding; does not interpret business output. |
123
130
  | `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | Generic text-source closure, full-manifest digest re-verification, and atomic landing of the target set; specific Profile/driver is not in the Harness. |
@@ -126,6 +133,12 @@ The capability remains **candidate**. Pin all three Foundation packages exactly
126
133
  | `readSnapshot` / `writeSnapshot` / `rebuildSnapshot` | Atomic derived snapshots and full-event rebuild; a bad event cannot be masked by an old snapshot, and a bad snapshot can be ignored by rebuild. |
127
134
  | `inspectStateStoreLock` / `recoverStateStoreLock` | Read-only lock diagnostics and explicit recovery; recovery must precisely match the observed owner + fencing. |
128
135
 
136
+ ## Replacing an Existing Fixed Set
137
+
138
+ `replaceFixedSetAtomic({ sourceRoot, targetParent, targetSegment })` requires `sourceRoot` and the existing target to be real sibling directories under the same canonical parent. It verifies both complete trees, then commits one Darwin `RENAME_SWAP` or Linux `RENAME_EXCHANGE`. A verified success leaves the new set at the target and the displaced old target at `sourceRoot`; Foundation does not delete either directory.
139
+
140
+ The operation is not idempotent: a second call with the same paths exchanges the directories back. Do not retry blindly after a verified success or an error whose state is post-commit or indeterminate. Mechanism failures use `SFC2004` with `details.kind: "atomic-replace-failed"` and include the phase, publication, commit, verification, and durability state needed for caller-controlled recovery.
141
+
129
142
  ## State Store Lock and Recovery Boundaries
130
143
 
131
144
  - The lock uses exclusive create; a second writer immediately receives `store-locked`; it does not queue, nor steals the lock by time, PID, or lease expiry.
@@ -248,4 +261,4 @@ When the actual threat includes malicious concurrency, return a minimal upstream
248
261
 
249
262
  The separate candidate `observeExecutableIdentity({ boundRoots, lookup, interpreterPolicy? })` provides a read-only point-in-time observation of only the caller-explicit roots and lookup paths, for an immediate re-observation before launch. When an `/usr/bin/env` shebang resolves an interpreter through explicit `pathEntries`, the observation preserves the interpreter candidate's complete symlink chain rather than collapsing it to the final file. It is not part of `host-adapter` and does not prove wrapper control flow, ambient `PATH`, fd-exec/kernel image, signature trust, cross-call caching, host support/lifecycle, or domain acceptance; the caller owns those semantics. The candidate entry alone does not qualify a host.
250
263
 
251
- Version 0.17.0 is a local source candidate and is not published. Consume the three locally verified tarballs; a version marker, unit test or successful install is not complete contract integration, migration completion, or real-host qualification.
264
+ Version 0.18.0 is a local source candidate and is not published. Consume the three locally verified tarballs; a version marker, unit test or successful install is not complete contract integration, migration completion, or real-host qualification.
package/README.zh-CN.md CHANGED
@@ -5,22 +5,28 @@
5
5
 
6
6
  # skill-family-harness-node
7
7
 
8
- <!-- release-skill:release-version: 0.17.0 -->
8
+ <!-- release-skill:release-version: 0.19.0 -->
9
9
 
10
10
  Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
11
11
 
12
12
  <!-- release-skill:managed:start id=latest-release -->
13
- **0.17.0** (2026-09-01)
13
+ **0.19.0** (2026-09-07)
14
14
 
15
- Harness 0.17.0 Foundation 锁步升版,不新增机制,不修改原生源码或公共 API
15
+ Harness 0.19.0 在既有固定机制 CLI 上新增 runMechanismCliBatch 与显式 --batch 模式,提供有界、有序、同操作批量传输,首批操作 canonical-json
16
+
17
+ **新增**
18
+
19
+ - 新增 runMechanismCliBatch({input, output, error}):读入一个批量请求,逐项返回 inputIndex、exitCode 与原单请求响应。
20
+ - 官方 Bundle 投影的 mechanisms-cli.mjs 新增显式 --batch 模式。不带 --batch 时旧单请求路径不变。
16
21
 
17
22
  **变更**
18
23
 
19
- - 包版本与 Contracts、Engineering Kit 一同升至 0.17.0,既有 Harness 能力和原生预构建表面保持不变。
24
+ - 批量结构与容量拒绝使用 TypeError,error.details.kind 封闭为 batch-structure-invalid、batch-item-limit、batch-input-limit、batch-output-limit。
25
+ - 单项机制失败写入该项结果位置,其余项继续执行;任一机制项失败时整批退出码为 2。
20
26
 
21
27
  **升级说明**
22
28
 
23
- 三个 Foundation 包须一起精确锁定到 0.17.0。工程基线校验与结构比较分别归 Contracts Engineering Kit 负责,无需迁移 Harness API。
29
+ 三个 Foundation 包须一起精确锁定到 0.19.0。固定容量政策为 256 项、16 MiB 输入字节、32 MiB 输出字节;相互独立请求的分组与超限切分由消费者负责。该入口为候选能力,升级后须重新验证。旧单请求 CLI 合同保持不变。
24
30
  <!-- release-skill:managed:end id=latest-release -->
25
31
 
26
32
  ## 解决的问题
@@ -33,7 +39,7 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
33
39
 
34
40
  ## 安装和最小示例
35
41
 
36
- 0.17.0 是本地候选版本。候选验证先把三个包分别打入同一个临时目录,再安装这三个精确 tarball:
42
+ 0.18.0 是本地候选版本。候选验证先把三个包分别打入同一个临时目录,再安装这三个精确 tarball:
37
43
 
38
44
  ```sh
39
45
  pack_dir="$(mktemp -d)"
@@ -41,13 +47,13 @@ pack_dir="$(mktemp -d)"
41
47
  (cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
42
48
  (cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
43
49
  mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
44
- (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.17.0.tgz" "$pack_dir/skill-family-harness-node-0.17.0.tgz" "$pack_dir/skill-family-engineering-kit-0.17.0.tgz")
50
+ (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.18.0.tgz" "$pack_dir/skill-family-harness-node-0.18.0.tgz" "$pack_dir/skill-family-engineering-kit-0.18.0.tgz")
45
51
  ```
46
52
 
47
53
  发布后再使用 registry 坐标:
48
54
 
49
55
  ```sh
50
- npm install skill-family-harness-node@0.17.0
56
+ npm install skill-family-harness-node@0.19.0
51
57
  npm info skill-family-harness-node --help
52
58
  ```
53
59
 
@@ -119,6 +125,7 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
119
125
  | `superviseProcess` / `validateTimeoutPolicy` | 唯一的受约束子进程监督器。0.11.0 的 `rawSink` 只向 fresh canonical 私有根写原始 stdout/stderr 字节,并等待子进程、流、排队写入、fsync 与句柄全部关闭。调用方必须在整个调用期间独占 sink 命名空间;句柄保护不证明 pathname 或根目录身份始终不变。 |
120
126
  | `observeFilesystemTree` | 观察完整绑定树。默认/reject 保留既有 UTF-16 成员顺序;record 使用码点顺序,记录 symlink target bytes 且不跟随目标。 |
121
127
  | `observeExecutableIdentity` | 观察调用方绑定的可执行入口、符号链接链、启动字节和脚本解释器链,供每次 spawn 前紧邻重观察。 |
128
+ | `createFixedSetPublicationManifest` / `publishFixedSet` / `replaceFixedSetAtomic` | 发布不替换的完整固定集合,或把完整暂存兄弟目录与一个既有目标原子交换。 |
122
129
  | `parseRequest` / `processRequest` | 解析 `operation-request`,输出终态 `operation-result`。 |
123
130
  | `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | 消费经 Contracts 验证的 report model,确定性渲染中性 Markdown 并校验来源/结果/报告绑定;不解释业务输出。 |
124
131
  | `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | 通用文本 source closure、manifest 全摘要复验和目标集合原子落盘;具体 Profile/driver 不在 Harness。 |
@@ -127,6 +134,12 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
127
134
  | `readSnapshot` / `writeSnapshot` / `rebuildSnapshot` | 原子派生快照与完整事件重建;坏事件不能被旧快照掩盖,坏快照可被重建忽略。 |
128
135
  | `inspectStateStoreLock` / `recoverStateStoreLock` | 只读锁诊断与显式恢复;恢复必须对观测到的 owner + fencing 做精确匹配。 |
129
136
 
137
+ ## 替换既有固定集合
138
+
139
+ `replaceFixedSetAtomic({ sourceRoot, targetParent, targetSegment })` 要求 `sourceRoot` 与既有目标是同一规范父目录下的真实兄弟目录。它先复验两棵完整目录树,再执行一次 Darwin `RENAME_SWAP` 或 Linux `RENAME_EXCHANGE`。确认成功后,新集合位于目标路径,被置换的旧目标留在 `sourceRoot`;Foundation 不删除任一目录。
140
+
141
+ 该操作不是幂等操作:相同路径再次调用会把两个目录交换回去。确认成功后,或错误状态为提交后及不确定时,不得盲目重试。机制失败使用 `SFC2004` 与 `details.kind: "atomic-replace-failed"`,并给出提交阶段、发布、提交、验证和持久化状态,供调用方自行恢复。
142
+
130
143
  ## 状态存储的锁与恢复边界
131
144
 
132
145
  - 锁使用 exclusive create,第二写者立即收到 `store-locked`;不排队,也不按时间、PID 或租约过期偷锁。
@@ -248,4 +261,4 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
248
261
 
249
262
  另一个独立候选 `observeExecutableIdentity({ boundRoots, lookup, interpreterPolicy? })` 只对调用方显式提供的根和查找路径做逐次只读观察,供正式启动前紧邻重观察。`/usr/bin/env` shebang 通过显式 `pathEntries` 找到解释器时,结果保留解释器候选的完整 symlink chain,不折叠成最终文件。它不属于 `host-adapter`,也不证明 wrapper 控制流、ambient `PATH`、fd-exec/内核映像、签名信任、跨调用缓存、宿主支持/生命周期或领域接受;这些语义仍由调用方负责。候选入口存在不等于宿主已获资格。
250
263
 
251
- 0.17.0 为本地源码候选,尚未发布。消费本地已验证的三包 tarball;版本标记、单元测试或安装成功都不等于契约接入完成、迁移完成或真实宿主资格。
264
+ 0.18.0 为本地源码候选,尚未发布。消费本地已验证的三包 tarball;版本标记、单元测试或安装成功都不等于契约接入完成、迁移完成或真实宿主资格。
@@ -6,9 +6,17 @@ import {
6
6
  invokeFoundationMechanism,
7
7
  verifyManagedBundleIdentity,
8
8
  } from "./quickstart-profile.mjs";
9
+ import { loadMechanismBatchPolicy } from "skill-family-contracts/quickstart-profile";
9
10
 
10
11
  const CLI_NAME = "mechanisms-cli.mjs";
11
12
 
13
+ // Fixed capacity authority and serialization envelope for the bounded
14
+ // mechanism batch transport (FND-DES-022); both derive from the single
15
+ // Contracts authority rather than a second copy.
16
+ const MECHANISM_BATCH_POLICY = loadMechanismBatchPolicy();
17
+ const BATCH_OUTPUT_PREFIX = '{"results":[';
18
+ const BATCH_OUTPUT_SUFFIX = "]}\n";
19
+
12
20
  async function readRequest(input) {
13
21
  const chunks = [];
14
22
  for await (const chunk of input) chunks.push(Buffer.from(chunk));
@@ -154,6 +162,210 @@ async function runSelfCheck(request) {
154
162
  return verifyManagedBundleIdentity({ cliUrl: import.meta.url, cliName: CLI_NAME });
155
163
  }
156
164
 
165
+ function batchRefusal(kind, message) {
166
+ const cause = new TypeError(message);
167
+ cause.details = { kind };
168
+ return cause;
169
+ }
170
+
171
+ /**
172
+ * Bounded raw-byte read through explicit listeners (never `for await`, whose
173
+ * early break would destroy the caller stream). The current chunk is kept only
174
+ * after the running total plus its bytes is confirmed within the limit, so an
175
+ * over-limit chunk is never copied into the accumulation cache. Every exit
176
+ * path releases this function's listeners; refusal never terminates the
177
+ * caller process.
178
+ */
179
+ function readBoundedBatchInput(input, byteLimit) {
180
+ return new Promise((resolve, reject) => {
181
+ const chunks = [];
182
+ let received = 0;
183
+ const cleanup = () => {
184
+ input.removeListener("data", onData);
185
+ input.removeListener("end", onEnd);
186
+ input.removeListener("error", onError);
187
+ };
188
+ const onData = (chunk) => {
189
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
190
+ const next = received + bytes.length;
191
+ if (next > byteLimit) {
192
+ cleanup();
193
+ reject(
194
+ batchRefusal(
195
+ "batch-input-limit",
196
+ `mechanism batch input exceeds ${byteLimit} raw UTF-8 bytes`,
197
+ ),
198
+ );
199
+ return;
200
+ }
201
+ received = next;
202
+ chunks.push(bytes);
203
+ };
204
+ const onEnd = () => {
205
+ cleanup();
206
+ resolve(Buffer.concat(chunks));
207
+ };
208
+ const onError = (cause) => {
209
+ cleanup();
210
+ reject(cause);
211
+ };
212
+ input.on("data", onData);
213
+ input.on("end", onEnd);
214
+ input.on("error", onError);
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Whole-batch shape precheck (FND-DES-022 section 3): the outer layer of every
220
+ * item is checked before the first item executes. Structure failures never
221
+ * reach the mechanism dispatcher and never fabricate per-item results.
222
+ */
223
+ function assertMechanismBatchShape(request) {
224
+ if (
225
+ request === null ||
226
+ typeof request !== "object" ||
227
+ Array.isArray(request) ||
228
+ Object.keys(request).sort().join(",") !== "inputs,operation" ||
229
+ request.operation !== "canonical-json" ||
230
+ !Array.isArray(request.inputs) ||
231
+ request.inputs.length === 0
232
+ ) {
233
+ throw batchRefusal(
234
+ "batch-structure-invalid",
235
+ "mechanism batch request must carry exactly operation canonical-json and a non-empty inputs array",
236
+ );
237
+ }
238
+ for (const item of request.inputs) {
239
+ if (
240
+ item === null ||
241
+ typeof item !== "object" ||
242
+ Array.isArray(item) ||
243
+ Object.keys(item).sort().join(",") !== "document"
244
+ ) {
245
+ throw batchRefusal(
246
+ "batch-structure-invalid",
247
+ "each mechanism batch input must be an object carrying exactly document",
248
+ );
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Waits until the writable accepted the whole payload, propagating both
255
+ * synchronous write throws and asynchronous stream errors.
256
+ */
257
+ function writeAll(stream, text) {
258
+ return new Promise((resolve, reject) => {
259
+ const onError = (cause) => {
260
+ stream.removeListener("drain", onDrain);
261
+ reject(cause);
262
+ };
263
+ const onDrain = () => {
264
+ stream.removeListener("error", onError);
265
+ resolve();
266
+ };
267
+ stream.on("error", onError);
268
+ try {
269
+ if (stream.write(text)) {
270
+ stream.removeListener("error", onError);
271
+ resolve();
272
+ return;
273
+ }
274
+ stream.once("drain", onDrain);
275
+ } catch (cause) {
276
+ stream.removeListener("error", onError);
277
+ reject(cause);
278
+ }
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Bounded, same-operation, ordered mechanism batch transport (FND-DES-022).
284
+ *
285
+ * The request stream is read once under the raw-byte budget, strictly decoded
286
+ * as UTF-8, parsed once, shape-prechecked, then executed item by item through
287
+ * the fixed single-request dispatcher — no second algorithm is introduced.
288
+ * Every item's serialized response bytes are accumulated with the envelope
289
+ * commas, prefix, and mandatory closing newline; nothing is written to the
290
+ * output until the complete response is confirmed within the output budget.
291
+ * Item failures follow the original CLI error projection into that item's
292
+ * `response` and continue; whole-batch refusals and transport failures write
293
+ * one batch error object to the error channel and never touch the output.
294
+ */
295
+ export async function runMechanismCliBatch({
296
+ input = stdin,
297
+ output = stdout,
298
+ error = stderr,
299
+ invoke = invokeFoundationMechanism,
300
+ } = {}) {
301
+ try {
302
+ const bytes = await readBoundedBatchInput(input, MECHANISM_BATCH_POLICY.inputByteLimit);
303
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
304
+ const request = JSON.parse(text);
305
+ assertMechanismBatchShape(request);
306
+ const inputs = request.inputs;
307
+ if (inputs.length > MECHANISM_BATCH_POLICY.itemLimit) {
308
+ throw batchRefusal(
309
+ "batch-item-limit",
310
+ `mechanism batch exceeds the ${MECHANISM_BATCH_POLICY.itemLimit} item limit`,
311
+ );
312
+ }
313
+ const fragments = [];
314
+ let outputBytes = Buffer.byteLength(BATCH_OUTPUT_PREFIX, "utf8");
315
+ let anyItemFailure = false;
316
+ for (let inputIndex = 0; inputIndex < inputs.length; inputIndex += 1) {
317
+ const item = inputs[inputIndex];
318
+ let exitCode = 0;
319
+ let response;
320
+ try {
321
+ response = await invoke({ operation: "canonical-json", params: item });
322
+ } catch (cause) {
323
+ response = errorResponse(cause);
324
+ exitCode = 2;
325
+ }
326
+ const fragment = JSON.stringify({ inputIndex, exitCode, response });
327
+ if (inputIndex > 0) outputBytes += 1; // separating comma between items
328
+ outputBytes += Buffer.byteLength(fragment, "utf8");
329
+ // The closing bytes are mandatory, so the check includes them before the
330
+ // next item may execute; an overrun never delivers a partial result.
331
+ if (outputBytes + Buffer.byteLength(BATCH_OUTPUT_SUFFIX, "utf8") > MECHANISM_BATCH_POLICY.outputByteLimit) {
332
+ throw batchRefusal(
333
+ "batch-output-limit",
334
+ `mechanism batch response exceeds the ${MECHANISM_BATCH_POLICY.outputByteLimit} byte limit`,
335
+ );
336
+ }
337
+ fragments.push(fragment);
338
+ if (exitCode === 2) anyItemFailure = true;
339
+ }
340
+ await writeAll(
341
+ output,
342
+ `${BATCH_OUTPUT_PREFIX}${fragments.join(",")}${BATCH_OUTPUT_SUFFIX}`,
343
+ );
344
+ return anyItemFailure ? 2 : 0;
345
+ } catch (cause) {
346
+ try {
347
+ await writeAll(error, `${JSON.stringify(errorResponse(cause))}\n`);
348
+ } catch {
349
+ // The error channel itself failed; the batch still exits failed.
350
+ }
351
+ return 2;
352
+ }
353
+ }
354
+
157
355
  if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
158
- process.exitCode = await runMechanismCli();
356
+ const args = process.argv.slice(2);
357
+ if (args[0] === "--batch") {
358
+ if (args.length > 1) {
359
+ // Refuse before reading any input: the batch mode takes no positional
360
+ // arguments, and the old single-request entry must stay unchanged.
361
+ stderr.write(
362
+ `${JSON.stringify(errorResponse(new TypeError("mechanisms CLI --batch accepts no additional arguments")))}\n`,
363
+ );
364
+ process.exitCode = 2;
365
+ } else {
366
+ process.exitCode = await runMechanismCliBatch();
367
+ }
368
+ } else {
369
+ process.exitCode = await runMechanismCli();
370
+ }
159
371
  }
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@pnpm/lockfile.fs": "1001.1.35",
12
12
  "ipaddr.js": "2.5.0",
13
- "skill-family-contracts": "0.17.0",
13
+ "skill-family-contracts": "0.19.0",
14
14
  "yaml": "2.9.0"
15
15
  },
16
16
  "description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
@@ -49,7 +49,7 @@
49
49
  "url": "https://github.com/ifoohoo/skill-family-harness-node.git"
50
50
  },
51
51
  "type": "module",
52
- "version": "0.17.0",
52
+ "version": "0.19.0",
53
53
  "scripts": {
54
54
  "check": "node --test",
55
55
  "test": "node --test"
@@ -0,0 +1,21 @@
1
+ version: 0.18.0
2
+ date: 2026-09-05
3
+ locales:
4
+ en:
5
+ summary: Harness 0.18.0 adds the stable replaceFixedSetAtomic operation for replacing one existing fixed-set directory with one complete staged sibling.
6
+ changes:
7
+ added:
8
+ - Adds replaceFixedSetAtomic to the package root and fixed-set-publication subpath. Source and target must be real sibling directories under the same canonical parent.
9
+ - Uses one Darwin RENAME_SWAP or Linux RENAME_EXCHANGE commit. On success, the complete new set occupies the target and the displaced old target remains at sourceRoot.
10
+ changed:
11
+ - Reports pre-commit, post-commit, publication, verification, commit, and durability state through the existing SFC2004 mechanism-error surface when replacement cannot return a verified success.
12
+ upgradeNotes: "Pin all three Foundation packages to exactly 0.18.0. replaceFixedSetAtomic is not idempotent: calling it again with the same paths exchanges the directories back. Never retry blindly after success or a post-commit or indeterminate error. The caller owns cleanup of the displaced target after a verified success."
13
+ zh-CN:
14
+ summary: Harness 0.18.0 新增稳定的 replaceFixedSetAtomic,用一个完整暂存目录替换一个既有固定集合目录。
15
+ changes:
16
+ added:
17
+ - 包根与 fixed-set-publication 子路径新增 replaceFixedSetAtomic。源目录和目标目录必须是同一规范父目录下的真实兄弟目录。
18
+ - 提交只执行一次 Darwin RENAME_SWAP 或 Linux RENAME_EXCHANGE。成功后,新集合完整位于目标路径,被置换的旧目标留在 sourceRoot。
19
+ changed:
20
+ - 替换无法返回已验证成功时,沿用 SFC2004 机制错误表面,报告提交前后、发布、验证、提交和持久化状态。
21
+ upgradeNotes: 三个 Foundation 包须一起精确锁定到 0.18.0。replaceFixedSetAtomic 不是幂等操作:相同路径再次调用会把两个目录交换回去。成功后,或收到提交后及不确定错误时,均不得盲目重试。调用方只在确认成功后负责清理被置换的旧目标。
@@ -0,0 +1,23 @@
1
+ version: 0.19.0
2
+ date: 2026-09-07
3
+ locales:
4
+ en:
5
+ summary: Harness 0.19.0 adds runMechanismCliBatch and the explicit --batch CLI mode for bounded, ordered, same-operation batch transport, with canonical-json as the first operation.
6
+ changes:
7
+ added:
8
+ - Adds runMechanismCliBatch({input, output, error}) on the existing fixed mechanism CLI, reading one batch request and returning per-item results with inputIndex, exitCode, and the original single-request response.
9
+ - Adds the explicit --batch mode to the official Bundle-projected mechanisms-cli.mjs. Without --batch the old single-request path is unchanged.
10
+ changed:
11
+ - Batch structure and capacity refusals use TypeError with error.details.kind from the closed set batch-structure-invalid, batch-item-limit, batch-input-limit, and batch-output-limit.
12
+ - A single item's mechanism failure is recorded in its result position and sibling items continue; the whole batch exits 2 when any item failed.
13
+ upgradeNotes: "Pin all three Foundation packages to exactly 0.19.0. The fixed capacity policy is 256 items, 16 MiB input bytes, and 32 MiB output bytes; consumers own grouping independent requests and splitting oversized batches. This entry is candidate: re-verify after upgrading. The old single-request CLI keeps its previous contract."
14
+ zh-CN:
15
+ summary: Harness 0.19.0 在既有固定机制 CLI 上新增 runMechanismCliBatch 与显式 --batch 模式,提供有界、有序、同操作批量传输,首批操作 canonical-json。
16
+ changes:
17
+ added:
18
+ - 新增 runMechanismCliBatch({input, output, error}):读入一个批量请求,逐项返回 inputIndex、exitCode 与原单请求响应。
19
+ - 官方 Bundle 投影的 mechanisms-cli.mjs 新增显式 --batch 模式。不带 --batch 时旧单请求路径不变。
20
+ changed:
21
+ - 批量结构与容量拒绝使用 TypeError,error.details.kind 封闭为 batch-structure-invalid、batch-item-limit、batch-input-limit、batch-output-limit。
22
+ - 单项机制失败写入该项结果位置,其余项继续执行;任一机制项失败时整批退出码为 2。
23
+ upgradeNotes: 三个 Foundation 包须一起精确锁定到 0.19.0。固定容量政策为 256 项、16 MiB 输入字节、32 MiB 输出字节;相互独立请求的分组与超限切分由消费者负责。该入口为候选能力,升级后须重新验证。旧单请求 CLI 合同保持不变。
@@ -1,5 +1,7 @@
1
- import { loadNativeBoundReadAddon } from "./native/loader.mjs";
1
+ import { loadNativeBoundReadAddon, stableNativePlatformKey } from "./native/loader.mjs";
2
2
 
3
3
  export async function loadNativeAddon() {
4
4
  return loadNativeBoundReadAddon();
5
5
  }
6
+
7
+ export { stableNativePlatformKey };