@stalfh233/omc-cli 0.6.12 → 0.6.14

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 (59) 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 +8 -2
  8. package/dist/cli.js +313 -72
  9. package/dist/describe-cache.js +8 -4
  10. package/dist/discovery.js +4 -0
  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 +20 -2
  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 +3 -2
  24. package/dist/lanes/field.js +785 -63
  25. package/dist/lanes/form.js +52 -6
  26. package/dist/lanes/list-view-contract.js +3 -3
  27. package/dist/lanes/list-view.js +143 -12
  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 +181 -31
  38. package/dist/read-shapes.js +31 -11
  39. package/dist/recipes/b2-registration.js +1 -1
  40. package/dist/recipes/field.js +16 -3
  41. package/dist/recipes/form.js +4 -3
  42. package/dist/recipes/runtime-data.js +58 -8
  43. package/dist/recipes/workflow.js +86 -1
  44. package/dist/refusal-guidance.js +18 -6
  45. package/dist/rollback-spec.js +41 -2
  46. package/dist/rollback.js +49 -11
  47. package/dist/script.js +72 -12
  48. package/dist/skills.js +91 -3
  49. package/dist/tools.js +113 -1
  50. package/dist/wire.js +13 -1
  51. package/dist/write-lock.js +92 -4
  52. package/dist/write.js +129 -16
  53. package/package.json +3 -3
  54. package/skills/omc-business-configuration.md +1 -1
  55. package/skills/omc-capabilities.md +5 -4
  56. package/skills/omc-playbook.md +41 -0
  57. package/skills/omc-refusals.md +58 -6
  58. package/skills/omc-tool-selection.md +2 -0
  59. package/skills/omc-write-hazards.md +3 -2
@@ -304,6 +304,98 @@ function resultLayerNote(result, path) {
304
304
  resultLayerHint: `「${head}」在 result 顶层(只是不在 artifact 副本里)。取它请不带 --jq 直接读回执,或改取 artifact 内的键(见 available)`,
305
305
  };
306
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 === "total" || 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
+ }
307
399
  export function projectResultExtract(result, paths, keysOnly) {
308
400
  const artifactId = typeof result.detailArtifactId === "string" ? result.detailArtifactId : typeof result.artifactId === "string" ? result.artifactId : undefined;
309
401
  // Tools hand an in-process artifact as `detailFile` (a local path);
@@ -328,17 +420,34 @@ export function projectResultExtract(result, paths, keysOnly) {
328
420
  catch {
329
421
  source = text;
330
422
  }
423
+ // r29f F5:外置后投影视图 = artifact 正文 ∪ 回执派生键(artifact 优先)。
424
+ source = mergeReceiptDerivedKeys(source, result);
331
425
  }
332
426
  }
333
427
  catch {
334
428
  // A pruned/expired artifact must not mask the real tool answer.
335
429
  return { ...result, extract: { artifactId: artifactId ?? null, error: "artifact not found", hint: "产物已过期或被清理;去掉 --extract 重新调用该工具即可重新生成" } };
336
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;
337
436
  const out = {
338
437
  status: result.status ?? "ok",
339
438
  ...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
340
439
  from: readFromArtifact ? "artifact" : "result",
341
440
  };
441
+ // 投影只改变取值路径,不改变源结果的完整性事实。尤其是外置 artifact
442
+ // 场景,调用方仍必须能判断 items 是否被折叠,以及源侧 total/itemsCount。
443
+ // 只透传源中实际存在的键;未截断结果不要凭空写 truncated:false。
444
+ const sourceRecord = source && typeof source === "object" && !Array.isArray(source)
445
+ ? source
446
+ : undefined;
447
+ for (const key of ["truncated", "truncatedKeys", "itemsCount", "total"]) {
448
+ if (sourceRecord !== undefined && Object.prototype.hasOwnProperty.call(sourceRecord, key))
449
+ out[key] = sourceRecord[key];
450
+ }
342
451
  // T6:列表投影里「未配置的列」也是 null 键——工具自己已经写了 projectionHint
343
452
  // (#15 A6),但投影层此前把它丢了,于是 `--jq items[].fields.x` 全 null 时
344
453
  // 调用方看不到任何线索。把源结果的提示原样透传(有才带,零额外体积)。
@@ -374,68 +483,109 @@ export function projectResultExtract(result, paths, keysOnly) {
374
483
  hint: unsupported,
375
484
  };
376
485
  }
