skill-family-harness-node 0.2.0 → 0.3.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 ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ <!-- release-skill:changelog:start version=0.3.0 locale=en baseline=sha256:7afef97792b4714abbd0412dcb7ea76ca66260ef9f8dd2191131b7c3f8539813 -->
4
+ ## [0.3.0] - 2026-08-12
5
+
6
+ This source candidate updates the Quickstart Profile harness to verify v2 Task and Result exchanges without taking ownership of consumer semantics.
7
+
8
+ ### Added
9
+
10
+ - Recomputes real bytes for path-backed outputs and evidence, and rejects duplicate Resource ids across observations, outputs, and evidence.
11
+ - Verifies operation identity, Task digest, every run/stage/attempt field, and the exact evidence binding set.
12
+
13
+ ### Changed
14
+
15
+ - Replaces the incompatible 0.2.1 candidate surface; consumers that still require v1 must remain pinned to exactly 0.2.1.
16
+ - Leaves method selection, retry policy, and domain result interpretation to the consumer.
17
+
18
+ ### Upgrade Notes
19
+
20
+ Version 0.3.0 is a local, unpublished source candidate. Pin the candidate subpath to an exact package version and update v1 exchange producers before adopting it.
21
+ <!-- release-skill:changelog:end version=0.3.0 locale=en -->
22
+
23
+
24
+ <!-- release-skill:changelog:start version=0.2.1 locale=en baseline=sha256:acd7d4e02eb309b149a31f4b88a8163c69ae094a53591f173c20c407e8ff4ed0 -->
25
+ ## [0.2.1] - 2026-08-10
26
+
27
+ This release adds candidate Quickstart Profile exchange helpers and makes the package release documentation available in English and Simplified Chinese.
28
+
29
+ ### Added
30
+
31
+ - Adds candidate helpers that create and revalidate observation Resources, build Tasks, wrap Results, and fail closed when a Result does not bind the exact Task and correlation fields.
32
+ - Adds complete English and Simplified Chinese package documentation, including an agent quick-reference section.
33
+
34
+ ### Changed
35
+
36
+ - Manages the current README and CHANGELOG release sections from one bilingual, versioned notes source.
37
+ - Distributes the project NOTICE separately from the Apache-2.0 LICENSE.
38
+
39
+ ### Upgrade Notes
40
+
41
+ The candidate helpers do not alter the stable Harness API or add lifecycle, retry, orchestration, model-call, network, or Git-write semantics.
42
+ <!-- release-skill:changelog:end version=0.2.1 locale=en -->
@@ -0,0 +1,42 @@
1
+ # 变更日志
2
+
3
+ <!-- release-skill:changelog:start version=0.3.0 locale=zh-CN baseline=sha256:b635e4170d3f7ac634e59f612939683ab04727f87101790d9a9f5613ea38fcfe -->
4
+ ## [0.3.0] - 2026-08-12
5
+
6
+ 本源码候选版把 Quickstart Profile Harness 更新为 v2 Task/Result 交换校验,同时不接管消费者语义。
7
+
8
+ ### 新增
9
+
10
+ - 对 path-backed output 与 evidence 重算真实字节摘要,并拒绝 observation、output、evidence 之间重复的 Resource id。
11
+ - 逐项复验 operation 身份、Task digest、run/stage/attempt 字段和 evidence 精确绑定集合。
12
+
13
+ ### 变更
14
+
15
+ - 替换与 0.2.1 不兼容的 candidate 面;仍依赖 v1 的消费者必须继续精确锁定 0.2.1。
16
+ - 方法选择、重试策略与领域结果解释继续归消费者所有。
17
+
18
+ ### 升级说明
19
+
20
+ 0.3.0 当前只是本地、未发布的源码候选。candidate 子路径必须精确锁定包版本,采用前需更新 v1 交换生产方。
21
+ <!-- release-skill:changelog:end version=0.3.0 locale=zh-CN -->
22
+
23
+
24
+ <!-- release-skill:changelog:start version=0.2.1 locale=zh-CN baseline=sha256:2f2c74ab9dcf0f1a84872743bf203eb43d15c5e722e823e670e8d81ca5f7de65 -->
25
+ ## [0.2.1] - 2026-08-10
26
+
27
+ 本版新增 Quickstart Profile 候选交换辅助函数,并为包发布文档提供完整英文版与简体中文版。
28
+
29
+ ### 新增
30
+
31
+ - 新增候选辅助函数,用于创建并复验 observation Resource、构造 Task、封装 Result,并在 Result 未精确绑定 Task 与关联字段时失败关闭。
32
+ - 新增完整的英文与简体中文包文档,并补充智能体快速参考章节。
33
+
34
+ ### 变更
35
+
36
+ - 使用同一份双语版本化说明源管理当前 README 与 CHANGELOG 的发布区域。
37
+ - 项目 NOTICE 与 Apache-2.0 LICENSE 分开分发。
38
+
39
+ ### 升级说明
40
+
41
+ 候选辅助函数不改变稳定 Harness API,也不引入生命周期、重试、编排、模型调用、网络或 Git 写入语义。
42
+ <!-- release-skill:changelog:end version=0.2.1 locale=zh-CN -->
package/NOTICE ADDED
@@ -0,0 +1,10 @@
1
+ Skill Family Foundation
2
+ =======================
3
+
4
+ Copyright 2026 广州市风荷科技有限公司
5
+
6
+ Licensed under the Apache License, Version 2.0.
7
+
8
+ This NOTICE contains project attribution for Skill Family Foundation. Third-party
9
+ attribution and license texts distributed with skill-family-engineering-kit are
10
+ recorded separately in THIRD_PARTY_NOTICES and the corresponding license files.
package/README.md CHANGED
@@ -1,77 +1,52 @@
1
1
  <!-- release-skill:safe-first-command -->
2
2
  <!-- release-skill:external-write-boundary -->
3
+ > 简体中文版:[README.zh-CN.md](./README.zh-CN.md)
3
4
 
4
5
  # skill-family-harness-node
5
6
 
6
- Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
7
+ <!-- release-skill:release-version: 0.3.0 -->
7
8
 
8
- ## 边界
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.
9
10
 
10
- - 消费 `skill-family-contracts`(工作区依赖),复用其方言路由的 Ajv validator、Kernel Protocol、冻结错误码与 fixture;不复制协议定义,不重新解释 Schema。
11
- - 只实现机制:Schema 校验、原子写、路径收容、临时工作区、资源闭包、operation-request → operation-result 管道、业务中立的事件日志与派生快照。
12
- - 明确排除:业务语义、任务编排、Git 写入、模型调用、远程网络、发布状态。见 `HARNESS_EXCLUSIONS`。
11
+ <!-- release-skill:managed:start id=latest-release -->
12
+ **0.3.0** (2026-08-12)
13
13
 
14
- ## 公共 API
14
+ This source candidate updates the Quickstart Profile harness to verify v2 Task and Result exchanges without taking ownership of consumer semantics.
15
15
 
16
- | 导出 | 职责 |
17
- | --- | --- |
18
- | `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | 能力与排除清单(冻结常量)。 |
19
- | `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | 机制失败统一携带注册错误码 `SFC2004`,`details.kind` 给出稳定细分。 |
20
- | `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | 按 Schema 方言路由并缓存 validator;复用 Contracts 的 Ajv 实例与 dialect/policy 语义。 |
21
- | `classifyPathInput` / `resolveContained` / `readFileContained` | 路径收容:拦截路径越界、符号链接逃逸、真实路径逃逸。 |
22
- | `writeFileAtomic` | 原子写:失败不留半成品(临时文件 + fsync + rename)。 |
23
- | `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | 自动清理的临时工作区,异常路径也清理。 |
24
- | `digestBytes` / `computeResourceClosure` / `closureContains` | 资源闭包与确定性 sha256 摘要。 |
25
- | `parseRequest` / `processRequest` | 解析 `operation-request`,输出终态 `operation-result`。 |
26
- | `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | 消费经 Contracts 验证的 report model,确定性渲染中性 Markdown 并校验来源/结果/报告绑定;不解释业务输出。 |
27
- | `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | 通用文本 source closure、manifest 全摘要复验和目标集合原子落盘;具体 Profile/driver 不在 Harness。 |
28
- | `probeVersionVector` | 默认禁用 spawn 的版本探测机制;显式启用时只执行绝对、无 symlink 的受审计向量,不使用 PATH/shell。 |
29
- | `openStateStore` / `appendEvent` / `readEvents` / `verifyStateStore` / `closeStateStore` | 严格单写者的 append-only 事件存储;事件目录是唯一状态权威,`chain-head.json` 只是缓存。 |
30
- | `readSnapshot` / `writeSnapshot` / `rebuildSnapshot` | 原子派生快照与完整事件重建;坏事件不能被旧快照掩盖,坏快照可被重建忽略。 |
31
- | `inspectStateStoreLock` / `recoverStateStoreLock` | 只读锁诊断与显式恢复;恢复必须对观测到的 owner + fencing 做精确匹配。 |
32
-
33
- ## 状态存储的锁与恢复边界
16
+ **Added**
34
17
 
