skill-family-harness-node 0.18.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,26 @@
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
+
3
24
  <!-- release-skill:changelog:start version=0.18.0 locale=en baseline=sha256:ca3679574ea53013c830563ae818326062794aec1f486e6953e89fa8937be644 -->
4
25
  ## [0.18.0] - 2026-09-05
5
26
 
@@ -1,5 +1,26 @@
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
+
3
24
  <!-- release-skill:changelog:start version=0.18.0 locale=zh-CN baseline=sha256:ba40c3c901c8f717b18d131344fe94a470c149ddb1d0f96e5611d38bf1c70d3d -->
4
25
  ## [0.18.0] - 2026-09-05
5
26
 
package/README.md CHANGED
@@ -4,27 +4,28 @@
4
4
 
5
5
  # skill-family-harness-node
6
6
 
7
- <!-- release-skill:release-version: 0.18.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.18.0** (2026-09-05)
12
+ **0.19.0** (2026-09-07)
13
13
 
14
- Harness 0.18.0 adds the stable replaceFixedSetAtomic operation for replacing one existing fixed-set directory with one complete staged sibling.
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
15
 
16
16
  **Added**
17
17
 
18
- - Adds replaceFixedSetAtomic to the package root and fixed-set-publication subpath. Source and target must be real sibling directories under the same canonical parent.
19
- - 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.
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.
20
20
 
21
21
  **Changed**
22
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.
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.
24
25
 
25
26
  **Upgrade Notes**
26
27
 
27
- 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.
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.
28
29
  <!-- release-skill:managed:end id=latest-release -->
29
30
 
30
31
  ## Problem It Solves
@@ -51,7 +52,7 @@ mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
51
52
  After publication, use the registry coordinate:
52
53
 
53
54
  ```sh
54
- npm install skill-family-harness-node@0.18.0
55
+ npm install skill-family-harness-node@0.19.0
55
56
  npm info skill-family-harness-node --help
56
57
  ```
57
58
 
package/README.zh-CN.md CHANGED
@@ -5,27 +5,28 @@
5
5
 
6
6
  # skill-family-harness-node
7
7
 
8
- <!-- release-skill:release-version: 0.18.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.18.0** (2026-09-05)
13
+ **0.19.0** (2026-09-07)
14
14
 
15
- Harness 0.18.0 新增稳定的 replaceFixedSetAtomic,用一个完整暂存目录替换一个既有固定集合目录。
15
+ Harness 0.19.0 在既有固定机制 CLI 上新增 runMechanismCliBatch 与显式 --batch 模式,提供有界、有序、同操作批量传输,首批操作 canonical-json。
16
16
 
17
17
  **新增**
18
18
 
19
- - 包根与 fixed-set-publication 子路径新增 replaceFixedSetAtomic。源目录和目标目录必须是同一规范父目录下的真实兄弟目录。
20
- - 提交只执行一次 Darwin RENAME_SWAP Linux RENAME_EXCHANGE。成功后,新集合完整位于目标路径,被置换的旧目标留在 sourceRoot。
19
+ - 新增 runMechanismCliBatch({input, output, error}):读入一个批量请求,逐项返回 inputIndex、exitCode 与原单请求响应。
20
+ - 官方 Bundle 投影的 mechanisms-cli.mjs 新增显式 --batch 模式。不带 --batch 时旧单请求路径不变。
21
21
 
22
22
  **变更**
23
23
 
24
- - 替换无法返回已验证成功时,沿用 SFC2004 机制错误表面,报告提交前后、发布、验证、提交和持久化状态。
24
+ - 批量结构与容量拒绝使用 TypeError,error.details.kind 封闭为 batch-structure-invalid、batch-item-limit、batch-input-limit、batch-output-limit。
25
+ - 单项机制失败写入该项结果位置,其余项继续执行;任一机制项失败时整批退出码为 2。
25
26
 
26
27
  **升级说明**
27
28
 
28
- 三个 Foundation 包须一起精确锁定到 0.18.0。replaceFixedSetAtomic 不是幂等操作:相同路径再次调用会把两个目录交换回去。成功后,或收到提交后及不确定错误时,均不得盲目重试。调用方只在确认成功后负责清理被置换的旧目标。
29
+ 三个 Foundation 包须一起精确锁定到 0.19.0。固定容量政策为 256 项、16 MiB 输入字节、32 MiB 输出字节;相互独立请求的分组与超限切分由消费者负责。该入口为候选能力,升级后须重新验证。旧单请求 CLI 合同保持不变。
29
30
  <!-- release-skill:managed:end id=latest-release -->
30
31
 
31
32
  ## 解决的问题
@@ -52,7 +53,7 @@ mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
52
53
  发布后再使用 registry 坐标:
53
54
 
54
55
  ```sh
55
- npm install skill-family-harness-node@0.18.0
56
+ npm install skill-family-harness-node@0.19.0
56
57
  npm info skill-family-harness-node --help
57
58
  ```
58
59
 
@@ -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.18.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.18.0",
52
+ "version": "0.19.0",
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 合同保持不变。
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.18.0";
2
+ export const FOUNDATION_PACKAGE_VERSION = "0.19.0";