@stalfh233/omc-cli 0.4.17 → 0.4.18

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.
@@ -1,4 +1,5 @@
1
1
  import { ToolRefusalError } from "./tool-types.js";
2
+ import { closestMatch } from "./closest-match.js";
2
3
  /**
3
4
  * Ticket 92 — business-integration write-face codec + preflight (pure functions,
4
5
  * zero DSH/wire dependencies). Reused by service.register (92), service.update
@@ -159,9 +160,13 @@ function commonlistValue(serviceConfig, code) {
159
160
  export function assertBizServiceAdapterWhitelisted(adapterCode) {
160
161
  if (typeof adapterCode !== "string" || !adapterCode)
161
162
  refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, "adapterCode 是必需的(结构化写面)", { adapterCode });
162
- if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode))
163
- refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, `adapterCode ${adapterCode} 不在写面支持范围内;本写面仅支持 ${BIZSERVICE_WRITE_ADAPTERS.join("/")}。` +
164
- `**自定义 Java 方法体的常规做法是:在扩展模块写一个接口,然后用 RESTFUL 适配器注册一个服务指向它**(service.register,adapterCode:RESTFUL,methodConfig.url 指向扩展端点);不要用 INTERNAL/custom_protocol 适配器(那是平台内部/自定义协议机制,不是二开接口的常规登记方式,也不是本工具的支持面)`, { adapterCode, supported: BIZSERVICE_WRITE_ADAPTERS });
163
+ if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode)) {
164
+ // F6: an off-by-one adapter guess gets a did-you-mean (same helper as the
165
+ // field families) before the prose.
166
+ const closest = closestMatch(adapterCode, BIZSERVICE_WRITE_ADAPTERS);
167
+ refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, `adapterCode ${adapterCode} 不在写面支持范围内;本写面仅支持 ${BIZSERVICE_WRITE_ADAPTERS.join("/")}。${closest !== undefined ? `你要找的可能是 ${closest}。` : ""}` +
168
+ `**自定义 Java 方法体的常规做法是:在扩展模块写一个接口,然后用 RESTFUL 适配器注册一个服务指向它**(service.register,adapterCode:RESTFUL,methodConfig.url 指向扩展端点);不要用 INTERNAL/custom_protocol 适配器(那是平台内部/自定义协议机制,不是二开接口的常规登记方式,也不是本工具的支持面)`, { adapterCode, supported: BIZSERVICE_WRITE_ADAPTERS, ...(closest !== undefined ? { closestMatch: closest } : {}) });
169
+ }
165
170
  return adapterCode;
166
171
  }
167
172
  /** The AES boundary is not bypassable: a raw configJson string is refused before any wire call. */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * F6 (r10-friction): the shared did-you-mean closest-match helper. Born from
3
+ * the live-measured field-family case (NUMBER → correct name NUMERICAL cost a
4
+ * full extra round); the same off-by-one guesses hit every enum-like gate
5
+ * (adapter codes, …). Case/underscore-insensitive, three acceptance shapes:
6
+ * prefix containment, small edit distance, shared-prefix + proportional
7
+ * distance. Ambiguous short inputs (TEXT, BOOL) intentionally match nothing —
8
+ * a misleading suggestion is worse than the plain list.
9
+ */
10
+ export function closestMatch(input, candidates) {
11
+ const wanted = input.toUpperCase().replace(/[\s_-]/g, "");
12
+ if (wanted.length < 2)
13
+ return undefined;
14
+ let best;
15
+ for (const name of candidates) {
16
+ const candidate = name.toUpperCase().replace(/[\s_-]/g, "");
17
+ // 1. prefix containment (STAFF→STAFF_SELECTOR, DATETIME→DATE)
18
+ const contained = (candidate.startsWith(wanted) || wanted.startsWith(candidate)) && Math.min(wanted.length, candidate.length) >= 4;
19
+ // 2. small edit distance (NUMERIC→NUMERICAL, SHORTTEXT→SHORT_TEXT)
20
+ const distance = editDistance(wanted, candidate);
21
+ const nearMiss = distance <= 2;
22
+ // 3. shared prefix + proportional distance (NUMBER→NUMERICAL, the live-measured case)
23
+ const sharedPrefix = commonPrefixLength(wanted, candidate);
24
+ const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
25
+ const accepted = contained || nearMiss || proportional;
26
+ if (accepted && (!best || distance < best.distance))
27
+ best = { name, distance };
28
+ }
29
+ return best?.name;
30
+ }
31
+ function commonPrefixLength(a, b) {
32
+ let index = 0;
33
+ while (index < a.length && index < b.length && a[index] === b[index])
34
+ index++;
35
+ return index;
36
+ }
37
+ function editDistance(a, b) {
38
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
39
+ for (let i = 1; i <= a.length; i++) {
40
+ let previous = row[0];
41
+ row[0] = i;
42
+ for (let j = 1; j <= b.length; j++) {
43
+ const temp = row[j];
44
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
45
+ previous = temp;
46
+ }
47
+ }
48
+ return row[b.length];
49
+ }
50
+ //# sourceMappingURL=closest-match.js.map
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { closestMatch } from "./closest-match.js";
2
3
  import { ToolRefusalError } from "./tool-types.js";
