@stalfh233/omc-cli 0.4.99 → 0.4.101
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/README.md +13 -1
- package/coverage/m1-coverage-manifest-v1.json +9 -3
- package/dist/call.js +8 -3
- package/dist/capability-overview.js +12 -0
- package/dist/cli-contract.js +5 -1
- package/dist/cli.js +238 -12
- package/dist/closest-match.js +34 -0
- package/dist/compound-writes.js +310 -0
- package/dist/discovery.js +230 -9
- package/dist/goal-routes.js +159 -57
- package/dist/governed-execution.js +1 -1
- package/dist/lanes/b2-registration.js +20 -3
- package/dist/lanes/businessrule-contract.js +4 -0
- package/dist/lanes/businessrule-coverage.js +1 -1
- package/dist/lanes/businessrule.js +93 -0
- package/dist/lanes/field.js +10 -1
- package/dist/lanes/form.js +2 -2
- package/dist/lanes/report.js +21 -5
- package/dist/lanes/workflow.js +38 -3
- package/dist/recipes/b2-registration.js +8 -7
- package/dist/recipes/businessrule.js +58 -1
- package/dist/refusal-guidance.js +207 -0
- package/dist/rule-support.js +96 -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/docs/images/omc-nixi/README.md +41 -0
- package/docs/omc-introduction.md +851 -0
- package/package.json +3 -3
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-refusals.md +1 -1
- package/skills/omc-start-here.md +2 -1
- package/skills/omc-tool-selection.md +1 -1
- package/skills/omc-write-hazards.md +5 -0
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
- **应用目录(分组)**——`folder.tree/create/update/delete` + `model.move`;
|
|
29
29
|
应用树是嵌套 `children`,读面已递归展平并给 `path`/`depth`。
|
|
30
30
|
- **方法论即可读**——`skills/` 下的方法论通过 `omc help <topic>` 按需读取;agent 不必去翻仓库文件。
|
|
31
|
+
- **一次调用就是答案(0.4.100)**——`omc help --search "<词>"` 跨全部 topic 全文检索、命中节正文内联(严禁自己 `--all | grep`);`omc describe <tool>` 默认带 `firstCall` 最小可跑形状(上下文键占位符化并列入 missing,绝不冒充合法值);`omc next "<目标>"` 首答只给一个可执行动作或一个问题(`firstAnswer`);`omc commands --filter` 支持能力语言(聚合/字典/业务集成…),零命中回 ≤3 个最近候选。
|
|
32
|
+
- **拒绝即教学**——写前拒绝信封自带「你错在哪、正确形状、同源配方命令、不要做什么」;平台/权限/环境类只给「先修什么」,不给注定失败的重试命令。
|
|
33
|
+
- **复合写命令**——`omc submit-verify`(逐条提交 → 真实 id 读回 → 差异汇总)、`omc rule-save-enable`(保存成功才启用,回执回答「会不会真的运行」)、`omc workflow-update-publish`(显式合成最终模板:默认保留旧节点,`--confirm-replace` 才丢弃)。全部默认 dry-run、第一步失败立即停、不自动重试。
|
|
31
34
|
- **凭据零回显**——凭据(0600 存储或环境变量)不会出现在任何输出、产物或错误里。
|
|
32
35
|
- **不确定性语义**——传输失败时冻结(`status: "uncertain", frozen: true`),
|
|
33
36
|
绝不伪造成功。
|
|
@@ -130,10 +133,19 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
|
|
|
130
133
|
- **intent**:`partial` 表示该能力可声明式(`omc intent`)覆盖一部分——通常 create/update;
|
|
131
134
|
`none` 表示只走 `omc call`。删除类、方法面、分类/连接池等仍走 `omc call` 逃生舱。
|
|
132
135
|
|
|
133
|
-
## 近期能力(0.4.
|
|
136
|
+
## 近期能力(0.4.84 → 0.4.100)
|
|
134
137
|
|
|
135
138
|
| 版本 | 新增 / 修复 |
|
|
136
139
|
|---|---|
|
|
140
|
+
| 0.4.100 | **回合经济学批(r19–r20,同题面实测 184 调用/503KB → 84 调用/98KB)**:`omc help --search "<词>"` 全文检索(命中节正文内联,单节 >6KB 只给精确回放命令);`describe` 默认带 `firstCall`(offline-valid 最小可跑形状,`executable === missing为空`);`omc next` 首答 `firstAnswer`(ready=无占位符可跑 / needs-input=一个问题 / blocked=不伪造路径),`discover_environment` 退出 firstCommands 首位、章节指针 ≤1 且非通用草稿模板、输出 ≤4096B;`commands --filter` 语义检索(工具名/能力别名/配方/摘要,大小写与分隔符归一;零命中最小信封+≤3 候选;宽词按能力分组 ≤3.5KB);拒绝即教学(拒绝信封自带正确形状+与 firstCall 同源的配方命令+不要做什么;平台/权限类只给先修什么);三条复合写命令 `submit-verify` / `rule-save-enable` / `workflow-update-publish`(默认 dry-run、第一步失败立即停、合并语义显式);`OMC_NO_USAGE=1` 计量隔离(读侧不受影响);报表能力按裁决暂缓(报表类目标 boundary 首答;`/dashboard/v1/*` 502 教学「环境边界不要重试」;`report.create` 半成功显式警示+清理命令) |
|
|
141
|
+
| 0.4.99 | 聚合算法三态实测定案:`LARGE/SMALL` 必带 k(缺 → 50000 掩码)、`UNION` 的 `resultType` 只能 0/1;审批提交**顶层 `agree` 才有效**(`approval.agree/opinion` 被静默忽略且仍按通过处理——写前拒绝 + `approvalNote` 顶层参数);聚合事件回算窗口 ≈3 分钟实测(`field.publish --force` 重置) |
|
|
142
|
+
| 0.4.98 | 聚合指向子表 schema 写前拒绝(会让目标模型整体 40000 无明细且匹配恒 0);`40000` 裸掩码给排查解释(先疑字段级配置);`omc next` 造数据场景路由 |
|
|
143
|
+
| 0.4.97 | 三个写前 gate:日期毫秒时间戳(落库 NULL)、单值关联传数组(50000)、更新时无 id 子表行按新增插入不判重(实测 2 行变 8 行);跨模型聚合行为图谱(混用条件/多组 OR/COUNTIF/MAX/来源删除回算) |
|
|
144
|
+
| 0.4.96 | 聚合定案(反编译 1.1.25 运行版 jar):`propertyType/operatorType/resultType` 等 `@JsonCreator get(int)` 枚举**只认整数码**(写名字 → 305003 掩码,CLI 自动转码);同模型子表聚合是平台死路(`checkCycleOfAggregation:1032` 判环,`field.create` 接受、`field.publish` 炸)→ 写前拒绝;跨模型汇总配方 `formula-aggregation-cross-model`(DYNAMIC 条件两侧字段分属两模型、`queryConditions` 不能为空) |
|
|
145
|
+
| 0.4.95 | `omc compute` 诚实化:`datarule` type2(shortTextStitch)在 REST 路径**永不执行**(`CalculateHandler` 由 `forceExecuteCalculateRule` 门控、全库 0 调用方;门户「能算」是客户端 JS 求值器)——不再假装可配 |
|
|
146
|
+
| 0.4.94 | QUERY 输出不可寻址写前拒绝;`100005` 判「同 workItem 已提交」(永久,非时间窗) |
|
|
147
|
+
| 0.4.92 | `rule.run` 真门(此前是假成功);custompage 八工具 |
|
|
148
|
+
| 0.4.89/90 | 修 0.4.88 DRAFT 实例回归;`/organization/department/tree` 深度 BFS(deptIds 只回直接子级);组织树参与人解析按 `ouScope ∩ 发起人部门链`(全空=静默回退发起人 → 写前拒绝);结构化 nextCommand |
|
|
137
149
|
| 0.4.88 | **`omc model-purge`**:一条命令按依赖倒序清掉一个模型(流程实例 → model.delete 级联带走流程/视图/字段/表单/规则)。这是 r12–r14 三次「脏沙盒幽灵故障」的根治——顺手修了它的死锁(视图最后一条不能删 306103 + 列表绑定流程 303024,所以不删流程、让模型级联)。另:ladder 的 **recipe 命令提到 firstCommands**(r14 抱怨「ladder 没给可直接跑的命令」)、recipe 增加 `aliases` 意图词、`listview.configure` 的拒绝恢复补上 `targetCode` 按 bindAction 的语义 |
|
|
138
150
|
| 0.4.87 | **r14 真机探针修复**:`workitem.list` 的 50000 **不再是无解题**——按 `participant` 走 JPA 查询端点(能查**别人**名下的待办、且不解析流程模板),全局搜索失败时回 `workitem-search-unavailable` 带成因与三条绕行;`workflow.delete` 的 402508 改 `workflow-has-instances` 并说明**不要改用 model.delete 绕过**;`model.delete` 在有未完结待办时回 `workflowWarning`(级联会留脏待办 → 全局待办搜索整页 NPE)。另修两个 composer 模板参数名写错(`form-default --value`→`--default`、`rule-when --rule`→`--on`),并加**模板 vs `--help` 自动校验测试** |
|
|
139
151
|
| 0.4.86 | **r13 真机探针修复**:`omc next` 增加 **composer 路由**(含建模型+字段/审批流/计算/条件规则/表单默认值 → 直接给出对应一条命令;读面仍走 ladder,条件分支流程仍走 recipe)——r13 读了文档里那行 `model-scaffold` 却仍调 26 次 `field.create`,说明「写进文档」不等于「能被发现」。`data.submit` 审批提交**写前校验身份**:非本人在办的待办直接回 `submit-workitem-not-mine`(带待办持有者 + 三条出路),不再让平台回一个 `40005 权限不足`让 agent 猜 |
|
|
@@ -1240,7 +1240,7 @@
|
|
|
1240
1240
|
{
|
|
1241
1241
|
"capability": "rule.list latest business-rule listing and rule.get node route definition readback",
|
|
1242
1242
|
"status": "executable",
|
|
1243
|
-
"scope": "GET /api/api/runtime/business_rule/list_by_schemacode returning the full latest-version business-field set (no server-side filters); GET /api/api/app/businessrule/get returning the saved node and route graph for readback",
|
|
1243
|
+
"scope": "GET /api/api/runtime/business_rule/list_by_schemacode returning the full latest-version business-field set (no server-side filters); GET /api/api/app/businessrule/get returning the saved node and route graph for readback; cross-rule reference search over model.list + businessrule list/get scans (rule.references)",
|
|
1244
1244
|
"version_family": "8.6",
|
|
1245
1245
|
"anchor_version": "8.6.26",
|
|
1246
1246
|
"evidence": [
|
|
@@ -1251,16 +1251,22 @@
|
|
|
1251
1251
|
{
|
|
1252
1252
|
"path": "packages/omc-cli/test/rule-list.test.ts",
|
|
1253
1253
|
"anchor": "contract tests"
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
"path": "packages/omc-cli/test/rule-references.test.ts",
|
|
1257
|
+
"anchor": "contract tests"
|
|
1254
1258
|
}
|
|
1255
1259
|
],
|
|
1256
1260
|
"lane": "B1-businessrule",
|
|
1257
1261
|
"tools": [
|
|
1258
1262
|
"rule.list",
|
|
1259
|
-
"rule.get"
|
|
1263
|
+
"rule.get",
|
|
1264
|
+
"rule.references"
|
|
1260
1265
|
],
|
|
1261
1266
|
"scopes": [
|
|
1262
1267
|
"latest rule business-field listing",
|
|
1263
|
-
"rule node route readback"
|
|
1268
|
+
"rule node route readback",
|
|
1269
|
+
"cross-rule reference search"
|
|
1264
1270
|
]
|
|
1265
1271
|
},
|
|
1266
1272
|
{
|
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-contract.js
CHANGED
|
@@ -220,7 +220,11 @@ export function classifyResult(result) {
|
|
|
220
220
|
return { status: "error", error: withNext({ code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
|
|
221
221
|
}
|
|
222
222
|
if (result.blocker === "tool-input-invalid")
|
|
223
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
status: "error",
|
|
225
|
+
error: { ...withNext({ code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }), nextCommand: `omc describe ${String(result.tool ?? "<tool>")}` },
|
|
226
|
+
exitCode: CLI_EXIT_CODES.invalidInput,
|
|
227
|
+
};
|
|
224
228
|
if (status === "uncertain")
|
|
225
229
|
return { status, error: withNext({ code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }), exitCode: CLI_EXIT_CODES.uncertain };
|
|
226
230
|
if (status === "refused") {
|
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
|