skill-family-harness-node 0.13.0 → 0.14.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.14.0 locale=en baseline=sha256:23ac2de57433409f1dafac90d7bdd130fd10bd399fd188d7d848a49320b86f30 -->
4
+ ## [0.14.0] - 2026-08-28
5
+
6
+ Harness 0.14.0 adds an official atomic-write fake, a synchronized package version export, and canonical temporary workspace roots for safe raw sinks.
7
+
8
+ ### Added
9
+
10
+ - Adds createAtomicWriteFake({ vector }) with deterministic write, replace, and observation facts and no filesystem side effects.
11
+ - Exposes FOUNDATION_PACKAGE_VERSION for lockstep consumers.
12
+ - Returns canonical realpaths from TemporaryWorkspace.create() and fromBaseline(). A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
13
+
14
+ ### Changed
15
+
16
+ - Documents that the fake verifies adapter wiring, not the domain guarantee or real-host qualification.
17
+
18
+ ### Upgrade Notes
19
+
20
+ Pin Contracts, Harness, and Engineering Kit to 0.14.0 together. Use the official fake with the Contracts vectors; retain real filesystem and domain tests for production guarantees. Temporary workspace roots are canonical. A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
21
+ <!-- release-skill:changelog:end version=0.14.0 locale=en -->
22
+
23
+
3
24
  <!-- release-skill:changelog:start version=0.13.0 locale=en baseline=sha256:ba0667eb4b4805aafc2423cd79c04df3c835b8ae9bb07391867175f6ad96c36b -->
4
25
  ## [0.13.0] - 2026-08-26
5
26
 
@@ -1,5 +1,26 @@
1
1
  # 变更日志
2
2
 
3
+ <!-- release-skill:changelog:start version=0.14.0 locale=zh-CN baseline=sha256:11857b4e61d5111fab3dbc5eb1afcf99fd8fd6fd41f190bad5a5e16c09633e25 -->
4
+ ## [0.14.0] - 2026-08-28
5
+
6
+ Harness 0.14.0 增加消费者契约测试用正式 atomic-write 测试替身(`fake`)、同步公开包版本导出,并让临时工作区根目录返回 canonical realpath。
7
+
8
+ ### 新增
9
+
10
+ - 新增 `createAtomicWriteFake({ vector })`,提供确定性的写入、替换和观察事实且不触碰文件系统。
11
+ - 公开 `FOUNDATION_PACKAGE_VERSION`,供锁步消费者使用。
12
+ - `TemporaryWorkspace.create()` 与 `fromBaseline()` 返回 canonical realpath。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
13
+
14
+ ### 变更
15
+
16
+ - 明确测试替身用于验证适配器接线,不代表领域保证或真实宿主资格。
17
+
18
+ ### 升级说明
19
+
20
+ Contracts、Harness 与 Engineering Kit 须一起精确锁定到 0.14.0。用正式测试替身配合 Contracts 向量;生产保证仍须由真实文件系统和领域测试覆盖。临时工作区根目录已经 canonical。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
21
+ <!-- release-skill:changelog:end version=0.14.0 locale=zh-CN -->
22
+
23
+
3
24
  <!-- release-skill:changelog:start version=0.13.0 locale=zh-CN baseline=sha256:1356b2ecff87da4a8bbe5bc04980b125ddf181873ff2b536be87a2e246b59609 -->
4
25
  ## [0.13.0] - 2026-08-26
5
26
 
package/README.md CHANGED
@@ -4,26 +4,28 @@
4
4
 
5
5
  # skill-family-harness-node
6
6
 
7
- <!-- release-skill:release-version: 0.13.0 -->
7
+ <!-- release-skill:release-version: 0.14.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.13.0** (2026-08-26)
12
+ **0.14.0** (2026-08-28)
13
13
 
14
- Harness 0.13.0 is a source candidate for complete bound-tree observations and optional subprocess output limits.
14
+ Harness 0.14.0 adds an official atomic-write fake, a synchronized package version export, and canonical temporary workspace roots for safe raw sinks.
15
15
 
