@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
@@ -16,11 +16,56 @@ const READ_CUES = ["查看", "读回", "盘点", "现状", "列出", "有哪些"
16
16
  const BUILD_CUES = ["新建", "创建", "搭建", "从零", "从无到有", "做", "建", "加", "实现", "生成", "新增", "保存", "启用", "录入", "添加", "造", "填报", "打卡", "录"];
17
17
  /** Whole-application asks — a single-model atom is the wrong shape for these. */
18
18
  const WHOLE_APP_CUES = ["应用", "系统", "整套", "从零", "从无到有", "端到端", "一条龙", "一整套", "业务应用", "平台"];
19
+ /** From-scratch cues — firstAnswer must not point at a schema that does not exist yet. */
20
+ const GREENFIELD_CUES = ["从零", "从无到有", "做一个", "做一套", "新建应用", "搭建应用", "端到端", "一条龙"];
19
21
  function looksWholePackage(goal, capabilityCount) {
20
22
  if (capabilityCount >= 4)
21
23
  return true;
22
24
  return WHOLE_APP_CUES.some((cue) => goal.includes(cue));
23
25
  }
26
+ function looksGreenfield(goal) {
27
+ return GREENFIELD_CUES.some((cue) => goal.includes(cue));
28
+ }
29
+ /**
30
+ * r31c F1: multi-face asks must NOT be stolen by the first matching composer.
31
+ * Conservative: only when ≥2 capability faces score AND the ask is whole-package
32
+ * shaped (WHOLE_APP cue or ≥4 faces). Single-face composer goals (G1/G2/G5) stay
33
+ * single-point answers.
34
+ */
35
+ function shouldPreferMultiFace(mode, goal, scoredCapabilityIds) {
36
+ if (mode !== "build" && mode !== "modify")
37
+ return false;
38
+ if (scoredCapabilityIds.length < 2)
39
+ return false;
40
+ return looksWholePackage(goal, scoredCapabilityIds.length);
41
+ }
42
+ /** Short why-line for a capability step in a multi-face ladder (r31c F1). */
43
+ function capabilityWhy(capabilityId, goal) {
44
+ const cues = CAPABILITY_CUES[capabilityId] ?? [];
45
+ const hits = cues.filter((cue) => goal.includes(cue)).slice(0, 3);
46
+ if (hits.length > 0)
47
+ return hits.join(" + ");
48
+ switch (capabilityId) {
49
+ case "model":
50
+ return "建模";
51
+ case "field":
52
+ return "字段 / 子表";
53
+ case "form":
54
+ return "表单";
55
+ case "listview":
56
+ return "列表 / 按钮";
57
+ case "datarule":
58
+ return "字段计算 / 校验";
59
+ case "businessrule":
60
+ return "业务规则 / 按钮";
61
+ case "workflow":
62
+ return "审批流程";
63
+ case "onlinejs":
64
+ return "在线 JS";
65
+ default:
66
+ return capabilityId;
67
+ }
68
+ }
24
69
  function detectMode(goal) {
25
70
  if (SCOUT_CUES.some((cue) => goal.includes(cue)))
26
71
  return "scout";
@@ -419,7 +464,8 @@ const COMPOSER_ROUTES = Object.freeze([
419
464
  command: "omc approval-flow --schema <模型code> --steps '<节点名>@role:<角色code>' --apply",
420
465
  usage: "omc approval-flow --help",
421
466
  // F1(r27):边界前置——非发起人审批无法在单会话 CLI 内推进,别等 submit 失败才说。
422
- summary: "线性审批流一条命令:建流程 → 模板+字段只读权限 → 发布(多级用重复 --steps)。边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);若要验跨账号审批,用第二账号另建环境,或把参与人设为 {Originator}",
467
+ // r31c F5:明说线性-only;条件分支退回 workflow.update;行级可见范围不支持。
468
+ summary: "线性审批流一条命令:建流程 → 模板+字段只读权限 → 发布(多级用重复 --steps)。⚠️ 只做线性流程,无 --condition;条件分支用 omc describe workflow.update --example workflow-conditional-branch。边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);若要验跨账号审批,用第二账号另建环境,或把参与人设为 {Originator}。workflow.permissions 不是行级数据权限(申请人只看自己的单据当前工具面不支持)",
423
469
  groups: [["审批", "流程"], ["建", "创建", "新建", "配置", "加", "搭"]],
424
470
  // Branching / countersign shapes are NOT this composer's business.
425
471
  avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人", "清理", "删除", "移除", "purge", "teardown"],
@@ -502,17 +548,30 @@ function matchComposer(goal, mode) {
502
548
  function isWholePackageWrite(mode, goal, capabilityCount) {
503
549
  return (mode === "build" || mode === "modify") && looksWholePackage(goal, capabilityCount);
504
550
  }
505
- function wholePackageRoute(registry, goal, index) {
551
+ function wholePackageRoute(registry, goal, index, hitCapabilities) {
506
552
  const routeById = new Map(index.routes.map((route) => [route.id, route]));
507
- const ordered = orderedCapabilities([...WHOLE_PACKAGE_CAPABILITIES], index).filter((id) => routeById.has(id));
553
+ // r31c F1: prefer the faces the ask actually named; fall back to the full
554
+ // package set when the ask only said "做应用" without naming faces.
555
+ const seed = hitCapabilities !== undefined && hitCapabilities.length >= 2
556
+ ? [...new Set([...hitCapabilities, "model", "field"])]
557
+ : [...WHOLE_PACKAGE_CAPABILITIES];
558
+ const ordered = orderedCapabilities(seed, index).filter((id) => routeById.has(id));
508
559
  // Round 16b (dogfood R1): the supplement used to repeat every capability's
509
560
  // full tool map (model.create/delete/…, field.create/…), ~60% of a 2.8KB
510
561
  // response the caller never used — intent already covers these. Keep it a
511
562
  // POINTER (capability + lane + one command), not a second ladder.
512
- const ladder = ordered.map((id) => {
513
- const capability = routeById.get(id);
514
- return { capability: id, lane: capability.lane, firstCommand: `omc commands --json --compact --lane ${capability.lane}` };
515
- });
563
+ // r31c F1: multi-face asks must SEE every face they named (not just the first
564
+ // composer hit). But this must not be called `ladder` — round16 settled that
565
+ // whole-package has ONE primary path (intent) and `route.ladder` is the
566
+ // atomic execution ladder, which for whole-package is deliberately absent
567
+ // (bench40: two co-equal paths made agents wander). So the face list rides
568
+ // `route.faces`: same information, no second execution path.
569
+ const faces = ordered.map((id, indexInFaces) => ({
570
+ step: indexInFaces + 1,
571
+ capability: id,
572
+ why: capabilityWhy(id, goal),
573
+ }));
574
+ const greenfield = looksGreenfield(goal);
516
575
  return {
517
576
  status: "ok",
518
577
  goal,
@@ -521,21 +580,34 @@ function wholePackageRoute(registry, goal, index) {
521
580
  match: { kind: "whole-package", ref: ordered.join("→"), capabilities: ordered },
522
581
  route: {
523
582
  primary: "intent",
524
- atomicSupplement: { note: "intent 已覆盖下列能力的创建;这些原子工具只用于 intent 未覆盖面(删除/生命周期/运行数据/精确 patch):", ladder },
525
- ...routeReads(sectionPointersFor(ordered)),
583
+ faceCount: faces.length,
584
+ atomicSupplement: {
585
+ note: "intent 已覆盖下列能力的创建;原子工具只用于 intent 未覆盖面(删除/生命周期/运行数据/精确 patch)",
586
+ capabilities: ordered,
587
+ // round16b: 这是**指针**(capability + why),不是第二套执行阶梯——
588
+ // 每步都不带 tools,否则就等于把 atomic ladder 又内联一遍。
589
+ ladder: faces,
590
+ },
591
+ ...routeReads(sectionPointersFor(ordered.slice(0, 3))),
526
592
  // r19 ticket 04: readiness (discover_environment) left firstCommands —
527
593
  // the probe executed it 0 times in 184 calls; it rides its own field now.
528
594
  readiness: DISCOVERY_AUTHORITY.readiness,
529
- firstCommands: ["omc intent schema"],
595
+ firstCommands: ["omc intent schema", "omc help omc-playbook --section 采购申请"],
530
596
  write: true,
531
597
  },
532
598
  write: true,
533
599
  intent: intentGuidance(),
534
- firstAnswer: {
535
- state: "ready",
536
- action: "omc intent schema",
537
- why: "整包需求走声明式意图:先看 intent YAML 的键位与可运行示例,再按 starter 写文件(validate → plan → apply)",
538
- },
600
+ firstAnswer: greenfield
601
+ ? {
602
+ state: "ready",
603
+ action: "omc intent schema",
604
+ why: "从零整包:模型还不存在——先看 intent 键位/示例,再按 starter 写 YAML(validate → plan → apply);不要先问已有 schemaCode",
605
+ }
606
+ : {
607
+ state: "ready",
608
+ action: "omc intent schema",
609
+ why: "整包需求走声明式意图:先看 intent YAML 的键位与可运行示例,再按 starter 写文件(validate → plan → apply)",
610
+ },
539
611
  };
540
612
  }
541
613
  /** A configuration read-back ask that names no capability ("给我一份现状说明"). */
@@ -571,8 +643,17 @@ export function goalRoute(registry, goal) {
571
643
  // matched to a nearby capability and handed the write ladder — that is the
572
644
  // "invent a capability for a blocked face" anti-pattern. Return a boundary
573
645
  // result pointing at capability-scouting + the (read-only) coverage proof.
646
+ //
647
+ // r31c F27 exception: a WHOLE-PACKAGE ask that merely MENTIONS an unsupported
648
+ // face (e.g. 「申请人只看自己」 inside an 8-clause purchase ask) must not
649
+ // steal the entire multi-face route. Soft-attach the boundary note and keep
650
+ // routing; pure boundary asks (only that face) stay blocked.
574
651
  const boundary = boundaryOf(goal);
575
- if (boundary) {
652
+ const earlyScores = capabilityScores(registry, goal);
653
+ const earlyScoredIds = [...earlyScores.entries()].filter((entry) => entry[1] >= 2).map((entry) => entry[0]);
654
+ const multiFaceMentionsBoundary = boundary !== undefined
655
+ && shouldPreferMultiFace(mode, goal, earlyScoredIds);
656
+ if (boundary && !multiFaceMentionsBoundary) {
576
657
  return {
577
658
  status: "ok",
578
659
  goal,
@@ -728,9 +809,41 @@ export function goalRoute(registry, goal) {
728
809
  };
729
810
  }
730
811
  }
812
+ // Score capabilities BEFORE composer capture so a multi-face ask (r31c F1)
813
+ // is not stolen by the first matching composer (e.g. 「合计」→ compute).
814
+ const scoreMap = capabilityScores(registry, goal);
815
+ const scores = [...scoreMap.entries()].sort((left, right) => right[1] - left[1]);
816
+ const scoredCapabilityIds = scores.filter((entry) => entry[1] >= 2).map((entry) => entry[0]);
731
817
  const composer = matchComposer(goal, mode);
818
+ if (composer && shouldPreferMultiFace(mode, goal, scoredCapabilityIds)) {
819
+ // Multi-face + whole-package shaped → surface the ordered ladder, not a
820
+ // single composer. Intent remains the primary path. Soft-attach any
821
+ // mentioned unsupported face (r31c F27) so the gap is named without
822
+ // blocking the rest of the package.
823
+ const routed = wholePackageRoute(registry, goal, index, scoredCapabilityIds);
824
+ if (boundary) {
825
+ return {
826
+ ...routed,
827
+ boundary: {
828
+ id: boundary.id,
829
+ status: boundary.status,
830
+ note: boundary.note,
831
+ soft: true,
832
+ verify: "omc help omc-capabilities --section \"边界\"",
833
+ },
834
+ };
835
+ }
836
+ return routed;
837
+ }
732
838
  if (composer) {
733
839
  const topics = (composer.topics ?? []).filter((topic) => availableTopics.has(topic));
840
+ // r31c F3: if the ask is greenfield, do not point firstAnswer at a schema
841
+ // that does not exist yet — rewrite the clarifying question to create-first.
842
+ const baseQuestion = COMPOSER_QUESTIONS[composer.id] ?? "补齐命令里的尖括号占位符后运行(形状见 pendingCommand)";
843
+ const greenfield = looksGreenfield(goal);
844
+ const question = greenfield
845
+ ? `模型还不存在:先创建模型(omc call model.create --args '{"code":"<模型code>","name":"<名称>","applicationCode":"<应用>"}' 或 omc model-scaffold …),再回答:${baseQuestion}`
846
+ : baseQuestion;
734
847
  return {
735
848
  status: "ok",
736
849
  goal,
@@ -753,15 +866,17 @@ export function goalRoute(registry, goal) {
753
866
  ...(topics.length > 0 ? { topics } : {}),
754
867
  firstAnswer: {
755
868
  state: "needs-input",
756
- question: COMPOSER_QUESTIONS[composer.id] ?? "补齐命令里的尖括号占位符后运行(形状见 pendingCommand)",
757
- pendingCommand: composer.command,
758
- ...(composer.id === "model-purge" || composer.id === "compute" ? { action: LIST_MODELS_ACTION } : {}),
759
- why: composer.summary,
869
+ question,
870
+ pendingCommand: greenfield && composer.id !== "model-scaffold" && composer.id !== "model-purge"
871
+ ? `omc call model.create --args '{"code":"<模型code>","name":"<名称>","applicationCode":"<应用>"}'`
872
+ : composer.command,
873
+ ...(composer.id === "model-purge" || composer.id === "compute" || greenfield ? { action: LIST_MODELS_ACTION } : {}),
874
+ why: greenfield
875
+ ? `从零需求:先确认/创建模型,再跑 ${composer.id}(${composer.summary})`
876
+ : composer.summary,
760
877
  },
761
878
  };
762
879
  }
763
- const scoreMap = capabilityScores(registry, goal);
764
- const scores = [...scoreMap.entries()].sort((left, right) => right[1] - left[1]);
765
880
  const fallback = {
766
881
  status: "ok",
767
882
  goal,
package/dist/identity.js CHANGED
@@ -1,18 +1,70 @@
1
1
  import { ENDPOINTS } from "./endpoints.js";
2
2
  import { wireCall, wireRows } from "./wire.js";
3
+ import { wireString } from "./workflow-support.js";
4
+ export const PLATFORM_ADMIN_START_BYPASS = "platform-admin bypasses start-permission whitelist";
3
5
  /**
4
- * Identity seam (round 10, ergonomics).
5
- *
6
- * One object carries several names across tools: a model is `schemaCode`, a
7
- * field is `code`/`propertyCode`, a business-rule row's wire key is `id` while
8
- * its business key is `(schemaCode, ruleCode)`, a workflow is `workflowCode`
9
- * while the caller only knows the model's `schemaCode`. Today that forces a
10
- * `list → get` dance and per-lane key memorisation.
11
- *
12
- * These resolvers let a tool accept the BUSINESS key and resolve the wire
13
- * identity internally. They are read-only and return `null` for a genuine miss,
14
- * so the caller can fail closed with a precise message.
6
+ * Resolve who is operating this session. Best-effort: never throws.
7
+ * `isAdmin` comes from login().administrator (info_login permissions contains
8
+ * ADMIN/SYS_MNG) — that is the platform start-permission bypass, not the
9
+ * login API's isAdmin boolean.
15
10
  */
11
+ export async function resolvePermissionActor(session, options = {}) {
12
+ let userId = null;
13
+ let name = null;
14
+ let isAdmin = false;
15
+ try {
16
+ const login = await session.login();
17
+ isAdmin = login.administrator === true;
18
+ const identity = login.accountIdentity;
19
+ if (identity !== undefined && identity.length > 0) {
20
+ const searchData = (await wireCall(session, {
21
+ path: ENDPOINTS.organizationUserSearch,
22
+ method: "POST",
23
+ body: { wd: identity, page: 0, size: 20 },
24
+ })).data;
25
+ const found = Array.isArray(searchData?.content)
26
+ ? searchData.content
27
+ : (() => {
28
+ try {
29
+ return wireRows(searchData);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ })();
35
+ const exact = found.find((row) => [row.username, row.userId, row.name].some((value) => String(value ?? "") === identity)) ??
36
+ found[0];
37
+ if (exact) {
38
+ userId = wireString(exact.id) ?? wireString(exact.userId);
39
+ name = wireString(exact.name) ?? identity;
40
+ if (userId) {
41
+ try {
42
+ const info = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { id: userId } })).data;
43
+ if (info) {
44
+ userId = wireString(info.id) ?? userId;
45
+ name = wireString(info.name) ?? name;
46
+ }
47
+ }
48
+ catch {
49
+ // keep search hit
50
+ }
51
+ }
52
+ }
53
+ else {
54
+ name = identity;
55
+ }
56
+ }
57
+ }
58
+ catch {
59
+ // best-effort actor — never poison the calling receipt
60
+ }
61
+ return {
62
+ userId,
63
+ name,
64
+ isAdmin,
65
+ ...(options.bypassReason ? { bypassReason: options.bypassReason } : {}),
66
+ };
67
+ }
16
68
  /** Resolve a rule ROW id from `(schemaCode, ruleCode)`. Reuses the list face. */
17
69
  export async function resolveRuleRowId(session, schemaCode, ruleCode) {
18
70
  const rows = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
@@ -56,15 +56,15 @@ export const APP_LANE_CONTRACT = Object.freeze({
56
56
  tools: Object.freeze({
57
57
  "app.list": Object.freeze({
58
58
  scope: "app package list",
59
- description: "列出应用(apppackage/list)及其所属分组;用于跨应用解析,以及 app.create/app.delete 前后的读回。",
59
+ description: "列出应用(apppackage/list)及分组;回执含 applicationsCount/omitted。用于跨应用解析与 app.create/delete 前后读回。",
60
60
  }),
61
61
  "app.create": Object.freeze({
62
62
  scope: "app package create",
63
- description: "创建一个设计时应用包(apppackage/create,不是应用内目录)。groupId 可选,缺省解析配置应用所在分组(操作者必然可见);配置应用不存在时回退租户第一个具名非兜底分组(auto-first-group,warning:resolved-to-first-group)。落到兜底分组 code=all 时读回带 warning:resolved-to-fallback-group; portalHidden:true。创建后以 apppackage/list 读回存在性;平台会自动生成默认分组(readback.defaultFolder,isDefault:true)。应用发布属边界外:建成后需人工在设计器发布并核对该分组,否则门户不可见。",
63
+ description: "创建设计时应用包(apppackage/create,非目录)。groupId 可选:缺省用配置应用所在分组;否则首个具名非兜底分组(auto-first-group / resolved-to-first-group)。落到 code=all → resolved-to-fallback-group; portalHidden:true。读回 apppackage/list;平台自动建默认分组(defaultFolder.isDefault)。发布属边界外,需设计器人工发布。",
64
64
  }),
65
65
  "app.update": Object.freeze({
66
66
  scope: "app package update",
67
- description: "更新一个应用(apppackage/update,管理员面;8.6.26 源码证据:AppPackageController.update → AppIntegrationFacadeImpl.updateAppPackage,按记录 id 整体提交,显式 groupId 生效,缺省 groupId 会被强制回默认分组,enabled 非 true 会清 workflow 收藏——故本工具读回记录后仅改 groupId 并原样保留 enabled)。当前仅支持改 `groupId`(应用迁移分组的补救路径),不隐式补写 i18nMessage,name 等其他字段以读回值原样回传。以 apppackage/list 读回分组变更。",
67
+ description: "更新应用(apppackage/update,管理员面;源码证据 AppPackageController.update → AppIntegrationFacadeImpl.updateAppPackage):按记录 id 整体提交;显式 groupId 生效,缺省会强制回默认分组,enabled≠true 清 workflow 收藏——故只改 groupId 并原样保留 enabled。不补 i18nMessage;其它字段读回原样回传。读回 apppackage/list。",
68
68
  }),
69
69
  "app.delete": Object.freeze({
70
70
  scope: "app package delete absence",
@@ -76,7 +76,7 @@ export function assertAppContract() {
76
76
  assertLaneContract(APP_LANE_CONTRACT);
77
77
  if (!APP_WIRE.defaultGroupResolution.includes("resolved-to-first-group"))
78
78
  throw new Error("app contract pins the first-group fallback warning token");
79
- assertDescriptionCarries(APP_LANE_CONTRACT, "app.list", ["apppackage/list", "跨应用"]);
79
+ assertDescriptionCarries(APP_LANE_CONTRACT, "app.list", ["apppackage/list", "跨应用", "applicationsCount"]);
80
80
  assertDescriptionCarries(APP_LANE_CONTRACT, "app.create", ["apppackage/create", "resolved-to-first-group", "边界外", "defaultFolder"]);
81
81
  if (!APP_WIRE.createReadiness.includes("zero"))
82
82
  throw new Error("app contract must pin the zero-wire prerequisite refusal");
package/dist/lanes/app.js CHANGED
@@ -53,7 +53,25 @@ export function registerAppLane(tools) {
53
53
  name: row.appName ?? row.name,
54
54
  groupId: row.groupId,
55
55
  }));
56
- return { applications };
56
+ // r29f F2:与 discover_environment 同构——一眼判断「内联是否全集」。
57
+ // 应用通常个位数,默认内联 + omitted:false;大租户由 governedRead 外置时
58
+ // 仍保留 applicationsCount,并由 diet 路径标 omitted。
59
+ return {
60
+ applications,
61
+ applicationsCount: applications.length,
62
+ omitted: false,
63
+ };
64
+ }, {
65
+ target: "app.list",
66
+ compact: (result) => {
67
+ const apps = Array.isArray(result.applications) ? result.applications : [];
68
+ return {
69
+ applicationsCount: apps.length,
70
+ omitted: true,
71
+ applications: apps.slice(0, 20),
72
+ tablesHint: "applications 已截断内联前 20 条(omitted:true);全集在 detailArtifactId,用 --jq applications[].code 或 artifact get 取回。",
73
+ };
74
+ },
57
75
  }),
58
76
  }));
59
77
  writeTool({
@@ -62,7 +80,7 @@ export function registerAppLane(tools) {
62
80
  lane: APP_LANE,
63
81
  scope: APP_TOOLS["app.create"].scope,
64
82
  description: APP_TOOLS["app.create"].description,
65
- inputSchema: { code: z.string().min(1), name: z.string().min(1), groupId: z.string().optional() },
83
+ inputSchema: { code: z.string().min(1), name: z.string().min(1), groupId: z.string().describe("省略时使用环境配置的默认应用分组(回执 resolvedFrom=auto-configured-app-group),不需要先查 groupId").optional() },
66
84
  execute: async (session, args) => {
67
85
  const existing = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data).find((row) => (row.appCode ?? row.code) === args.code);
68
86
  if (existing)
@@ -50,12 +50,22 @@ export const DICTIONARY_LANE_CONTRACT = Object.freeze({
50
50
  ]),
51
51
  tools: Object.freeze({
52
52
  "dictionary.list": Object.freeze({ scope: "dictionary list", description: "分页读取数据字典(data_dictionary/searchDictionary),返回字典元数据。" }),
53
- "dictionary.get": Object.freeze({ scope: "dictionary read", description: "按 id 或 code 读取数据字典及其元数据。" }),
54
- "dictionary.records": Object.freeze({ scope: "dictionary records read", description: "读取字典记录;支持按 id/code、启用状态和 parentId 读取,保留树形 children。" }),
53
+ "dictionary.get": Object.freeze({ scope: "dictionary read", description: "按 id 或 code 读取数据字典头;附 recordsCount(明细仍走 dictionary.records)。" }),
54
+ "dictionary.records": Object.freeze({
55
+ scope: "dictionary records read",
56
+ description: "读取字典记录(id/code/enabledOnly/parentId,保留 children)。回执恒 {records:{totalElements,content[]}, total}。",
57
+ }),
55
58
  "dictionary.save": Object.freeze({ scope: "dictionary save", description: "创建或更新数据字典及记录(data_dictionary/saveDictionary);KEY_VALUE=0,TREE=1,写入前需审批。records 是增量 upsert:传 [] 不会清空已有记录,平台也没有字典/记录删除端点——停用请用 dictionary.status,清理需维护者数据库操作。" }),
56
59
  "dictionary.status": Object.freeze({ scope: "dictionary status", description: "启用或停用字典或字典记录;停用字典时运行时不会返回选项。" }),
57
60
  "dictionary.class": Object.freeze({ scope: "dictionary classification", description: "读取、新建、删除字典分类并维护分类关联字典。" }),
58
- "dictionary.usage": Object.freeze({ scope: "dictionary record usage", description: "批量设置字典记录 initialUsed 使用状态;记录新增/更新仍随 dictionary.save.records。" }),
61
+ "dictionary.usage": Object.freeze({
62
+ scope: "dictionary record usage",
63
+ description: "写:批量设置字典记录 initialUsed 使用标记(不是引用查询)。查谁引用了字典请用 dictionary.references。",
64
+ }),
65
+ "dictionary.references": Object.freeze({
66
+ scope: "dictionary refs",
67
+ description: "只读:谁引用了该字典(上限50+truncated)。",
68
+ }),
59
69
  "dictionary.type": Object.freeze({ scope: "dictionary type read", description: "按 dictionaryType 读取字典。" }),
60
70
  "dictionary.names": Object.freeze({ scope: "dictionary names read", description: "读取字典 id/name 摘要。" }),
61
71
  "dictionary.parent": Object.freeze({ scope: "dictionary parent records read", description: "读取树型字典指定父节点的直接子记录。" }),
@@ -67,9 +77,10 @@ export const DICTIONARY_LANE_CONTRACT = Object.freeze({
67
77
  export function assertDictionaryContract() {
68
78
  assertLaneContract(DICTIONARY_LANE_CONTRACT);
69
79
  assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.save", ["saveDictionary", "KEY_VALUE=0", "TREE=1"]);
70
- assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.records", ["children"]);
80
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.records", ["children", "totalElements"]);
71
81
  assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.class", ["分类"]);
72
- assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.usage", ["initialUsed"]);
82
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.usage", ["initialUsed", "不是引用查询"]);
83
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.references", ["truncated"]);
73
84
  if (!String(DICTIONARY_LANE_CONTRACT.wire.selectionDictionary).includes("checkedDictionary"))
74
85
  throw new Error("dictionary contract must pin dictionary option keys");
75
86
  if (!String(DICTIONARY_LANE_CONTRACT.wire.selectionBusinessModel).includes("sheetDataItem"))
@@ -15,6 +15,7 @@ export const DICTIONARY_COVERAGE = declareLaneCoverage(DICTIONARY_LANE_CONTRACT.
15
15
  claim("dictionary.save", T["dictionary.save"].scope),
16
16
  claim("dictionary.status", T["dictionary.status"].scope),
17
17
  claim("dictionary.usage", T["dictionary.usage"].scope),
18
+ claim("dictionary.references", T["dictionary.references"].scope),
18
19
  claim("dictionary.classes", T["dictionary.classes"].scope),
19
20
  claim("dictionary.class.save", T["dictionary.class.save"].scope),
20
21
  claim("dictionary.class.delete", T["dictionary.class.delete"].scope),