377
- // `items[].fields.planNo` maps over every row; a plain path stays a single
378
- // value (issue 2026-10-25 §8). Both faces share one shape so a caller can
379
- // switch between `--extract`/`--jq` without relearning the envelope.
380
- if (path.includes("[]")) {
381
- const fanout = projectJsonPathFanout(source, path);
382
- if (fanout.missed) {
383
- const didYouMean = pathMissDidYouMean(fanout.at, fanout.available);
486
+ // Prefer in-memory result when the path resolves there and would miss
487
+ // on the artifact (never silently substitute a wrong-shaped parent object).
488
+ const onArtifact = projectPathOnSource(projectionSource, path);
489
+ const onResult = readFromArtifact ? projectPathOnSource(result, path) : undefined;
490
+ const preferResult = onResult !== undefined && !onResult.projected.missed && onArtifact.projected.missed ? onResult : undefined;
491
+ const { projected, fanout, autoStripped } = preferResult ?? onArtifact;
492
+ if (fanout) {
493
+ const fan = projected;
494
+ if (fan.missed) {
495
+ const didYouMean = pathMissDidYouMean(fan.at, fan.available);
384
496
  return {
385
497
  path,
386
498
  content: null,
387
499
  pathMiss: true,
388
- missedAt: fanout.at,
389
- available: fanout.available,
390
- missedEntries: fanout.missedEntries,
500
+ missedAt: fan.at,
501
+ available: fan.available,
502
+ missedEntries: fan.missedEntries,
391
503
  ...(didYouMean !== undefined ? { didYouMean } : {}),
392
504
  ...(readFromArtifact ? resultLayerNote(result, path) ?? {} : {}),
393
- hint: missHintFor(fanout.at, fanout.available),
505
+ hint: missHintFor(fan.at, fan.available),
394
506
  };
395
507
  }
396
508
  return {
397
509
  path,
398
- content: fanout.values,
399
- mapped: fanout.values.length,
400
- ...(fanout.nullEntries > 0 ? { nullEntries: fanout.nullEntries } : {}),
401
- ...(fanout.nullEntries > 0 && fanout.nullEntries === fanout.values.length ? { hint: ALL_NULL_HINT } : {}),
402
- ...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries, hint: `有 ${fanout.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}),
510
+ content: fan.values,
511
+ mapped: fan.values.length,
512
+ ...(autoStripped !== undefined ? { autoStripped } : {}),
513
+ ...(preferResult ? { from: "result" } : {}),
514
+ ...(fan.nullEntries > 0 ? { nullEntries: fan.nullEntries } : {}),
515
+ ...(fan.nullEntries > 0 && fan.nullEntries === fan.values.length ? { hint: ALL_NULL_HINT } : {}),
516
+ ...(fan.missedEntries > 0 ? { missedEntries: fan.missedEntries, hint: `有 ${fan.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}),
403
517
  };
404
518
  }
405
- const projected = projectJsonPath(source, path);
406
- if (projected.missed) {
407
- // projectJsonPath 也会拦 unsupportedSyntax;双保险统一出口。
408
- if (projected.unsupportedSyntax === true) {
519
+ const single = projected;
520
+ if (single.missed) {
521
+ if (single.unsupportedSyntax === true) {
409
522
  return {
410
523
  path,
411
524
  content: null,
412
525
  pathMiss: false,
413
526
  unsupportedSyntax: true,
414
527
  unanswered: true,
415
- available: projected.available,
416
- hint: typeof projected.available === "string" ? projected.available : UNSUPPORTED_JQ_OBJECT_HINT,
528
+ available: single.available,
529
+ hint: typeof single.available === "string" ? single.available : UNSUPPORTED_JQ_OBJECT_HINT,
417
530
  };
418
531
  }
419
- const didYouMean = pathMissDidYouMean(projected.at, projected.available);
532
+ const didYouMean = pathMissDidYouMean(single.at, single.available);
420
533
  return {
421
534
  path,
422
535
  content: null,
423
536
  pathMiss: true,
424
- missedAt: projected.at,
425
- available: projected.available,
537
+ missedAt: single.at,
538
+ available: single.available,
426
539
  ...(didYouMean !== undefined ? { didYouMean } : {}),
427
540
  ...(readFromArtifact ? resultLayerNote(result, path) ?? {} : {}),
428
- hint: missHintFor(projected.at, projected.available),
541
+ hint: missHintFor(single.at, single.available),
429
542
  };
430
543
  }
431
- return { path, content: projected.value ?? null };
544
+ return {
545
+ path,
546
+ content: single.value ?? null,
547
+ ...(autoStripped !== undefined ? { autoStripped } : {}),
548
+ ...(preferResult ? { from: "result" } : {}),
549
+ };
432
550
  });
433
- if (paths.length === 1)
434
- out.content = out.extract[0]?.content ?? null;
551
+ // r30a F21 / r31c F7: single path keeps top-level `content`. Multi-path used
552
+ // to ALSO dump a path→value `content` map that duplicated `extract[]`
553
+ // (probe: roles[].name + roles[].code returned twice). Keep ONE shape:
554
+ // extract[] for per-path detail, plus `rows` that zip parallel array
555
+ // projections so two columns align without a second round trip.
556
+ const extracts = out.extract;
557
+ if (paths.length === 1) {
558
+ out.content = extracts[0]?.content ?? null;
559
+ }
560
+ else {
561
+ const columns = extracts.map((entry) => ({
562
+ path: entry.path,
563
+ values: entry.pathMiss === true || entry.unsupportedSyntax === true
564
+ ? []
565
+ : Array.isArray(entry.content)
566
+ ? entry.content
567
+ : entry.content === undefined
568
+ ? []
569
+ : [entry.content],
570
+ }));
571
+ const rowCount = Math.max(0, ...columns.map((column) => column.values.length));
572
+ const rows = [];
573
+ for (let index = 0; index < rowCount; index += 1) {
574
+ const row = {};
575
+ for (const column of columns)
576
+ row[column.path] = index < column.values.length ? column.values[index] : null;
577
+ rows.push(row);
578
+ }
579
+ out.rows = rows;
580
+ out.rowCount = rowCount;
581
+ // Deliberately NO top-level `content` on multi-path — that was the duplicate.
582
+ }
583
+ // Keep one machine-readable shape for artifact/call consumers. `content` on
584
+ // the single-path branch remains as a backwards-compatible convenience.
585
+ out.projections = extracts;
435
586
  // A projection that missed is not an answer. `status` stays `ok` (adding a
436
587
  // new status value would fall through classifyResult to error/exit 15 and
437
588
  // change the exit contract), but the envelope now says so out loud.
438
- const extracts = out.extract;
439
589
  if (extracts.some((entry) => entry.pathMiss === true || entry.unsupportedSyntax === true)) {
440
590
  out.unanswered = true;
441
591
  // 多路径:语法说明给一次,不再逐条复制。不支持语法优先于 pathMiss 语法课。
@@ -443,7 +593,7 @@ export function projectResultExtract(result, paths, keysOnly) {
443
593
  out.hint = UNSUPPORTED_JQ_OBJECT_HINT;
444
594
  }
445
595
  else if (multiPath) {
446
- out.hint = `${RESULT_RELATIVE_PATH_NOTE}${JQ_PATH_SYNTAX}${JQ_PATH_HELP_POINTER}`;
596
+ out.hint = `${RESULT_RELATIVE_PATH_NOTE}${JQ_PATH_SYNTAX}多路径时用 extract[](逐条)或 rows[](列对齐);不再重复 content 映射。${JQ_PATH_HELP_POINTER}`;
447
597
  }
448
598
  }
449
599
  }