16
16
  **Added**
17
17
 
18
- - Adds observeFilesystemTree({ root, rootBinding }) as a public candidate entry for fresh private tree facts, including file bytes and POSIX mode observations.
18
+ - Adds createAtomicWriteFake({ vector }) with deterministic write, replace, and observation facts and no filesystem side effects.
19
+ - Exposes FOUNDATION_PACKAGE_VERSION for lockstep consumers.
20
+ - Returns canonical realpaths from TemporaryWorkspace.create() and fromBaseline(). A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
19
21
 
20
22
  **Changed**
21
23
 
22
- - Extends superviseProcess with independent stdout/stderr byte limits using the existing termination path. Equal-to-limit output is allowed; uncapped behavior remains compatible.
24
+ - Documents that the fake verifies adapter wiring, not the domain guarantee or real-host qualification.
23
25
 
24
26
  **Upgrade Notes**
25
27
 
26
- Pin Contracts and Harness together. Tree observation does not apply payload acceptance policy or promise a transaction snapshot. Candidate preparation is not publication.
28
+ Pin Contracts, Harness, and Engineering Kit to 0.14.0 together. Use the official fake with the Contracts vectors; retain real filesystem and domain tests for production guarantees. Temporary workspace roots are canonical. A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
27
29
  <!-- release-skill:managed:end id=latest-release -->
28
30
 
29
31
  ## Problem It Solves
@@ -36,17 +38,28 @@ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing
36
38
 
37
39
  ## Installation and Minimal Example
38
40
 
39
- Version 0.13.0 is not published. The registry command below is for use after publication; this iteration installs the three local candidate tarballs in an isolated directory.
41
+ Version 0.14.0 is a local candidate. Build all three tarballs into one temporary directory and install those exact files for a candidate check:
40
42
 
41
43
  ```sh
42
- npm install skill-family-harness-node@0.13.0
44
+ pack_dir="$(mktemp -d)"
45
+ (cd packages/skill-family-contracts && pnpm pack --pack-destination "$pack_dir")
46
+ (cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
47
+ (cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
48
+ mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
49
+ (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.14.0.tgz" "$pack_dir/skill-family-harness-node-0.14.0.tgz" "$pack_dir/skill-family-engineering-kit-0.14.0.tgz")
50
+ ```
51
+
52
+ After publication, use the registry coordinate:
53
+
54
+ ```sh
55
+ npm install skill-family-harness-node@0.14.0
43
56
  npm info skill-family-harness-node --help
44
57
  ```
45
58
 
46
59
  The minimal example shows validating a contract document inside Node:
47
60
 
48
61
  ```js
49
- // Run from an empty directory: npm install skill-family-harness-node@0.13.0
62
+ // Run from an installed consumer directory after publication.
50
63
  import { validateContractDocument } from "skill-family-harness-node";
51
64
 
52
65
  const document = {
@@ -100,10 +113,12 @@ The capability remains **candidate**. Pin all three Foundation packages exactly
100
113
  | Export | Responsibility |
101
114
  | --- | --- |
102
115
  | `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | Capability and exclusion lists (frozen constants). |
116
+ | `FOUNDATION_PACKAGE_VERSION` | Exact Foundation package version used for lockstep checks. |
103
117
  | `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | Mechanism failures uniformly carry the registered error code `SFC2004`; `details.kind` gives a stable subcategory. |
104
118
  | `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | Routes and caches validators by Schema dialect; reuses Contracts' Ajv instances and dialect/policy semantics. |
105
119
  | `classifyPathInput` / `resolveContained` / `readFileContained` | Path containment: intercepts path overruns, symlink escapes, and realpath escapes. |
106
120
  | `writeFileAtomic` | Atomic write: leaves no half-written artifact on failure (temp file + fsync + rename). |