3
4
  import { decodeJsonObjectColumn } from "./json-column.js";
4
5
  import { FIELD_CONTROL_CODES, FIELD_DATE_DEFAULT_OPTIONS, FIELD_DATE_FORMATS, FIELD_FAMILY_LIST } from "./lanes/field-contract.js";
@@ -266,47 +267,9 @@ export function fieldTypeHint(type) {
266
267
  return `「${type}」不是族名;你要找的可能是 ${closest}`;
267
268
  return undefined;
268
269
  }
269
- /** Case/underscore-insensitive closest match over the family list. */
270
+ /** Case/underscore-insensitive closest match over the family list (shared helper, see closest-match.ts). */
270
271
  function closestFamilyName(type) {
271
- const wanted = type.toUpperCase().replace(/[\s_-]/g, "");
272
- if (wanted.length < 2)
273
- return undefined;
274
- let best;
275
- for (const name of Object.keys(FIELD_PROPERTY_TYPES)) {
276
- const candidate = name.toUpperCase().replace(/[\s_-]/g, "");
277
- // Three acceptance shapes, from exact-ish to fuzzy:
278
- // 1. prefix containment (STAFF→STAFF_SELECTOR, DATETIME→DATE)
279
- const contained = (candidate.startsWith(wanted) || wanted.startsWith(candidate)) && Math.min(wanted.length, candidate.length) >= 4;
280
- // 2. small edit distance (NUMERIC→NUMERICAL, SHORTTEXT→SHORT_TEXT)
281
- const distance = editDistance(wanted, candidate);
282
- const nearMiss = distance <= 2;
283
- // 3. shared prefix + proportional distance (NUMBER→NUMERICAL, the live-measured case)
284
- const sharedPrefix = commonPrefixLength(wanted, candidate);
285
- const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
286
- const accepted = contained || nearMiss || proportional;
287
- if (accepted && (!best || distance < best.distance))
288
- best = { name, distance };
289
- }
290
- return best?.name;
291
- }
292
- function commonPrefixLength(a, b) {
293
- let index = 0;
294
- while (index < a.length && index < b.length && a[index] === b[index])
295
- index++;
296
- return index;
297
- }
298
- function editDistance(a, b) {
299
- const row = Array.from({ length: b.length + 1 }, (_, index) => index);
300
- for (let i = 1; i <= a.length; i++) {
301
- let previous = row[0];
302
- row[0] = i;
303
- for (let j = 1; j <= b.length; j++) {
304
- const temp = row[j];
305
- row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
306
- previous = temp;
307
- }
308
- }
309
- return row[b.length];
272
+ return closestMatch(type, Object.keys(FIELD_PROPERTY_TYPES));
310
273
  }
311
274
  /** wire propertyType (int) -> form controlType (type-matrix.md §3.1). */
312
275
  export const FIELD_CONTROL_TYPES = { ...CAPABILITY_CONTROL_TYPES };
