@stalfh233/omc-cli 0.4.98 → 0.4.100
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/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +223 -9
- package/dist/field-families.js +27 -0
- package/dist/goal-routes.js +127 -57
- package/dist/lanes/report.js +21 -5
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +30 -0
- package/dist/recipes/field.js +1 -1
- package/dist/recipes/runtime-data.js +3 -1
- package/dist/refusal-guidance.js +207 -0
- package/dist/skills.js +89 -21
- package/dist/tools.js +35 -0
- package/dist/usage-log.js +21 -2
- package/dist/write.js +2 -2
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-start-here.md +2 -1
- package/skills/omc-write-hazards.md +17 -9
package/dist/call.js
CHANGED
|
@@ -7,6 +7,7 @@ import { redact } from "./redact.js";
|
|
|
7
7
|
import { assertSafeInputPath } from "./artifacts.js";
|
|
8
8
|
import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
9
9
|
import { absencePointer } from "./capability-absences.js";
|
|
10
|
+
import { attachRefusalRecovery } from "./refusal-guidance.js";
|
|
10
11
|
export const CALL_EXIT_CODES = { ok: 0, failed: 1, unknownTool: 2, invalidInput: 10 };
|
|
11
12
|
export const CLI_ARGS_MAX_BYTES = 1024 * 1024;
|
|
12
13
|
function readBounded(fd, label) {
|
|
@@ -89,7 +90,7 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
89
90
|
args = parseToolInput(tool, args);
|
|
90
91
|
}
|
|
91
92
|
catch (error) {
|
|
92
|
-
const result = error instanceof ToolInputError ? error.payload : { status: "error", message: error instanceof Error ? error.message : String(error) };
|
|
93
|
+
const result = attachRefusalRecovery(error instanceof ToolInputError ? error.payload : { status: "error", message: error instanceof Error ? error.message : String(error) }, toolName, registry);
|
|
93
94
|
print(redact(result, []));
|
|
94
95
|
return { exitCode: CALL_EXIT_CODES.invalidInput, result };
|
|
95
96
|
}
|
|
@@ -121,13 +122,17 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
121
122
|
clearTimeout(timer);
|
|
122
123
|
}
|
|
123
124
|
})();
|
|
125
|
+
// r19 ticket 06 — 拒绝即教学:refused 结果在出口统一附加 recovery(同源配
|
|
126
|
+
// 方命令、正确形状、不要做什么)。一处附加,call/script/--args-list/组装器/
|
|
127
|
+
// intent 全部经过这里。已自带 recovery 的拒绝(write-lock-held)不覆盖。
|
|
128
|
+
const taught = attachRefusalRecovery(result, toolName, registry);
|
|
124
129
|
// The stderr line is a human diagnostic, NOT a second copy of the result: the
|
|
125
130
|
// stdout envelope is the single machine truth. Dumping the whole result here
|
|
126
131
|
// doubled every call's payload on stderr (unbounded and un-sanitized — it also
|
|
127
132
|
// leaked the raw `detailFile` path; live 2026-09-25). Print the short summary a
|
|
128
133
|
// write already carries, or the (small) error payload; stay silent on a bare
|
|
129
134
|
// successful read.
|
|
130
|
-
const humanLine = humanResultLine(
|
|
135
|
+
const humanLine = humanResultLine(taught);
|
|
131
136
|
if (humanLine !== null)
|
|
132
137
|
print(humanLine);
|
|
133
138
|
const status = String(result?.status ?? "ok");
|
|
@@ -136,7 +141,7 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
136
141
|
// containing a dry-run line reported failure (found while adding --args-list,
|
|
137
142
|
// r13 2026-10-24). Keep the two verdicts in agreement.
|
|
138
143
|
const exitCode = status === "ok" || status === "dry-run" ? CALL_EXIT_CODES.ok : result?.blocker === "tool-input-invalid" ? CALL_EXIT_CODES.invalidInput : CALL_EXIT_CODES.failed;
|
|
139
|
-
return { exitCode, result };
|
|
144
|
+
return { exitCode, result: taught };
|
|
140
145
|
}
|
|
141
146
|
/**
|
|
142
147
|
* The single stderr diagnostic line for a call result, or null when the result
|
|
@@ -274,6 +274,18 @@ export const BOUNDARY_CUES = Object.freeze({
|
|
|
274
274
|
cues: ["子表套子表", "嵌套子表", "多级子表"],
|
|
275
275
|
note: "嵌套子表(子表套子表)在 W6-S27 为 blocked;一级子表可用,嵌套不可",
|
|
276
276
|
},
|
|
277
|
+
/**
|
|
278
|
+
* r19 p14(2026-10-26 用户裁决:报表**暂不优先支持、先隐藏**,不是「平台
|
|
279
|
+
* 不支持」):报表分两层——平台层 `/api/api/report/*`(树/改名/存在性)在
|
|
280
|
+
* 报表服务未启动时仍可用;BI 层 `/dashboard/v1/*`(详情/数据源/图表/配置)
|
|
281
|
+
* 需要独立报表服务(本机有、当前未启动,网关统一 502,p9 实证)。边界首答
|
|
282
|
+
* 让 agent 一轮转向;确需报表时先启动报表服务,不在 502 上重试。
|
|
283
|
+
*/
|
|
284
|
+
"report-bi": {
|
|
285
|
+
status: "blocked",
|
|
286
|
+
cues: ["报表", "仪表盘", "dashboard", "图表数据", "数据源绑定"],
|
|
287
|
+
note: "报表能力暂不优先支持(2026-10-26 维护者裁决,先隐藏):BI 层(/dashboard/v1/*:详情/数据源/图表/配置)需要报表服务(本机有、当前未启动,网关统一 502)——不要重试这些子面。平台层仍可用:report.tree / report.update / report.get 存在性判定;report.create 在服务未启动时半成功(平台层建行、BI 初始化失败)。确需报表:先启动报表服务再深进",
|
|
288
|
+
},
|
|
277
289
|
});
|
|
278
290
|
/** The first boundary category whose cue appears in the goal, if any. */
|
|
279
291
|
export function boundaryOf(goal) {
|
package/dist/cli.js
CHANGED
|
@@ -27,19 +27,21 @@ import { acquireWriteLock } from "./write-lock.js";
|
|
|
27
27
|
import { cmdCompute } from "./compute.js";
|
|
28
28
|
import { cmdRuleWhen } from "./rule-when.js";
|
|
29
29
|
import { cmdApprovalFlow } from "./approval-flow.js";
|
|
30
|
+
import { runSubmitVerify, runRuleSaveEnable, runWorkflowUpdatePublish } from "./compound-writes.js";
|
|
30
31
|
import { cmdFormDefault } from "./form-default.js";
|
|
31
32
|
// --utf8:单次调用明文输出(等效 OMC_ASCII_STDOUT=0)。在 asciiStdout 缓存读取前生效。
|
|
32
33
|
if (process.argv.includes("--utf8"))
|
|
33
34
|
process.env.OMC_ASCII_STDOUT = "0";
|
|
34
|
-
import { commandIndex, commandInventory, describeContract, describeContractFull, describeContractTool, describeExample, describeParam, describeSchemaOnly, describeTool, describeToolFull } from "./discovery.js";
|
|
35
|
+
import { commandIndex, commandInventory, describeContract, describeContractFull, describeContractTool, describeExample, describeParam, describeSchemaOnly, describeTool, describeToolFull, filterTools } from "./discovery.js";
|
|
36
|
+
import { closestMatches } from "./closest-match.js";
|
|
35
37
|
import { repeatFullDescribe } from "./describe-cache.js";
|
|
36
|
-
import { buildCapabilityOverview, capabilityDetail, compactCapabilities, executableCapabilityBundle, resolveCapabilityId, DISCOVERY_AUTHORITY } from "./capability-overview.js";
|
|
38
|
+
import { buildCapabilityOverview, capabilityDetail, capabilityRouteIndex, compactCapabilities, executableCapabilityBundle, resolveCapabilityId, DISCOVERY_AUTHORITY } from "./capability-overview.js";
|
|
37
39
|
import { goalRoute } from "./goal-routes.js";
|
|
38
40
|
import { absencePointer } from "./capability-absences.js";
|
|
39
41
|
import { intentConfigRoot, readIntentFile, validateIntentText } from "./intent/cli.js";
|
|
40
42
|
import { intentSchemaBlock, intentExample, intentExampleIds } from "./intent/schema.js";
|
|
41
43
|
import { planIntent, readbackIntent, applyIntent, intentWarnings } from "./intent/plan-apply.js";
|
|
42
|
-
import { listSkills, readSkill, readSkillOutline, readSkillSection } from "./skills.js";
|
|
44
|
+
import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills } from "./skills.js";
|
|
43
45
|
import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
44
46
|
import { WireError, wireCall } from "./wire.js";
|
|
45
47
|
import { ENDPOINTS } from "./endpoints.js";
|
|
@@ -778,20 +780,38 @@ function cmdDiscovery(kind, transport, value, options = {}) {
|
|
|
778
780
|
return emitTransportResult({ status: "ok", ...result }, transport);
|
|
779
781
|
}
|
|
780
782
|
const inventory = commandInventory(registry, { compact: options.compact === true });
|
|
783
|
+
// r19 ticket 05: above this many hits a filter word is "broad" — the filtered
|
|
784
|
+
// face groups names by capability instead of shipping full rows (a 39-tool
|
|
785
|
+
// "数据" match was 8 KB; the grouped face keeps every name in ~1.5 KB).
|
|
786
|
+
const BROAD_FILTER_TOOL_THRESHOLD = 24;
|
|
787
|
+
/** A query that IS a capability id or lane id is precise intent, never "broad". */
|
|
788
|
+
function isCapabilityIdQuery(registryInput, query) {
|
|
789
|
+
const normalized = query.trim().toLowerCase().replace(/[._\s-]/g, "");
|
|
790
|
+
if (normalized.length === 0)
|
|
791
|
+
return false;
|
|
792
|
+
const index = capabilityRouteIndex(registryInput);
|
|
793
|
+
return index.routes.some((route) => route.id.toLowerCase() === normalized || String(route.lane).toLowerCase().replace(/[_\s-]/g, "") === normalized);
|
|
794
|
+
}
|
|
781
795
|
// `commands --json` is the programmatic selection entry: filtering server-side
|
|
782
796
|
// keeps the caller from pulling the whole >50 KB inventory to find one lane.
|
|
783
797
|
if (options.filter !== undefined || options.lane !== undefined) {
|
|
784
798
|
const tools = inventory.tools;
|
|
785
|
-
|
|
799
|
+
// r19 ticket 05: semantic filter — capability language(聚合/字典/业务集成)
|
|
800
|
+
// must locate tools, not dead-end on a name-substring miss.
|
|
801
|
+
const semantic = options.filter !== undefined ? filterTools(registry, options.filter) : undefined;
|
|
802
|
+
const semanticByName = semantic ? new Map(semantic.map((hit) => [hit.name, hit.via])) : undefined;
|
|
786
803
|
const matched = tools.filter((tool) => {
|
|
787
804
|
if (options.lane !== undefined && tool.lane !== options.lane)
|
|
788
805
|
return false;
|
|
789
|
-
if (
|
|
806
|
+
if (semanticByName === undefined)
|
|
790
807
|
return true;
|
|
791
|
-
return
|
|
808
|
+
return semanticByName.has(String(tool.name));
|
|
792
809
|
});
|
|
793
810
|
inventory.filtered = { filter: options.filter ?? null, lane: options.lane ?? null, matched: matched.length, total: tools.length };
|
|
794
811
|
inventory.tools = matched;
|
|
812
|
+
if (semanticByName !== undefined)
|
|
813
|
+
for (const tool of inventory.tools)
|
|
814
|
+
tool.matchedVia = semanticByName.get(String(tool.name));
|
|
795
815
|
// A filtered query wants tool rows, not the whole catalogue's discovery,
|
|
796
816
|
// skills, and intent prose. Keep only the authoritative route pointer.
|
|
797
817
|
delete inventory.skills;
|
|
@@ -799,6 +819,34 @@ function cmdDiscovery(kind, transport, value, options = {}) {
|
|
|
799
819
|
const discovery = inventory.discovery;
|
|
800
820
|
if (discovery)
|
|
801
821
|
inventory.discovery = { authoritative: discovery.authoritative, route: discovery.route };
|
|
822
|
+
// Zero hits: minimal envelope + up to three nearest candidates with an
|
|
823
|
+
// exact next command — NOT the full catalogue to re-read (r18: the empty
|
|
824
|
+
// match plus governance prose pushed the agent back to the 20 KB face).
|
|
825
|
+
if (matched.length === 0 && semantic !== undefined) {
|
|
826
|
+
delete inventory.governance;
|
|
827
|
+
inventory.filtered.candidates = closestMatches(options.filter ?? "", registry.map((tool) => tool.name), 3).map((candidate) => ({ ...candidate, next: `omc describe ${candidate.name}` }));
|
|
828
|
+
inventory.note = "零命中:换能力词再试(如 聚合/子表/字典/报表/业务集成/定时任务),或用 omc next \"<目标>\" 按任务路由;全量目录仍走 commands --json";
|
|
829
|
+
}
|
|
830
|
+
else if (matched.length > BROAD_FILTER_TOOL_THRESHOLD && !isCapabilityIdQuery(registry, options.filter ?? "")) {
|
|
831
|
+
// A broad word("数据" 39 hits / 8 KB)is half a catalogue: keep every
|
|
832
|
+
// NAME (nothing is hidden) but group them by capability instead of
|
|
833
|
+
// shipping full rows — the agent narrows with a specific word or --lane
|
|
834
|
+
// and gets full rows then. Bounded output without losing tools.
|
|
835
|
+
// Exception: a query that IS a capability/lane id("field"、"data")is
|
|
836
|
+
// precise intent — the capability's full rows are the answer, not noise.
|
|
837
|
+
const groups = new Map();
|
|
838
|
+
for (const tool of matched) {
|
|
839
|
+
const key = String(tool.capability ?? tool.lane ?? "other");
|
|
840
|
+
const entry = groups.get(key) ?? { capability: key, lane: tool.lane ?? null, tools: [] };
|
|
841
|
+
entry.tools.push(String(tool.name));
|
|
842
|
+
groups.set(key, entry);
|
|
843
|
+
}
|
|
844
|
+
inventory.filtered.matchedAll = matched.length;
|
|
845
|
+
inventory.filtered.grouped = true;
|
|
846
|
+
inventory.tools = [];
|
|
847
|
+
inventory.groups = [...groups.values()].sort((left, right) => right.tools.length - left.tools.length);
|
|
848
|
+
inventory.note = `命中 ${matched.length} 个工具(宽词):已按能力分组只列名字;加更具体的词或 --lane <lane> 取完整行(含 requiredArgs/recipes)`;
|
|
849
|
+
}
|
|
802
850
|
}
|
|
803
851
|
result = inventory;
|
|
804
852
|
}
|
|
@@ -1028,14 +1076,93 @@ function cmdIntentSchema(transport, example, exampleRequested) {
|
|
|
1028
1076
|
}
|
|
1029
1077
|
return emitResult(intentSchemaBlock(), transport);
|
|
1030
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* `omc help --search "<q>"` — cross-topic retrieval, not navigation.
|
|
1081
|
+
*
|
|
1082
|
+
* r18 (2026-10-26) spent 173.8 KB re-reading five whole topics twice to grep
|
|
1083
|
+
* for `40000`; `omc help <topic>` is a bookshelf and there was no way to ask a
|
|
1084
|
+
* question. Two rules keep this from becoming another two-call ritual:
|
|
1085
|
+
*
|
|
1086
|
+
* 1. a hit carries its section body, so search itself is the answer;
|
|
1087
|
+
* 2. the hit names the exact `--section` command for the same bytes, so the
|
|
1088
|
+
* only follow-up (if any) is the last one.
|
|
1089
|
+
*
|
|
1090
|
+
* A miss is `ok` + `totalMatches:0`; only an empty query or a topic+search
|
|
1091
|
+
* contradiction is invalid input. Excerpts are never presented as complete:
|
|
1092
|
+
* `sectionComplete:false` + `truncated:true` say the body did not fit.
|
|
1093
|
+
*/
|
|
1094
|
+
function cmdHelpSearch(topic, query, transport, searchMode, limit) {
|
|
1095
|
+
if (topic !== undefined)
|
|
1096
|
+
return emitInvalid(transport, { message: `--search 与 topic 不能同时给:要么 omc help --search "<词>"(跨 topic 检索),要么 omc help <topic> --section "<标题>"(读一节)` });
|
|
1097
|
+
const mode = searchMode === undefined ? "all" : searchMode;
|
|
1098
|
+
if (mode !== "all" && mode !== "any")
|
|
1099
|
+
return emitInvalid(transport, { message: `--search-mode 只能是 all 或 any(收到 "${mode}"):all=同一节内包含全部词,any=包含任一` });
|
|
1100
|
+
const rawLimit = limit === undefined ? 5 : Number(limit);
|
|
1101
|
+
if (!Number.isSafeInteger(rawLimit) || rawLimit < 1 || rawLimit > 50)
|
|
1102
|
+
return emitInvalid(transport, { message: `--limit 必须是 1–50 的整数(收到 "${limit ?? ""}")` });
|
|
1103
|
+
if (query.trim() === "")
|
|
1104
|
+
return emitInvalid(transport, { message: `--search 需要一个非空查询词(例:omc help --search "40000")` });
|
|
1105
|
+
const result = searchSkills(query, { mode, limit: rawLimit });
|
|
1106
|
+
// A hit bigger than this is a book, not an answer: inlining it would recreate
|
|
1107
|
+
// the 12 KB sections agents already pay for. Name the exact `--section`
|
|
1108
|
+
// command instead and say the body is not included.
|
|
1109
|
+
const SEARCH_INLINE_SECTION_CEILING = 6_000;
|
|
1110
|
+
const matches = result.matches.map((match) => {
|
|
1111
|
+
const complete = match.sectionBytes <= SEARCH_INLINE_SECTION_CEILING;
|
|
1112
|
+
return {
|
|
1113
|
+
topic: match.topic,
|
|
1114
|
+
heading: match.heading,
|
|
1115
|
+
// Display-only position; `--section` takes the heading (or its leading
|
|
1116
|
+
// ordinal embedded in the heading), never this index.
|
|
1117
|
+
sectionOrdinal: match.sectionOrdinal,
|
|
1118
|
+
matchLines: match.matchLines,
|
|
1119
|
+
sectionBytes: match.sectionBytes,
|
|
1120
|
+
sectionCommand: `omc help ${match.topic} --section "${match.heading}"`,
|
|
1121
|
+
sectionComplete: complete,
|
|
1122
|
+
...(complete ? { sectionContent: match.section } : { inlineOmitted: `${match.sectionBytes}B 超过单节内联上限 ${SEARCH_INLINE_SECTION_CEILING}B;用 sectionCommand 精确取这一节` }),
|
|
1123
|
+
};
|
|
1124
|
+
});
|
|
1125
|
+
// Keep the whole answer inside one budget: drop bodies from the tail (and say
|
|
1126
|
+
// so) rather than letting the transport spill the envelope to an artifact —
|
|
1127
|
+
// that would recreate the second call this feature removes.
|
|
1128
|
+
const envelopeFor = (entries, truncated) => ({
|
|
1129
|
+
status: "ok",
|
|
1130
|
+
query: query.trim(),
|
|
1131
|
+
mode,
|
|
1132
|
+
totalMatches: result.total,
|
|
1133
|
+
returnedMatches: entries.length,
|
|
1134
|
+
truncated: truncated || entries.some((entry) => entry.sectionComplete === false),
|
|
1135
|
+
matches: entries,
|
|
1136
|
+
...(result.total === 0 ? { message: "没有匹配章节;换更短的字面词(错误码、字段名、命令名)或加 --search-mode any" } : {}),
|
|
1137
|
+
...(result.total > 0 && entries.length === 0 ? { message: "命中存在但连指针都放不进预算;加 --limit 1 或换更短的查询" } : {}),
|
|
1138
|
+
});
|
|
1139
|
+
let truncating = false;
|
|
1140
|
+
while (matches.length > 0 && Buffer.byteLength(JSON.stringify(envelopeFor(matches, truncating)), "utf8") > transport.maxOutputBytes) {
|
|
1141
|
+
const last = matches[matches.length - 1];
|
|
1142
|
+
if ("sectionContent" in last && last.sectionContent !== undefined) {
|
|
1143
|
+
// Drop the body but keep the actionable pointer; the next call is then
|
|
1144
|
+
// the last one because sectionCommand carries the exact heading.
|
|
1145
|
+
last.sectionComplete = false;
|
|
1146
|
+
delete last.sectionContent;
|
|
1147
|
+
truncating = true;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
matches.pop();
|
|
1151
|
+
truncating = true;
|
|
1152
|
+
}
|
|
1153
|
+
const anyOmitted = matches.some((entry) => entry.sectionComplete === false);
|
|
1154
|
+
console.error(`omc help --search "${query.trim()}":${result.total} 节命中,返回 ${matches.length} 节${truncating || anyOmitted ? "(含未内联正文,见 sectionCommand)" : ""}`);
|
|
1155
|
+
return emitTransportResult(envelopeFor(matches, truncating), transport);
|
|
1156
|
+
}
|
|
1031
1157
|
/**
|
|
1032
1158
|
* Ticket 122 — `omc help [topic]`: the methodology layer, reachable exactly
|
|
1033
1159
|
* like `git help <concept>`. The command list is the "what"; these topics are
|
|
1034
1160
|
* the "how".
|
|
1035
|
-
*/
|
|
1036
|
-
function cmdHelp(topic, commandNames, transport, section, sections = false, all = false) {
|
|
1161
|
+
*/ function cmdHelp(topic, commandNames, transport, section, sections = false, all = false, search, searchMode, limit) {
|
|
1037
1162
|
const topics = listSkills();
|
|
1038
1163
|
const commands = [...commandNames].sort();
|
|
1164
|
+
if (search !== undefined)
|
|
1165
|
+
return cmdHelpSearch(topic, search, transport, searchMode, limit);
|
|
1039
1166
|
// Several sections in one call: `--section a --section b` or `--section a,b`.
|
|
1040
1167
|
// Round 15: bench60-62 each spent ~18 separate `help --section` round-trips
|
|
1041
1168
|
// (one per heading the route listed). Fetching them together collapses those
|
|
@@ -1057,7 +1184,7 @@ function cmdHelp(topic, commandNames, transport, section, sections = false, all
|
|
|
1057
1184
|
: "")
|
|
1058
1185
|
: "";
|
|
1059
1186
|
if (topic === undefined) {
|
|
1060
|
-
console.error("用法:omc help <topic>");
|
|
1187
|
+
console.error("用法:omc help <topic> | omc help --search \"<词>\"(跨 topic 检索,命中节正文随结果返回)");
|
|
1061
1188
|
console.error("权威路线:omc help omc-start-here(随包 skills/,不是分发镜像)");
|
|
1062
1189
|
console.error("环境就绪:omc call discover_environment --env <env> --args '{}'");
|
|
1063
1190
|
console.error(`命令:${commands.join("、")}`);
|
|
@@ -1169,7 +1296,7 @@ function cmdHelp(topic, commandNames, transport, section, sections = false, all
|
|
|
1169
1296
|
function onboardingHelp() {
|
|
1170
1297
|
return [
|
|
1171
1298
|
"首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
|
|
1172
|
-
"方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all
|
|
1299
|
+
"方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
|
|
1173
1300
|
"目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
|
|
1174
1301
|
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
|
|
1175
1302
|
"工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
|
|
@@ -1250,7 +1377,7 @@ async function main() {
|
|
|
1250
1377
|
command.outputHelp();
|
|
1251
1378
|
return CLI_EXIT_CODES.ok;
|
|
1252
1379
|
}
|
|
1253
|
-
command.command("help [topic]").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)"
|
|
1380
|
+
command.command("help [topic]").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)").option("--sections").option("--all", "回整篇正文(默认只回节索引:标题 + 各节字节)").option("--search <query>", "跨 topic 全文检索(错误码/字段名/命令名);命中节正文随结果返回,不需要再取一次").option("--search-mode <mode>", "all(默认,同一节包含全部词)或 any(包含任一)").option("--limit <n>", "检索最多返回几节(默认 5,1–50)").option("--max-output-bytes <bytes>").action(async (topic, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdHelp(topic, command.commands.map((entry) => entry.name()), transport, options.section, options.sections === true, options.all === true, typeof options.search === "string" ? options.search : undefined, typeof options.searchMode === "string" ? options.searchMode : undefined, typeof options.limit === "string" ? options.limit : undefined)); });
|
|
1254
1381
|
command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
|
|
1255
1382
|
commandExitCode = await cmdSetup(new ConfigStore(defaultPaths()), [...(options.list ? ["--list"] : []), ...(options.edit ? ["--edit", options.edit] : []), ...(options.remove ? ["--remove", options.remove] : [])]);
|
|
1256
1383
|
});
|
|
@@ -1324,7 +1451,7 @@ async function main() {
|
|
|
1324
1451
|
.option("--env <environment>")
|
|
1325
1452
|
.option("--environment <environment>")
|
|
1326
1453
|
.option("--apply")
|
|
1327
|
-
.addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联)。默认 dry-run。\n")
|
|
1454
|
+
.addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联)。默认 dry-run。\n业务数据:--purge-instances 删流程实例;普通业务行(无流程)不会自动带走——先 data.delete 清(sweep-all-readback 配方),再 purge,否则留孤儿数据。\n")
|
|
1328
1455
|
.action(async (options) => {
|
|
1329
1456
|
const rest = ["model-purge",
|
|
1330
1457
|
...(options.code ? ["--code", options.code] : []),
|
|
@@ -1335,6 +1462,105 @@ async function main() {
|
|
|
1335
1462
|
...(options.apply ? ["--apply"] : [])];
|
|
1336
1463
|
commandExitCode = await cmdModelPurge(rest);
|
|
1337
1464
|
});
|
|
1465
|
+
// r19 tickets 11–13 — 复合写命令:两步一调,两阶段各自回报,默认 dry-run。
|
|
1466
|
+
const compoundEmit = (request) => (receipt, status) => {
|
|
1467
|
+
writeEnvelope(envelope(status, receipt, request));
|
|
1468
|
+
};
|
|
1469
|
+
const parseJsonFlag = (request, label, raw) => {
|
|
1470
|
+
if (raw === undefined)
|
|
1471
|
+
return {};
|
|
1472
|
+
try {
|
|
1473
|
+
return { value: JSON.parse(raw) };
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${label} 不是合法 JSON:${error instanceof Error ? error.message : String(error)}`, retryable: false }));
|
|
1477
|
+
return { error: CLI_EXIT_CODES.invalidInput };
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
command
|
|
1481
|
+
.command("submit-verify")
|
|
1482
|
+
.option("--schema <schemaCode>", "模型 code")
|
|
1483
|
+
.option("--schema-code <schemaCode>")
|
|
1484
|
+
.option("--rows <json>", "formData 的 JSON 数组(或单个对象)")
|
|
1485
|
+
.option("--env <environment>")
|
|
1486
|
+
.option("--environment <environment>")
|
|
1487
|
+
.option("--apply", "执行(默认 dry-run 预览)")
|
|
1488
|
+
.addHelpText("after", "\n提交并读回:--apply 逐条 data.submit → 用真实 bizObjectId data.load → 同一回执汇总差异。提交失败立即停(不重试、不降级);读回差异是发现不是失败。批量提交不需要读回时用 omc call data.submit --args-list。\n")
|
|
1489
|
+
.action(async (options) => {
|
|
1490
|
+
const request = requestId();
|
|
1491
|
+
const parsed = parseJsonFlag(request, "--rows", options.rows);
|
|
1492
|
+
if (parsed.error !== undefined) {
|
|
1493
|
+
commandExitCode = parsed.error;
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const raw = parsed.value;
|
|
1497
|
+
const rows = Array.isArray(raw) ? raw : raw !== undefined ? [raw] : [];
|
|
1498
|
+
commandExitCode = await runSubmitVerify({
|
|
1499
|
+
schema: options.schema ?? options.schemaCode,
|
|
1500
|
+
rows,
|
|
1501
|
+
apply: options.apply === true,
|
|
1502
|
+
environment: options.env ?? options.environment,
|
|
1503
|
+
emit: compoundEmit(request),
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
command
|
|
1507
|
+
.command("rule-save-enable")
|
|
1508
|
+
.option("--schema <schemaCode>", "模型 code")
|
|
1509
|
+
.option("--schema-code <schemaCode>")
|
|
1510
|
+
.option("--rule-code <ruleCode>")
|
|
1511
|
+
.option("--name <name>")
|
|
1512
|
+
.option("--node-graph <json>", "规则图 {nodes,routes} JSON")
|
|
1513
|
+
.option("--publish", "保存后同时发布规则版本")
|
|
1514
|
+
.option("--env <environment>")
|
|
1515
|
+
.option("--environment <environment>")
|
|
1516
|
+
.option("--apply", "执行(默认 dry-run 预览)")
|
|
1517
|
+
.addHelpText("after", "\n保存并启用规则:--apply 先 rule.save(upsert),保存成功才 rule.enable;启用失败如实报「已保存但未启用」。规则保存后默认禁用——不启用不会运行。\n")
|
|
1518
|
+
.action(async (options) => {
|
|
1519
|
+
const request = requestId();
|
|
1520
|
+
const parsed = parseJsonFlag(request, "--node-graph", options.nodeGraph);
|
|
1521
|
+
if (parsed.error !== undefined) {
|
|
1522
|
+
commandExitCode = parsed.error;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
commandExitCode = await runRuleSaveEnable({
|
|
1526
|
+
schema: options.schema ?? options.schemaCode,
|
|
1527
|
+
ruleCode: options.ruleCode,
|
|
1528
|
+
name: options.name,
|
|
1529
|
+
nodeGraph: (parsed.value ?? {}),
|
|
1530
|
+
publish: options.publish === true,
|
|
1531
|
+
apply: options.apply === true,
|
|
1532
|
+
environment: options.env ?? options.environment,
|
|
1533
|
+
emit: compoundEmit(request),
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
command
|
|
1537
|
+
.command("workflow-update-publish")
|
|
1538
|
+
.option("--schema <schemaCode>", "模型 code")
|
|
1539
|
+
.option("--schema-code <schemaCode>")
|
|
1540
|
+
.option("--code <workflowCode>", "流程 code")
|
|
1541
|
+
.option("--template <json>", "流程模板 JSON(至少 {activities})")
|
|
1542
|
+
.option("--confirm-replace", "确认替换:丢弃新模板未覆盖的旧节点(默认保留并在回执列明)")
|
|
1543
|
+
.option("--env <environment>")
|
|
1544
|
+
.option("--environment <environment>")
|
|
1545
|
+
.option("--apply", "执行(默认 dry-run 合成预览)")
|
|
1546
|
+
.addHelpText("after", "\n更新并发布流程:先读草稿显式合成最终模板(默认保留旧节点,--confirm-replace 才丢弃),--apply 走 workflow.update → workflow.publish;更新失败不发布,发布失败保留草稿。\n")
|
|
1547
|
+
.action(async (options) => {
|
|
1548
|
+
const request = requestId();
|
|
1549
|
+
const parsed = parseJsonFlag(request, "--template", options.template);
|
|
1550
|
+
if (parsed.error !== undefined) {
|
|
1551
|
+
commandExitCode = parsed.error;
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
commandExitCode = await runWorkflowUpdatePublish({
|
|
1555
|
+
schema: options.schema ?? options.schemaCode,
|
|
1556
|
+
code: options.code,
|
|
1557
|
+
template: (parsed.value ?? {}),
|
|
1558
|
+
confirmReplace: options.confirmReplace === true,
|
|
1559
|
+
apply: options.apply === true,
|
|
1560
|
+
environment: options.env ?? options.environment,
|
|
1561
|
+
emit: compoundEmit(request),
|
|
1562
|
+
});
|
|
1563
|
+
});
|
|
1338
1564
|
command
|
|
1339
1565
|
.command("model-scaffold")
|
|
1340
1566
|
.option("--code <schemaCode>", "模型 code")
|
package/dist/closest-match.js
CHANGED
|
@@ -47,4 +47,38 @@ function editDistance(a, b) {
|
|
|
47
47
|
}
|
|
48
48
|
return row[b.length];
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* r19 ticket 05 — the zero-hit `commands --filter` candidates. Up to `limit`
|
|
52
|
+
* names ranked by the same acceptance shapes as `closestMatch` (containment,
|
|
53
|
+
* small edit distance, proportional distance), each with its reason so the
|
|
54
|
+
* caller sees WHY it was offered instead of the miss.
|
|
55
|
+
*/
|
|
56
|
+
export function closestMatches(input, candidates, limit = 3) {
|
|
57
|
+
const wanted = input.toUpperCase().replace(/[\s._-]/g, "");
|
|
58
|
+
if (wanted.length < 2)
|
|
59
|
+
return [];
|
|
60
|
+
const scored = [];
|
|
61
|
+
const everything = [];
|
|
62
|
+
for (const name of candidates) {
|
|
63
|
+
const candidate = name.toUpperCase().replace(/[\s._-]/g, "");
|
|
64
|
+
const distance = editDistance(wanted, candidate);
|
|
65
|
+
everything.push({ name, distance });
|
|
66
|
+
if (candidate.includes(wanted)) {
|
|
67
|
+
scored.push({ name, reason: `名称包含 "${input.trim()}"(分隔符归一后)`, distance: 0 });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const sharedPrefix = commonPrefixLength(wanted, candidate);
|
|
71
|
+
const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
|
|
72
|
+
if (distance <= 3 || proportional)
|
|
73
|
+
scored.push({ name, reason: `与 "${input.trim()}" 编辑距离最近(≈${distance})`, distance });
|
|
74
|
+
}
|
|
75
|
+
// Fallback: a query that matches NO acceptance shape still gets the least-bad
|
|
76
|
+
// names with their real distance — an honest "did you mean" beats an empty
|
|
77
|
+
// envelope that sends the caller back to the full catalogue (r19 ticket 05).
|
|
78
|
+
if (scored.length === 0) {
|
|
79
|
+
return everything.sort((left, right) => left.distance - right.distance || left.name.localeCompare(right.name)).slice(0, limit)
|
|
80
|
+
.map(({ name, distance }) => ({ name, reason: `与 "${input.trim()}" 编辑距离最接近(≈${distance},可能不相关)` }));
|
|
81
|
+
}
|
|
82
|
+
return scored.sort((left, right) => left.distance - right.distance || left.name.localeCompare(right.name)).slice(0, limit).map(({ name, reason }) => ({ name, reason }));
|
|
83
|
+
}
|
|
50
84
|
//# sourceMappingURL=closest-match.js.map
|