@@ -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. */
@@ -54,7 +54,7 @@ const bizserviceSelfCallCorp = Object.freeze({
54
54
  environment,
55
55
  serviceCode: "<service_code>",
56
56
  methodCode: "<method_code>",
57
- inputParameters: [
57
+ methodInputParameters: [
58
58
  { code: "X-LowCode-Corpid", name: "corpId头", required: false, codeType: 0, bizCode: "<corpId 常量值>", configJson: { position: "REQUEST_HEADER" } },
59
59
  ],
60
60
  },
@@ -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
@@ -1,10 +1,10 @@
1
1
  import { environment, model, commonRecovery } from "./shared.js";
2
2
  const configureFormRecipe = Object.freeze({
3
3
  id: "publish-default-form",
4
- title: "把已发布字段绑进默认表单并发布(高层编排)",
4
+ title: "把已发布字段绑进默认表单(发布请随后调用 form.publish)",
5
5
  status: "offline-valid",
6
6
  tool: "configure_form",
7
- args: { environment, schemaCode: model, publish: true },
7
+ args: { environment, schemaCode: model },
8
8
  preconditions: ["字段已 field.publish"],
9
9
  conditionalRequirements: ["表单层条件必填/只读请用 field.update 的 options(form-conditional-required / form-always-readonly recipe),不是本编排器"],
10
10
  priorToolReferences: [{ tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认字段已发布" }],
@@ -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"] }),