121
+ | `createAtomicWriteFake({ vector })` | Official no-filesystem fake for consumer contract tests; emits deterministic write/replace/observation facts. |
107
122
  | `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | Auto-cleanup temporary workspace, cleaned up even on exception paths. |
108
123
  | `digestBytes` / `computeResourceClosure` / `closureContains` | Resource closure and deterministic sha256 digest. |
109
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. |
@@ -229,4 +244,4 @@ Mechanism failures uniformly throw `SFC2004` (EXECUTION_FAILED), with `details.k
229
244
 
230
245
  The candidate observeFilesystemTree({ root, rootBinding }) reads complete tree facts. Existing superviseProcess accepts optional per-stream raw-byte caps. Observing a payload does not accept it.
231
246
 
232
- Version 0.13.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 host qualification or release approval.
247
+ Version 0.14.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,26 +5,28 @@
5
5
 
6
6
  # skill-family-harness-node
7
7
 
8
- <!-- release-skill:release-version: 0.13.0 -->
8
+ <!-- release-skill:release-version: 0.14.0 -->
9
9
 
10
10
  Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
11
11
 
12
12
  <!-- release-skill:managed:start id=latest-release -->
13
- **0.13.0** (2026-08-26)
13
+ **0.14.0** (2026-08-28)
14
14
 
15
- Harness 0.13.0 源码候选增加完整绑定树观察与可选子进程输出上限。
15
+ Harness 0.14.0 增加消费者契约测试用正式 atomic-write 测试替身(`fake`)、同步公开包版本导出,并让临时工作区根目录返回 canonical realpath。
16
16
 
17
17
  **新增**
18
18
 
19
- - 新增公开候选入口 observeFilesystemTree({ root, rootBinding }),每次读取私有树事实,包含文件字节与 POSIX 模式观察。
19
+ - 新增 `createAtomicWriteFake({ vector })`,提供确定性的写入、替换和观察事实且不触碰文件系统。
20
+ - 公开 `FOUNDATION_PACKAGE_VERSION`,供锁步消费者使用。
21
+ - `TemporaryWorkspace.create()` 与 `fromBaseline()` 返回 canonical realpath。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
20
22
 
21
23
  **变更**
22
24
 
23
- - 为 superviseProcess 增加 stdout/stderr 独立字节上限,复用既有终止路径;等于上限允许,未设置上限保持旧行为。
25
+ - 明确测试替身用于验证适配器接线,不代表领域保证或真实宿主资格。
24
26
 
25
27
  **升级说明**
26
28
 
27
- Contracts 与 Harness 须精确锁步。树观察不执行载荷接受政策,也不承诺事务快照。候选准备不代表发布完成。
29
+ Contracts、HarnessEngineering Kit 须一起精确锁定到 0.14.0。用正式测试替身配合 Contracts 向量;生产保证仍须由真实文件系统和领域测试覆盖。临时工作区根目录已经 canonical。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。
28
30
  <!-- release-skill:managed:end id=latest-release -->
29
31
 
30
32
  ## 解决的问题
@@ -37,17 +39,28 @@ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言
37
39
 
38
40
  ## 安装和最小示例
39
41
 
40
- 0.13.0 尚未发布。下面的 registry 安装命令供发布后使用;本轮验证应在隔离目录安装三个本地候选 tarball
42
+ 0.14.0 是本地候选版本。候选验证先把三个包分别打入同一个临时目录,再安装这三个精确 tarball
41
43
 
42
44
  ```sh
43
- npm install skill-family-harness-node@0.13.0
45
+ pack_dir="$(mktemp -d)"
46
+ (cd packages/skill-family-contracts && pnpm pack --pack-destination "$pack_dir")
47
+ (cd packages/skill-family-harness-node && pnpm pack --pack-destination "$pack_dir")
48
+ (cd packages/skill-family-engineering-kit && pnpm pack --pack-destination "$pack_dir")
49
+ mkdir "$pack_dir/consumer" && (cd "$pack_dir/consumer" && npm init -y)
50
+ (cd "$pack_dir/consumer" && npm install "$pack_dir/skill-family-contracts-0.14.0.tgz" "$pack_dir/skill-family-harness-node-0.14.0.tgz" "$pack_dir/skill-family-engineering-kit-0.14.0.tgz")
51
+ ```
52
+
53
+ 发布后再使用 registry 坐标:
54
+
55
+ ```sh
56
+ npm install skill-family-harness-node@0.14.0
44
57
  npm info skill-family-harness-node --help
45
58
  ```
