skill-family-harness-node 0.18.0 → 0.19.1
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 +38 -0
- package/CHANGELOG.zh-CN.md +38 -0
- package/README.md +12 -15
- package/README.zh-CN.md +12 -15
- package/candidate/mechanisms-cli.mjs +382 -1
- package/package.json +2 -2
- package/release-notes/0.19.0.yaml +23 -0
- package/release-notes/0.19.1.yaml +19 -0
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.19.1 locale=en baseline=sha256:76ded57ddca31105929a12a1b198bd6da407090e353dbf90096c17da5532a3ea -->
|
|
4
|
+
## [0.19.1] - 2026-09-08
|
|
5
|
+
|
|
6
|
+
Harness 0.19.1 makes the candidate mechanism-batch entry point settle on the current transfer's actual completion or failure while preserving caller ownership of programmatic streams.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Waits for the current write callback, handles premature input and output closure, and removes only listeners installed by the batch helper.
|
|
11
|
+
- Stops the library helper's own reading and releases its buffer after input overflow without destroying a caller-owned stream.
|
|
12
|
+
- Releases CLI-owned standard input after an oversized request is reported, allowing the process to exit 2 while the upstream pipe remains open.
|
|
13
|
+
|
|
14
|
+
### Upgrade Notes
|
|
15
|
+
|
|
16
|
+
Pin all three Foundation packages to exactly 0.19.1. The batch operation, capacity policy, per-item order, and single-request CLI remain unchanged. This entry is candidate and requires re-verification after an upgrade.
|
|
17
|
+
<!-- release-skill:changelog:end version=0.19.1 locale=en -->
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<!-- release-skill:changelog:start version=0.19.0 locale=en baseline=sha256:c3b3a06b509cc53719c2de75d1edacd22c1711c99c5f7687ad91f350976262e3 -->
|
|
21
|
+
## [0.19.0] - 2026-09-07
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
- Adds the explicit --batch mode to the official Bundle-projected mechanisms-cli.mjs. Without --batch the old single-request path is unchanged.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
### Upgrade Notes
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
<!-- release-skill:changelog:end version=0.19.0 locale=en -->
|
|
39
|
+
|
|
40
|
+
|
|
3
41
|
<!-- release-skill:changelog:start version=0.18.0 locale=en baseline=sha256:ca3679574ea53013c830563ae818326062794aec1f486e6953e89fa8937be644 -->
|
|
4
42
|
## [0.18.0] - 2026-09-05
|
|
5
43
|
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
<!-- release-skill:changelog:start version=0.19.1 locale=zh-CN baseline=sha256:398aacf147e429237ff5b72fa1d22652afb76eb49da388eeb333d073bbd0a032 -->
|
|
4
|
+
## [0.19.1] - 2026-09-08
|
|
5
|
+
|
|
6
|
+
Harness 0.19.1 让候选机制批量入口等待本次传输真正完成或失败,同时保留程序化入口中调用方对流的所有权。
|
|
7
|
+
|
|
8
|
+
### 修复
|
|
9
|
+
|
|
10
|
+
- 等待本次写入回调,处理输入与输出提前关闭,并且只移除批量辅助函数自己安装的监听器。
|
|
11
|
+
- 输入超限后停止库辅助函数自己的读取并释放缓存,不销毁调用方拥有的流。
|
|
12
|
+
- 超限错误写出后释放 CLI 自己拥有的标准输入,使上游管道保持打开时进程仍能自行退出 2。
|
|
13
|
+
|
|
14
|
+
### 升级说明
|
|
15
|
+
|
|
16
|
+
三个 Foundation 包须一起精确锁定到 0.19.1。批量操作、容量政策、逐项顺序和旧单请求 CLI 保持不变。该入口仍是候选能力,升级后须重新验证。
|
|
17
|
+
<!-- release-skill:changelog:end version=0.19.1 locale=zh-CN -->
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<!-- release-skill:changelog:start version=0.19.0 locale=zh-CN baseline=sha256:e0a716c69bbc9033af79ab9b1d1b607ba997b5324bb8301dfc8e0b7f4072dab2 -->
|
|
21
|
+
## [0.19.0] - 2026-09-07
|
|
22
|
+
|
|
23
|
+
Harness 0.19.0 在既有固定机制 CLI 上新增 runMechanismCliBatch 与显式 --batch 模式,提供有界、有序、同操作批量传输,首批操作 canonical-json。
|
|
24
|
+
|
|
25
|
+
### 新增
|
|
26
|
+
|
|
27
|
+
- 新增 runMechanismCliBatch({input, output, error}):读入一个批量请求,逐项返回 inputIndex、exitCode 与原单请求响应。
|
|
28
|
+
- 官方 Bundle 投影的 mechanisms-cli.mjs 新增显式 --batch 模式。不带 --batch 时旧单请求路径不变。
|
|
29
|
+
|
|
30
|
+
### 变更
|
|
31
|
+
|
|
32
|
+
- 批量结构与容量拒绝使用 TypeError,error.details.kind 封闭为 batch-structure-invalid、batch-item-limit、batch-input-limit、batch-output-limit。
|
|
33
|
+
- 单项机制失败写入该项结果位置,其余项继续执行;任一机制项失败时整批退出码为 2。
|
|
34
|
+
|
|
35
|
+
### 升级说明
|
|
36
|
+
|
|
37
|
+
三个 Foundation 包须一起精确锁定到 0.19.0。固定容量政策为 256 项、16 MiB 输入字节、32 MiB 输出字节;相互独立请求的分组与超限切分由消费者负责。该入口为候选能力,升级后须重新验证。旧单请求 CLI 合同保持不变。
|
|
38
|
+
<!-- release-skill:changelog:end version=0.19.0 locale=zh-CN -->
|
|
39
|
+
|
|
40
|
+
|
|
3
41
|
<!-- release-skill:changelog:start version=0.18.0 locale=zh-CN baseline=sha256:ba40c3c901c8f717b18d131344fe94a470c149ddb1d0f96e5611d38bf1c70d3d -->
|
|
4
42
|
## [0.18.0] - 2026-09-05
|
|
5
43
|
|
package/README.md
CHANGED
|
@@ -4,27 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
# skill-family-harness-node
|
|
6
6
|
|
|
7
|
-
<!-- release-skill:release-version: 0.
|
|
7
|
+
<!-- release-skill:release-version: 0.19.1 -->
|
|
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.
|
|
12
|
+
**0.19.1** (2026-09-08)
|
|
13
13
|
|
|
14
|
-
Harness 0.
|
|
14
|
+
Harness 0.19.1 makes the candidate mechanism-batch entry point settle on the current transfer's actual completion or failure while preserving caller ownership of programmatic streams.
|
|
15
15
|
|
|
16
|
-
**
|
|
16
|
+
**Fixed**
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
**Changed**
|
|
22
|
-
|
|
23
|
-
- 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.
|
|
18
|
+
- Waits for the current write callback, handles premature input and output closure, and removes only listeners installed by the batch helper.
|
|
19
|
+
- Stops the library helper's own reading and releases its buffer after input overflow without destroying a caller-owned stream.
|
|
20
|
+
- Releases CLI-owned standard input after an oversized request is reported, allowing the process to exit 2 while the upstream pipe remains open.
|
|
24
21
|
|
|
25
22
|
**Upgrade Notes**
|
|
26
23
|
|
|
27
|
-
Pin all three Foundation packages to exactly 0.
|
|
24
|
+
Pin all three Foundation packages to exactly 0.19.1. The batch operation, capacity policy, per-item order, and single-request CLI remain unchanged. This entry is candidate and requires re-verification after an upgrade.
|
|
28
25
|
<!-- release-skill:managed:end id=latest-release -->
|
|
29
26
|
|
|
30
27
|
## Problem It Solves
|
|
@@ -37,7 +34,7 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
|
|
|
37
34
|
|
|
38
35
|
## Installation and Minimal Example
|
|
39
36
|
|
|
40
|
-
Version 0.
|
|
37
|
+
Version 0.19.1 is the local source candidate. Build all three tarballs into one temporary directory and install those exact files for a candidate check:
|
|
41
38
|
|
|
42
39
|
```sh
|
|
43
40
|
pack_dir="$(mktemp -d)"
|
|
@@ -45,13 +42,13 @@ pack_dir="$(mktemp -d)"
|
|
|
45
42
|
(cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
|
|
46
43
|
(cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
|
|
47
44
|
mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
|
|
48
|
-
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.
|
|
45
|
+
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.19.1.tgz" "$pack_dir/skill-family-harness-node-0.19.1.tgz" "$pack_dir/skill-family-engineering-kit-0.19.1.tgz")
|
|
49
46
|
```
|
|
50
47
|
|
|
51
48
|
After publication, use the registry coordinate:
|
|
52
49
|
|
|
53
50
|
```sh
|
|
54
|
-
npm install skill-family-harness-node@0.
|
|
51
|
+
npm install skill-family-harness-node@0.19.1
|
|
55
52
|
npm info skill-family-harness-node --help
|
|
56
53
|
```
|
|
57
54
|
|
|
@@ -260,4 +257,4 @@ When the actual threat includes malicious concurrency, return a minimal upstream
|
|
|
260
257
|
|
|
261
258
|
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.
|
|
262
259
|
|
|
263
|
-
Version 0.
|
|
260
|
+
Version 0.19.1 is the local source candidate. Remote availability must be established by the corresponding release-skill post-release evidence. Consume the three locally verified tarballs for candidate checks; 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,27 +5,24 @@
|
|
|
5
5
|
|
|
6
6
|
# skill-family-harness-node
|
|
7
7
|
|
|
8
|
-
<!-- release-skill:release-version: 0.
|
|
8
|
+
<!-- release-skill:release-version: 0.19.1 -->
|
|
9
9
|
|
|
10
10
|
Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
|
|
11
11
|
|
|
12
12
|
<!-- release-skill:managed:start id=latest-release -->
|
|
13
|
-
**0.
|
|
13
|
+
**0.19.1** (2026-09-08)
|
|
14
14
|
|
|
15
|
-
Harness 0.
|
|
15
|
+
Harness 0.19.1 让候选机制批量入口等待本次传输真正完成或失败,同时保留程序化入口中调用方对流的所有权。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**修复**
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
**变更**
|
|
23
|
-
|
|
24
|
-
- 替换无法返回已验证成功时,沿用 SFC2004 机制错误表面,报告提交前后、发布、验证、提交和持久化状态。
|
|
19
|
+
- 等待本次写入回调,处理输入与输出提前关闭,并且只移除批量辅助函数自己安装的监听器。
|
|
20
|
+
- 输入超限后停止库辅助函数自己的读取并释放缓存,不销毁调用方拥有的流。
|
|
21
|
+
- 超限错误写出后释放 CLI 自己拥有的标准输入,使上游管道保持打开时进程仍能自行退出 2。
|
|
25
22
|
|
|
26
23
|
**升级说明**
|
|
27
24
|
|
|
28
|
-
三个 Foundation 包须一起精确锁定到 0.
|
|
25
|
+
三个 Foundation 包须一起精确锁定到 0.19.1。批量操作、容量政策、逐项顺序和旧单请求 CLI 保持不变。该入口仍是候选能力,升级后须重新验证。
|
|
29
26
|
<!-- release-skill:managed:end id=latest-release -->
|
|
30
27
|
|
|
31
28
|
## 解决的问题
|
|
@@ -38,7 +35,7 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
|
|
|
38
35
|
|
|
39
36
|
## 安装和最小示例
|
|
40
37
|
|
|
41
|
-
0.
|
|
38
|
+
0.19.1 是本地源码候选。候选验证先把三个包分别打入同一个临时目录,再安装这三个精确 tarball:
|
|
42
39
|
|
|
43
40
|
```sh
|
|
44
41
|
pack_dir="$(mktemp -d)"
|
|
@@ -46,13 +43,13 @@ pack_dir="$(mktemp -d)"
|
|
|
46
43
|
(cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
|
|
47
44
|
(cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
|
|
48
45
|
mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
|
|
49
|
-
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.
|
|
46
|
+
(cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.19.1.tgz" "$pack_dir/skill-family-harness-node-0.19.1.tgz" "$pack_dir/skill-family-engineering-kit-0.19.1.tgz")
|
|
50
47
|
```
|
|
51
48
|
|
|
52
49
|
发布后再使用 registry 坐标:
|
|
53
50
|
|
|
54
51
|
```sh
|
|
55
|
-
npm install skill-family-harness-node@0.
|
|
52
|
+
npm install skill-family-harness-node@0.19.1
|
|
56
53
|
npm info skill-family-harness-node --help
|
|
57
54
|
```
|
|
58
55
|
|
|
@@ -260,4 +257,4 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
|
|
|
260
257
|
|
|
261
258
|
另一个独立候选 `observeExecutableIdentity({ boundRoots, lookup, interpreterPolicy? })` 只对调用方显式提供的根和查找路径做逐次只读观察,供正式启动前紧邻重观察。`/usr/bin/env` shebang 通过显式 `pathEntries` 找到解释器时,结果保留解释器候选的完整 symlink chain,不折叠成最终文件。它不属于 `host-adapter`,也不证明 wrapper 控制流、ambient `PATH`、fd-exec/内核映像、签名信任、跨调用缓存、宿主支持/生命周期或领域接受;这些语义仍由调用方负责。候选入口存在不等于宿主已获资格。
|
|
262
259
|
|
|
263
|
-
0.
|
|
260
|
+
0.19.1 是本地源码候选,远端可用性须由对应的 release-skill 发布后证据证明。候选检查使用本地已验证的三包 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,379 @@ 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, outputWatch) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const chunks = [];
|
|
182
|
+
let received = 0;
|
|
183
|
+
let settled = false;
|
|
184
|
+
let preexistingErrorFallback;
|
|
185
|
+
let deferredFailure;
|
|
186
|
+
let stopWatchingOutput = () => {};
|
|
187
|
+
const startedFlowing = input.readableFlowing !== true && input.listenerCount("data") === 0;
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
input.removeListener("data", onData);
|
|
190
|
+
input.removeListener("end", onEnd);
|
|
191
|
+
input.removeListener("error", onError);
|
|
192
|
+
input.removeListener("close", onClose);
|
|
193
|
+
stopWatchingOutput();
|
|
194
|
+
if (preexistingErrorFallback !== undefined) {
|
|
195
|
+
clearImmediate(preexistingErrorFallback);
|
|
196
|
+
preexistingErrorFallback = undefined;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const rejectRead = (cause) => {
|
|
200
|
+
if (settled) return;
|
|
201
|
+
settled = true;
|
|
202
|
+
cleanup();
|
|
203
|
+
chunks.length = 0;
|
|
204
|
+
if (
|
|
205
|
+
startedFlowing &&
|
|
206
|
+
input.readableFlowing === true &&
|
|
207
|
+
input.listenerCount("data") === 0
|
|
208
|
+
) {
|
|
209
|
+
input.pause();
|
|
210
|
+
}
|
|
211
|
+
reject(cause);
|
|
212
|
+
};
|
|
213
|
+
const onData = (chunk) => {
|
|
214
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
215
|
+
const next = received + bytes.length;
|
|
216
|
+
if (next > byteLimit) {
|
|
217
|
+
rejectRead(
|
|
218
|
+
batchRefusal(
|
|
219
|
+
"batch-input-limit",
|
|
220
|
+
`mechanism batch input exceeds ${byteLimit} raw UTF-8 bytes`,
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
received = next;
|
|
226
|
+
chunks.push(bytes);
|
|
227
|
+
};
|
|
228
|
+
const onEnd = () => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
cleanup();
|
|
232
|
+
const bytes = Buffer.concat(chunks, received);
|
|
233
|
+
chunks.length = 0;
|
|
234
|
+
resolve(bytes);
|
|
235
|
+
};
|
|
236
|
+
const onError = (cause) => {
|
|
237
|
+
rejectRead(deferredFailure ?? cause);
|
|
238
|
+
};
|
|
239
|
+
const onClose = () => {
|
|
240
|
+
rejectRead(
|
|
241
|
+
deferredFailure ?? input.errored ?? new Error("mechanism batch input closed before the request ended"),
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
const onOutputFailure = (cause) => {
|
|
245
|
+
if (input.errored) {
|
|
246
|
+
deferredFailure ??= cause;
|
|
247
|
+
if (preexistingErrorFallback === undefined) {
|
|
248
|
+
preexistingErrorFallback = setImmediate(() => rejectRead(deferredFailure));
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
rejectRead(cause);
|
|
253
|
+
};
|
|
254
|
+
input.on("data", onData);
|
|
255
|
+
input.on("end", onEnd);
|
|
256
|
+
input.on("error", onError);
|
|
257
|
+
input.on("close", onClose);
|
|
258
|
+
// A destroy(error) may have set `closed` while its public error event is
|
|
259
|
+
// still queued. Keep our listener through that event; the immediate also
|
|
260
|
+
// handles a stream whose error event had already been observed by callers.
|
|
261
|
+
if (input.errored) {
|
|
262
|
+
preexistingErrorFallback = setImmediate(() => rejectRead(deferredFailure ?? input.errored));
|
|
263
|
+
} else if (input.closed || input.readableEnded || input.destroyed) {
|
|
264
|
+
onClose();
|
|
265
|
+
}
|
|
266
|
+
stopWatchingOutput = outputWatch.onFailure(onOutputFailure);
|
|
267
|
+
if (settled) stopWatchingOutput();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Whole-batch shape precheck (FND-DES-022 section 3): the outer layer of every
|
|
273
|
+
* item is checked before the first item executes. Structure failures never
|
|
274
|
+
* reach the mechanism dispatcher and never fabricate per-item results.
|
|
275
|
+
*/
|
|
276
|
+
function assertMechanismBatchShape(request) {
|
|
277
|
+
if (
|
|
278
|
+
request === null ||
|
|
279
|
+
typeof request !== "object" ||
|
|
280
|
+
Array.isArray(request) ||
|
|
281
|
+
Object.keys(request).sort().join(",") !== "inputs,operation" ||
|
|
282
|
+
request.operation !== "canonical-json" ||
|
|
283
|
+
!Array.isArray(request.inputs) ||
|
|
284
|
+
request.inputs.length === 0
|
|
285
|
+
) {
|
|
286
|
+
throw batchRefusal(
|
|
287
|
+
"batch-structure-invalid",
|
|
288
|
+
"mechanism batch request must carry exactly operation canonical-json and a non-empty inputs array",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
for (const item of request.inputs) {
|
|
292
|
+
if (
|
|
293
|
+
item === null ||
|
|
294
|
+
typeof item !== "object" ||
|
|
295
|
+
Array.isArray(item) ||
|
|
296
|
+
Object.keys(item).sort().join(",") !== "document"
|
|
297
|
+
) {
|
|
298
|
+
throw batchRefusal(
|
|
299
|
+
"batch-structure-invalid",
|
|
300
|
+
"each mechanism batch input must be an object carrying exactly document",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Keeps only the batch call's short-lived listeners active before the actual
|
|
307
|
+
// write begins. This closes the gap where an already-destroyed Writable still
|
|
308
|
+
// owes its public error event, without ending or otherwise managing the stream.
|
|
309
|
+
function watchBatchWritable(stream, channelName) {
|
|
310
|
+
let failure = stream.errored;
|
|
311
|
+
let failureListener;
|
|
312
|
+
let pendingErrorFallback;
|
|
313
|
+
let resolvePendingError;
|
|
314
|
+
const pendingError = failure
|
|
315
|
+
? new Promise((resolve) => {
|
|
316
|
+
resolvePendingError = resolve;
|
|
317
|
+
pendingErrorFallback = setImmediate(resolve);
|
|
318
|
+
})
|
|
319
|
+
: undefined;
|
|
320
|
+
const onError = (cause) => {
|
|
321
|
+
failure ??= cause;
|
|
322
|
+
failureListener?.(failure);
|
|
323
|
+
if (resolvePendingError) {
|
|
324
|
+
clearImmediate(pendingErrorFallback);
|
|
325
|
+
pendingErrorFallback = undefined;
|
|
326
|
+
const resolve = resolvePendingError;
|
|
327
|
+
resolvePendingError = undefined;
|
|
328
|
+
resolve();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const onClose = () => {
|
|
332
|
+
failure ??= stream.errored ?? new Error(
|
|
333
|
+
`mechanism batch ${channelName} closed before its write completed`,
|
|
334
|
+
);
|
|
335
|
+
failureListener?.(failure);
|
|
336
|
+
};
|
|
337
|
+
stream.on("error", onError);
|
|
338
|
+
stream.on("close", onClose);
|
|
339
|
+
if (!failure && (stream.closed || stream.writableEnded || stream.destroyed)) onClose();
|
|
340
|
+
return {
|
|
341
|
+
pendingError,
|
|
342
|
+
failure: () => failure,
|
|
343
|
+
onFailure(listener) {
|
|
344
|
+
failureListener = listener;
|
|
345
|
+
if (failure) listener(failure);
|
|
346
|
+
return () => {
|
|
347
|
+
if (failureListener === listener) failureListener = undefined;
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
release() {
|
|
351
|
+
failureListener = undefined;
|
|
352
|
+
stream.removeListener("error", onError);
|
|
353
|
+
stream.removeListener("close", onClose);
|
|
354
|
+
if (pendingErrorFallback !== undefined) clearImmediate(pendingErrorFallback);
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Waits for this write's callback rather than treating the writable's buffer
|
|
361
|
+
* signal as delivery completion. The caller retains ownership of the stream,
|
|
362
|
+
* so completion neither ends it nor waits for the whole stream to finish.
|
|
363
|
+
*/
|
|
364
|
+
function writeAll(stream, text, channelName) {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
let settled = false;
|
|
367
|
+
let callbackFailureFallback;
|
|
368
|
+
const cleanup = () => {
|
|
369
|
+
stream.removeListener("error", onError);
|
|
370
|
+
stream.removeListener("close", onClose);
|
|
371
|
+
if (callbackFailureFallback !== undefined) {
|
|
372
|
+
clearImmediate(callbackFailureFallback);
|
|
373
|
+
callbackFailureFallback = undefined;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
const rejectWrite = (cause) => {
|
|
377
|
+
if (settled) return;
|
|
378
|
+
settled = true;
|
|
379
|
+
cleanup();
|
|
380
|
+
reject(cause);
|
|
381
|
+
};
|
|
382
|
+
const onError = (cause) => {
|
|
383
|
+
rejectWrite(cause);
|
|
384
|
+
};
|
|
385
|
+
const onClose = () => {
|
|
386
|
+
rejectWrite(
|
|
387
|
+
stream.errored ?? new Error(`mechanism batch ${channelName} closed before its write completed`),
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
const onWrite = (cause) => {
|
|
391
|
+
if (settled) return;
|
|
392
|
+
if (cause) {
|
|
393
|
+
// A standard Writable emits its matching error just after invoking the
|
|
394
|
+
// write callback. Keep our listener through that event so the failure
|
|
395
|
+
// cannot become an unhandled error; the immediate is a fallback for a
|
|
396
|
+
// writable-like caller that reports only through the callback.
|
|
397
|
+
callbackFailureFallback = setImmediate(() => rejectWrite(cause));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
settled = true;
|
|
401
|
+
cleanup();
|
|
402
|
+
resolve();
|
|
403
|
+
};
|
|
404
|
+
stream.on("error", onError);
|
|
405
|
+
stream.on("close", onClose);
|
|
406
|
+
// As with input, an errored destroy may still owe its public error event.
|
|
407
|
+
// Waiting for that event preserves the cause and prevents it from becoming
|
|
408
|
+
// unhandled after an eager preflight rejection.
|
|
409
|
+
if (stream.errored) {
|
|
410
|
+
callbackFailureFallback = setImmediate(() => rejectWrite(stream.errored));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (stream.closed || stream.writableEnded || stream.destroyed) {
|
|
414
|
+
onClose();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
stream.write(text, onWrite);
|
|
419
|
+
} catch (cause) {
|
|
420
|
+
rejectWrite(cause);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Bounded, same-operation, ordered mechanism batch transport (FND-DES-022).
|
|
427
|
+
*
|
|
428
|
+
* The request stream is read once under the raw-byte budget, strictly decoded
|
|
429
|
+
* as UTF-8, parsed once, shape-prechecked, then executed item by item through
|
|
430
|
+
* the fixed single-request dispatcher — no second algorithm is introduced.
|
|
431
|
+
* Every item's serialized response bytes are accumulated with the envelope
|
|
432
|
+
* commas, prefix, and mandatory closing newline; nothing is written to the
|
|
433
|
+
* output until the complete response is confirmed within the output budget.
|
|
434
|
+
* Item failures follow the original CLI error projection into that item's
|
|
435
|
+
* `response` and continue; whole-batch refusals and transport failures write
|
|
436
|
+
* one batch error object to the error channel and never touch the output.
|
|
437
|
+
*/
|
|
438
|
+
export async function runMechanismCliBatch({
|
|
439
|
+
input = stdin,
|
|
440
|
+
output = stdout,
|
|
441
|
+
error = stderr,
|
|
442
|
+
invoke = invokeFoundationMechanism,
|
|
443
|
+
} = {}) {
|
|
444
|
+
const outputWatch = watchBatchWritable(output, "output");
|
|
445
|
+
const errorWatch = watchBatchWritable(error, "error output");
|
|
446
|
+
try {
|
|
447
|
+
// Start the input read synchronously so a queued input error is covered
|
|
448
|
+
// before any output preflight can await or fail this batch.
|
|
449
|
+
const bytes = await readBoundedBatchInput(
|
|
450
|
+
input,
|
|
451
|
+
MECHANISM_BATCH_POLICY.inputByteLimit,
|
|
452
|
+
outputWatch,
|
|
453
|
+
);
|
|
454
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
455
|
+
const request = JSON.parse(text);
|
|
456
|
+
assertMechanismBatchShape(request);
|
|
457
|
+
const inputs = request.inputs;
|
|
458
|
+
if (inputs.length > MECHANISM_BATCH_POLICY.itemLimit) {
|
|
459
|
+
throw batchRefusal(
|
|
460
|
+
"batch-item-limit",
|
|
461
|
+
`mechanism batch exceeds the ${MECHANISM_BATCH_POLICY.itemLimit} item limit`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const fragments = [];
|
|
465
|
+
let outputBytes = Buffer.byteLength(BATCH_OUTPUT_PREFIX, "utf8");
|
|
466
|
+
let anyItemFailure = false;
|
|
467
|
+
for (let inputIndex = 0; inputIndex < inputs.length; inputIndex += 1) {
|
|
468
|
+
const item = inputs[inputIndex];
|
|
469
|
+
let exitCode = 0;
|
|
470
|
+
let response;
|
|
471
|
+
try {
|
|
472
|
+
response = await invoke({ operation: "canonical-json", params: item });
|
|
473
|
+
} catch (cause) {
|
|
474
|
+
response = errorResponse(cause);
|
|
475
|
+
exitCode = 2;
|
|
476
|
+
}
|
|
477
|
+
const fragment = JSON.stringify({ inputIndex, exitCode, response });
|
|
478
|
+
if (inputIndex > 0) outputBytes += 1; // separating comma between items
|
|
479
|
+
outputBytes += Buffer.byteLength(fragment, "utf8");
|
|
480
|
+
// The closing bytes are mandatory, so the check includes them before the
|
|
481
|
+
// next item may execute; an overrun never delivers a partial result.
|
|
482
|
+
if (outputBytes + Buffer.byteLength(BATCH_OUTPUT_SUFFIX, "utf8") > MECHANISM_BATCH_POLICY.outputByteLimit) {
|
|
483
|
+
throw batchRefusal(
|
|
484
|
+
"batch-output-limit",
|
|
485
|
+
`mechanism batch response exceeds the ${MECHANISM_BATCH_POLICY.outputByteLimit} byte limit`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
fragments.push(fragment);
|
|
489
|
+
if (exitCode === 2) anyItemFailure = true;
|
|
490
|
+
}
|
|
491
|
+
const outputFailure = outputWatch.failure();
|
|
492
|
+
outputWatch.release();
|
|
493
|
+
if (outputFailure) throw outputFailure;
|
|
494
|
+
await writeAll(
|
|
495
|
+
output,
|
|
496
|
+
`${BATCH_OUTPUT_PREFIX}${fragments.join(",")}${BATCH_OUTPUT_SUFFIX}`,
|
|
497
|
+
"output",
|
|
498
|
+
);
|
|
499
|
+
if (errorWatch.pendingError) await errorWatch.pendingError;
|
|
500
|
+
errorWatch.release();
|
|
501
|
+
return anyItemFailure ? 2 : 0;
|
|
502
|
+
} catch (cause) {
|
|
503
|
+
const pendingErrors = [outputWatch.pendingError, errorWatch.pendingError].filter(Boolean);
|
|
504
|
+
if (pendingErrors.length > 0) await Promise.all(pendingErrors);
|
|
505
|
+
outputWatch.release();
|
|
506
|
+
const errorFailure = errorWatch.failure();
|
|
507
|
+
errorWatch.release();
|
|
508
|
+
if (!errorFailure) {
|
|
509
|
+
try {
|
|
510
|
+
await writeAll(error, `${JSON.stringify(errorResponse(cause))}\n`, "error output");
|
|
511
|
+
} catch {
|
|
512
|
+
// The error channel itself failed; the batch still exits failed.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return 2;
|
|
516
|
+
} finally {
|
|
517
|
+
outputWatch.release();
|
|
518
|
+
errorWatch.release();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
157
522
|
if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
|
|
158
|
-
|
|
523
|
+
const args = process.argv.slice(2);
|
|
524
|
+
if (args[0] === "--batch") {
|
|
525
|
+
if (args.length > 1) {
|
|
526
|
+
// Refuse before reading any input: the batch mode takes no positional
|
|
527
|
+
// arguments, and the old single-request entry must stay unchanged.
|
|
528
|
+
stderr.write(
|
|
529
|
+
`${JSON.stringify(errorResponse(new TypeError("mechanisms CLI --batch accepts no additional arguments")))}\n`,
|
|
530
|
+
);
|
|
531
|
+
process.exitCode = 2;
|
|
532
|
+
} else {
|
|
533
|
+
const exitCode = await runMechanismCliBatch();
|
|
534
|
+
if (exitCode === 2 && !stdin.destroyed) stdin.destroy();
|
|
535
|
+
process.exitCode = exitCode;
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
process.exitCode = await runMechanismCli();
|
|
539
|
+
}
|
|
159
540
|
}
|
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.
|
|
13
|
+
"skill-family-contracts": "0.19.1",
|
|
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.
|
|
52
|
+
"version": "0.19.1",
|
|
53
53
|
"scripts": {
|
|
54
54
|
"check": "node --test",
|
|
55
55
|
"test": "node --test"
|
|
@@ -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 合同保持不变。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: 0.19.1
|
|
2
|
+
date: 2026-09-08
|
|
3
|
+
locales:
|
|
4
|
+
en:
|
|
5
|
+
summary: Harness 0.19.1 makes the candidate mechanism-batch entry point settle on the current transfer's actual completion or failure while preserving caller ownership of programmatic streams.
|
|
6
|
+
changes:
|
|
7
|
+
fixed:
|
|
8
|
+
- Waits for the current write callback, handles premature input and output closure, and removes only listeners installed by the batch helper.
|
|
9
|
+
- Stops the library helper's own reading and releases its buffer after input overflow without destroying a caller-owned stream.
|
|
10
|
+
- Releases CLI-owned standard input after an oversized request is reported, allowing the process to exit 2 while the upstream pipe remains open.
|
|
11
|
+
upgradeNotes: Pin all three Foundation packages to exactly 0.19.1. The batch operation, capacity policy, per-item order, and single-request CLI remain unchanged. This entry is candidate and requires re-verification after an upgrade.
|
|
12
|
+
zh-CN:
|
|
13
|
+
summary: Harness 0.19.1 让候选机制批量入口等待本次传输真正完成或失败,同时保留程序化入口中调用方对流的所有权。
|
|
14
|
+
changes:
|
|
15
|
+
fixed:
|
|
16
|
+
- 等待本次写入回调,处理输入与输出提前关闭,并且只移除批量辅助函数自己安装的监听器。
|
|
17
|
+
- 输入超限后停止库辅助函数自己的读取并释放缓存,不销毁调用方拥有的流。
|
|
18
|
+
- 超限错误写出后释放 CLI 自己拥有的标准输入,使上游管道保持打开时进程仍能自行退出 2。
|
|
19
|
+
upgradeNotes: 三个 Foundation 包须一起精确锁定到 0.19.1。批量操作、容量政策、逐项顺序和旧单请求 CLI 保持不变。该入口仍是候选能力,升级后须重新验证。
|
package/src/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Exact Foundation release coordinate shared by the public Harness exports. */
|
|
2
|
-
export const FOUNDATION_PACKAGE_VERSION = "0.
|
|
2
|
+
export const FOUNDATION_PACKAGE_VERSION = "0.19.1";
|