@stalfh233/omc-cli 0.6.11 → 0.6.13

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.
Files changed (58) hide show
  1. package/coverage/m1-coverage-manifest-v1.json +2 -0
  2. package/dist/approval-flow.js +84 -3
  3. package/dist/batch-call.js +32 -2
  4. package/dist/bench/call-face-eval.js +2 -2
  5. package/dist/call.js +35 -2
  6. package/dist/capability-overview.js +21 -7
  7. package/dist/cli-contract.js +34 -4
  8. package/dist/cli-output.js +6 -2
  9. package/dist/cli.js +303 -69
  10. package/dist/describe-cache.js +8 -4
  11. package/dist/environment-policy.js +20 -0
  12. package/dist/field-families.js +62 -4
  13. package/dist/form-support.js +293 -6
  14. package/dist/goal-routes.js +137 -22
  15. package/dist/identity.js +63 -11
  16. package/dist/lanes/app-contract.js +4 -4
  17. package/dist/lanes/app.js +19 -1
  18. package/dist/lanes/dictionary-contract.js +16 -5
  19. package/dist/lanes/dictionary-coverage.js +1 -0
  20. package/dist/lanes/dictionary.js +200 -8
  21. package/dist/lanes/environment-contract.js +18 -6
  22. package/dist/lanes/environment.js +129 -16
  23. package/dist/lanes/field-contract.js +2 -1
  24. package/dist/lanes/field.js +707 -59
  25. package/dist/lanes/form.js +52 -6
  26. package/dist/lanes/list-view-contract.js +2 -2
  27. package/dist/lanes/list-view.js +98 -2
  28. package/dist/lanes/meta.js +36 -10
  29. package/dist/lanes/model-contract.js +7 -6
  30. package/dist/lanes/model.js +154 -10
  31. package/dist/lanes/organization.js +67 -7
  32. package/dist/lanes/runtime-data-contract.js +4 -2
  33. package/dist/lanes/runtime-data.js +395 -41
  34. package/dist/lanes/workflow-contract.js +2 -2
  35. package/dist/lanes/workflow.js +381 -11
  36. package/dist/model-scaffold.js +11 -3
  37. package/dist/path-projection.js +192 -32
  38. package/dist/read-shapes.js +31 -11
  39. package/dist/recipes/field.js +16 -3
  40. package/dist/recipes/form.js +2 -1
  41. package/dist/recipes/runtime-data.js +58 -8
  42. package/dist/recipes/workflow.js +86 -1
  43. package/dist/refusal-guidance.js +18 -6
  44. package/dist/rollback-spec.js +41 -2
  45. package/dist/rollback.js +49 -11
  46. package/dist/script.js +72 -12
  47. package/dist/skills.js +91 -3
  48. package/dist/tools.js +113 -1
  49. package/dist/wire.js +13 -1
  50. package/dist/write-lock.js +92 -4
  51. package/dist/write.js +129 -16
  52. package/package.json +5 -3
  53. package/skills/omc-business-configuration.md +1 -1
  54. package/skills/omc-capabilities.md +5 -4
  55. package/skills/omc-playbook.md +41 -0
  56. package/skills/omc-refusals.md +58 -6
  57. package/skills/omc-tool-selection.md +2 -0
  58. package/skills/omc-write-hazards.md +3 -2