46
59
 
47
60
  最小示例演示在 Node 内校验一份契约文档:
48
61
 
49
62
  ```js
50
- // 从空目录运行:npm install skill-family-harness-node@0.13.0
63
+ // 发布后从已安装的消费者目录运行。
51
64
  import { validateContractDocument } from "skill-family-harness-node";
52
65
 
53
66
  const document = {
@@ -101,10 +114,12 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
101
114
  | 导出 | 职责 |
102
115
  | --- | --- |
103
116
  | `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | 能力与排除清单(冻结常量)。 |
117
+ | `FOUNDATION_PACKAGE_VERSION` | 供锁步检查使用的精确 Foundation 包版本。 |
104
118
  | `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | 机制失败统一携带注册错误码 `SFC2004`,`details.kind` 给出稳定细分。 |
105
119
  | `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | 按 Schema 方言路由并缓存 validator;复用 Contracts 的 Ajv 实例与 dialect/policy 语义。 |
106
120
  | `classifyPathInput` / `resolveContained` / `readFileContained` | 路径收容:拦截路径越界、符号链接逃逸、真实路径逃逸。 |
107
121
  | `writeFileAtomic` | 原子写:失败不留半成品(临时文件 + fsync + rename)。 |
122
+ | `createAtomicWriteFake({ vector })` | 消费者契约测试用正式无文件系统测试替身;产生确定性的写入/替换/观察事实。 |
108
123
  | `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | 自动清理的临时工作区,异常路径也清理。 |
109
124
  | `digestBytes` / `computeResourceClosure` / `closureContains` | 资源闭包与确定性 sha256 摘要。 |
110
125
  | `superviseProcess` / `validateTimeoutPolicy` | 唯一的受约束子进程监督器。0.11.0 的 `rawSink` 只向 fresh canonical 私有根写原始 stdout/stderr 字节,并等待子进程、流、排队写入、fsync 与句柄全部关闭。调用方必须在整个调用期间独占 sink 命名空间;句柄保护不证明 pathname 或根目录身份始终不变。 |
@@ -229,4 +244,4 @@ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字
229
244
 
230
245
  新增候选 observeFilesystemTree({ root, rootBinding }) 读取完整树事实;既有 superviseProcess 支持可选每流原始字节上限。观察完成不等于接受载荷。
231
246
 
232
- 0.13.0 为本地源码候选,尚未发布。消费本地已验证的三包 tarball,不能把版本标记、单元测试或安装成功当作完整宿主资格与发布批准。
247
+ 0.14.0 为本地源码候选,尚未发布。消费本地已验证的三包 tarball;版本标记、单元测试或安装成功都不等于契约接入完成、迁移完成或真实宿主资格。
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.0",
13
+ "skill-family-contracts": "0.14.0",
14
14
  "yaml": "2.9.0"
15
15
  },
16
16
  "description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
@@ -22,6 +22,7 @@
22
22
  ".": "./src/index.mjs",
23
23
  "./candidate/quickstart-profile": "./candidate/quickstart-profile.mjs",
24
24
  "./candidate/rename-directory-no-replace": "./candidate/rename-directory-no-replace/rename-directory-no-replace.mjs",
25
+ "./contract-testing": "./src/contract-testing.mjs",
25
26
  "./fixed-set-publication": "./src/fixed-set-publication.mjs",
26
27
  "./quickstart-profile": "./candidate/quickstart-profile.mjs",
27
28
  "./rename-directory-no-replace": "./candidate/rename-directory-no-replace/rename-directory-no-replace.mjs"
@@ -48,7 +49,7 @@
48
49
  "url": "https://github.com/ifoohoo/skill-family-harness-node.git"
49
50
  },
50
51
  "type": "module",
51
- "version": "0.13.0",
52
+ "version": "0.14.0",
52
53
  "scripts": {
53
54
  "check": "node --test",
54
55
  "test": "node --test"
@@ -0,0 +1,23 @@
1
+ version: 0.14.0
2
+ date: 2026-08-28
3
+ locales:
4
+ en:
5
+ summary: Harness 0.14.0 adds an official atomic-write fake, a synchronized package version export, and canonical temporary workspace roots for safe raw sinks.
6
+ changes:
7
+ added:
8
+ - Adds createAtomicWriteFake({ vector }) with deterministic write, replace, and observation facts and no filesystem side effects.
9
+ - Exposes FOUNDATION_PACKAGE_VERSION for lockstep consumers.
10
+ - Returns canonical realpaths from TemporaryWorkspace.create() and fromBaseline(). A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
11
+ changed:
12
+ - Documents that the fake verifies adapter wiring, not the domain guarantee or real-host qualification.
13
+ upgradeNotes: Pin Contracts, Harness, and Engineering Kit to 0.14.0 together. Use the official fake with the Contracts vectors; retain real filesystem and domain tests for production guarantees. Temporary workspace roots are canonical. A root returned by create() can be passed directly to superviseProcess rawSink while it remains fresh and empty. A materialized non-empty baseline root is rejected by rawSink freshness validation.
14
+ zh-CN:
15
+ summary: 'Harness 0.14.0 增加消费者契约测试用正式 atomic-write 测试替身(`fake`)、同步公开包版本导出,并让临时工作区根目录返回 canonical realpath。'
16
+ changes:
17
+ added:
18
+ - 新增 `createAtomicWriteFake({ vector })`,提供确定性的写入、替换和观察事实且不触碰文件系统。
19
+ - 公开 `FOUNDATION_PACKAGE_VERSION`,供锁步消费者使用。
20
+ - '`TemporaryWorkspace.create()` 与 `fromBaseline()` 返回 canonical realpath。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。'
21
+ changed:
22
+ - 明确测试替身用于验证适配器接线,不代表领域保证或真实宿主资格。
23
+ upgradeNotes: 'Contracts、Harness 与 Engineering Kit 须一起精确锁定到 0.14.0。用正式测试替身配合 Contracts 向量;生产保证仍须由真实文件系统和领域测试覆盖。临时工作区根目录已经 canonical。`create()` 返回的根目录在保持 fresh 且为空时,可以直接传给 `superviseProcess` 的 `rawSink`。物化后的非空 baseline 根目录会被 `rawSink` 的 freshness 校验拒绝。'
@@ -0,0 +1,173 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import {
4
+ ContractsError,
5
+ assertRegisteredErrorCode,
6
+ verifyConsumerContractVector,
7
+ } from "skill-family-contracts";
8
+ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
9
+ import { FOUNDATION_PACKAGE_VERSION } from "./version.mjs";
10
+
11
+ const ATOMIC_WRITE_CAPABILITY = "foundation.harness.atomic-write";
12
+ const ATOMIC_WRITE_VECTOR_SET = "foundation.harness.atomic-write.consumer-v1";
13
+ const ATOMIC_WRITE_CONTRACT = "skill-family-harness-node:writeFileAtomic";
14
+
15
+ function deepFreeze(value, seen = new Set()) {
16
+ if (value === null || typeof value !== "object" || seen.has(value)) return value;
17
+ seen.add(value);
18
+ for (const child of Object.values(value)) deepFreeze(child, seen);
19
+ return Object.freeze(value);
20
+ }
21
+
22
+ function sameValue(left, right, seen = new Set()) {
23
+ if (Object.is(left, right)) return true;
24
+ if (left === null || right === null || typeof left !== typeof right) return false;
25
+ if (typeof left !== "object") return false;
26
+ if (Buffer.isBuffer(left) || Buffer.isBuffer(right)) {
27
+ return Buffer.isBuffer(left) && Buffer.isBuffer(right) && left.equals(right);
28
+ }
29
+ if (Array.isArray(left) || Array.isArray(right)) {
30
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length &&
31
+ left.every((value, index) => sameValue(value, right[index], seen));
32
+ }
33
+ if (seen.has(left)) return true;
34
+ seen.add(left);
35
+ const leftKeys = Object.keys(left).sort();
36
+ const rightKeys = Object.keys(right).sort();
37
+ return leftKeys.length === rightKeys.length &&
38
+ leftKeys.every((key, index) => key === rightKeys[index] && sameValue(left[key], right[key], seen));
39
+ }
40
+
41
+ function dataSummary(data) {
42
+ if (typeof data === "string") return data;
43
+ if (Buffer.isBuffer(data)) {
44
+ return {
45
+ kind: "Buffer",
46
+ bytes: data.byteLength,
47
+ sha256: createHash("sha256").update(data).digest("hex"),
48
+ };
49
+ }
50
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
51
+ const bytes = data instanceof ArrayBuffer
52
+ ? Buffer.from(data)
53
+ : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
54
+ return {
55
+ kind: data.constructor?.name ?? "ArrayBufferView",
56
+ bytes: bytes.byteLength,
57
+ sha256: createHash("sha256").update(bytes).digest("hex"),
58
+ };
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ function cloneForObservation(value, seen = new Map()) {
64
+ if (value === null || typeof value !== "object") return value;
65
+ if (Buffer.isBuffer(value)) return dataSummary(value);
66
+ if (seen.has(value)) return "[Circular]";
67
+ seen.set(value, true);
68
+ if (Array.isArray(value)) return value.map((child) => cloneForObservation(child, seen));
69
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, cloneForObservation(value[key], seen)]));
70
+ }
71
+
72
+ function contractMismatch(validation) {
73
+ const code = validation?.mismatchCode ?? "SFC1001";
74
+ return new ContractsError(code, "atomic-write fake requires an official verified consumer contract vector", {
75
+ vectorId: validation?.vectorId ?? null,
76
+ observed: validation,
77
+ });
78
+ }
79
+
80
+ function invalidRequest(message, details) {
81
+ return mechanismError(HARNESS_ERROR_KINDS.ATOMIC_WRITE_FAILED, message, details);
82
+ }
83
+
84
+ function requestMatches(vector, root, relPath, data, mode) {
85
+ const expected = vector.request;
86
+ return typeof root === "string" && root.length > 0 && path.isAbsolute(root) &&
87
+ path.normalize(root) === root &&
88
+ relPath === expected.relPath && sameValue(data, expected.data) &&
89
+ sameValue({ mode }, expected.options);
90
+ }
91
+
92
+ /**
93
+ * Creates the atomic-write fake published for the exact Foundation vector set.
94
+ * The fake is deliberately single-use and performs no filesystem operation.
95
+ */
96
+ export function createAtomicWriteFake({ vector } = {}) {
97
+ let vectorSnapshot;
98
+ try {
99
+ vectorSnapshot = structuredClone(vector);
100
+ } catch (cause) {
101
+ throw new ContractsError("SFC1001", "atomic-write fake requires a structured consumer contract vector", {
102
+ cause: cause?.name ?? "DataCloneError",
103
+ });
104
+ }
105
+ const validation = verifyConsumerContractVector(vectorSnapshot, {
106
+ capabilityId: ATOMIC_WRITE_CAPABILITY,
107
+ foundationVersion: FOUNDATION_PACKAGE_VERSION,
108
+ vectorSetId: ATOMIC_WRITE_VECTOR_SET,
109
+ });
110
+ if (!validation.ok) throw contractMismatch(validation);
111
+ if (vectorSnapshot.contractId !== ATOMIC_WRITE_CONTRACT || vectorSnapshot.strategy !== "official-fake") {
112
+ throw contractMismatch({ ...validation, mismatchCode: "SFC1013" });
113
+ }
114
+ deepFreeze(vectorSnapshot);
115
+
116
+ const identity = deepFreeze({
117
+ capabilityId: ATOMIC_WRITE_CAPABILITY,
118
+ vectorSetId: ATOMIC_WRITE_VECTOR_SET,
119
+ foundationVersion: FOUNDATION_PACKAGE_VERSION,
120
+ });
121
+ let consumed = false;
122
+ let state = { callCount: 0, request: null, outcome: "indeterminate" };
123
+
124
+ function observation() {
125
+ return deepFreeze(cloneForObservation(state));
126
+ }
127
+
128
+ async function writeFileAtomic(root, relPath, data, { mode = 0o644 } = {}) {
129
+ if (consumed) {
130
+ throw invalidRequest("atomic-write fake vector was already consumed", { vectorId: vectorSnapshot.vectorId });
131
+ }
132
+ if (!requestMatches(vectorSnapshot, root, relPath, data, mode)) {
133
+ throw invalidRequest("atomic-write fake received a request different from its verified vector", {
134
+ vectorId: vectorSnapshot.vectorId,
135
+ });
136
+ }
137
+ consumed = true;
138
+ state = {
139
+ callCount: 1,
140
+ request: {
141
+ root: cloneForObservation(root),
142
+ relPath: cloneForObservation(relPath),
143
+ data: cloneForObservation(data),
144
+ options: cloneForObservation({ mode }),
145
+ },
146
+ outcome: vectorSnapshot.expected.outcome,
147
+ };
148
+
149
+ if (vectorSnapshot.expected.outcome === "return" || vectorSnapshot.expected.outcome === "indeterminate") {
150
+ const value = path.join(root, vectorSnapshot.request.relPath);
151
+ if (!path.isAbsolute(value) || value === root) {
152
+ state = { ...state, outcome: "throw", errorCode: "SFC2004" };
153
+ throw invalidRequest("atomic-write fake could not project an absolute target", { vectorId: vectorSnapshot.vectorId });
154
+ }
155
+ state = { ...state, outcome: "return", value };
156
+ return value;
157
+ }
158
+
159
+ const errorCode = vectorSnapshot.expected.errorCode ?? "SFC2004";
160
+ const errorKind = vectorSnapshot.expected.errorKind;
161
+ assertRegisteredErrorCode(errorCode);
162
+ state = { ...state, errorCode, errorKind };
163
+ if (errorCode === "SFC2004") {
164
+ if (errorKind === HARNESS_ERROR_KINDS.PATH_TRAVERSAL) {
165
+ throw mechanismError(errorKind, "atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
166
+ }
167
+ throw invalidRequest("atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
168
+ }
169
+ throw new ContractsError(errorCode, "atomic-write fake projected the vector failure", { vectorId: vectorSnapshot.vectorId });
170
+ }
171
+
172
+ return Object.freeze({ identity, writeFileAtomic, observation });
173
+ }
package/src/index.mjs CHANGED
@@ -36,6 +36,8 @@ export const HARNESS_CAPABILITIES = Object.freeze([
36
36
  "url-credential-redaction",
37
37
  ]);
38
38
 
39
+ export { FOUNDATION_PACKAGE_VERSION } from "./version.mjs";
40
+
39
41
  export const HARNESS_EXCLUSIONS = Object.freeze([
40
42
  "business-semantics",
41
43
  "workflow-orchestration",
@@ -0,0 +1,2 @@
1
+ /** Exact Foundation release coordinate shared by the public Harness exports. */
2
+ export const FOUNDATION_PACKAGE_VERSION = "0.14.0";
package/src/workspace.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
1
+ import fsPromises from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { materializeBaseline } from "./baseline.mjs";
@@ -6,6 +6,46 @@ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
6
6
  import { readFileContained, resolveContained } from "./paths.mjs";
7
7
  import { writeFileAtomic } from "./atomic.mjs";
8
8
 
9
+ const CLEANUP_OPTIONS = { recursive: true, force: true, maxRetries: 3 };
10
+
11
+ async function canonicalizeCreatedRoot(root, label) {
12
+ try {
13
+ return await fsPromises.realpath(root);
14
+ } catch (cause) {
15
+ // The root was created/materialized before canonicalization failed. Keep
16
+ // the existing create-failed contract while reporting cleanup failure as
17
+ // part of the same mechanism error instead of hiding it.
18
+ let cleanupCode;
19
+ try {
20
+ await fsPromises.rm(root, CLEANUP_OPTIONS);
21
+ } catch (cleanupCause) {
22
+ cleanupCode = cleanupCause && cleanupCause.code ? cleanupCause.code : "unknown";
23
+ }
24
+ const code = cause && cause.code ? cause.code : "unknown";
25
+ throw mechanismError(
26
+ HARNESS_ERROR_KINDS.WORKSPACE_CREATE_FAILED,
27
+ `cannot canonicalize ${label}: ${code}`,
28
+ {
29
+ code,
30
+ ...(cleanupCode ? { cleanup: { code: cleanupCode } } : {}),
31
+ },
32
+ );
33
+ }
34
+ }
35
+
36
+ async function createCanonicalTemporaryRoot(prefix, label) {
37
+ let root;
38
+ try {
39
+ root = await fsPromises.mkdtemp(path.join(os.tmpdir(), prefix));
40
+ } catch (cause) {
41
+ throw mechanismError(
42
+ HARNESS_ERROR_KINDS.WORKSPACE_CREATE_FAILED,
43
+ `cannot create ${label}: ${cause && cause.code ? cause.code : "unknown"}`,
44
+ );
45
+ }
46
+ return canonicalizeCreatedRoot(root, label);
47
+ }
48
+
9
49
  /**
10
50
  * Auto-cleaning temporary workspaces.
11
51
  *
@@ -28,15 +68,8 @@ export class TemporaryWorkspace {
28
68
  if (typeof prefix !== "string" || prefix.length === 0 || prefix.includes("/") || prefix.includes("\0")) {
29
69
  throw new TypeError("TemporaryWorkspace.create: prefix must be a non-empty, separator-free string");
30
70
  }
31
- try {
32
- const root = await mkdtemp(path.join(os.tmpdir(), prefix));
33
- return new TemporaryWorkspace(root);
34
- } catch (cause) {
35
- throw mechanismError(
36
- HARNESS_ERROR_KINDS.WORKSPACE_CREATE_FAILED,
37
- `cannot create temporary workspace: ${cause && cause.code ? cause.code : "unknown"}`,
38
- );
39
- }
71
+ const root = await createCanonicalTemporaryRoot(prefix, "temporary workspace");
72
+ return new TemporaryWorkspace(root);
40
73
  }
41
74
 
42
75
  /**
@@ -58,7 +91,8 @@ export class TemporaryWorkspace {
58
91
  if (contentGuard !== undefined && typeof contentGuard !== "function") {
59
92
  throw new TypeError("TemporaryWorkspace.fromBaseline: contentGuard must be a function or undefined");
60
93
  }
61
- const root = await materializeBaseline({ baselineDir, baselineDigest, prefix });
94
+ const materializedRoot = await materializeBaseline({ baselineDir, baselineDigest, prefix });
95
+ const root = await canonicalizeCreatedRoot(materializedRoot, "baseline workspace");
62
96
  const workspace = new TemporaryWorkspace(root);
63
97
  if (contentGuard === undefined) return workspace;
64
98
  try {
@@ -119,7 +153,7 @@ export class TemporaryWorkspace {
119
153
  if (this.#disposed) return;
120
154
  this.#disposed = true;
121
155
  try {
122
- await rm(this.#root, { recursive: true, force: true, maxRetries: 3 });
156
+ await fsPromises.rm(this.#root, { recursive: true, force: true, maxRetries: 3 });
123
157
  } catch (cause) {
124
158
  throw mechanismError(
125
159
  HARNESS_ERROR_KINDS.WORKSPACE_DISPOSE_FAILED,