35
- - 锁使用 exclusive create,第二写者立即收到 `store-locked`;不排队,也不按时间、PID 或租约过期偷锁。
36
- - `inspectStateStoreLock` 不创建任何文件,只返回 `owner`、单调 `fencing`、`ageMs` 和恢复中标记。`ageMs` 仅供诊断,从不参与正确性判断。
37
- - 崩溃遗留锁只能由调用方在 Foundation 之外确认旧写者已经终止后,调用 `recoverStateStoreLock`,同时提交精确匹配的 `expectedOwner`、`expectedFencing` 与 `confirmOwnerTerminated: true`。不匹配或缺少确认均失败关闭。
38
- - 恢复产生更大的 fencing。旧 handle 每次 append 都重新核对 owner、fencing 和 acquisition id;事件最终发布使用同目录临时普通文件、fsync 和 exclusive link,绝不覆盖既有 sequence。
39
- - append、snapshot、close 与 recovery 由短期 `writer-mutation.lock` 串行化;恢复不能越过已经持有 mutation guard 的权威写入。
40
- - 如果恢复进程自身在持有 `writer-recovery.lock` 时崩溃,系统保持可诊断的锁死状态,不自动删除该 guard。它需要新的外部取证与人工处置;当前 API 不声称解决不可信调用方谎报“旧写者已终止”的场景。
41
- - state root、`events/`、`snapshots/`、事件和快照拒绝 symlink、硬链接、FIFO、设备与其它非普通条目。payload 必须是纯 JSON,且 `eventType + payloadSchemaVersion` 必须命中调用方在 open/recover 时冻结的 Schema 对。
18
+ - Recomputes real bytes for path-backed outputs and evidence, and rejects duplicate Resource ids across observations, outputs, and evidence.
19
+ - Verifies operation identity, Task digest, every run/stage/attempt field, and the exact evidence binding set.
42
20
 
43
- ## 稳定错误码
21
+ **Changed**
44
22
 
45
- 全部复用 Contracts 冻结登记表,不新增未登记码。机制失败统一为 `SFC2004`(EXECUTION_FAILED),`details.kind` `HARNESS_ERROR_KINDS` 中的稳定值,例如 `path-traversal`、`symlink-escape`、`realpath-escape`、`atomic-write-failed`、`missing-resource`、`workspace-disposed`。
23
+ - Replaces the incompatible 0.2.1 candidate surface; consumers that still require v1 must remain pinned to exactly 0.2.1.
24
+ - Leaves method selection, retry policy, and domain result interpretation to the consumer.
46
25
 
47
- 新增一个全新的 SFC 码属于 Contracts 变更(登记表在 contracts 包内),超出本包写集;因此用「`SFC2004` + 稳定 `details.kind`」组合保持对外语义稳定。
26
+ **Upgrade Notes**
48
27
 
49
- ## 路径收容模型
28
+ Version 0.3.0 is a local, unpublished source candidate. Pin the candidate subpath to an exact package version and update v1 exchange producers before adopting it.
29
+ <!-- release-skill:managed:end id=latest-release -->
50
30
 
51
- `resolveContained(root, rel)` 是所有文件系统访问的唯一入口,按序拒绝:
31
+ ## Problem It Solves
52
32
 
53
- 1. 输入分级(`classifyPathInput`,纯函数可测):拒绝绝对路径、Windows 盘符/UNC 路径、POSIX 上的反斜杠路径、空输入、NUL 字节。
54
- 2. 词法收容:`path.resolve` 后离开根 → `path-traversal`。
55
- 3. 符号链接逃逸:末位组件是指向根外的符号链接(或断链)→ `symlink-escape`。
56
- 4. 真实路径逃逸:任一中间符号链接链的规范化结果离开根 → `realpath-escape`。
33
+ Contracts defines "what should be", and the Harness turns that into "can be safely reused" mechanisms at the Node runtime. If multiple skill-family projects each implement path containment, atomic writes, resource closure, report rendering, host integration, and the state base independently, you get inconsistent security boundaries and behavioral drift. The Harness consolidates these business-neutral mechanisms into one default implementation, which callers pick up as needed.
57
34
 
58
- 比较是基于 `realpath` 之后的规范根,避免 macOS `/var → /private/var` 一类系统级符号链接造成误判。
35
+ ## Core Mental Model
59
36
 
60
- ## 测试
37
+ The Harness consumes `skill-family-contracts` (a workspace dependency), reusing its dialect-routed Ajv validator, Kernel Protocol, frozen error codes, and fixtures; it does not copy protocol definitions or re-interpret the Schema. It only implements mechanisms: Schema validation, atomic writes, path containment, temporary workspaces, resource closure, the operation-request → operation-result pipeline, and business-neutral event logging with derived snapshots. Explicitly excluded: business semantics, task orchestration, Git writes, model calls, remote networking, and publish state. See `HARNESS_EXCLUSIONS`.
61
38
 
62
- `node --test` 覆盖:Contracts fixture 全量回放、安全反例、原子性失败路径、临时工作区、闭包确定性、报告事实绑定与 Markdown 注入、宿主 manifest/路径/命令信任,以及状态存储的崩溃、并发、损坏、fencing、显式恢复、symlink、硬链接与 FIFO 反例。
63
-
64
- ## 安装
39
+ ## Installation and Minimal Example
65
40
 
66
41
  ```sh
67
- npm install skill-family-harness-node@0.2.0
42
+ npm install skill-family-harness-node@0.3.0
68
43
  npm info skill-family-harness-node --help
69
44
  ```
70
45
 
71
- ## 最小示例
46
+ The minimal example shows validating a contract document inside Node:
72
47
 
73
48
  ```js
74
- // 从空目录运行:npm install skill-family-harness-node@0.2.0
49
+ // Run from an empty directory: npm install skill-family-harness-node@0.3.0
75
50
  import { validateContractDocument } from "skill-family-harness-node";
76
51
 
77
52
  const document = {
@@ -89,6 +64,160 @@ const result = validateContractDocument(document, {
89
64
  if (!result.valid) console.error(result.errorCode);
90
65
  ```
91
66
 
