@actiondock/core 2.12.0 → 2.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/dist/catalog/graph.d.ts +2 -4
- package/dist/catalog/graph.js +182 -32
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/input/describe.d.ts +12 -1
- package/dist/input/describe.js +55 -1
- package/dist/input/flat-materializer.d.ts +24 -0
- package/dist/input/flat-materializer.js +51 -14
- package/dist/input/index.d.ts +1 -0
- package/dist/input/index.js +1 -0
- package/dist/input/input-resolver.d.ts +10 -2
- package/dist/input/input-resolver.js +123 -4
- package/dist/input/output-contract.d.ts +72 -0
- package/dist/input/output-contract.js +274 -0
- package/dist/input/stdin-input.d.ts +11 -1
- package/dist/input/stdin-input.js +15 -3
- package/dist/project/index.d.ts +2 -1
- package/dist/project/index.js +3 -2
- package/dist/project/init.js +2 -2
- package/dist/project/skill-dir.d.ts +56 -0
- package/dist/project/skill-dir.js +214 -0
- package/dist/project/types.d.ts +2 -0
- package/dist/registry/registry.js +27 -15
- package/dist/runtime/action-collection.js +3 -0
- package/dist/runtime/standalone-commands.js +18 -3
- package/dist/runtime/standalone-help.js +2 -0
- package/dist/runtime/standalone.d.ts +2 -0
- package/dist/storage/sqlite-driver.d.ts +0 -1
- package/dist/storage/sqlite-driver.js +0 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
- package/dist/utils/semver.d.ts +2 -1
- package/dist/utils/semver.js +11 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -7
- package/dist/utils/warning.d.ts +0 -1
- package/dist/utils/warning.js +0 -52
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { inputConflict, inputLimitExceeded, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
|
|
1
|
+
import { inputConflict, inputLimitExceeded, inputPathConflict, invalidFlatArgument, invalidJson, InputError, FlatInputError, } from "./flat-errors.js";
|
|
2
|
+
import { isFlatPathPropertyName, isForbiddenActionInputPropertyName, } from "./flat-predicates.js";
|
|
2
3
|
import { decodeFlatInput } from "./flat-decode.js";
|
|
4
|
+
import { assertTopLevelLeafAvailable, assertMaterializedInputWithinLimits, } from "./flat-materializer.js";
|
|
3
5
|
import { validateJsonValue, DEFAULT_MAX_JSON_DEPTH, } from "../value-validator.js";
|
|
4
6
|
import { scanJsonDepth } from "./json-depth-scanner.js";
|
|
5
7
|
import { decodeUtf8Strict, stripBom } from "./utf8.js";
|
|
@@ -103,6 +105,62 @@ function sanitizeError(err, source) {
|
|
|
103
105
|
}
|
|
104
106
|
return err;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 检测单个扁平 token 是否占用 stdin 绑定目标字段。
|
|
110
|
+
*
|
|
111
|
+
* 判定范围(在读 stdin 与执行业务之前):
|
|
112
|
+
* - 目标字段本身:重复赋值(DUPLICATE_ASSIGNMENT);
|
|
113
|
+
* - 目标字段的任意嵌套路径:叶/容器冲突(LEAF_CONTAINER_CONFLICT)。
|
|
114
|
+
*
|
|
115
|
+
* @param token 扁平赋值 token
|
|
116
|
+
* @param field stdin 绑定的目标字段名
|
|
117
|
+
*/
|
|
118
|
+
function detectFlatTokenFieldConflict(token, field) {
|
|
119
|
+
const colonEqIndex = token.indexOf(":=");
|
|
120
|
+
const eqIndex = token.indexOf("=");
|
|
121
|
+
let opIndex = -1;
|
|
122
|
+
if (colonEqIndex !== -1 && (eqIndex === -1 || colonEqIndex < eqIndex)) {
|
|
123
|
+
opIndex = colonEqIndex;
|
|
124
|
+
}
|
|
125
|
+
else if (eqIndex !== -1) {
|
|
126
|
+
opIndex = eqIndex;
|
|
127
|
+
}
|
|
128
|
+
const rawPath = opIndex === -1 ? token : token.slice(0, opIndex);
|
|
129
|
+
if (rawPath !== field && !rawPath.startsWith(`${field}.`))
|
|
130
|
+
return null;
|
|
131
|
+
return inputPathConflict(`Input path conflict at "${rawPath}": field "${field}" is already bound to stdin text via --stdin-field`, { path: rawPath, reason: rawPath === field ? "DUPLICATE_ASSIGNMENT" : "LEAF_CONTAINER_CONFLICT" });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 校验 stdin 字段绑定名是否为合法安全顶层属性名。
|
|
135
|
+
*
|
|
136
|
+
* 拒绝空、纯空白、危险属性(`__proto__`、`constructor`、`prototype`)与
|
|
137
|
+
* 不符合扁平命名段规范(含点分嵌套路径)的表达,错误为 INVALID_FLAT_ARGUMENT。
|
|
138
|
+
*
|
|
139
|
+
* @param field 用户提供的字段名
|
|
140
|
+
*/
|
|
141
|
+
function assertStdinFieldName(field) {
|
|
142
|
+
const problem = (() => {
|
|
143
|
+
if (field.length === 0)
|
|
144
|
+
return "empty";
|
|
145
|
+
if (field.trim() !== field || field.trim().length === 0)
|
|
146
|
+
return "whitespace";
|
|
147
|
+
if (isForbiddenActionInputPropertyName(field))
|
|
148
|
+
return "forbidden";
|
|
149
|
+
if (field.includes("."))
|
|
150
|
+
return "dotted";
|
|
151
|
+
if (!isFlatPathPropertyName(field))
|
|
152
|
+
return "invalid";
|
|
153
|
+
return null;
|
|
154
|
+
})();
|
|
155
|
+
if (problem === null)
|
|
156
|
+
return;
|
|
157
|
+
const reason = problem === "forbidden"
|
|
158
|
+
? "FORBIDDEN_PROPERTY"
|
|
159
|
+
: problem === "dotted"
|
|
160
|
+
? "INVALID_DOT_NOTATION"
|
|
161
|
+
: "INVALID_SEGMENT";
|
|
162
|
+
throw invalidFlatArgument(`Invalid --stdin-field name (length: ${field.length}): must be a non-empty top-level property name without dot notation`, { length: field.length, reason });
|
|
163
|
+
}
|
|
106
164
|
/**
|
|
107
165
|
* 解析完整 JSON 文档字符串。
|
|
108
166
|
* 若解析失败抛出 INVALID_JSON 异常。
|
|
@@ -152,11 +210,15 @@ export function parseJson(text, source, policy) {
|
|
|
152
210
|
* - effectiveFlat: flatArgs 存在且 length > 0
|
|
153
211
|
* - hasInlineJson: options.input !== undefined(即使为 "")
|
|
154
212
|
* - hasFileJson: options.inputFile !== undefined(包含 "-")
|
|
155
|
-
* -
|
|
156
|
-
* -
|
|
213
|
+
* - hasStdinField: options.stdinField !== undefined
|
|
214
|
+
* - flat / inline JSON / file JSON 三者仍保持原有严格互斥;
|
|
215
|
+
* - stdinField 可与 flatArgs 组合(正文绑定到字段 + 扁平补充其他字段),
|
|
216
|
+
* 但与 inline JSON、file JSON(含 inputFile="-")任何来源严格互斥;
|
|
217
|
+
* - 若四者均为 false,返回默认 {}。
|
|
157
218
|
* - flatArgs = [] 视为未指定。
|
|
158
219
|
* - options.input === "" 为显式 Full JSON 模式,进入解析并得到 INVALID_JSON / SYNTAX_ERROR,不可退化为 {}。
|
|
159
220
|
* - 显式空 JSON 输入(--input ""、0 字节文件、空 stdin、纯空白、纯 BOM)统一返回 INVALID_JSON / SYNTAX_ERROR。
|
|
221
|
+
* - stdinField 的 stdin 读取为原始文本绑定:不做 JSON 解析、不剥 BOM、保留首尾空白,空 stdin 得到空字符串。
|
|
160
222
|
*
|
|
161
223
|
* @param options 输入解析选项
|
|
162
224
|
* @returns 解析后的 JSON 对象
|
|
@@ -165,6 +227,22 @@ export async function resolveActionInput(options) {
|
|
|
165
227
|
const effectiveFlat = (options.flatArgs?.length ?? 0) > 0;
|
|
166
228
|
const hasInlineJson = options.input !== undefined;
|
|
167
229
|
const hasFileJson = options.inputFile !== undefined;
|
|
230
|
+
const hasStdinField = options.stdinField !== undefined;
|
|
231
|
+
// stdin 字段绑定与完整 JSON 来源(内联 / 文件 / stdin JSON)严格互斥
|
|
232
|
+
if (hasStdinField && (hasInlineJson || hasFileJson)) {
|
|
233
|
+
const err = inputConflict("Input options conflict: --stdin-field binds raw stdin text to an action input field and cannot be combined with inline JSON (-i/--input) or file input (-f/--input-file).", {
|
|
234
|
+
reason: "MULTIPLE_INPUT_MODES",
|
|
235
|
+
hasFlatArgs: effectiveFlat,
|
|
236
|
+
hasInput: hasInlineJson,
|
|
237
|
+
hasInputFile: hasFileJson,
|
|
238
|
+
hasStdinField,
|
|
239
|
+
});
|
|
240
|
+
if (options.policy?.sanitizeInputErrors) {
|
|
241
|
+
throw sanitizeError(err, "stdin");
|
|
242
|
+
}
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
// 原有三种完整输入模式之间的严格互斥保持不变
|
|
168
246
|
let modeCount = 0;
|
|
169
247
|
if (effectiveFlat)
|
|
170
248
|
modeCount++;
|
|
@@ -184,11 +262,52 @@ export async function resolveActionInput(options) {
|
|
|
184
262
|
}
|
|
185
263
|
throw err;
|
|
186
264
|
}
|
|
187
|
-
if (modeCount === 0) {
|
|
265
|
+
if (modeCount === 0 && !hasStdinField) {
|
|
188
266
|
return {};
|
|
189
267
|
}
|
|
190
268
|
const policy = options.policy;
|
|
191
269
|
const maxBytes = policy?.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES;
|
|
270
|
+
// 0. stdin 原始文本字段绑定模式(可与 flatArgs 组合)
|
|
271
|
+
if (hasStdinField) {
|
|
272
|
+
const field = options.stdinField;
|
|
273
|
+
try {
|
|
274
|
+
assertStdinFieldName(field);
|
|
275
|
+
// 冲突检测与扁平完整解码均先于 stdin 读取:
|
|
276
|
+
// 目标字段占用冲突与任何已知的 flat 语法/路径/字面量错误在读 stdin 前拒绝,
|
|
277
|
+
// 不因等待上游 EOF 而挂起;解码结果复用,读取后不再重复解码。
|
|
278
|
+
let result = {};
|
|
279
|
+
if (effectiveFlat) {
|
|
280
|
+
for (const token of options.flatArgs) {
|
|
281
|
+
const conflict = detectFlatTokenFieldConflict(token, field);
|
|
282
|
+
if (conflict) {
|
|
283
|
+
throw conflict;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
result = decodeFlatInput(options.flatArgs, options.flatOptions);
|
|
287
|
+
assertTopLevelLeafAvailable(result, field);
|
|
288
|
+
}
|
|
289
|
+
const text = await readStdinBounded(options.stdin || process.stdin, {
|
|
290
|
+
maxInputBytes: maxBytes,
|
|
291
|
+
signal: options.signal,
|
|
292
|
+
byteStreamOnly: policy?.byteStreamOnly,
|
|
293
|
+
strictUtf8: policy?.strictUtf8 ?? true,
|
|
294
|
+
rawText: policy?.stdinRawText ?? true,
|
|
295
|
+
});
|
|
296
|
+
// 绑定后对最终入参执行既有实体化结构/总大小校验:
|
|
297
|
+
// stdin 源字节上限(maxInputBytes)与最终对象序列化上限
|
|
298
|
+
// (flatOptions.maxMaterializedSizeBytes,默认 10MiB)各自独立生效,
|
|
299
|
+
// 正文中的引号/反斜杠转义放大亦被最终上限拦截。
|
|
300
|
+
result[field] = text;
|
|
301
|
+
assertMaterializedInputWithinLimits(result, options.flatOptions);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (policy?.sanitizeInputErrors) {
|
|
306
|
+
throw sanitizeError(err, "stdin");
|
|
307
|
+
}
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
192
311
|
// 1. 扁平入参模式
|
|
193
312
|
if (effectiveFlat) {
|
|
194
313
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action 输出正文字段与 Schema 契约静态分析工具。
|
|
3
|
+
*
|
|
4
|
+
* 核心设计原则:
|
|
5
|
+
* - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
|
|
6
|
+
* - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
|
|
7
|
+
* - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 契约诊断项。
|
|
11
|
+
*/
|
|
12
|
+
export interface OutputContractDiagnostic {
|
|
13
|
+
type: "error" | "warning";
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 注解解析与检验结果。
|
|
19
|
+
*/
|
|
20
|
+
export interface AnnotationInspectionResult {
|
|
21
|
+
declared: boolean;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
textField?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 输出契约分析输入项。
|
|
28
|
+
*/
|
|
29
|
+
export interface ActionOutputContractInput {
|
|
30
|
+
annotations?: unknown;
|
|
31
|
+
outputSchema?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 输出契约全量分析结果。
|
|
35
|
+
*/
|
|
36
|
+
export interface ActionOutputContractAnalysis {
|
|
37
|
+
declared: boolean;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
textField?: string;
|
|
40
|
+
errors: string[];
|
|
41
|
+
warnings: string[];
|
|
42
|
+
diagnostics: OutputContractDiagnostic[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
|
|
46
|
+
*
|
|
47
|
+
* 契约规范:
|
|
48
|
+
* - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
|
|
49
|
+
* - 正文字段键名为 textField,值必须为非空字符串。
|
|
50
|
+
* - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
|
|
51
|
+
* - 注解存在但类型非法时返回 valid: false 与对应错误描述。
|
|
52
|
+
*/
|
|
53
|
+
export declare function inspectActionTextFieldAnnotation(annotations?: unknown): AnnotationInspectionResult;
|
|
54
|
+
/**
|
|
55
|
+
* 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
|
|
56
|
+
*
|
|
57
|
+
* 错误判断(确定矛盾):
|
|
58
|
+
* - 注解结构非法或 textField 不是非空字符串;
|
|
59
|
+
* - Schema 明确拒绝所有结果(boolean false);
|
|
60
|
+
* - Schema 明确只允许非对象根类型;
|
|
61
|
+
* - Schema 明确禁止目标属性(property schema 为 false);
|
|
62
|
+
* - Schema 明确要求目标属性为非字符串类型;
|
|
63
|
+
* - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
|
|
64
|
+
*
|
|
65
|
+
* 风险提示(无法完全静态证明安全):
|
|
66
|
+
* - 目标字段未声明为 required;
|
|
67
|
+
* - 目标字段允许 null 等非字符串类型;
|
|
68
|
+
* - outputSchema 缺失或为布尔 true;
|
|
69
|
+
* - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
|
|
70
|
+
* - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
|
|
71
|
+
*/
|
|
72
|
+
export declare function analyzeActionOutputContract(input: ActionOutputContractInput): ActionOutputContractAnalysis;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action 输出正文字段与 Schema 契约静态分析工具。
|
|
3
|
+
*
|
|
4
|
+
* 核心设计原则:
|
|
5
|
+
* - 纯数据分析:仅基于 Action 清单与 Schema 纯元数据推导,无数据库、无模块加载、无业务执行副作用。
|
|
6
|
+
* - 静态契约门禁:区分确定矛盾(错误)与无法证明安全(风险警告),不将不确定误判为合法,亦不为证明安全而试跑动作。
|
|
7
|
+
* - 单一事实源:声明解析、展示描述与门禁校验共用同一套底层纯规则。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 纯静态解析并校验 Action annotations 中的 actiondock.cli 声明。
|
|
11
|
+
*
|
|
12
|
+
* 契约规范:
|
|
13
|
+
* - 注解键名固定为 actiondock.cli,值必须为非空非数组对象。
|
|
14
|
+
* - 正文字段键名为 textField,值必须为非空字符串。
|
|
15
|
+
* - 无相关注解或未声明 textField 时返回 declared: false, valid: true。
|
|
16
|
+
* - 注解存在但类型非法时返回 valid: false 与对应错误描述。
|
|
17
|
+
*/
|
|
18
|
+
export function inspectActionTextFieldAnnotation(annotations) {
|
|
19
|
+
if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
|
|
20
|
+
return { declared: false, valid: true };
|
|
21
|
+
}
|
|
22
|
+
const record = annotations;
|
|
23
|
+
if (!Object.hasOwn(record, "actiondock.cli")) {
|
|
24
|
+
return { declared: false, valid: true };
|
|
25
|
+
}
|
|
26
|
+
const cli = record["actiondock.cli"];
|
|
27
|
+
if (cli === undefined) {
|
|
28
|
+
return { declared: false, valid: true };
|
|
29
|
+
}
|
|
30
|
+
if (typeof cli !== "object" || cli === null || Array.isArray(cli)) {
|
|
31
|
+
return {
|
|
32
|
+
declared: true,
|
|
33
|
+
valid: false,
|
|
34
|
+
error: `Invalid 'actiondock.cli' annotation: expected an object, but received ${cli === null ? "null" : Array.isArray(cli) ? "an array" : typeof cli}.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const cliRecord = cli;
|
|
38
|
+
if (!Object.hasOwn(cliRecord, "textField") || cliRecord.textField === undefined) {
|
|
39
|
+
return { declared: false, valid: true };
|
|
40
|
+
}
|
|
41
|
+
const textField = cliRecord.textField;
|
|
42
|
+
if (typeof textField !== "string") {
|
|
43
|
+
return {
|
|
44
|
+
declared: true,
|
|
45
|
+
valid: false,
|
|
46
|
+
error: `Invalid 'actiondock.cli.textField' annotation: expected a string, but received ${textField === null ? "null" : Array.isArray(textField) ? "an array" : typeof textField}.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (textField.trim() === "") {
|
|
50
|
+
return {
|
|
51
|
+
declared: true,
|
|
52
|
+
valid: false,
|
|
53
|
+
error: "Invalid 'actiondock.cli.textField' annotation: cannot be an empty string.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
declared: true,
|
|
58
|
+
valid: true,
|
|
59
|
+
textField,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 静态分析 Action 输出契约(textField 注解与 outputSchema 的兼容性)。
|
|
64
|
+
*
|
|
65
|
+
* 错误判断(确定矛盾):
|
|
66
|
+
* - 注解结构非法或 textField 不是非空字符串;
|
|
67
|
+
* - Schema 明确拒绝所有结果(boolean false);
|
|
68
|
+
* - Schema 明确只允许非对象根类型;
|
|
69
|
+
* - Schema 明确禁止目标属性(property schema 为 false);
|
|
70
|
+
* - Schema 明确要求目标属性为非字符串类型;
|
|
71
|
+
* - 目标属性不存在且 Schema 明确禁止额外属性(additionalProperties 为 false)。
|
|
72
|
+
*
|
|
73
|
+
* 风险提示(无法完全静态证明安全):
|
|
74
|
+
* - 目标字段未声明为 required;
|
|
75
|
+
* - 目标字段允许 null 等非字符串类型;
|
|
76
|
+
* - outputSchema 缺失或为布尔 true;
|
|
77
|
+
* - 未声明 properties 但允许额外属性,或未声明目标属性但允许额外属性;
|
|
78
|
+
* - Schema 包含 oneOf/anyOf/allOf/$ref 复杂组合无法简单静态推导。
|
|
79
|
+
*/
|
|
80
|
+
export function analyzeActionOutputContract(input) {
|
|
81
|
+
const inspection = inspectActionTextFieldAnnotation(input.annotations);
|
|
82
|
+
if (!inspection.valid) {
|
|
83
|
+
const errorMsg = inspection.error;
|
|
84
|
+
return {
|
|
85
|
+
declared: true,
|
|
86
|
+
valid: false,
|
|
87
|
+
errors: [errorMsg],
|
|
88
|
+
warnings: [],
|
|
89
|
+
diagnostics: [
|
|
90
|
+
{
|
|
91
|
+
type: "error",
|
|
92
|
+
code: "INVALID_ANNOTATION",
|
|
93
|
+
message: errorMsg,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!inspection.declared || !inspection.textField) {
|
|
99
|
+
return {
|
|
100
|
+
declared: false,
|
|
101
|
+
valid: true,
|
|
102
|
+
errors: [],
|
|
103
|
+
warnings: [],
|
|
104
|
+
diagnostics: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const textField = inspection.textField;
|
|
108
|
+
const errors = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
const diagnostics = [];
|
|
111
|
+
function addError(code, message) {
|
|
112
|
+
errors.push(message);
|
|
113
|
+
diagnostics.push({ type: "error", code, message });
|
|
114
|
+
}
|
|
115
|
+
function addWarning(code, message) {
|
|
116
|
+
warnings.push(message);
|
|
117
|
+
diagnostics.push({ type: "warning", code, message });
|
|
118
|
+
}
|
|
119
|
+
const schema = input.outputSchema;
|
|
120
|
+
if (schema === undefined) {
|
|
121
|
+
addWarning("OUTPUT_SCHEMA_ABSENT", `Output schema is absent; cannot statically verify text field '${textField}'. Runtime validation will apply.`);
|
|
122
|
+
return {
|
|
123
|
+
declared: true,
|
|
124
|
+
valid: true,
|
|
125
|
+
textField,
|
|
126
|
+
errors,
|
|
127
|
+
warnings,
|
|
128
|
+
diagnostics,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (schema === false) {
|
|
132
|
+
addError("OUTPUT_SCHEMA_REJECTS_ALL", `Output schema explicitly rejects all outputs (schema is false); cannot produce object containing text field '${textField}'.`);
|
|
133
|
+
return {
|
|
134
|
+
declared: true,
|
|
135
|
+
valid: false,
|
|
136
|
+
textField,
|
|
137
|
+
errors,
|
|
138
|
+
warnings,
|
|
139
|
+
diagnostics,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (schema === true) {
|
|
143
|
+
addWarning("OUTPUT_SCHEMA_ARBITRARY", `Output schema allows arbitrary values (schema is true); text field '${textField}' is not guaranteed to exist or be a string.`);
|
|
144
|
+
return {
|
|
145
|
+
declared: true,
|
|
146
|
+
valid: true,
|
|
147
|
+
textField,
|
|
148
|
+
errors,
|
|
149
|
+
warnings,
|
|
150
|
+
diagnostics,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
154
|
+
addError("INVALID_OUTPUT_SCHEMA", "Invalid outputSchema: expected a schema object or boolean.");
|
|
155
|
+
return {
|
|
156
|
+
declared: true,
|
|
157
|
+
valid: false,
|
|
158
|
+
textField,
|
|
159
|
+
errors,
|
|
160
|
+
warnings,
|
|
161
|
+
diagnostics,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const s = schema;
|
|
165
|
+
// 1. 检查根节点类型约束
|
|
166
|
+
if (s.type === undefined) {
|
|
167
|
+
addWarning("ROOT_TYPE_UNSPECIFIED", `Output schema does not explicitly specify root type as 'object'; non-object outputs cannot satisfy text field '${textField}'.`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const rootTypes = Array.isArray(s.type) ? s.type : [s.type];
|
|
171
|
+
if (!rootTypes.includes("object")) {
|
|
172
|
+
addError("ROOT_TYPE_NOT_OBJECT", `Output schema root type is '${String(s.type)}', which does not allow an object required for text field '${textField}'.`);
|
|
173
|
+
return {
|
|
174
|
+
declared: true,
|
|
175
|
+
valid: false,
|
|
176
|
+
textField,
|
|
177
|
+
errors,
|
|
178
|
+
warnings,
|
|
179
|
+
diagnostics,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const nonObjectTypes = rootTypes.filter((t) => t !== "object");
|
|
183
|
+
if (nonObjectTypes.length > 0) {
|
|
184
|
+
addWarning("ROOT_TYPE_ALLOWS_NON_OBJECT", `Output schema root type allows non-object types (${nonObjectTypes.join(", ")}); text output requires an object at runtime.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 2. 检查复杂组合关键字(oneOf, anyOf, allOf, $ref)
|
|
188
|
+
if (s.oneOf || s.anyOf || s.allOf || s.$ref) {
|
|
189
|
+
addWarning("COMPLEX_SCHEMA_COMPOSITION", `Output schema contains complex composition keywords (oneOf/anyOf/allOf/$ref); text field '${textField}' cannot be fully verified statically.`);
|
|
190
|
+
}
|
|
191
|
+
const hasPatternProps = Boolean(s.patternProperties &&
|
|
192
|
+
typeof s.patternProperties === "object" &&
|
|
193
|
+
!Array.isArray(s.patternProperties) &&
|
|
194
|
+
Object.keys(s.patternProperties).length > 0);
|
|
195
|
+
// 3. 检查属性定义与目标字段
|
|
196
|
+
if (s.properties && typeof s.properties === "object" && !Array.isArray(s.properties)) {
|
|
197
|
+
const props = s.properties;
|
|
198
|
+
if (Object.hasOwn(props, textField)) {
|
|
199
|
+
const fieldSchema = props[textField];
|
|
200
|
+
if (fieldSchema === false) {
|
|
201
|
+
addError("TEXT_FIELD_FORBIDDEN", `Output schema property '${textField}' is explicitly forbidden (property schema is false).`);
|
|
202
|
+
}
|
|
203
|
+
else if (fieldSchema === true) {
|
|
204
|
+
addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
|
|
205
|
+
}
|
|
206
|
+
else if (typeof fieldSchema === "object" &&
|
|
207
|
+
fieldSchema !== null &&
|
|
208
|
+
!Array.isArray(fieldSchema)) {
|
|
209
|
+
const propObj = fieldSchema;
|
|
210
|
+
if (propObj.oneOf || propObj.anyOf || propObj.allOf || propObj.$ref) {
|
|
211
|
+
addWarning("TEXT_FIELD_COMPLEX_COMPOSITION", `Property '${textField}' contains complex composition keywords (oneOf/anyOf/allOf/$ref); text output cannot be fully verified statically.`);
|
|
212
|
+
}
|
|
213
|
+
if (propObj.type === undefined) {
|
|
214
|
+
addWarning("TEXT_FIELD_UNTYPED", `Property '${textField}' does not specify a type; cannot guarantee it will be a string.`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const fieldTypes = Array.isArray(propObj.type) ? propObj.type : [propObj.type];
|
|
218
|
+
if (!fieldTypes.includes("string")) {
|
|
219
|
+
addError("TEXT_FIELD_NOT_STRING", `Output schema property '${textField}' has type '${String(propObj.type)}', which does not allow string.`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const nonStringTypes = fieldTypes.filter((t) => t !== "string");
|
|
223
|
+
if (nonStringTypes.length > 0) {
|
|
224
|
+
addWarning("TEXT_FIELD_ALLOWS_NON_STRING", `Property '${textField}' allows non-string types (${nonStringTypes.join(", ")}); text output requires a string at runtime.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// 字段未在 properties 中明确定义
|
|
232
|
+
if (s.additionalProperties === false) {
|
|
233
|
+
if (hasPatternProps) {
|
|
234
|
+
addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
addError("TEXT_FIELD_DISALLOWED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties and additionalProperties is false.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
addWarning("TEXT_FIELD_UNDECLARED_ADDITIONAL", `Text field '${textField}' is not declared in outputSchema properties (allowed via additionalProperties); existence and type cannot be statically verified.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// 未定义 properties
|
|
247
|
+
if (s.additionalProperties === false) {
|
|
248
|
+
if (hasPatternProps) {
|
|
249
|
+
addWarning("TEXT_FIELD_PATTERN_PROPERTIES", `Text field '${textField}' is not declared in properties; presence may be governed by patternProperties and cannot be fully verified statically.`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
addError("TEXT_FIELD_DISALLOWED_NO_PROPERTIES", `Output schema has no properties declared and additionalProperties is false; text field '${textField}' cannot exist.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
addWarning("TEXT_FIELD_NO_PROPERTIES", `Output schema does not define properties; text field '${textField}' existence cannot be statically verified.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 4. 检查 required 约束(若无确定阻止字段存在的错误)
|
|
260
|
+
if (errors.length === 0) {
|
|
261
|
+
const isRequired = Array.isArray(s.required) && s.required.includes(textField);
|
|
262
|
+
if (!isRequired) {
|
|
263
|
+
addWarning("TEXT_FIELD_NOT_REQUIRED", `Text field '${textField}' is not marked as required in outputSchema; if absent at runtime, text extraction will fail with OUTPUT_FORMAT_ERROR (business execution remains successful in run records).`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
declared: true,
|
|
268
|
+
valid: errors.length === 0,
|
|
269
|
+
textField,
|
|
270
|
+
errors,
|
|
271
|
+
warnings,
|
|
272
|
+
diagnostics,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -10,6 +10,15 @@ export interface ReadStdinBoundedOptions {
|
|
|
10
10
|
byteStreamOnly?: boolean;
|
|
11
11
|
/** 是否执行严格 UTF-8 校验与解码(默认 true) */
|
|
12
12
|
strictUtf8?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 原始文本模式(默认 false)。
|
|
15
|
+
*
|
|
16
|
+
* 语义差异(仅在 strictUtf8 下生效):
|
|
17
|
+
* - 保留开头 BOM(U+FEFF)作为正文一部分,不执行剥离;
|
|
18
|
+
* - 编码失败映射为 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")读取失败,
|
|
19
|
+
* 而不是完整 JSON 场景的 INVALID_JSON 解析失败。
|
|
20
|
+
*/
|
|
21
|
+
rawText?: boolean;
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* 以有界大小限制从标准输入流中安全读取全部内容。
|
|
@@ -17,7 +26,8 @@ export interface ReadStdinBoundedOptions {
|
|
|
17
26
|
* 核心契约:
|
|
18
27
|
* - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
|
|
19
28
|
* - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
|
|
20
|
-
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON
|
|
29
|
+
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
|
|
30
|
+
* rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
|
|
21
31
|
*
|
|
22
32
|
* @param stream 标准输入可读流,默认为 process.stdin
|
|
23
33
|
* @param options 读取配置选项
|
|
@@ -7,7 +7,8 @@ import { DEFAULT_MAX_INPUT_BYTES } from "./file-input.js";
|
|
|
7
7
|
* 核心契约:
|
|
8
8
|
* - 纯字节流策略拦截:若启用 byteStreamOnly 且流发射 string chunk,抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_STREAM_CHUNK_TYPE")。
|
|
9
9
|
* - 终态竞态裁决(First Observed Terminal Event Wins):EOF、limit、abort 谁先观测谁生效,后续事件仅执行清理,不篡改已确定的错误与结果。
|
|
10
|
-
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON
|
|
10
|
+
* - 严格 UTF-8 校验:在 strictUtf8 下通过 decodeUtf8Strict 校验并抛出结构化 INVALID_JSON 异常;
|
|
11
|
+
* rawText 模式改为保留 BOM 的 fatal 解码,编码失败抛出 INPUT_FILE_READ_FAILED(reason: "INVALID_UTF8")。
|
|
11
12
|
*
|
|
12
13
|
* @param stream 标准输入可读流,默认为 process.stdin
|
|
13
14
|
* @param options 读取配置选项
|
|
@@ -18,6 +19,7 @@ export async function readStdinBounded(stream = process.stdin, options) {
|
|
|
18
19
|
const signal = options?.signal;
|
|
19
20
|
const byteStreamOnly = options?.byteStreamOnly ?? false;
|
|
20
21
|
const strictUtf8 = options?.strictUtf8 ?? true;
|
|
22
|
+
const rawText = options?.rawText ?? false;
|
|
21
23
|
// 启动前检查 abort
|
|
22
24
|
if (signal?.aborted) {
|
|
23
25
|
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
@@ -101,8 +103,18 @@ export async function readStdinBounded(stream = process.stdin, options) {
|
|
|
101
103
|
try {
|
|
102
104
|
const fullBuffer = Buffer.concat(chunks);
|
|
103
105
|
if (strictUtf8) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
if (rawText) {
|
|
107
|
+
// 原始文本模式:保留开头 BOM,编码失败视为读取失败而非 JSON 解析失败
|
|
108
|
+
try {
|
|
109
|
+
resolve(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fullBuffer));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
reject(inputFileReadFailed("stdin", new Error(`Invalid UTF-8 encoding: ${err instanceof Error ? err.message : String(err)}`), { reason: "INVALID_UTF8" }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
resolve(decodeUtf8Strict(fullBuffer, "full-json-stdin"));
|
|
117
|
+
}
|
|
106
118
|
}
|
|
107
119
|
else {
|
|
108
120
|
resolve(fullBuffer.toString("utf8"));
|
package/dist/project/index.d.ts
CHANGED
|
@@ -3,13 +3,14 @@ export * from "./init.js";
|
|
|
3
3
|
export * from "./loader.js";
|
|
4
4
|
export * from "./lockfile.js";
|
|
5
5
|
export * from "./manifest.js";
|
|
6
|
+
export * from "./skill-dir.js";
|
|
6
7
|
export { SNAPSHOT_TRACKED_FILES, type TransactionFileRecord, type TransactionMetadata, type ProjectTransaction, isProjectLockHeld, acquireProjectLock, runFrozenInstall, hasPendingTransactions, beginTransaction, recoverPendingTransactions, } from "./transactions.js";
|
|
7
8
|
export * from "./types.js";
|
|
8
9
|
export * from "./types-generator.js";
|
|
9
10
|
export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, type TraverseDirectoryEntry, type TraverseDirectoryOptions, } from "../utils/index.js";
|
|
10
11
|
export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
|
|
11
12
|
export { runDoctorChecks, } from "../doctor/doctor.js";
|
|
12
|
-
export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
|
|
13
|
+
export { resolveActionInput, buildActionDescribePayload, formatActionDetail, type FormatActionDetailOptions, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, type AnnotationInspectionResult, type ActionOutputContractInput, type ActionOutputContractAnalysis, type OutputContractDiagnostic, mapInputValidationFailure, stripBom, readStdinBounded, type ResolveActionInputOptions, type ReadStdinBoundedOptions, } from "../input/index.js";
|
|
13
14
|
export { validateActionInputValue } from "../value-validator.js";
|
|
14
15
|
export { parseJson } from "../input/input-resolver.js";
|
|
15
16
|
export { InputError, FlatInputError, } from "../input/flat-errors.js";
|
package/dist/project/index.js
CHANGED
|
@@ -3,14 +3,15 @@ export * from "./init.js";
|
|
|
3
3
|
export * from "./loader.js";
|
|
4
4
|
export * from "./lockfile.js";
|
|
5
5
|
export * from "./manifest.js";
|
|
6
|
+
export * from "./skill-dir.js";
|
|
6
7
|
export { SNAPSHOT_TRACKED_FILES, isProjectLockHeld, acquireProjectLock, runFrozenInstall, hasPendingTransactions, beginTransaction, recoverPendingTransactions, } from "./transactions.js";
|
|
7
8
|
export * from "./types.js";
|
|
8
9
|
export * from "./types-generator.js";
|
|
9
10
|
export { assertPathWithinRoot, isPathOutsideBoundary, getPackageSlug, parseDuration, traverseDirectory, } from "../utils/index.js";
|
|
10
11
|
export { isSecretConfigKey, maskSecretValue, sanitizeConfigDefinitions, } from "../storage/mask.js";
|
|
11
12
|
export { runDoctorChecks, } from "../doctor/doctor.js";
|
|
12
|
-
//
|
|
13
|
-
export { resolveActionInput, buildActionDescribePayload, ACTION_DESCRIBE_SYNTAX_REFERENCE, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
|
|
13
|
+
// 动作入参解析、输出契约分析、校验与格式化(含 BOM 剥离与标准输入有界读取的单一事实源转引)
|
|
14
|
+
export { resolveActionInput, buildActionDescribePayload, formatActionDetail, ACTION_DESCRIBE_SYNTAX_REFERENCE, inspectActionTextFieldAnnotation, analyzeActionOutputContract, mapInputValidationFailure, stripBom, readStdinBounded, } from "../input/index.js";
|
|
14
15
|
export { validateActionInputValue } from "../value-validator.js";
|
|
15
16
|
export { parseJson } from "../input/input-resolver.js";
|
|
16
17
|
export { InputError, FlatInputError, } from "../input/flat-errors.js";
|
package/dist/project/init.js
CHANGED
|
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
|
|
|
86
86
|
node: ">=24.12.0",
|
|
87
87
|
},
|
|
88
88
|
dependencies: {
|
|
89
|
-
"@actiondock/sdk": "^2.
|
|
89
|
+
"@actiondock/sdk": "^2.14.0",
|
|
90
90
|
},
|
|
91
91
|
devDependencies: {
|
|
92
|
-
"@actiondock/testing": "^2.
|
|
92
|
+
"@actiondock/testing": "^2.14.0",
|
|
93
93
|
"@types/node": "^22.13.0",
|
|
94
94
|
"tsx": "^4.19.0",
|
|
95
95
|
"typescript": "^5.7.0",
|