@@ -220,6 +220,24 @@ export function pathMissEntryHint(at, available) {
220
220
  * available 不是字符串数组(范围说明 / 空对象文案)或匹配不到时不加字段——
221
221
  * 误导建议比没有更糟;过短输入由 closestMatch 自己静默。
222
222
  */
223
+ /**
224
+ * 读面里「人人都会猜错一次」的键名(2026-11-05 用户 Windows 实测):
225
+ * `app.list` 回的是 `applications`,但 `--jq .apps[]` 是所有人的第一反应,
226
+ * 而 closestMatch(编辑距离)判不了 app→application 这种**语义改名**,于是
227
+ * pathMiss 只给键列表,用户要再猜一轮。这里补一张极小的已知别名表。
228
+ * 只放**已证实的高频误写**,不要变成第二套命名。
229
+ */
230
+ const PATH_KEY_ALIASES = Object.freeze({
231
+ apps: "applications",
232
+ application: "applications",
233
+ app: "applications",
234
+ models: "items",
235
+ rows: "items",
236
+ list: "items",
237
+ content: "publishedContent",
238
+ data: "fields",
239
+ props: "properties",
240
+ });
223
241
  export function pathMissDidYouMean(at, available) {
224
242
  if (at === undefined || at.length === 0)
225
243
  return undefined;
@@ -233,7 +251,12 @@ export function pathMissDidYouMean(at, available) {
233
251
  const leaf = at.includes(".") || at.includes("/") || at.includes("[")
234
252
  ? at.replace(/^[.\/]+/, "").split(/[./\[]/).filter(Boolean).pop() ?? at
235
253
  : at;
236
- return closestMatch(leaf, keys);
254
+ const closest = closestMatch(leaf, keys);
255
+ if (closest !== undefined)
256
+ return closest;
257
+ // 语义改名(apps→applications 这类编辑距离判不出的),只在该键**确实存在**时建议。
258
+ const alias = PATH_KEY_ALIASES[leaf.toLowerCase()];
259
+ return alias !== undefined && keys.includes(alias) ? alias : undefined;
237
260
  }
238
261
  /**
239
262
  * 多路径一次投影时的**唯一一份**语法说明(放结果顶层,不再逐条复制)。
@@ -281,6 +304,98 @@ function resultLayerNote(result, path) {
281
304
  resultLayerHint: `「${head}」在 result 顶层(只是不在 artifact 副本里)。取它请不带 --jq 直接读回执,或改取 artifact 内的键(见 available)`,
282
305
  };
283
306
  }
307
+ /** Envelope keys of the stdout wrapper — never part of the tool result. */
308
+ const ENVELOPE_HEAD_KEYS = new Set(["ok", "status", "result", "error", "requestId"]);
309
+ /**
310
+ * r29f F1:调用方按信封写 `--jq result.applications[].code` 时首段 pathMiss。
311
+ * 首段恰为信封键时剥掉重试一次;成功则标 `autoStripped`,失败保持原 miss。
312
+ */
313
+ function stripEnvelopeHead(path) {
314
+ const trimmed = path.replace(/^\$\.?/, "").replace(/^[.\/]+/, "");
315
+ const match = /^([A-Za-z_][\w]*)/.exec(trimmed);
316
+ if (match === null)
317
+ return { path };
318
+ const head = match[1];
319
+ if (!ENVELOPE_HEAD_KEYS.has(head))
320
+ return { path };
321
+ const rest = trimmed.slice(head.length).replace(/^[.\/]+/, "");
322
+ return { path: rest, stripped: head };
323
+ }
324
+ /**
325
+ * r29f F5:外置后 `--jq` 在 artifact 正文上求值,回执派生键(*Count / *Omitted /
326
+ * detailArtifactId / from)消失。把回执派生键合进视图:artifact 同名键优先,
327
+ * 回执派生键补位;保留 `from:"artifact"`。
328
+ */
329
+ function isReceiptDerivedKey(key) {
330
+ // `detailFile` 故意**不在此列**:它是本地文件系统路径(进程内才有效),
331
+ // 合进投影视图会让 `--keys` 把本机路径当"结果键"吐出来(2026-11-05 回归:
332
+ // readpath-roundtrip 1.2 的 keys 断言多了 detailFile)。对外只认 artifactId。
333
+ return /(?:Count|Omitted)$/.test(key) || key === "detailArtifactId" || key === "detailSha256" || key === "from" || key === "truncated" || key === "truncatedKeys" || key === "fetch" || key === "tablesHint";
334
+ }
335
+ function mergeReceiptDerivedKeys(artifactSource, result) {
336
+ if (artifactSource === null || typeof artifactSource !== "object" || Array.isArray(artifactSource))
337
+ return artifactSource;
338
+ const view = { ...artifactSource };
339
+ for (const [key, value] of Object.entries(result)) {
340
+ if (!isReceiptDerivedKey(key))
341
+ continue;
342
+ if (Object.prototype.hasOwnProperty.call(view, key))
343
+ continue;
344
+ view[key] = value;
345
+ }
346
+ return view;
347
+ }
348
+ function projectPathOnSource(source, path) {
349
+ const tryOnce = (candidate) => {
350
+ if (candidate.includes("[]"))
351
+ return { projected: projectJsonPathFanout(source, candidate), fanout: true };
352
+ return { projected: projectJsonPath(source, candidate), fanout: false };
353
+ };
354
+ const first = tryOnce(path);
355
+ if (!first.projected.missed)
356
+ return { ...first };
357
+ const stripped = stripEnvelopeHead(path);
358
+ // r30a F7/F8: stripping `status`/`ok` down to an EMPTY path used to return
359
+ // the whole source object (looked like a successful answer). Keep those as
360
+ // pathMiss. r31c F6: `--jq result` is the one intentional alias — callers
361
+ // habitually mean "the whole result package", so return it with a note
362
+ // instead of miss-then-hint.
363
+ if (stripped.stripped === undefined || stripped.path === path)
364
+ return { ...first };
365
+ if (stripped.path.trim() === "") {
366
+ if (stripped.stripped === "result") {
367
+ return {
368
+ projected: { missed: false, value: source },
369
+ fanout: false,
370
+ autoStripped: {
371
+ field: "result",
372
+ reason: "--jq 的路径相对 result;写 result 等于整包",
373
+ },
374
+ };
375
+ }
376
+ return {
377
+ projected: {
378
+ missed: true,
379
+ at: stripped.stripped,
380
+ available: Array.isArray(source) || source === null || typeof source !== "object"
381
+ ? `「${stripped.stripped}」是信封顶层键;投影相对 result,剥掉后路径为空——不要用 --jq ${stripped.stripped} 取信封字段`
382
+ : Object.keys(source).slice(0, 12),
383
+ },
384
+ fanout: false,
385
+ };
386
+ }
387
+ const second = tryOnce(stripped.path);
388
+ if (!second.projected.missed) {
389
+ return {
390
+ ...second,
391
+ autoStripped: {
392
+ field: stripped.stripped,
393
+ reason: `剥掉了信封前缀「${stripped.stripped}.」;投影相对 result,请直接写 --jq ${stripped.path || "<path>"}`,
394
+ },
395
+ };
396
+ }
397
+ return { ...first };
398
+ }
284
399
  export function projectResultExtract(result, paths, keysOnly) {
285
400
  const artifactId = typeof result.detailArtifactId === "string" ? result.detailArtifactId : typeof result.artifactId === "string" ? result.artifactId : undefined;
286
401
  // Tools hand an in-process artifact as `detailFile` (a local path);
@@ -305,12 +420,19 @@ export function projectResultExtract(result, paths, keysOnly) {
305
420
  catch {
306
421
  source = text;
307
422
  }
423
+ // r29f F5:外置后投影视图 = artifact 正文 ∪ 回执派生键(artifact 优先)。
424
+ source = mergeReceiptDerivedKeys(source, result);
308
425
  }
309
426
  }
310
427
  catch {
311
428
  // A pruned/expired artifact must not mask the real tool answer.
312
429
  return { ...result, extract: { artifactId: artifactId ?? null, error: "artifact not found", hint: "产物已过期或被清理;去掉 --extract 重新调用该工具即可重新生成" } };
313
430
  }
431
+ // r30a F8: when projecting against an artifact that is a write-history record
432
+ // (tool/requestId/performed/…), prefer the in-memory result for keys that
433
+ // exist on the result but would silently resolve to a wrong-shaped value on
434
+ // the artifact (or miss entirely). Never fall back to "parent object".
435
+ const projectionSource = source;
314
436
  const out = {
315
437
  status: result.status ?? "ok",
316
438
  ...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
@@ -351,68 +473,106 @@ export function projectResultExtract(result, paths, keysOnly) {
351
473
  hint: unsupported,
352
474
  };
353
475
  }
354
- // `items[].fields.planNo` maps over every row; a plain path stays a single
355
- // value (issue 2026-10-25 §8). Both faces share one shape so a caller can
356
- // switch between `--extract`/`--jq` without relearning the envelope.
357
- if (path.includes("[]")) {
358
- const fanout = projectJsonPathFanout(source, path);
359
- if (fanout.missed) {
360
- const didYouMean = pathMissDidYouMean(fanout.at, fanout.available);
476
+ // Prefer in-memory result when the path resolves there and would miss
477
+ // on the artifact (never silently substitute a wrong-shaped parent object).
478
+ const onArtifact = projectPathOnSource(projectionSource, path);
479
+ const onResult = readFromArtifact ? projectPathOnSource(result, path) : undefined;
480
+ const preferResult = onResult !== undefined && !onResult.projected.missed && onArtifact.projected.missed ? onResult : undefined;
481
+ const { projected, fanout, autoStripped } = preferResult ?? onArtifact;
482
+ if (fanout) {
483
+ const fan = projected;
484
+ if (fan.missed) {
485
+ const didYouMean = pathMissDidYouMean(fan.at, fan.available);
361
486
  return {
362
487
  path,
363
488
  content: null,
364
489
  pathMiss: true,
365
- missedAt: fanout.at,
366
- available: fanout.available,
367
- missedEntries: fanout.missedEntries,
490
+ missedAt: fan.at,
491
+ available: fan.available,
492
+ missedEntries: fan.missedEntries,
368
493
  ...(didYouMean !== undefined ? { didYouMean } : {}),
369
494
  ...(readFromArtifact ? resultLayerNote(result, path) ?? {} : {}),
370
- hint: missHintFor(fanout.at, fanout.available),
495
+ hint: missHintFor(fan.at, fan.available),
371
496
  };
372
497
  }
373
498
  return {
374
499
  path,
375
- content: fanout.values,
376
- mapped: fanout.values.length,
377
- ...(fanout.nullEntries > 0 ? { nullEntries: fanout.nullEntries } : {}),
378
- ...(fanout.nullEntries > 0 && fanout.nullEntries === fanout.values.length ? { hint: ALL_NULL_HINT } : {}),
379
- ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries, hint: `有 ${fanout.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}),
500
+ content: fan.values,
501
+ mapped: fan.values.length,
502
+ ...(autoStripped !== undefined ? { autoStripped } : {}),
503
+ ...(preferResult ? { from: "result" } : {}),
504
+ ...(fan.nullEntries > 0 ? { nullEntries: fan.nullEntries } : {}),
505
+ ...(fan.nullEntries > 0 && fan.nullEntries === fan.values.length ? { hint: ALL_NULL_HINT } : {}),
506
+ ...(fan.missedEntries > 0 ? { missedEntries: fan.missedEntries, hint: `有 ${fan.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}),
380
507
  };
381
508
  }
382
- const projected = projectJsonPath(source, path);
383
- if (projected.missed) {
384
- // projectJsonPath 也会拦 unsupportedSyntax;双保险统一出口。
385
- if (projected.unsupportedSyntax === true) {
509
+ const single = projected;
510
+ if (single.missed) {
511
+ if (single.unsupportedSyntax === true) {
386
512
  return {
387
513
  path,
388
514
  content: null,
389
515
  pathMiss: false,
390
516
  unsupportedSyntax: true,
391
517
  unanswered: true,
392
- available: projected.available,
393
- hint: typeof projected.available === "string" ? projected.available : UNSUPPORTED_JQ_OBJECT_HINT,
518
+ available: single.available,
519
+ hint: typeof single.available === "string" ? single.available : UNSUPPORTED_JQ_OBJECT_HINT,
394
520
  };
395
521
  }
396
- const didYouMean = pathMissDidYouMean(projected.at, projected.available);
522
+ const didYouMean = pathMissDidYouMean(single.at, single.available);
397
523
  return {
398
524
  path,
399
525
  content: null,
400
526
  pathMiss: true,
401
- missedAt: projected.at,
402
- available: projected.available,
527
+ missedAt: single.at,
528
+ available: single.available,
403
529
  ...(didYouMean !== undefined ? { didYouMean } : {}),
404
530
  ...(readFromArtifact ? resultLayerNote(result, path) ?? {} : {}),
405
- hint: missHintFor(projected.at, projected.available),
531
+ hint: missHintFor(single.at, single.available),
406
532
  };
407
533
  }
408
- return { path, content: projected.value ?? null };
534
+ return {
535
+ path,
536
+ content: single.value ?? null,
537
+ ...(autoStripped !== undefined ? { autoStripped } : {}),
538
+ ...(preferResult ? { from: "result" } : {}),
539
+ };
409
540
  });
410
- if (paths.length === 1)
411
- out.content = out.extract[0]?.content ?? null;
541
+ // r30a F21 / r31c F7: single path keeps top-level `content`. Multi-path used
542
+ // to ALSO dump a path→value `content` map that duplicated `extract[]`
543
+ // (probe: roles[].name + roles[].code returned twice). Keep ONE shape:
544
+ // extract[] for per-path detail, plus `rows` that zip parallel array
545
+ // projections so two columns align without a second round trip.
546
+ const extracts = out.extract;
547
+ if (paths.length === 1) {
548
+ out.content = extracts[0]?.content ?? null;
549
+ }
550
+ else {
551
+ const columns = extracts.map((entry) => ({
552
+ path: entry.path,
553
+ values: entry.pathMiss === true || entry.unsupportedSyntax === true
554
+ ? []
555
+ : Array.isArray(entry.content)
556
+ ? entry.content
557
+ : entry.content === undefined
558
+ ? []
559
+ : [entry.content],
560
+ }));
561
+ const rowCount = Math.max(0, ...columns.map((column) => column.values.length));
562
+ const rows = [];
563
+ for (let index = 0; index < rowCount; index += 1) {
564
+ const row = {};
565
+ for (const column of columns)
566
+ row[column.path] = index < column.values.length ? column.values[index] : null;
567
+ rows.push(row);
568
+ }
569
+ out.rows = rows;
570
+ out.rowCount = rowCount;
571
+ // Deliberately NO top-level `content` on multi-path — that was the duplicate.
572
+ }
412
573
  // A projection that missed is not an answer. `status` stays `ok` (adding a
413
574
  // new status value would fall through classifyResult to error/exit 15 and
414
575
  // change the exit contract), but the envelope now says so out loud.
415
- const extracts = out.extract;
416
576
  if (extracts.some((entry) => entry.pathMiss === true || entry.unsupportedSyntax === true)) {
417
577
  out.unanswered = true;
418
578
  // 多路径:语法说明给一次,不再逐条复制。不支持语法优先于 pathMiss 语法课。
@@ -420,7 +580,7 @@ export function projectResultExtract(result, paths, keysOnly) {
420
580
  out.hint = UNSUPPORTED_JQ_OBJECT_HINT;
421
581
  }
422
582
  else if (multiPath) {
423
- out.hint = `${RESULT_RELATIVE_PATH_NOTE}${JQ_PATH_SYNTAX}${JQ_PATH_HELP_POINTER}`;
583
+ out.hint = `${RESULT_RELATIVE_PATH_NOTE}${JQ_PATH_SYNTAX}多路径时用 extract[](逐条)或 rows[](列对齐);不再重复 content 映射。${JQ_PATH_HELP_POINTER}`;
424
584
  }
425
585
  }
426
586
  }
@@ -12,15 +12,16 @@
12
12
  const READ_SHAPES = {
13
13
  // r28k 治理面形状钉:截断≠空表;指纹有用途;doctor/get_status 分流。
14
14
  "discover_environment": {
15
- keys: ["applications:array<{code,name,groupId?,portalHidden?}>", "applicationsCount", "groups", "groupsCount", "foldersAndModels?", "foldersAndModelsCount?", "foldersAndModelsOmitted?", "models?", "modelsCount?", "modelsOmitted?", "tablesHint?", "applicationCode", "applicationResolution", "readiness", "environmentFingerprint", "fingerprintHint?", "detailArtifactId?"],
15
+ keys: ["applications:array<{code,name,groupId?,portalHidden?}>", "applicationsCount", "groups", "groupsCount", "foldersAndModels?", "foldersAndModelsCount?", "foldersAndModelsOmitted?", "models?", "modelsCount?", "modelsOmitted?", "tablesHint?", "applicationCode", "applicationResolution", "readiness", "detectedVersion", "inAnchorFamily", "versionSource", "versionFamily", "anchorVersion", "versionNotice?", "environmentFingerprint", "fingerprintHint?", "detailArtifactId?"],
16
16
  notes: [
17
17
  "applications/groups 内联;foldersAndModels/models 外置时是 null + *Omitted:true(不是 [])",
18
18
  "environmentFingerprint 用途见 fingerprintHint(写前漂移/计划绑定/读缓存键,非审批令牌)",
19
+ "detectedVersion/inAnchorFamily/versionSource 与 doctor/fingerprint 同源",
19
20
  ],
20
21
  },
21
22
  "environment.fingerprint": {
22
- keys: ["environment", "fingerprint", "fingerprintHint"],
23
- notes: ["fingerprint 随 env/origin/application/enterprise 变;写前漂移检测 + 计划/审计绑定"],
23
+ keys: ["environment", "fingerprint", "fingerprintHint", "detectedVersion", "inAnchorFamily", "versionSource", "versionFamily", "anchorVersion", "versionNotice?"],
24
+ notes: ["fingerprint 随 env/origin/application/enterprise 变;写前漂移/计划绑定", "版本证据与 discover/doctor 同源"],
24
25
  },
25
26
  "get_status": {
26
27
  keys: ["environment", "session", "coverage", "environmentScope", "unsupportedCapabilities?", "writeLease"],
@@ -31,8 +32,8 @@ const READ_SHAPES = {
31
32
  notes: ["unsupportedCapabilities.coverageClaimMeaning:只列已知缺口;未列 ≠ 已验证可用"],
32
33
  },
33
34
  "coverage.manifest": {
34
- keys: ["manifest:{version_family,anchor_version,items[],counts?,filter?,hint?}"],
35
- notes: ["按 lane 索引(同 lane 可多行);答能力能不能用 → omc capability <id>"],
35
+ keys: ["manifest:{version_family,anchor_version,items[],counts?{total,totalScope,totalBeforeFilter?,byStatus},filter?,hint?}"],
36
+ notes: ["按 lane 索引(同 lane 可多行);capability 参数可传能力描述名或 lane id(如 W1-list-view);答能力能不能用 → omc capability listview"],
36
37
  },
37
38
  "apply_changes": {
38
39
  keys: ["status", "steps?", "failedStep?", "progressFile?", "deliveryState?"],
@@ -42,7 +43,21 @@ const READ_SHAPES = {
42
43
  // items are {id, fields:{<code>: value}} — business columns live under fields.
43
44
  keys: ["items:array<{id,fields:{<code>:value}}>", "total", "totalPages", "page", "size"],
44
45
  // r28i F4: agents try rows[] / items[].sku first; pin the real shape here too.
45
- notes: ["业务列在 items[].fields.<code>,不在 item 顶层,也不是 rows[](--jq items[].sku / rows[].sku 会 miss;正确:items[].fields.sku)"],
46
+ notes: [
47
+ "业务列在 items[].fields.<code>,不在 item 顶层,也不是 rows[](--jq items[].sku / rows[].sku 会 miss;正确:items[].fields.sku)",
48
+ "字典字段回记录 code;显示名需 dictionary.records 映射(伴生 *_key 常为 null,勿当显示名)",
49
+ ],
50
+ },
51
+ "dictionary.records": {
52
+ keys: ["records:{totalElements,content[]}", "total", "orderNote", "enabledOnly?", "dictionaryId?", "parentId?"],
53
+ notes: ["enabledOnly/parentId 也走同一分页壳,不再回裸数组"],
54
+ },
55
+ "dictionary.get": {
56
+ keys: ["dictionary:{id,code,name,...}", "recordsCount?"],
57
+ notes: ["头读面不含 records 明细;条数看 recordsCount,明细走 dictionary.records"],
58
+ },
59
+ "dictionary.references": {
60
+ keys: ["dictionary", "references:array<{schemaCode,fieldCode,fieldName,propertyType}>", "count", "truncated", "modelsScanned"],
46
61
  },
47
62
  "folder.tree": {
48
63
  keys: ["applicationCode", "applicationExists", "nodeCount", "folderCount", "modelCount", "nodes[]", "note"],
@@ -55,12 +70,17 @@ const READ_SHAPES = {
55
70
  "data.load": {
56
71
  keys: ["bizObjectId", "exists", "data:{...}"],
57
72
  },
73
+ "app.list": {
74
+ keys: ["applications:array<{code,name,groupId?}>", "applicationsCount", "omitted"],
75
+ notes: ["applicationsCount + omitted 与 discover 同构;omitted:false 表示内联即全集"],
76
+ },
58
77
  "model.list": {
59
78
  // Compact diet may rewrite models to "code|name" pairs and add modelsCount.
60
- keys: ["models:array<code|name>", "count", "modelsCount", "scopeHint"],
79
+ keys: ["models:array<{code,schemaCode,name}>", "count", "modelsCount", "applicationCode", "applicationResolution", "scopeHint"],
61
80
  notes: [
62
- "下游工具的 schemaCode = 本面 models 的 code(内联 diet 后是「code|name」里的 code)",
63
- "modelsCount 是内联 result 键;若 --jq 走 artifact 副本可能 pathMiss——用 --jq count 或先看内联 result",
81
+ "下游工具的 schemaCode = 本面 models 的 code",
82
+ "applicationCode 不在 app.list → app-not-found(applicationResolution.status=unresolved)",
83
+ "modelsCount 等派生键外置后也可 --jq(投影层会合进 artifact 视图)",
64
84
  ],
65
85
  },
66
86
  "model.inspect": {
@@ -135,8 +155,8 @@ const READ_SHAPES = {
135
155
  notes: ["roleId 与 roleCode 二者其一;members[].id 是用户 recId"],
136
156
  },
137
157
  "org.role.userScope": {
138
- keys: ["roleId", "userId", "count", "scopes:array<{id,name,type,deptPath}>"],
139
- notes: ["不是枚举档位:scopes 是部门节点列表;roleId 与 roleCode 二者其一"],
158
+ keys: ["roleId", "userId", "count", "scopes:array<{id,deptId,name,type,deptPath,includeChildren,source}>", "actor?", "readback"],
159
+ notes: ["不是枚举档位:scopes 是部门节点列表;roleId 与 roleCode 二者其一;includeChildren 平台不返回(unknown);source:configured;空结果是合法空集"],
140
160
  },
141
161
  };
142
162
  /** Lookup the authored return shape for a tool, if any. */
@@ -148,6 +148,20 @@ const dictionarySelection = Object.freeze({
148
148
  requiresEnvironmentDerivedIds: true,
149
149
  prerequisiteData: { source: "dictionary.names → dictionary.records(enabledOnly:true)", fields: ["dictionary.code", "record.code", "record.id"] },
150
150
  });
151
+ const dictionaryMultiSelection = Object.freeze({
152
+ id: "dictionary-backed-multi-select",
153
+ title: "字典多选",
154
+ status: "requires-prior-readback",
155
+ tool: "field.create",
156
+ args: { environment, schemaCode: model, code: "tags", name: "标签", propertyType: "DROPDOWN_MULTI_BOX", options: { optionsSet: { optionsType: "dictionary", dictionary: { checkedDictionary: "<id>", checkedDictionaryCode: "<code>", useDictionariesData: [{ code: "<c>", id: "<id>" }] } } } },
157
+ preconditions: ["模型/字典可用"],
158
+ conditionalRequirements: ["optionsSet同单选;值=分号串"],
159
+ priorToolReferences: [],
160
+ dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "" },
161
+ expectedReadback: { next: ["field.publish"] },
162
+ refusalRecovery: commonRecovery,
163
+ requiresEnvironmentDerivedIds: true,
164
+ });
151
165
  const childTable = Object.freeze({
152
166
  id: "minimal-child-table",
153
167
  title: "创建带两个标量列的子表字段",
@@ -315,8 +329,7 @@ const externalLookupModel = Object.freeze({
315
329
  conditionalRequirements: [
316
330
  "**数据段首选(平台标准模式,2026-10-29 playwright 门户实证定案)**:映射模型的 GetList/Load 规则挂 BIZ_ACTION 实时调接口(接口方法配 frontEndExtensionEnable:true)——门户关联控件**打开即触发**前端查询链→GetList 规则→BIZ_ACTION 实时调外部接口(引擎日志 bizBusService.invoke + 外部网关日志双侧铁证)。**验证必须走门户填表;data.list 后端查询不反映此链,别用它判死**(r29 曾据此误判不成立)。配置侧三个收尾点:① WORK_SHEET 字段必须带 relativeCode(仅 options.schemaCode 发布表单会 301015 关联业务模型为空);② GetList 规则的 BIZ_ACTION 入参映射要接前端搜索词(实测未配词时 invoke 参数为空 {});③ 出参→列表行的映射配完候选才回显(未配时弹窗 Total 0 但接口已调)。「下拉框形态(输关键字回车查询)」是表单 attributes 层的控件形态设置,默认弹窗形态。备选(不需要实时时):预置 Create/Update 规则落映射表 / TIMING 定时同步。图内「BIZ_ACTION→CREATE 落表→再查」回环在 1.1.25 不可表达,不要走这条岔路。",
317
331
  "**表单段是平台原生能力**:关联单选指向映射模型后,门户上输入关键字回车即查映射表、选中即 mappings 带出——前台填表时映射自然出现,无需按钮/脚本。",
318
- "下拉框形态(输入关键字回车查询而非弹窗选择)在表单 attributes 层配置(form.draft 改控件形态),不在字段 options。",
319
- "映射模型表要有数据下拉才有选项——空表=空下拉;数据段先通再配表单段。",
332
+ "下拉框形态在 form.draft 控件层配置;映射表要有数据下拉才有选项。",
320
333
  ],
321
334
  priorToolReferences: [
322
335
  { tool: "model.create", args: { environment }, purpose: "建映射模型(字段与接口返回一一对应)" },
@@ -333,6 +346,6 @@ const externalLookupModel = Object.freeze({
333
346
  });
334
347
  export const FIELD_METADATA = Object.freeze({
335
348
  "field.update": Object.freeze({ recipes: [fieldSerialNumber, formConditionalRequired, formAlwaysReadonly, formConditionalReadonly], relatedTools: ["field.list", "field.publish", "form.draft", "form.publish"] }),
336
- "field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, childTable, businessModelDropdown, formulaAggregationCrossModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
349
+ "field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, dictionaryMultiSelection, childTable, businessModelDropdown, formulaAggregationCrossModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
337
350
  });
338
351
  //# sourceMappingURL=field.js.map
@@ -36,7 +36,7 @@ const formDraftTwoColumn = Object.freeze({
36
36
  draftViewJson: '[[\"visitNo\",\"customer\"],[\"remark\"]]',
37
37
  draftAttributesJson: JSON.stringify({
38
38
  visitNo: { key: "visitNo", type: 1, options: { name: "拜访单号", span: 12 } },
39
- customer: { key: "customer", type: 1, options: { name: "客户", span: 12 } },
39
+ customer: { key: "customer", type: 9, options: { name: "客户", span: 12 } },
40
40
  remark: { key: "remark", type: 1, options: { name: "备注", span: 24 } },
41
41
  }, null, 0),
42
42
  },
@@ -44,6 +44,7 @@ const formDraftTwoColumn = Object.freeze({
44
44
  conditionalRequirements: [
45
45
  "**列数由行分组决定,不由 span 决定**:一行放几个 code 就是几列;span 只控制控件在行内的 24 栅格宽度,且必须与行分组自洽(两列 span:12+12,整宽 span:24)",
46
46
  "draftAttributesJson 的每个控件的 `options` 必须是**对象**,不能是 JSON 字符串(否则运行时写 options.name_i18n 抛错、整页空白)",
47
+ "控件 type 与 field propertyType 数字同源(WORK_SHEET=9)",
47
48
  "rows 与 attrs 的 code 集合必须一致,否则 `layout_coverage_mismatch`",
48
49
  "改列数就改行分组、重写 attrs 的 span —— 不要只改 span",
49
50
  ],
@@ -76,6 +76,7 @@ const dataListFilter = Object.freeze({
76
76
  conditionalRequirements: [
77
77
  "filter 是**一个**子句 `{op, field, value?}`;op ∈ eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull(camelCase 别名 equals/notEquals/contains/isNull/isNotNull 等价——r36 体检发现别名合法但只列规范名会被误判『静默接受坏 op』)。",
78
78
  "多条件 `filters:[…]`(AND);分组 `filterGroup:{op:'and'|'or',clauses:[…]}`;系统/外部字段用 rawCondition。",
79
+ "列投影:`fields` / `displayFields` / `columns` 三名同义(与 data.load 的 fields 对齐)。",
79
80
  ],
80
81
  priorToolReferences: [
81
82
  { tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认要过滤的字段 code 已发布" },
@@ -85,30 +86,79 @@ const dataListFilter = Object.freeze({
85
86
  refusalRecovery: [{ code: "invalid-filter-DSL", action: "按错误里的 minimalValidFilter/acceptedOps 修正 op/field/value" }],
86
87
  requiresEnvironmentDerivedIds: false,
87
88
  });
89
+ const childRowsUpsert = Object.freeze({
90
+ id: "child-rows-upsert",
91
+ title: "子表三合一:改既有行 / 追加 / 显式删除",
92
+ status: "offline-valid",
93
+ tool: "data.update",
94
+ args: {
95
+ environment,
96
+ schemaCode: model,
97
+ bizObjectId: "<data.load 的 id>",
98
+ formData: {
99
+ items: [
100
+ { id: "<既有行 id>", qty: 2, price: 10, amount: 20 },
101
+ { sku: "NEW", qty: 1, price: 5, amount: 5, rowStatus: "Added" },
102
+ { id: "<要删的行 id>", rowStatus: "Deleted" },
103
+ ],
104
+ },
105
+ },
106
+ preconditions: ["data.load 拿子行 id"],
107
+ conditionalRequirements: [
108
+ "既有行带 id(省 id 当新增);追加 rowStatus:Added;删除 {id,rowStatus:Deleted};**少传行不删除**(readback.warnings: child-rows-not-deleted)",
109
+ ],
110
+ priorToolReferences: [
111
+ { tool: "data.load", args: { environment, schemaCode: model, bizObjectId: "<id>" }, purpose: "取子行 id" },
112
+ ],
113
+ dryRun: { supported: true, command: `omc call data.update --env ${environment} --dry-run --args '<recipe.args>'`, notes: "校验子行形状" },
114
+ expectedReadback: { readback: { authoritative: "子表行按 Added/Modified/Deleted 落地" } },
115
+ refusalRecovery: [
116
+ { code: "child-rows-missing-id", action: "无 id 的行补 id 或改 rowStatus:Added" },
117
+ ],
118
+ notes: "data.save 新建时子行可不带 id(工具 stamp Added);本配方指 update/既有记录",
119
+ requiresEnvironmentDerivedIds: true,
120
+ });
88
121
  const dataDeleteSweep = Object.freeze({
89
122
  id: "sweep-all-readback",
90
- title: "清空模型数据并权威验证(list 取 ids → delete → 复验 total:0)",
123
+ title: "清空模型数据并权威验证(list 取 ids → 逐条 delete → 逐条 load 确认 absent)",
91
124
  status: "offline-valid",
92
125
  tool: "data.delete",
93
- args: { environment, schemaCode: model, bizObjectIds: ["<data.list 逐条 id>"] },
126
+ // 1.1.x 上 bizObjectIds:[多个] 是假成功(blocker:data-delete-batch-fake-success)。
127
+ // recipe.args 给单条形状;多条用 dryRun.command 的 --args-list 一次跑完。
128
+ args: { environment, schemaCode: model, bizObjectId: "<data.list 一条 id>" },
94
129
  preconditions: [
95
130
  "先 data.list 取全量 ids(`all:true` 才含 DRAFT;翻页取全),清理对象按 data.list 全量",
96
131
  "**行数以 data.list total 为准,别按「N 提交=2N 行」外推**:带流程的 submit 原地推进同一行(r28b 实测 3→3);双行只在平台另起草稿行时出现(r30 的 5→10)",
97
132
  ],
98
133
  conditionalRequirements: [
99
- "参数名是 `bizObjectIds`(数组)——不是 ids/bizObjectId(单条删除另有其参)",
134
+ "1.1.x 上批量 `bizObjectIds` 是假成功(回执 DELIVERED+absent:true 但行仍在),必须单条 `bizObjectId`;用 `--args-list` 一次跑多条单删,不要抄 `bizObjectIds:[多个]`",
135
+ "删完逐条 `data.load` 确认 `exists:false`(`absent:true` / `modelResidue.total` 递减都不是删除成功的证明);最后再 `data.list` 复验 total:0",
100
136
  "id 已被删时幂等 no-op,重复清理安全",
101
137
  ],
102
138
  priorToolReferences: [
103
- { tool: "data.list", args: { environment, schemaCode: model }, purpose: "取全量 bizObjectIds(含 DRAFT 行)" },
139
+ { tool: "data.list", args: { environment, schemaCode: model }, purpose: "取全量 ids(含 DRAFT 行),再拆成逐条 bizObjectId" },
140
+ ],
141
+ dryRun: {
142
+ supported: true,
143
+ command: `omc call data.delete --env ${environment} --args-list '[{"schemaCode":"${model}","bizObjectId":"<id1>"},{"schemaCode":"${model}","bizObjectId":"<id2>"}]'`,
144
+ notes: "多条用 --args-list 逐条单删(每条一个 bizObjectId);不要用 bizObjectIds 数组。dry-run 可加 --dry-run 预检不落库",
145
+ },
146
+ expectedReadback: {
147
+ readback: { deleted: true, exists: false },
148
+ next: [
149
+ "对每条 id:omc call data.load --args '{\"schemaCode\":\"…\",\"bizObjectId\":\"<id>\"}' 确认 exists:false",
150
+ "最后 data.list 复验 total:0 / items:[](权威清空判据)",
151
+ ],
152
+ },
153
+ refusalRecovery: [
154
+ { code: "data-delete-batch-fake-success", action: "改用单条 bizObjectId,或 --args-list 一次跑多条单删;不要再传 bizObjectIds:[多个]" },
155
+ { code: "wire-business", action: "模型须已创建且有已发布字段;运行面 50000 时先 model.inspect modelPresent 判存在" },
104
156
  ],
105
- dryRun: { supported: true, command: `omc call data.delete --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 出删除计划不落库" },
106
- expectedReadback: { readback: { deleted: "N", modelResidue: { total: 0 } }, next: ["data.list 复验 total:0 / items:[](权威清空判据)"] },
107
- refusalRecovery: [{ code: "wire-business", action: "模型须已创建且有已发布字段;运行面 50000 时先 model.inspect modelPresent 判存在" }],
108
157
  requiresEnvironmentDerivedIds: false,
109
158
  });
110
159
  export const RUNTIME_DATA_METADATA = Object.freeze({
111
- "data.save": Object.freeze({ recipes: [workflowSave], relatedTools: ["workflow.get_published", "data.submit", "workitem.list"] }),
160
+ "data.save": Object.freeze({ recipes: [workflowSave], relatedTools: ["workflow.get_published", "data.submit", "workitem.list", "data.update"] }),
161
+ "data.update": Object.freeze({ recipes: [childRowsUpsert], relatedTools: ["data.load", "data.save", "data.submit"] }),
112
162
  "data.submit": Object.freeze({ recipes: [plainSubmit, workflowSubmit], relatedTools: ["data.save", "workitem.list", "data.load"] }),
113
163
  "data.list": Object.freeze({ recipes: [dataListFilter], relatedTools: ["data.load", "data.delete", "listview.configure", "field.list"] }),
114
164
  "data.delete": Object.freeze({ recipes: [dataDeleteSweep], relatedTools: ["data.list", "data.load", "data.submit"] }),