@@ -829,7 +792,7 @@ function choiceOptionsError(type, options) {
829
792
  // No optionsSet: refuse the shapes that LOOK like choices but the platform ignores.
830
793
  for (const key of ["items", "labels", "custom", "dictionary", "businessModel", "options", "dictionaryData"]) {
831
794
  if (options[key] !== undefined)
832
- return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet({optionsType:'custom',custom:[{code,value,name_i18n}]} 或 dictionary/businessModel);裸的 options.${key} 平台不消费,门户会渲染成空控件。用 field.create 的 choices 参数,或 options.optionsSet`;
795
+ return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet;裸的 options.${key} 平台不消费,门户会渲染成空控件。最小可抄样本 ${JSON.stringify({ options: { optionsSet: { optionsType: "custom", custom: [{ code: "opt1", value: "1" }, { code: "opt2", value: "2" }] } } })},或 field.create 直接传 choices:["opt1","opt2"]`;
833
796
  }
834
797
  return null;
835
798
  }
@@ -853,7 +816,7 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
853
816
  refuseFieldOptions("FORMULA options carried the legacy top-level resultType/expression keys, which the platform does not read", "rejected: FORMULA(100) options must use formulaConfig (nested JSON string) + persistent:true; top-level resultType/expression are not platform keys");
854
817
  const formulaConfig = options?.formulaConfig;
855
818
  if (!isNonEmptyString(formulaConfig))
856
- refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression) — see field-contract");
819
+ refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression)。最小可抄样本 " + JSON.stringify({ options: { formulaConfig: JSON.stringify({ formulaType: "EXPRESSION", expression: "{amount}*2", resultType: 0 }), persistent: true } }) + "(resultType 0=SHORT_TEXT/2=NUMERICAL;expression 引用模型字段)");
857
820
  const config = parseOptionsObject(formulaConfig);
858
821
  if (!config)
859
822
  refuseFieldOptions("FORMULA formulaConfig is not a parseable JSON object string", "rejected: FORMULA(100) options.formulaConfig must be a JSON object string");
@@ -231,13 +231,36 @@ function batchReadCommands(pointers) {
231
231
  byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
232
232
  return [...byTopic.entries()].map(([topic, sections]) => `omc help ${topic} ${sections.map((section) => `--section "${section}"`).join(" ")}`);
233
233
  }
234
- /** The route's methodology-read surface: pointers (+ per-section bytes) + ready-to-run batched commands + hint. */
234
+ /**
235
+ * The route's methodology-read surface: pointers (+ per-section bytes) +
236
+ * ready-to-run batched commands + hint.
237
+ *
238
+ * F9 (r10-friction): the most common task flow was `next` followed
239
+ * IMMEDIATELY by fetching the first readSection — one extra process +
240
+ * round-trip every task. The first section now rides inline when it fits the
241
+ * inline budget (~2 KB); larger sections stay pointer-only so `next` never
242
+ * balloons.
243
+ */
244
+ const FIRST_SECTION_INLINE_BUDGET_BYTES = 2560;
235
245
  function routeReads(readSections) {
236
246
  // Per-section UTF-8 bytes ride each pointer (one field, not a parallel array)
237
247
  // so a caller can budget which sections to fetch — dogfood R1 found the
238
248
  // batched readCommands could exceed a window with no way to see sizes.
239
249
  const budgeted = readSections.map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
240
- return { readSections: budgeted, readCommands: batchReadCommands(readSections), readHint: READ_HINT };
250
+ // Inline the FIRST pointer that fits the budget (order = relevance); an
251
+ // oversized head section falls through to the next fitting one rather than
252
+ // dropping the inline content entirely.
253
+ let firstSection;
254
+ for (const pointer of budgeted.slice(0, 3)) {
255
+ if (pointer.bytes > 0 && pointer.bytes <= FIRST_SECTION_INLINE_BUDGET_BYTES) {
256
+ const resolved = readSkillSection(pointer.topic, pointer.section);
257
+ if (resolved) {
258
+ firstSection = { topic: pointer.topic, section: pointer.section, bytes: pointer.bytes, content: resolved.section };
259
+ break;
260
+ }
261
+ }
262
+ }
263
+ return { readSections: budgeted, readCommands: batchReadCommands(readSections), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
241
264
  }
242
265
  /** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
243
266
  function sectionBytes(pointer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.17",
3
+ "version": "0.4.18",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "commander": "14.0.3",
54
- "omc-core": "0.4.17",
54
+ "omc-core": "0.4.18",
55
55
  "proper-lockfile": "4.1.2",
56
56
  "write-file-atomic": "6.0.0",
57
57
  "zod": "^3.23.8",