92
- ## 故障诊断
67
+ The code above shows the basic `validateContractDocument` call; it reuses the Contracts validator and caches instances keyed by schema, without recompiling.
68
+
69
+ ## Candidate Quickstart Profile
70
+
71
+ Use the candidate subpath to construct an observation-backed Task, wrap its terminal Result, and verify that both documents bind the exact observation bytes and correlation fields:
72
+
73
+ ```js
74
+ import {
75
+ createQuickstartTask,
76
+ wrapQuickstartResult,
77
+ verifyQuickstartExchange,
78
+ } from "skill-family-harness-node/candidate/quickstart-profile";
79
+ ```
80
+
81
+ The v2 mechanism recomputes the bytes of every path-backed output and evidence Resource. It also rejects duplicate Resource ids, correlation drift, a changed Task digest, and incomplete or mismatched evidence bindings. It does not perform a domain audit, choose a method, retry work, or own lifecycle state.
82
+
83
+ The subpath is public but **not stable** and may change or be removed in a later minor release. Pin exactly `0.3.0` for v2; integrations that still produce candidate v1 exchanges must stay pinned to exactly `0.2.1`.
84
+
85
+ ## Typical Use Cases
86
+
87
+ - Need to safely read/write contained paths inside Node: use path containment and atomic write.
88
+ - Need to normalize resources into a recomputable closure or generate a digest: use resource closure.
89
+ - Need to generate a human report from a machine result: use report model/render/binding/check.
90
+ - Need to persist an event log with derived snapshots: use state-store (event meaning is owned by the caller).
91
+
92
+ ## Boundaries
93
+
94
+ - Consumes `skill-family-contracts`, reusing its dialect-routed Ajv validator, Kernel Protocol, frozen error codes, and fixtures; does not copy protocol definitions or re-interpret the Schema.
95
+ - Only implements mechanisms: Schema validation, atomic writes, path containment, temporary workspaces, resource closure, the operation-request → operation-result pipeline, and business-neutral event logging with derived snapshots.
96
+ - Explicitly excluded: business semantics, task orchestration, Git writes, model calls, remote networking, and publish state. See `HARNESS_EXCLUSIONS`.
97
+
98
+ ## Public API
99
+
100
+ | Export | Responsibility |
101
+ | --- | --- |
102
+ | `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | Capability and exclusion lists (frozen constants). |
103
+ | `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | Mechanism failures uniformly carry the registered error code `SFC2004`; `details.kind` gives a stable subcategory. |
104
+ | `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | Routes and caches validators by Schema dialect; reuses Contracts' Ajv instances and dialect/policy semantics. |
105
+ | `classifyPathInput` / `resolveContained` / `readFileContained` | Path containment: intercepts path overruns, symlink escapes, and realpath escapes. |
106
+ | `writeFileAtomic` | Atomic write: leaves no half-written artifact on failure (temp file + fsync + rename). |
107
+ | `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | Auto-cleanup temporary workspace, cleaned up even on exception paths. |
108
+ | `digestBytes` / `computeResourceClosure` / `closureContains` | Resource closure and deterministic sha256 digest. |
109
+ | `parseRequest` / `processRequest` | Parse `operation-request`, output terminal `operation-result`. |
110
+ | `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. |
111
+ | `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. |
112
+ | `probeVersionVector` | A version-probe mechanism that disables spawn by default; when explicitly enabled, executes only absolute, symlink-free, audited vectors, using no PATH/shell. |
113
+ | `openStateStore` / `appendEvent` / `readEvents` / `verifyStateStore` / `closeStateStore` | Strict single-writer append-only event store; the event directory is the sole state authority, `chain-head.json` is only a cache. |
114
+ | `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. |
115
+ | `inspectStateStoreLock` / `recoverStateStoreLock` | Read-only lock diagnostics and explicit recovery; recovery must precisely match the observed owner + fencing. |
116
+
117
+ ## State Store Lock and Recovery Boundaries
118
+
119
+ - 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.
120
+ - `inspectStateStoreLock` creates no file, only returns `owner`, monotonic `fencing`, `ageMs`, and an in-recovery flag. `ageMs` is for diagnostics only and never participates in correctness decisions.
121
+ - A crash-left lock can only be recovered by the caller, after confirming outside Foundation that the old writer has terminated, by calling `recoverStateStoreLock` while submitting the precisely matching `expectedOwner`, `expectedFencing`, and `confirmOwnerTerminated: true`. A mismatch or missing confirmation fails closed.
122
+ - Recovery produces a larger fencing. The old handle re-checks owner, fencing, and acquisition id on every append; final event publication uses a same-directory temporary regular file, fsync, and exclusive link, never overwriting an existing sequence.
123
+ - Append, snapshot, close, and recovery are serialized by a short-lived `writer-mutation.lock`; recovery cannot cross an authoritative write that already holds the mutation guard.
124
+ - If the recovering process itself crashes while holding `writer-recovery.lock`, the system stays in a diagnosable deadlock state and does not auto-delete that guard. It requires fresh external forensics and manual handling; the current API does not claim to solve the scenario where an untrusted caller falsely reports "old writer terminated".
125
+ - The state root, `events/`, `snapshots/`, events, and snapshots reject symlinks, hard links, FIFOs, devices, and other non-regular entries. Payload must be pure JSON, and `eventType + payloadSchemaVersion` must hit the Schema pair frozen by the caller at open/recover.
126
+
127
+ ## Stable Error Codes
128
+
129
+ All reuse the Contracts frozen registry; no unregistered codes are added. Mechanism failures are uniformly `SFC2004` (EXECUTION_FAILED), and `details.kind` takes a stable value from `HARNESS_ERROR_KINDS`, such as `path-traversal`, `symlink-escape`, `realpath-escape`, `atomic-write-failed`, `missing-resource`, `workspace-disposed`.
130
+
131
+ Adding an entirely new SFC code is a Contracts change (the registry is inside the contracts package) and is outside this package's write set; therefore the combination "`SFC2004` + stable `details.kind`" keeps external semantics stable.
132
+
133
+ ## Path Containment Model
134
+
135
+ `resolveContained(root, rel)` is the single entry point for all filesystem access, rejecting in order:
136
+
137
+ 1. Input classification (`classifyPathInput`, a pure testable function): rejects absolute paths, Windows drive/UNC paths, backslash paths on POSIX, empty input, and NUL bytes.
138
+ 2. Lexical containment: after `path.resolve`, leaving the root → `path-traversal`.
139
+ 3. Symlink escape: the final component is a symlink (or dangling link) pointing outside the root → `symlink-escape`.
140
+ 4. Realpath escape: any intermediate symlink chain's normalized result leaves the root → `realpath-escape`.
141
+
142
+ Comparison is based on the canonical root after `realpath`, avoiding misjudgment from system-level symlinks such as macOS `/var → /private/var`.
143
+
144
+ ## Testing
145
+
146
+ `node --test` covers: full Contracts fixture replay, security negative cases, atomic-failure paths, temporary workspaces, closure determinism, report fact binding and Markdown injection, host manifest/path/command trust, and state-store crashes, concurrency, corruption, fencing, explicit recovery, symlinks, hard links, and FIFO negative cases.
147
+
148
+ ## Troubleshooting
149
+
150
+ Mechanism failures uniformly throw `SFC2004` (EXECUTION_FAILED), with `details.kind` giving a stable subcategory (e.g., `path-traversal`, `atomic-write-failed`). If it fails, check that the root path is correct and the target file is not locked.
151
+
152
+ ## Further Documentation
153
+
154
+ - Architecture boundaries and routing: [Architecture](https://ifoohoo.github.io/skill-family-engineering-kit/architecture/), [Agent architecture routing](https://ifoohoo.github.io/skill-family-engineering-kit/agents/architecture-routing/)
155
+ - Capability catalog: [capability-catalog.json](https://ifoohoo.github.io/skill-family-engineering-kit/agents/capability-catalog.json)
156
+ - Side-effect matrix: [Failure and side-effect matrix](https://ifoohoo.github.io/skill-family-engineering-kit/reference/failure-and-side-effect-matrix/)
157
+
158
+ <!-- agent-quick-reference:start -->
159
+ ## Agent Quick Reference
160
+
161
+ ### Use when
162
+
163
+ - You need to validate contracts inside Node, safely read/write contained paths, compute resource closures, or render deterministic reports.
164
+ - You need to persist an event log with derived snapshots, or normalize host adapter sources.
165
+ - You need to trial the non-stable Quickstart exchange and verify its observation/task/result binding.
166
+
167
+ ### Do not use when
168
+
169
+ - You need to put file-selection business rules into the Foundation (business rules are owned by the caller).
170
+ - You need host apply/install/update/uninstall, a full Qoder driver, or binary adapter source (explicitly unsupported).
171
+ - You need domain audit semantics, retry orchestration, or a compatibility-frozen Quickstart API.
172
+
173
+ ### Capability selection
174
+
175
+ - `foundation.harness.contract-validation`: contract validation and validator caching inside Node.
176
+ - `foundation.harness.path-containment`: path classification and contained resolution, rejecting the three escape types.
177
+ - `foundation.harness.atomic-write`: atomic write within contained paths, rolling back on failure.
178
+ - `foundation.harness.temporary-workspace`: auto-cleanup temporary workspace.
179
+ - `foundation.harness.resource-closure`: deterministic resource closure and sha256 digest.
180
+ - `foundation.harness.request-processing`: operation-request → terminal operation-result.
181
+ - `foundation.harness.report`: report-model validation/render/binding/check.
182
+ - `foundation.harness.host-adapter`: adapter source closure/build/materialize and version probe.
183
+ - `foundation.harness.state-store`: append-only events, hash chain, snapshots, and lock recovery.
184
+ - `foundation.harness.errors`: mechanism error types and stable error classes.
185
+ - `foundation.harness.quickstart-profile-candidate`: exact-version observation/task/result construction and binding verification.
186
+
187
+ ### Required inputs
188
+
189
+ - The contained root directory (the boundary for path containment).
190
+ - The document, resource, or event payload to validate/write.
191
+
192
+ ### Outputs and evidence
193
+
194
+ - Validation result, contained absolute path, atomically written file, closure digest, terminal result, report text, events/snapshots.
195
+ - Evidence: `packages/skill-family-harness-node/test/validation.test.mjs`, `atomic.test.mjs`, `containment.test.mjs`, `closure.test.mjs`, `report.test.mjs`, `state-store.test.mjs`.
196
+
197
+ ### Side effects
198
+
199
+ - Read-only filesystem access (path/atomic/workspace/state-store read and write within contained paths).
200
+ - `HARNESS_EXCLUSIONS` explicitly excludes release-state, remote-network-access, business-semantics, workflow-orchestration, model-calls, git-writes.
201
+
202
+ ### Failure semantics
203
+
204
+ - Mechanism failures are uniformly `SFC2004`, with `details.kind` as a stable subcategory (e.g., `path-traversal`, `atomic-write-failed`).
205
+ - Residual state after failure: atomic write rolls back the temp file; a broken state-store hash chain throws, and old snapshots can be ignored by rebuild.
206
+
207
+ ### Architectural invariants
208
+
209
+ - Event meaning and reducer transitions remain consumer-owned; state-store only provides the base.
210
+ - Only text adapter source (utf8) is supported; binary projection is not supported.
211
+
212
+ ### Route elsewhere when
213
+
214
+ - Business state machine / terminal states: route to loop-agent.
215
+ - Host apply: explicitly unsupported.
216
+ - Domain audit semantics: route to a standalone audit consumer.
217
+
218
+ ### Machine-readable sources
93
219
 
94
- 机制失败统一抛出 `SFC2004`(EXECUTION_FAILED),`details.kind` 给出稳定细分(如 `path-traversal`、`atomic-write-failed`)。如失败,检查 root 路径是否正确且目标文件未被锁定。
220
+ - Public capability catalog: [`capability-catalog.json`](https://ifoohoo.github.io/skill-family-engineering-kit/agents/capability-catalog.json) (`foundation.harness.*` entries).
221
+ - Package-local source: `src/*.mjs`.
222
+ - Package-local candidate source: `candidate/quickstart-profile.mjs`; public import: `skill-family-harness-node/candidate/quickstart-profile`.
223
+ <!-- agent-quick-reference:end -->
@@ -0,0 +1,224 @@
1
+ <!-- release-skill:safe-first-command -->
2
+ <!-- release-skill:external-write-boundary -->
3
+
4
+ > English version: [README.md](./README.md)
5
+
6
+ # skill-family-harness-node
7
+
8
+ <!-- release-skill:release-version: 0.3.0 -->
9
+
10
+ Contracts 机制协议的**唯一默认 Node 实现**。这是一个薄运行时(thin runtime):只实现机制协议,不引入业务语义,不做第二语言实现。
11
+
12
+ <!-- release-skill:managed:start id=latest-release -->
13
+ **0.3.0** (2026-08-12)
14
+
15
+ 本源码候选版把 Quickstart Profile Harness 更新为 v2 Task/Result 交换校验,同时不接管消费者语义。
16
+
17
+ **新增**
18
+
19
+ - 对 path-backed output 与 evidence 重算真实字节摘要,并拒绝 observation、output、evidence 之间重复的 Resource id。
20
+ - 逐项复验 operation 身份、Task digest、run/stage/attempt 字段和 evidence 精确绑定集合。
21
+
22
+ **变更**
23
+
24
+ - 替换与 0.2.1 不兼容的 candidate 面;仍依赖 v1 的消费者必须继续精确锁定 0.2.1。
25
+ - 方法选择、重试策略与领域结果解释继续归消费者所有。
26
+
27
+ **升级说明**
28
+
29
+ 0.3.0 当前只是本地、未发布的源码候选。candidate 子路径必须精确锁定包版本,采用前需更新 v1 交换生产方。
30
+ <!-- release-skill:managed:end id=latest-release -->
31
+
32
+ ## 解决的问题
33
+
34
+ Contracts 定义了「应当如何」,Harness 把它在 Node 运行时变成「可以安全复用」的机制。多个技能族项目如果各自实现路径收容、原子写、资源闭包、报告渲染、宿主接入与状态底座,会出现安全边界不一致、行为漂移。Harness 把这些业务中立机制收口成一份默认实现,调用方按需取用。
35
+
36
+ ## 核心心智模型
37
+
38
+ Harness 消费 `skill-family-contracts`(工作区依赖),复用其方言路由的 Ajv validator、Kernel Protocol、冻结错误码与 fixture;不复制协议定义,不重新解释 Schema。它只实现机制:Schema 校验、原子写、路径收容、临时工作区、资源闭包、operation-request → operation-result 管道、业务中立的事件日志与派生快照。明确排除:业务语义、任务编排、Git 写入、模型调用、远程网络、发布状态。见 `HARNESS_EXCLUSIONS`。
39
+
40
+ ## 安装和最小示例
41
+
42
+ ```sh
43
+ npm install skill-family-harness-node@0.3.0
44
+ npm info skill-family-harness-node --help
45
+ ```
46
+
47
+ 最小示例演示在 Node 内校验一份契约文档:
48
+
49
+ ```js
50
+ // 从空目录运行:npm install skill-family-harness-node@0.3.0
51
+ import { validateContractDocument } from "skill-family-harness-node";
52
+
53
+ const document = {
54
+ schemaVersion: 1,
55
+ kind: "skill-family.project-manifest",
56
+ project: { id: "my-project", name: "My Project", description: "Example" },
57
+ contracts: { version: "1.0.0", profile: "generic" },
58
+ managedFiles: ["package.json"],
59
+ updatedAt: "2026-01-01T00:00:00Z",
60
+ };
61
+
62
+ const result = validateContractDocument(document, {
63
+ schemaId: "https://contracts.skill-family.example/v1/project-manifest.json",
64
+ });
65
+ if (!result.valid) console.error(result.errorCode);
66
+ ```
67
+
68
+ 以上代码展示了 `validateContractDocument` 的基本调用;它复用 Contracts 的校验器并以 schema 为键缓存实例,不重新编译。
69
+
70
+ ## Candidate Quickstart Profile
71
+
72
+ 需要构造带 observation 绑定的 Task、封装终态 Result,并复验两个文档是否绑定精确 observation 字节与 correlation 字段时,使用 candidate 子路径:
73
+
74
+ ```js
75
+ import {
76
+ createQuickstartTask,
77
+ wrapQuickstartResult,
78
+ verifyQuickstartExchange,
79
+ } from "skill-family-harness-node/candidate/quickstart-profile";
80
+ ```
81
+
82
+ v2 机制会重算每个 path-backed output 和 evidence Resource 的真实字节摘要,并拒绝重复 Resource id、correlation 漂移、Task digest 变化,以及缺失或错配的 evidence binding。它不执行领域审计,不选择 method,不编排重试,也不拥有生命周期状态。
83
+
84
+ 该子路径公开但**不稳定**,后续小版本可以修改或移除。使用 v2 时应精确锁定 `0.3.0`;仍生产 candidate v1 交换的接入必须继续精确锁定 `0.2.1`。
85
+
86
+ ## 典型使用场景
87
+
88
+ - 需要在 Node 内安全地读/写受收容路径:用 path containment 与 atomic write。
89
+ - 需要把资源归一成可复算闭包或生成摘要:用 resource closure。
90
+ - 需要从机器结果生成人类报告:用 report model/render/binding/check。
91
+ - 需要持久化事件日志与派生快照:用 state-store(事件含义由调用方拥有)。
92
+
93
+ ## 边界
94
+
95
+ - 消费 `skill-family-contracts`,复用其方言路由的 Ajv validator、Kernel Protocol、冻结错误码与 fixture;不复制协议定义,不重新解释 Schema。
96
+ - 只实现机制:Schema 校验、原子写、路径收容、临时工作区、资源闭包、operation-request → operation-result 管道、业务中立的事件日志与派生快照。
97
+ - 明确排除:业务语义、任务编排、Git 写入、模型调用、远程网络、发布状态。见 `HARNESS_EXCLUSIONS`。
98
+
99
+ ## 公共 API
100
+
101
+ | 导出 | 职责 |
102
+ | --- | --- |
103
+ | `HARNESS_CAPABILITIES` / `HARNESS_EXCLUSIONS` | 能力与排除清单(冻结常量)。 |
104
+ | `HarnessError` / `HARNESS_ERROR_KINDS` / `mechanismError` | 机制失败统一携带注册错误码 `SFC2004`,`details.kind` 给出稳定细分。 |
105
+ | `validateContractDocument` / `getValidator` / `resolveSchemaContext` / `validatorCacheSize` | 按 Schema 方言路由并缓存 validator;复用 Contracts 的 Ajv 实例与 dialect/policy 语义。 |
106
+ | `classifyPathInput` / `resolveContained` / `readFileContained` | 路径收容:拦截路径越界、符号链接逃逸、真实路径逃逸。 |
107
+ | `writeFileAtomic` | 原子写:失败不留半成品(临时文件 + fsync + rename)。 |
108
+ | `TemporaryWorkspace` / `createTemporaryWorkspace` / `withTemporaryWorkspace` | 自动清理的临时工作区,异常路径也清理。 |
109
+ | `digestBytes` / `computeResourceClosure` / `closureContains` | 资源闭包与确定性 sha256 摘要。 |
110
+ | `parseRequest` / `processRequest` | 解析 `operation-request`,输出终态 `operation-result`。 |
111
+ | `validateReportModel` / `renderReportMarkdown` / `buildBinding` / `checkReport` | 消费经 Contracts 验证的 report model,确定性渲染中性 Markdown 并校验来源/结果/报告绑定;不解释业务输出。 |
112
+ | `normalizeAdapterSource` / `buildAdapterClosure` / `verifyAdapterBuildManifest` / `materializeAdapterBuild` | 通用文本 source closure、manifest 全摘要复验和目标集合原子落盘;具体 Profile/driver 不在 Harness。 |
113
+ | `probeVersionVector` | 默认禁用 spawn 的版本探测机制;显式启用时只执行绝对、无 symlink 的受审计向量,不使用 PATH/shell。 |
114
+ | `openStateStore` / `appendEvent` / `readEvents` / `verifyStateStore` / `closeStateStore` | 严格单写者的 append-only 事件存储;事件目录是唯一状态权威,`chain-head.json` 只是缓存。 |
115
+ | `readSnapshot` / `writeSnapshot` / `rebuildSnapshot` | 原子派生快照与完整事件重建;坏事件不能被旧快照掩盖,坏快照可被重建忽略。 |
116
+ | `inspectStateStoreLock` / `recoverStateStoreLock` | 只读锁诊断与显式恢复;恢复必须对观测到的 owner + fencing 做精确匹配。 |
117
+
118
+ ## 状态存储的锁与恢复边界
119
+
120
+ - 锁使用 exclusive create,第二写者立即收到 `store-locked`;不排队,也不按时间、PID 或租约过期偷锁。
121
+ - `inspectStateStoreLock` 不创建任何文件,只返回 `owner`、单调 `fencing`、`ageMs` 和恢复中标记。`ageMs` 仅供诊断,从不参与正确性判断。
122
+ - 崩溃遗留锁只能由调用方在 Foundation 之外确认旧写者已经终止后,调用 `recoverStateStoreLock`,同时提交精确匹配的 `expectedOwner`、`expectedFencing` 与 `confirmOwnerTerminated: true`。不匹配或缺少确认均失败关闭。
123
+ - 恢复产生更大的 fencing。旧 handle 每次 append 都重新核对 owner、fencing 和 acquisition id;事件最终发布使用同目录临时普通文件、fsync 和 exclusive link,绝不覆盖既有 sequence。
124
+ - append、snapshot、close 与 recovery 由短期 `writer-mutation.lock` 串行化;恢复不能越过已经持有 mutation guard 的权威写入。
125
+ - 如果恢复进程自身在持有 `writer-recovery.lock` 时崩溃,系统保持可诊断的锁死状态,不自动删除该 guard。它需要新的外部取证与人工处置;当前 API 不声称解决不可信调用方谎报“旧写者已终止”的场景。
126
+ - state root、`events/`、`snapshots/`、事件和快照拒绝 symlink、硬链接、FIFO、设备与其它非普通条目。payload 必须是纯 JSON,且 `eventType + payloadSchemaVersion` 必须命中调用方在 open/recover 时冻结的 Schema 对。
127
+
128
+ ## 稳定错误码
129
+
130
+ 全部复用 Contracts 冻结登记表,不新增未登记码。机制失败统一为 `SFC2004`(EXECUTION_FAILED),`details.kind` 取 `HARNESS_ERROR_KINDS` 中的稳定值,例如 `path-traversal`、`symlink-escape`、`realpath-escape`、`atomic-write-failed`、`missing-resource`、`workspace-disposed`。
131
+
132
+ 新增一个全新的 SFC 码属于 Contracts 变更(登记表在 contracts 包内),超出本包写集;因此用「`SFC2004` + 稳定 `details.kind`」组合保持对外语义稳定。
133
+
134
+ ## 路径收容模型
135
+
136
+ `resolveContained(root, rel)` 是所有文件系统访问的唯一入口,按序拒绝:
137
+
138
+ 1. 输入分级(`classifyPathInput`,纯函数可测):拒绝绝对路径、Windows 盘符/UNC 路径、POSIX 上的反斜杠路径、空输入、NUL 字节。
139
+ 2. 词法收容:`path.resolve` 后离开根 → `path-traversal`。
140
+ 3. 符号链接逃逸:末位组件是指向根外的符号链接(或断链)→ `symlink-escape`。
141
+ 4. 真实路径逃逸:任一中间符号链接链的规范化结果离开根 → `realpath-escape`。
142
+
143
+ 比较是基于 `realpath` 之后的规范根,避免 macOS `/var → /private/var` 一类系统级符号链接造成误判。
144
+
145
+ ## 测试
146
+
147
+ `node --test` 覆盖:Contracts fixture 全量回放、安全反例、原子性失败路径、临时工作区、闭包确定性、报告事实绑定与 Markdown 注入、宿主 manifest/路径/命令信任,以及状态存储的崩溃、并发、损坏、fencing、显式恢复、symlink、硬链接与 FIFO 反例。
148
+
149
+ ## 故障诊断
150
+
151
+ 机制失败统一抛出 `SFC2004`(EXECUTION_FAILED),`details.kind` 给出稳定细分(如 `path-traversal`、`atomic-write-failed`)。如失败,检查 root 路径是否正确且目标文件未被锁定。
152
+
153
+ ## 深入文档入口
154
+
155
+ - 架构边界与路由:[架构说明](https://ifoohoo.github.io/skill-family-engineering-kit/architecture/)、[智能体架构路由](https://ifoohoo.github.io/skill-family-engineering-kit/agents/architecture-routing/)
156
+ - 能力目录:[capability-catalog.json](https://ifoohoo.github.io/skill-family-engineering-kit/agents/capability-catalog.json)
157
+ - 副作用矩阵:[失败与副作用矩阵](https://ifoohoo.github.io/skill-family-engineering-kit/reference/failure-and-side-effect-matrix/)
158
+
159
+ <!-- agent-quick-reference:start -->
160
+ ## Agent Quick Reference
161
+
162
+ ### Use when
163
+
164
+ - 需要在 Node 内校验契约、安全读写受收容路径、计算资源闭包、渲染确定性报告。
165
+ - 需要持久化事件日志与派生快照,或归一化宿主适配源。
166
+ - 需要试用非稳定 Quickstart 交换,并复验 observation/task/result 绑定。
167
+
168
+ ### Do not use when
169
+
170
+ - 需要把文件选择的业务规则放入 Foundation(业务规则由调用方拥有)。
171
+ - 需要 host apply/install/update/uninstall、Qoder 完整 driver 或二进制 adapter source(明确 unsupported)。
172
+ - 需要领域审计语义、重试编排或兼容性已冻结的 Quickstart API。
173
+
174
+ ### Capability selection
175
+
176
+ - `foundation.harness.contract-validation`:Node 内契约校验与校验器缓存。
177
+ - `foundation.harness.path-containment`:路径分类与受收容解析,拒绝三类逃逸。
178
+ - `foundation.harness.atomic-write`:受收容路径内原子写,失败回滚。
179
+ - `foundation.harness.temporary-workspace`:自动清理的临时工作区。
180
+ - `foundation.harness.resource-closure`:确定性资源闭包与 sha256 摘要。
181
+ - `foundation.harness.request-processing`:operation-request → 终态 operation-result。
182
+ - `foundation.harness.report`:report-model 校验/渲染/绑定/检查。
183
+ - `foundation.harness.host-adapter`:adapter source closure/build/materialize 与版本探测。
184
+ - `foundation.harness.state-store`:append-only 事件、hash chain、快照与锁恢复。
185
+ - `foundation.harness.errors`:机制错误类型与稳定错误类。
186
+ - `foundation.harness.quickstart-profile-candidate`:锁定精确版本后构造 observation/task/result 并复验绑定。
187
+
188
+ ### Required inputs
189
+
190
+ - 受收容根目录(路径收容的边界)。
191
+ - 待校验/待写入的文档、资源或事件负载。
192
+
193
+ ### Outputs and evidence
194
+
195
+ - 校验结果、受收容绝对路径、原子写文件、闭包摘要、终态结果、报告文本、事件/快照。
196
+ - 证据:`packages/skill-family-harness-node/test/validation.test.mjs`、`atomic.test.mjs`、`containment.test.mjs`、`closure.test.mjs`、`report.test.mjs`、`state-store.test.mjs`。
197
+
198
+ ### Side effects
199
+
200
+ - 只读文件系统访问(path/atomic/workspace/state-store 在受收容路径内读写)。
201
+ - `HARNESS_EXCLUSIONS` 明确排除 release-state、remote-network-access、business-semantics、workflow-orchestration、model-calls、git-writes。
202
+
203
+ ### Failure semantics
204
+
205
+ - 机制失败统一 `SFC2004`,`details.kind` 为稳定细分(如 `path-traversal`、`atomic-write-failed`)。
206
+ - 失败后残余状态:原子写回滚临时文件;状态存储链断裂抛错,旧快照可被重建忽略。
207
+
208
+ ### Architectural invariants
209
+
210
+ - Event meaning and reducer transitions remain consumer-owned;state-store 只提供底座。
211
+ - 仅支持文本 adapter source(utf8),不支持二进制投影。
212
+
213
+ ### Route elsewhere when
214
+
215
+ - 业务状态机/终态:转 loop-agent。
216
+ - 宿主 apply:明确 unsupported。
217
+ - 领域审计语义:转独立审计消费者。
218
+
219
+ ### Machine-readable sources
220
+
221
+ - 公开能力目录:[`capability-catalog.json`](https://ifoohoo.github.io/skill-family-engineering-kit/agents/capability-catalog.json)(`foundation.harness.*` 条目)。
222
+ - 包内源:`src/*.mjs`。
223
+ - 包内 Candidate 源:`candidate/quickstart-profile.mjs`;公共导入:`skill-family-harness-node/candidate/quickstart-profile`。
224
+ <!-- agent-quick-reference:end -->
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { stdin, stdout, stderr } from "node:process";
3
+ import {
4
+ canonicalJson,
5
+ computeResourceClosure,
6
+ digestDocument,
7
+ } from "./quickstart-profile.mjs";
8
+
9
+ async function readRequest() {
10
+ const chunks = [];
11
+ for await (const chunk of stdin) chunks.push(chunk);
12
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
13
+ }
14
+
15
+ export async function runMechanismCli() {
16
+ try {
17
+ const request = await readRequest();
18
+ let result;
19
+ if (request.operation === "canonical-json") {
20
+ result = { text: canonicalJson(request.document) };
21
+ } else if (request.operation === "digest-document") {
22
+ result = { digest: digestDocument(request.document) };
23
+ } else if (request.operation === "resource-closure") {
24
+ result = await computeResourceClosure({
25
+ root: request.root,
26
+ resources: request.resources,
27
+ });
28
+ } else {
29
+ throw new TypeError(`unknown Foundation mechanism: ${String(request.operation)}`);
30
+ }
31
+ stdout.write(`${JSON.stringify(result)}\n`);
32
+ return 0;
33
+ } catch (cause) {
34
+ stderr.write(`${cause?.message ?? String(cause)}\n`);
35
+ return 2;
36
+ }
37
+ }
38
+
39
+ if (import.meta.url === `file://${process.argv[1]}`) {
40
+ process.exitCode = await runMechanismCli();
41
+ }
@@ -0,0 +1,485 @@
1
+ import { canonicalJson, digestDocument, isRegisteredErrorCode } from "skill-family-contracts";
2
+ import {
3
+ QUICKSTART_PROTOCOL,
4
+ findNonJsonValue,
5
+ validateQuickstartProfileDocument,
6
+ } from "skill-family-contracts/candidate/quickstart-profile";
7
+ import { computeResourceClosure, digestBytes } from "../src/closure.mjs";
8
+ import { HARNESS_ERROR_KINDS, HarnessError, mechanismError } from "../src/errors.mjs";
9
+
10
+ /**
11
+ * Candidate quickstart profile v2 mechanisms (unstable).
12
+ *
13
+ * The harness only interprets mechanism constraints: contained reads, real
14
+ * byte digests, profile validation, and per-field Task/Result binding. The
15
+ * single operation is the business-neutral execute-method; params.method,
16
+ * params.parameters, and domainResult are caller-owned and never read here.
17
+ */
18
+
19
+ const QUICKSTART_OPERATION = "execute-method";
20
+ const RESULT_STATES = Object.freeze(["succeeded", "failed", "rejected"]);
21
+ const CORRELATION_FIELDS = Object.freeze(["run", "stage", "attempt"]);
22
+ const ERROR_ENTRY_FIELDS = new Set(["code", "message", "path", "details"]);
23
+
24
+ function invalidProfile(kind, outcome) {
25
+ return mechanismError(
26
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
27
+ `quickstart ${kind} violates the Foundation candidate profile`,
28
+ { category: "profile", profileKind: kind, findings: outcome.errors },
29
+ );
30
+ }
31
+
32
+ function assertProfile(kind, document) {
33
+ const outcome = validateQuickstartProfileDocument(kind, document);
34
+ if (!outcome.valid) throw invalidProfile(kind, outcome);
35
+ return outcome.data;
36
+ }
37
+
38
+ /**
39
+ * Caller-owned values must be pure JSON before they enter any Task or
40
+ * Result: this refuses BigInt and friends before structuredClone,
41
+ * digestDocument, or JSON.stringify could throw or silently drift.
42
+ */
43
+ function assertJsonValue(caller, label, value) {
44
+ const issue = findNonJsonValue(value);
45
+ if (issue) {
46
+ throw new TypeError(
47
+ `${caller}: ${label} must be a JSON value; found ${issue.reason}` +
48
+ (issue.instancePath ? ` at ${issue.instancePath}` : ""),
49
+ );
50
+ }
51
+ }
52
+
53
+ function containedPath(resource, roleDescription) {
54
+ const relPath = resource?.location?.path;
55
+ if (typeof relPath !== "string") {
56
+ throw mechanismError(
57
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
58
+ `the quickstart ${roleDescription} Resource must use a contained relative path`,
59
+ { category: "resource-location", resourceId: resource?.id },
60
+ );
61
+ }
62
+ return relPath;
63
+ }
64
+
65
+ /** Create one observation Resource from the actual contained file bytes. */
66
+ export async function createObservationResource({ root, path, id = "observation" } = {}) {
67
+ const closure = await computeResourceClosure({
68
+ root,
69
+ resources: [{ path, role: "input" }],
70
+ });
71
+ const record = closure.resources[0];
72
+ const resource = {
73
+ schemaVersion: 1,
74
+ kind: "skill-family.resource",
75
+ id,
76
+ location: { path: record.path },
77
+ role: "observation",
78
+ digest: { algorithm: "sha256", value: record.sha256 },
79
+ };
80
+ return assertProfile("resource", resource);
81
+ }
82
+
83
+ /**
84
+ * Recomputes the real byte digest of one path-backed Resource and compares it
85
+ * with the declared digest. URI-backed Resources are structurally checked
86
+ * only: Foundation never fetches URIs.
87
+ */
88
+ export async function verifyResourceBytes({ root, resource } = {}) {
89
+ const normalized = assertProfile("resource", resource);
90
+ const relPath = normalized.location.path;
91
+ if (typeof relPath !== "string") {
92
+ return { resource: normalized, byteDigest: null };
93
+ }
94
+ const closure = await computeResourceClosure({
95
+ root,
96
+ resources: [{ path: relPath, role: "input" }],
97
+ });
98
+ const actual = closure.resources[0].sha256;
99
+ if (actual !== normalized.digest.value) {
100
+ throw mechanismError(
101
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
102
+ `quickstart ${normalized.role} Resource digest does not match its current bytes`,
103
+ {
104
+ category: "resource-bytes",
105
+ role: normalized.role,
106
+ resourceId: normalized.id,
107
+ expected: normalized.digest.value,
108
+ actual,
109
+ },
110
+ );
111
+ }
112
+ return { resource: normalized, byteDigest: actual };
113
+ }
114
+
115
+ /** Verify the single observation Resource: role, path-backed location, bytes. */
116
+ export async function verifyObservationResource({ root, resource } = {}) {
117
+ const normalized = assertProfile("resource", resource);
118
+ if (normalized.role !== "observation") {
119
+ throw mechanismError(
120
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
121
+ "quickstart task input must have the observation role",
122
+ { category: "resource-role", resourceId: normalized.id, role: normalized.role },
123
+ );
124
+ }
125
+ containedPath(normalized, "observation");
126
+ const { byteDigest } = await verifyResourceBytes({ root, resource: normalized });
127
+ return { resource: normalized, byteDigest };
128
+ }
129
+
130
+ /** Build a candidate v2 Task inside the stable operation-request envelope. */
131
+ export async function createQuickstartTask({
132
+ root,
133
+ observationPath,
134
+ observationId = "observation",
135
+ operationId,
136
+ method,
137
+ parameters = {},
138
+ run,
139
+ stage,
140
+ attempt,
141
+ } = {}) {
142
+ assertJsonValue("createQuickstartTask", "parameters", parameters);
143
+ const observation = await createObservationResource({
144
+ root,
145
+ path: observationPath,
146
+ id: observationId,
147
+ });
148
+ const task = {
149
+ schemaVersion: 1,
150
+ kind: "skill-family.operation-request",
151
+ protocol: { ...QUICKSTART_PROTOCOL },
152
+ operationId,
153
+ operation: QUICKSTART_OPERATION,
154
+ params: {
155
+ method,
156
+ parameters: structuredClone(parameters),
157
+ inputs: [observation],
158
+ correlation: { run, stage, attempt },
159
+ },
160
+ };
161
+ return assertProfile("task", task);
162
+ }
163
+
164
+ function normalizeErrorEntries(errors) {
165
+ if (!Array.isArray(errors) || errors.length === 0) {
166
+ throw new TypeError(
167
+ "wrapQuickstartResult: failed and rejected results require at least one error entry",
168
+ );
169
+ }
170
+ assertJsonValue("wrapQuickstartResult", "errors", errors);
171
+ return errors.map((entry, index) => {
172
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
173
+ throw new TypeError(`wrapQuickstartResult: error entry ${index} must be an object`);
174
+ }
175
+ const extraField = Object.keys(entry).find((field) => !ERROR_ENTRY_FIELDS.has(field));
176
+ if (extraField !== undefined) {
177
+ throw new TypeError(
178
+ `wrapQuickstartResult: error entry ${index} has unsupported field ${extraField}`,
179
+ );
180
+ }
181
+ const { code, message, path, details } = entry;
182
+ if (typeof code !== "string" || !isRegisteredErrorCode(code)) {
183
+ throw new TypeError(
184
+ `wrapQuickstartResult: error entry ${index} code must be registered in the frozen contracts error registry`,
185
+ );
186
+ }
187
+ if (typeof message !== "string" || message.length === 0) {
188
+ throw new TypeError(
189
+ `wrapQuickstartResult: error entry ${index} must carry a non-empty message`,
190
+ );
191
+ }
192
+ if (path !== undefined) {
193
+ assertJsonValue("wrapQuickstartResult", `error entry ${index} path`, path);
194
+ }
195
+ if (details !== undefined) {
196
+ assertJsonValue("wrapQuickstartResult", `error entry ${index} details`, details);
197
+ }
198
+ const normalized = { code, message };
199
+ if (path !== undefined) normalized.path = path;
200
+ if (details !== undefined) normalized.details = structuredClone(details);
201
+ return normalized;
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Wrap one terminal Result for the candidate protocol. succeeded builds the
207
+ * full outputs envelope (summary, outputs, evidence, domainResult,
208
+ * taskBinding with exactly one evidenceBindings entry per evidence
209
+ * Resource); failed and rejected always carry null outputs and at least one
210
+ * registry-registered error.
211
+ */
212
+ export function wrapQuickstartResult({
213
+ task,
214
+ state = "succeeded",
215
+ summary,
216
+ outputs = [],
217
+ evidence = [],
218
+ domainResult,
219
+ errors,
220
+ } = {}) {
221
+ const normalizedTask = assertProfile("task", task);
222
+ if (!RESULT_STATES.includes(state)) {
223
+ throw new TypeError(
224
+ `wrapQuickstartResult: state must be one of ${RESULT_STATES.join(", ")}`,
225
+ );
226
+ }
227
+ const observation = normalizedTask.params.inputs[0];
228
+ const base = {
229
+ schemaVersion: 1,
230
+ kind: "skill-family.operation-result",
231
+ protocol: structuredClone(normalizedTask.protocol),
232
+ operationId: normalizedTask.operationId,
233
+ operation: normalizedTask.operation,
234
+ state,
235
+ };
236
+ if (state !== "succeeded") {
237
+ for (const [name, value] of [
238
+ ["summary", summary],
239
+ ["outputs", outputs],
240
+ ["evidence", evidence],
241
+ ["domainResult", domainResult],
242
+ ]) {
243
+ const untouched =
244
+ (name === "outputs" || name === "evidence") && Array.isArray(value) && value.length === 0;
245
+ if (!untouched && value !== undefined) {
246
+ throw new TypeError(
247
+ `wrapQuickstartResult: ${state} results never carry ${name}; outputs must be null`,
248
+ );
249
+ }
250
+ }
251
+ return assertProfile("result", {
252
+ ...base,
253
+ outputs: null,
254
+ errors: normalizeErrorEntries(errors),
255
+ });
256
+ }
257
+ if (typeof summary !== "string" || summary.length === 0) {
258
+ throw new TypeError("wrapQuickstartResult: succeeded results require a non-empty summary");
259
+ }
260
+ assertJsonValue("wrapQuickstartResult", "outputs", outputs);
261
+ assertJsonValue("wrapQuickstartResult", "evidence", evidence);
262
+ assertJsonValue("wrapQuickstartResult", "domainResult", domainResult);
263
+ const normalizedEvidence = structuredClone(evidence);
264
+ const correlation = structuredClone(normalizedTask.params.correlation);
265
+ return assertProfile("result", {
266
+ ...base,
267
+ outputs: {
268
+ summary,
269
+ outputs: structuredClone(outputs),
270
+ evidence: normalizedEvidence,
271
+ domainResult: structuredClone(domainResult),
272
+ taskBinding: {
273
+ operationId: normalizedTask.operationId,
274
+ taskDigest: digestDocument(normalizedTask),
275
+ observationId: observation.id,
276
+ observationDigest: observation.digest.value,
277
+ correlation,
278
+ evidenceBindings: normalizedEvidence.map((resource) => ({
279
+ resourceId: resource?.id,
280
+ operationId: normalizedTask.operationId,
281
+ observationId: observation.id,
282
+ correlation: structuredClone(correlation),
283
+ })),
284
+ },
285
+ },
286
+ errors: [],
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Per-field correlation comparison: run, stage, and attempt mismatches are
292
+ * reported as distinguishable paths instead of one opaque binding mismatch.
293
+ */
294
+ function correlationMismatches(actual, expected, prefix) {
295
+ const mismatches = [];
296
+ for (const field of CORRELATION_FIELDS) {
297
+ if (actual[field] !== expected[field]) mismatches.push(`${prefix}.${field}`);
298
+ }
299
+ return mismatches;
300
+ }
301
+
302
+ /**
303
+ * Cross-checks taskBinding.evidenceBindings against the declared evidence
304
+ * Resources: exactly one entry per evidence Resource id, each echoing the
305
+ * exact operationId, observationId, and run/stage/attempt of this Task.
306
+ */
307
+ function evidenceBindingMismatches(bindings, evidenceResources, normalizedTask, observation) {
308
+ const mismatches = [];
309
+ const evidenceIds = new Set(evidenceResources.map((resource) => resource.id));
310
+ const bound = new Set();
311
+ bindings.forEach((entry, index) => {
312
+ const { resourceId } = entry;
313
+ if (!evidenceIds.has(resourceId)) {
314
+ mismatches.push(`binding.evidenceBindings.extra:${resourceId}`);
315
+ return;
316
+ }
317
+ if (bound.has(resourceId)) {
318
+ mismatches.push(`binding.evidenceBindings.duplicate:${resourceId}`);
319
+ return;
320
+ }
321
+ bound.add(resourceId);
322
+ const prefix = `binding.evidenceBindings[${index}]`;
323
+ if (entry.operationId !== normalizedTask.operationId) {
324
+ mismatches.push(`${prefix}.operationId`);
325
+ }
326
+ if (entry.observationId !== observation.id) {
327
+ mismatches.push(`${prefix}.observationId`);
328
+ }
329
+ mismatches.push(
330
+ ...correlationMismatches(entry.correlation, normalizedTask.params.correlation, `${prefix}.correlation`),
331
+ );
332
+ });
333
+ for (const resourceId of [...evidenceIds].filter((id) => !bound.has(id)).sort()) {
334
+ mismatches.push(`binding.evidenceBindings.missing:${resourceId}`);
335
+ }
336
+ return mismatches;
337
+ }
338
+
339
+ /**
340
+ * Fail-closed exchange assertion. It validates both candidate profiles,
341
+ * refuses duplicate Resource ids across the Task observation and all
342
+ * output/evidence Resources, recomputes the real bytes of every path-backed
343
+ * Resource, and proves the result echoes and binds the exact
344
+ * protocol/operation/operationId/Task-digest/observation/correlation fields
345
+ * plus one evidenceBindings entry per evidence Resource. No retry or
346
+ * lifecycle state is introduced and no Result file is written.
347
+ */
348
+ export async function assertQuickstartExchange({ root, task, result } = {}) {
349
+ const normalizedTask = assertProfile("task", task);
350
+ const normalizedResult = assertProfile("result", result);
351
+
352
+ const mismatches = [];
353
+ if (canonicalJson(normalizedResult.protocol) !== canonicalJson(normalizedTask.protocol)) {
354
+ mismatches.push("protocol");
355
+ }
356
+ if (normalizedResult.operation !== normalizedTask.operation) mismatches.push("operation");
357
+ if (normalizedResult.operationId !== normalizedTask.operationId) mismatches.push("operationId");
358
+
359
+ const observation = normalizedTask.params.inputs[0];
360
+ if (normalizedResult.state === "succeeded") {
361
+ const declared = [
362
+ observation,
363
+ ...normalizedResult.outputs.outputs,
364
+ ...normalizedResult.outputs.evidence,
365
+ ];
366
+ const seen = new Set();
367
+ for (const resource of declared) {
368
+ if (seen.has(resource.id)) {
369
+ throw mechanismError(
370
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
371
+ `quickstart result declares a duplicate Resource id: ${resource.id}`,
372
+ { category: "duplicate-resource-id", resourceId: resource.id },
373
+ );
374
+ }
375
+ seen.add(resource.id);
376
+ }
377
+
378
+ const binding = normalizedResult.outputs?.taskBinding;
379
+ if (!binding) {
380
+ mismatches.push("taskBinding");
381
+ } else {
382
+ if (binding.operationId !== normalizedTask.operationId) {
383
+ mismatches.push("binding.operationId");
384
+ }
385
+ if (binding.taskDigest !== digestDocument(normalizedTask)) {
386
+ mismatches.push("binding.taskDigest");
387
+ }
388
+ if (binding.observationId !== observation.id) mismatches.push("binding.observationId");
389
+ if (binding.observationDigest !== observation.digest.value) {
390
+ mismatches.push("binding.observationDigest");
391
+ }
392
+ mismatches.push(
393
+ ...correlationMismatches(
394
+ binding.correlation,
395
+ normalizedTask.params.correlation,
396
+ "binding.correlation",
397
+ ),
398
+ );
399
+ mismatches.push(
400
+ ...evidenceBindingMismatches(
401
+ binding.evidenceBindings,
402
+ normalizedResult.outputs.evidence,
403
+ normalizedTask,
404
+ observation,
405
+ ),
406
+ );
407
+ }
408
+ }
409
+ if (mismatches.length > 0) {
410
+ throw mechanismError(
411
+ HARNESS_ERROR_KINDS.INVALID_RESULT,
412
+ `quickstart result does not bind the exact task: ${mismatches.join(", ")}`,
413
+ { category: "binding", mismatches },
414
+ );
415
+ }
416
+
417
+ await verifyObservationResource({ root, resource: observation });
418
+
419
+ if (normalizedResult.state === "succeeded") {
420
+ const declared = [
421
+ ...normalizedResult.outputs.outputs,
422
+ ...normalizedResult.outputs.evidence,
423
+ ];
424
+ for (const resource of declared) {
425
+ await verifyResourceBytes({ root, resource });
426
+ }
427
+ }
428
+
429
+ return {
430
+ valid: true,
431
+ state: normalizedResult.state,
432
+ taskDigest: digestDocument(normalizedTask),
433
+ observationDigest: observation.digest.value,
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Non-throwing form for callers that need a structured finding. Every
439
+ * failure resolves to code SFC2004 with a deterministic string details.kind
440
+ * and details.category: HarnessError paths from the closure and containment
441
+ * machinery (missing-resource, path, symlink escapes) keep their stable kind
442
+ * and every useful detail field, ordinary TypeErrors and omitted inputs fall
443
+ * back to the invalid-result/unexpected-failure pair.
444
+ */
445
+ export async function verifyQuickstartExchange(input) {
446
+ try {
447
+ return await assertQuickstartExchange(input);
448
+ } catch (cause) {
449
+ const message =
450
+ cause instanceof Error && typeof cause.message === "string" && cause.message.length > 0
451
+ ? cause.message
452
+ : String(cause);
453
+ if (cause instanceof HarnessError) {
454
+ const rawDetails =
455
+ cause.details && typeof cause.details === "object" ? cause.details : {};
456
+ const kind =
457
+ typeof rawDetails.kind === "string" && rawDetails.kind.length > 0
458
+ ? rawDetails.kind
459
+ : HARNESS_ERROR_KINDS.EXECUTION_FAILED;
460
+ const category =
461
+ typeof rawDetails.category === "string" && rawDetails.category.length > 0
462
+ ? rawDetails.category
463
+ : "resource-closure";
464
+ return {
465
+ valid: false,
466
+ code: "SFC2004",
467
+ message,
468
+ details: { ...rawDetails, kind, category },
469
+ };
470
+ }
471
+ return {
472
+ valid: false,
473
+ code: "SFC2004",
474
+ message,
475
+ details: {
476
+ kind: HARNESS_ERROR_KINDS.INVALID_RESULT,
477
+ category: "unexpected-failure",
478
+ },
479
+ };
480
+ }
481
+ }
482
+
483
+ // Candidate consumers may use these generic Foundation mechanisms directly.
484
+ // They are thin exports of the stable implementations, not parallel algorithms.
485
+ export { canonicalJson, computeResourceClosure, digestBytes, digestDocument };
package/package.json CHANGED
@@ -5,15 +5,26 @@
5
5
  "url": "https://github.com/ifoohoo/skill-family-harness-node/issues"
6
6
  },
7
7
  "dependencies": {
8
- "skill-family-contracts": "0.2.0"
8
+ "skill-family-contracts": "0.3.0"
9
9
  },
10
10
  "description": "Thin Node.js mechanism runtime for Skill Family engineering contracts.",
11
11
  "engines": {
12
- "node": ">=22.22.2 <23"
12
+ "node": ">=22.22.2 <23",
13
+ "pnpm": "10.30.0"
14
+ },
15
+ "exports": {
16
+ ".": "./src/index.mjs",
17
+ "./candidate/quickstart-profile": "./candidate/quickstart-profile.mjs"
13
18
  },
14
- "exports": "./src/index.mjs",
15
19
  "files": [
16
20
  "src",
21
+ "candidate",
22
+ "release-notes",
23
+ "README.md",
24
+ "README.zh-CN.md",
25
+ "CHANGELOG.md",
26
+ "CHANGELOG.zh-CN.md",
27
+ "NOTICE",
17
28
  "SECURITY.md",
18
29
  "CONTRIBUTING.md",
19
30
  "CODE_OF_CONDUCT.md"
@@ -27,7 +38,7 @@
27
38
  "url": "https://github.com/ifoohoo/skill-family-harness-node.git"
28
39
  },
29
40
  "type": "module",
30
- "version": "0.2.0",
41
+ "version": "0.3.0",
31
42
  "scripts": {
32
43
  "check": "node --test",
33
44
  "test": "node --test"
@@ -0,0 +1,23 @@
1
+ version: 0.2.1
2
+ date: 2026-08-10
3
+ locales:
4
+ en:
5
+ summary: This release adds candidate Quickstart Profile exchange helpers and makes the package release documentation available in English and Simplified Chinese.
6
+ changes:
7
+ added:
8
+ - Adds candidate helpers that create and revalidate observation Resources, build Tasks, wrap Results, and fail closed when a Result does not bind the exact Task and correlation fields.
9
+ - Adds complete English and Simplified Chinese package documentation, including an agent quick-reference section.
10
+ changed:
11
+ - Manages the current README and CHANGELOG release sections from one bilingual, versioned notes source.
12
+ - Distributes the project NOTICE separately from the Apache-2.0 LICENSE.
13
+ upgradeNotes: The candidate helpers do not alter the stable Harness API or add lifecycle, retry, orchestration, model-call, network, or Git-write semantics.
14
+ zh-CN:
15
+ summary: 本版新增 Quickstart Profile 候选交换辅助函数,并为包发布文档提供完整英文版与简体中文版。
16
+ changes:
17
+ added:
18
+ - 新增候选辅助函数,用于创建并复验 observation Resource、构造 Task、封装 Result,并在 Result 未精确绑定 Task 与关联字段时失败关闭。
19
+ - 新增完整的英文与简体中文包文档,并补充智能体快速参考章节。
20
+ changed:
21
+ - 使用同一份双语版本化说明源管理当前 README 与 CHANGELOG 的发布区域。
22
+ - 项目 NOTICE 与 Apache-2.0 LICENSE 分开分发。
23
+ upgradeNotes: 候选辅助函数不改变稳定 Harness API,也不引入生命周期、重试、编排、模型调用、网络或 Git 写入语义。
@@ -0,0 +1,23 @@
1
+ version: 0.3.0
2
+ date: 2026-08-12
3
+ locales:
4
+ en:
5
+ summary: This source candidate updates the Quickstart Profile harness to verify v2 Task and Result exchanges without taking ownership of consumer semantics.
6
+ changes:
7
+ added:
8
+ - Recomputes real bytes for path-backed outputs and evidence, and rejects duplicate Resource ids across observations, outputs, and evidence.
9
+ - Verifies operation identity, Task digest, every run/stage/attempt field, and the exact evidence binding set.
10
+ changed:
11
+ - Replaces the incompatible 0.2.1 candidate surface; consumers that still require v1 must remain pinned to exactly 0.2.1.
12
+ - Leaves method selection, retry policy, and domain result interpretation to the consumer.
13
+ upgradeNotes: Version 0.3.0 is a local, unpublished source candidate. Pin the candidate subpath to an exact package version and update v1 exchange producers before adopting it.
14
+ zh-CN:
15
+ summary: 本源码候选版把 Quickstart Profile Harness 更新为 v2 Task/Result 交换校验,同时不接管消费者语义。
16
+ changes:
17
+ added:
18
+ - 对 path-backed output 与 evidence 重算真实字节摘要,并拒绝 observation、output、evidence 之间重复的 Resource id。
19
+ - 逐项复验 operation 身份、Task digest、run/stage/attempt 字段和 evidence 精确绑定集合。
20
+ changed:
21
+ - 替换与 0.2.1 不兼容的 candidate 面;仍依赖 v1 的消费者必须继续精确锁定 0.2.1。
22
+ - 方法选择、重试策略与领域结果解释继续归消费者所有。
23
+ upgradeNotes: 0.3.0 当前只是本地、未发布的源码候选。candidate 子路径必须精确锁定包版本,采用前需更新 v1 交换生产方。