@stalfh233/omc-cli 0.0.0-stage → 0.4.0

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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. package/skills/omc-write-hazards.md +87 -0
@@ -0,0 +1,78 @@
1
+ import { assertDescriptionCarries, assertLaneContract, behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION } from "./contract-support.js";
2
+ import { LANES } from "./lane-ids.js";
3
+ export const DICTIONARY_LANE_CONTRACT = Object.freeze({
4
+ lane: LANES.dictionary,
5
+ family: CONTRACT_FAMILY,
6
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
7
+ endpoints: Object.freeze({
8
+ search: "dictionarySearch",
9
+ get: "dictionaryGet",
10
+ getByCode: "dictionaryGetByCode",
11
+ save: "dictionarySave",
12
+ updateStatus: "dictionaryUpdateStatus",
13
+ records: "dictionaryRecords",
14
+ recordsByCode: "dictionaryRecordsByCode",
15
+ enabledRecords: "dictionaryEnabledRecords",
16
+ enabledRecordsByCode: "dictionaryEnabledRecordsByCode",
17
+ parentRecords: "dictionaryParentRecords",
18
+ recordStatus: "dictionaryRecordStatus",
19
+ recordUsed: "dictionaryRecordUsed",
20
+ type: "dictionaryGetByType",
21
+ idAndNames: "dictionaryIdAndNames",
22
+ classes: "dictionaryClasses",
23
+ classDetail: "dictionaryClassDetail",
24
+ classSave: "dictionaryClassSave",
25
+ classDelete: "dictionaryClassDelete",
26
+ classDeleteBatch: "dictionaryClassDeleteBatch",
27
+ }),
28
+ wire: Object.freeze({
29
+ types: "KEY_VALUE=0, TREE=1",
30
+ save: "POST /api/api/data_dictionary/saveDictionary {id?,code,name,dictionaryType,classificationId?,records:[...]}",
31
+ record: "record {id?,code,name,name_i18n?,parentId?,status,dictionaryId?,sortKey,initialUsed?,children?}",
32
+ selectionDictionary: "optionsSet {optionsType:'dictionary',dictionary:{checkedDictionary,checkedDictionaryCode,valueText,dictionariesType,useDictionariesData}}",
33
+ selectionBusinessModel: "optionsSet {optionsType:'businessModel',businessModel:{schemaCode,sheetDataItem,modelName?,dataItemType?,orderByFields?,orderType?,conditions?}}",
34
+ relationFilters: "WORK_SHEET/MULT_WORK_SHEET options.searchFormula and options.conditions are JSON strings consumed by the form control",
35
+ }),
36
+ wireBindings: Object.freeze({
37
+ types: Object.freeze([behaviorBinding("test/dictionary.test.ts#dictionary.save sends KEY_VALUE records and dictionary.records reads enabled values", assertDictionaryContract)]),
38
+ save: Object.freeze([behaviorBinding("test/dictionary.test.ts#dictionary.save sends KEY_VALUE records and dictionary.records reads enabled values", assertDictionaryContract)]),
39
+ record: Object.freeze([behaviorBinding("test/dictionary.test.ts#dictionary.save preserves TREE children and dictionary option source emits the platform optionsSet", assertDictionaryContract)]),
40
+ selectionDictionary: Object.freeze([behaviorBinding("test/dictionary.test.ts#dictionary.save preserves TREE children and dictionary option source emits the platform optionsSet", assertDictionaryContract)]),
41
+ selectionBusinessModel: Object.freeze([behaviorBinding("test/dictionary.test.ts#dictionary.save preserves TREE children and dictionary option source emits the platform optionsSet", assertDictionaryContract)]),
42
+ relationFilters: Object.freeze([behaviorBinding("test/dictionary.test.ts#relation filters require JSON strings and are carried onto the relation control", assertDictionaryContract)]),
43
+ }),
44
+ evidence: Object.freeze([
45
+ "cp-sources/backend/decompiled-full/launcher-8.6.26/.../DataDictionaryController.java:157-410",
46
+ "cp-sources/backend/decompiled-full/launcher-8.6.26/.../DictionaryFacadeImpl.java:141-196",
47
+ "cp-sources/backend/decompiled-full/launcher-8.6.26/.../SchemaAnalyzer.java:349-454",
48
+ "cp-sources/frontend-dist/admin/js/30525.0fa42d55.js",
49
+ "packages/omc-cli/test/dictionary.test.ts",
50
+ ]),
51
+ tools: Object.freeze({
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。" }),
55
+ "dictionary.save": Object.freeze({ scope: "dictionary save", description: "创建或更新数据字典及记录(data_dictionary/saveDictionary);KEY_VALUE=0,TREE=1,写入前需审批。" }),
56
+ "dictionary.status": Object.freeze({ scope: "dictionary status", description: "启用或停用字典或字典记录;停用字典时运行时不会返回选项。" }),
57
+ "dictionary.class": Object.freeze({ scope: "dictionary classification", description: "读取、新建、删除字典分类并维护分类关联字典。" }),
58
+ "dictionary.usage": Object.freeze({ scope: "dictionary record usage", description: "批量设置字典记录 initialUsed 使用状态;记录新增/更新仍随 dictionary.save.records。" }),
59
+ "dictionary.type": Object.freeze({ scope: "dictionary type read", description: "按 dictionaryType 读取字典。" }),
60
+ "dictionary.names": Object.freeze({ scope: "dictionary names read", description: "读取字典 id/name 摘要。" }),
61
+ "dictionary.parent": Object.freeze({ scope: "dictionary parent records read", description: "读取树型字典指定父节点的直接子记录。" }),
62
+ "dictionary.classes": Object.freeze({ scope: "dictionary classes", description: "读取字典分类及分类详情。" }),
63
+ "dictionary.class.save": Object.freeze({ scope: "dictionary class save", description: "创建或更新字典分类。" }),
64
+ "dictionary.class.delete": Object.freeze({ scope: "dictionary class delete", description: "删除一个或多个未被使用的字典分类。" }),
65
+ }),
66
+ });
67
+ export function assertDictionaryContract() {
68
+ assertLaneContract(DICTIONARY_LANE_CONTRACT);
69
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.save", ["saveDictionary", "KEY_VALUE=0", "TREE=1"]);
70
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.records", ["children"]);
71
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.class", ["分类"]);
72
+ assertDescriptionCarries(DICTIONARY_LANE_CONTRACT, "dictionary.usage", ["initialUsed"]);
73
+ if (!String(DICTIONARY_LANE_CONTRACT.wire.selectionDictionary).includes("checkedDictionary"))
74
+ throw new Error("dictionary contract must pin dictionary option keys");
75
+ if (!String(DICTIONARY_LANE_CONTRACT.wire.selectionBusinessModel).includes("sheetDataItem"))
76
+ throw new Error("dictionary contract must pin business-model option keys");
77
+ }
78
+ //# sourceMappingURL=dictionary-contract.js.map
@@ -0,0 +1,24 @@
1
+ import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
+ import { DICTIONARY_LANE_CONTRACT } from "./dictionary-contract.js";
3
+ const T = DICTIONARY_LANE_CONTRACT.tools;
4
+ export const DICTIONARY_COVERAGE = declareLaneCoverage(DICTIONARY_LANE_CONTRACT.lane, [
5
+ {
6
+ capability: "Data dictionary lifecycle and field option sources",
7
+ evidence: ["packages/omc-cli/src/lanes/dictionary.ts", "packages/omc-cli/test/dictionary.test.ts"],
8
+ tools: [
9
+ claim("dictionary.list", T["dictionary.list"].scope),
10
+ claim("dictionary.type", T["dictionary.type"].scope),
11
+ claim("dictionary.names", T["dictionary.names"].scope),
12
+ claim("dictionary.get", T["dictionary.get"].scope),
13
+ claim("dictionary.records", T["dictionary.records"].scope),
14
+ claim("dictionary.parent", T["dictionary.parent"].scope),
15
+ claim("dictionary.save", T["dictionary.save"].scope),
16
+ claim("dictionary.status", T["dictionary.status"].scope),
17
+ claim("dictionary.usage", T["dictionary.usage"].scope),
18
+ claim("dictionary.classes", T["dictionary.classes"].scope),
19
+ claim("dictionary.class.save", T["dictionary.class.save"].scope),
20
+ claim("dictionary.class.delete", T["dictionary.class.delete"].scope),
21
+ ],
22
+ },
23
+ ]);
24
+ //# sourceMappingURL=dictionary-coverage.js.map
@@ -0,0 +1,235 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { capabilityArg, environmentArg } from "../args.js";
4
+ import { governedRead } from "../read.js";
5
+ import { writeTool } from "../write.js";
6
+ import { defineTool, ToolRefusalError } from "../tool-types.js";
7
+ import { wireCall, WireError } from "../wire.js";
8
+ import { DICTIONARY_LANE_CONTRACT } from "./dictionary-contract.js";
9
+ const LANE = DICTIONARY_LANE_CONTRACT.lane;
10
+ const TOOLS = DICTIONARY_LANE_CONTRACT.tools;
11
+ const dictionaryType = z.union([z.literal(0), z.literal(1)]).describe("KEY_VALUE=0 or TREE=1");
12
+ const nameI18n = z.record(z.string(), z.unknown()).optional();
13
+ const recordSchema = z.object({
14
+ id: z.string().optional(),
15
+ code: z.string().min(1),
16
+ name: z.string().min(1),
17
+ name_i18n: nameI18n,
18
+ parentId: z.string().nullable().optional(),
19
+ status: z.boolean().default(true),
20
+ dictionaryId: z.string().optional(),
21
+ sortKey: z.number().int().default(0),
22
+ initialUsed: z.boolean().optional(),
23
+ }).passthrough();
24
+ const recordsArg = z.array(recordSchema.extend({ children: z.array(recordSchema).optional() })).optional();
25
+ function normalizeRecord(record, dictionaryId, parentId) {
26
+ const children = Array.isArray(record.children) ? record.children.map((child) => normalizeRecord(child, dictionaryId, String(record.id ?? parentId ?? "") || null)) : undefined;
27
+ return {
28
+ ...record,
29
+ ...(dictionaryId ? { dictionaryId } : {}),
30
+ ...(parentId !== undefined ? { parentId } : {}),
31
+ ...(children ? { children } : {}),
32
+ name_i18n: typeof record.name_i18n === "string" ? record.name_i18n : JSON.stringify(record.name_i18n ?? { "zh-CN": record.name, en: record.name }),
33
+ status: record.status !== false,
34
+ sortKey: Number.isInteger(record.sortKey) ? record.sortKey : 0,
35
+ };
36
+ }
37
+ function dictionaryView(value) {
38
+ const row = (value && typeof value === "object" ? value : {});
39
+ return {
40
+ id: row.id,
41
+ code: row.code,
42
+ name: row.name,
43
+ name_i18n: row.name_i18n,
44
+ dictionaryType: row.dictionaryType,
45
+ status: row.status,
46
+ sortKey: row.sortKey,
47
+ classificationId: row.classificationId,
48
+ classificationName: row.classificationName,
49
+ };
50
+ }
51
+ function recordsPage(value) {
52
+ const page = (value && typeof value === "object" ? value : {});
53
+ return {
54
+ totalElements: page.totalElements ?? 0,
55
+ totalPages: page.totalPages ?? 0,
56
+ content: Array.isArray(page.content) ? page.content : [],
57
+ };
58
+ }
59
+ function requireExactlyOne(id, code) {
60
+ if ((id ? 1 : 0) + (code ? 1 : 0) !== 1)
61
+ throw new ToolRefusalError({ status: "refused", blocker: "dictionary-target-required", message: "dictionary.get/records 必须且只能提供 id 或 code" });
62
+ }
63
+ export function registerDictionaryLane(tools) {
64
+ tools.push(defineTool({
65
+ name: "dictionary.list",
66
+ layer: "low",
67
+ kind: "read",
68
+ lane: LANE,
69
+ scope: TOOLS["dictionary.list"].scope,
70
+ description: TOOLS["dictionary.list"].description,
71
+ inputSchema: { environment: environmentArg, capability: capabilityArg, classificationId: z.string().optional(), searchWord: z.string().optional(), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(20), sortColumn: z.string().optional(), sortDirection: z.enum(["asc", "desc"]).optional() },
72
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
73
+ const data = await wireCall(session, { path: ENDPOINTS.dictionarySearch, query: { classificationId: args.classificationId ?? "", searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size), sortColumn: args.sortColumn ?? "", sortDirection: args.sortDirection ?? "desc" } });
74
+ return { dictionaries: recordsPage(data.data) };
75
+ }),
76
+ }));
77
+ tools.push(defineTool({
78
+ name: "dictionary.type",
79
+ layer: "low",
80
+ kind: "read",
81
+ lane: LANE,
82
+ scope: TOOLS["dictionary.type"].scope,
83
+ description: TOOLS["dictionary.type"].description,
84
+ inputSchema: { environment: environmentArg, capability: capabilityArg, type: dictionaryType },
85
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
86
+ const data = await wireCall(session, { path: ENDPOINTS.dictionaryGetByType, query: { type: String(args.type) } });
87
+ return { dictionaryType: args.type, dictionaries: Array.isArray(data.data) ? data.data.map(dictionaryView) : [] };
88
+ }),
89
+ }));
90
+ tools.push(defineTool({
91
+ name: "dictionary.names",
92
+ layer: "low",
93
+ kind: "read",
94
+ lane: LANE,
95
+ scope: TOOLS["dictionary.names"].scope,
96
+ description: TOOLS["dictionary.names"].description,
97
+ inputSchema: { environment: environmentArg, capability: capabilityArg },
98
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
99
+ const data = await wireCall(session, { path: ENDPOINTS.dictionaryIdAndNames, query: {} });
100
+ return { dictionaries: Array.isArray(data.data) ? data.data.map(dictionaryView) : [] };
101
+ }),
102
+ }));
103
+ tools.push(defineTool({
104
+ name: "dictionary.get",
105
+ layer: "low",
106
+ kind: "read",
107
+ lane: LANE,
108
+ scope: TOOLS["dictionary.get"].scope,
109
+ description: TOOLS["dictionary.get"].description,
110
+ inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional() },
111
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
112
+ requireExactlyOne(args.id, args.code);
113
+ const data = await wireCall(session, { path: args.id ? ENDPOINTS.dictionaryGet : ENDPOINTS.dictionaryGetByCode, query: args.id ? { id: args.id } : { code: args.code } });
114
+ return { dictionary: dictionaryView(data.data) };
115
+ }),
116
+ }));
117
+ tools.push(defineTool({
118
+ name: "dictionary.records",
119
+ layer: "low",
120
+ kind: "read",
121
+ lane: LANE,
122
+ scope: TOOLS["dictionary.records"].scope,
123
+ description: TOOLS["dictionary.records"].description,
124
+ inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional(), parentId: z.string().optional(), searchWord: z.string().optional(), enabledOnly: z.boolean().default(false), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(200) },
125
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
126
+ requireExactlyOne(args.id, args.code);
127
+ if (args.parentId) {
128
+ const data = await wireCall(session, { path: ENDPOINTS.dictionaryParentRecords, query: { parentId: args.parentId } });
129
+ return { parentId: args.parentId, records: Array.isArray(data.data) ? data.data : [] };
130
+ }
131
+ if (args.enabledOnly) {
132
+ const dictionary = args.id ? { id: args.id } : (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
133
+ const id = String(dictionary.id ?? "");
134
+ if (!id)
135
+ throw new WireError("business", "dictionary code not found", 301001);
136
+ const data = await wireCall(session, { path: ENDPOINTS.dictionaryEnabledRecords, query: { dicId: id } });
137
+ return { dictionaryId: id, enabledOnly: true, records: Array.isArray(data.data) ? data.data : [] };
138
+ }
139
+ const path = args.code ? ENDPOINTS.dictionaryRecordsByCode : ENDPOINTS.dictionaryRecords;
140
+ const query = args.code
141
+ ? { code: args.code, searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size) }
142
+ : { id: args.id, searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size) };
143
+ const data = await wireCall(session, { path, query });
144
+ return { records: recordsPage(data.data) };
145
+ }),
146
+ }));
147
+ writeTool(tools, "dictionary.save", LANE, TOOLS["dictionary.save"].scope, TOOLS["dictionary.save"].description, {
148
+ id: z.string().optional(), code: z.string().min(1), name: z.string().min(1), name_i18n: nameI18n, dictionaryType, classificationId: z.string().optional(), sortKey: z.number().int().optional(), status: z.boolean().optional(), records: recordsArg,
149
+ }, async (session, args) => {
150
+ const existing = (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
151
+ if (!args.id && existing && typeof existing === "object" && Object.keys(existing).length > 0)
152
+ throw new ToolRefusalError({ status: "refused", blocker: "dictionary-create-duplicate", code: args.code, message: `字典 ${args.code} 已存在;更新请传 id` });
153
+ const body = {
154
+ ...(args.id ? { id: args.id } : {}), code: args.code, name: args.name, name_i18n: JSON.stringify(args.name_i18n ?? { "zh-CN": args.name, en: args.name }), dictionaryType: args.dictionaryType,
155
+ classificationId: args.classificationId ?? "", sortKey: args.sortKey ?? 0, status: args.status ?? true,
156
+ records: (args.records ?? []).map((record) => normalizeRecord(record)),
157
+ };
158
+ const performed = await wireCall(session, { path: ENDPOINTS.dictionarySave, method: "POST", body });
159
+ const after = (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
160
+ // The create/update response only carries the header; the records the caller
161
+ // just sent are invisible unless they make another call (dogfood R3). Read
162
+ // them back here so "the header saved" cannot be mistaken for "the records
163
+ // were verified".
164
+ let recordsReadback = { totalElements: 0, totalPages: 0, content: [] };
165
+ try {
166
+ const stored = (await wireCall(session, { path: ENDPOINTS.dictionaryEnabledRecordsByCode, query: { code: args.code } })).data;
167
+ const all = (await wireCall(session, { path: ENDPOINTS.dictionaryRecordsByCode, query: { code: args.code, searchWord: "", page: "1", size: "200" } })).data;
168
+ recordsReadback = { ...recordsPage(all), enabledCount: Array.isArray(stored) ? stored.length : 0 };
169
+ }
170
+ catch {
171
+ // best-effort: the save itself is already authoritative; a record-read
172
+ // failure must not turn a landed write into a failure.
173
+ }
174
+ return { performed: { errcode: performed.errcode }, readback: { present: Boolean(after && Object.keys(after).length > 0), dictionary: dictionaryView(after), records: recordsReadback } };
175
+ });
176
+ writeTool(tools, "dictionary.status", LANE, TOOLS["dictionary.status"].scope, TOOLS["dictionary.status"].description, {
177
+ target: z.enum(["dictionary", "record"]), id: z.string().min(1), status: z.boolean(),
178
+ }, async (session, args) => {
179
+ const path = args.target === "dictionary" ? ENDPOINTS.dictionaryUpdateStatus : ENDPOINTS.dictionaryRecordStatus;
180
+ const performed = await wireCall(session, { path, method: "POST", body: { id: args.id, status: args.status } });
181
+ return { performed: { errcode: performed.errcode }, readback: { id: args.id, status: args.status } };
182
+ });
183
+ writeTool(tools, "dictionary.usage", LANE, TOOLS["dictionary.usage"].scope, TOOLS["dictionary.usage"].description, {
184
+ ids: z.array(z.string().min(1)).min(1).max(1000),
185
+ }, async (session, args) => {
186
+ const performed = await wireCall(session, { path: ENDPOINTS.dictionaryRecordUsed, method: "POST", body: { id: args.ids.join(";") } });
187
+ return { performed: { errcode: performed.errcode, ids: args.ids }, readback: { ids: args.ids, initialUsed: true } };
188
+ });
189
+ tools.push(defineTool({
190
+ name: "dictionary.parent",
191
+ layer: "low",
192
+ kind: "read",
193
+ lane: LANE,
194
+ scope: TOOLS["dictionary.parent"].scope,
195
+ description: TOOLS["dictionary.parent"].description,
196
+ inputSchema: { environment: environmentArg, capability: capabilityArg, parentId: z.string().min(1) },
197
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
198
+ const data = await wireCall(session, { path: ENDPOINTS.dictionaryParentRecords, query: { parentId: args.parentId } });
199
+ return { parentId: args.parentId, records: Array.isArray(data.data) ? data.data : [] };
200
+ }),
201
+ }));
202
+ tools.push(defineTool({
203
+ name: "dictionary.classes",
204
+ layer: "low",
205
+ kind: "read",
206
+ lane: LANE,
207
+ scope: TOOLS["dictionary.classes"].scope,
208
+ description: TOOLS["dictionary.classes"].description,
209
+ inputSchema: { environment: environmentArg, capability: capabilityArg, detail: z.boolean().default(false) },
210
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
211
+ const data = await wireCall(session, { path: args.detail ? ENDPOINTS.dictionaryClassDetail : ENDPOINTS.dictionaryClasses, query: {} });
212
+ return { detail: args.detail, classes: Array.isArray(data.data) ? data.data : [] };
213
+ }),
214
+ }));
215
+ writeTool(tools, "dictionary.class.save", LANE, TOOLS["dictionary.class.save"].scope, TOOLS["dictionary.class.save"].description, {
216
+ id: z.string().optional(), name: z.string().min(1), dictionaryIds: z.array(z.string()).optional(), sortKey: z.number().int().optional(),
217
+ }, async (session, args) => {
218
+ const performed = await wireCall(session, { path: ENDPOINTS.dictionaryClassSave, method: "POST", body: { ...(args.id ? { id: args.id } : {}), name: args.name, dictionaryIds: args.dictionaryIds ?? [], sortKey: args.sortKey ?? 0 } });
219
+ // Read back the created/updated class so the caller (and the audit trail)
220
+ // gets its id, not just the name it sent.
221
+ const listed = (await wireCall(session, { path: ENDPOINTS.dictionaryClasses, query: {} })).data;
222
+ const classes = Array.isArray(listed) ? listed : [];
223
+ const saved = classes.find((entry) => entry.name === args.name && (args.id === undefined || entry.id === args.id)) ?? classes.find((entry) => entry.name === args.name);
224
+ return { performed: { errcode: performed.errcode }, readback: { present: saved !== undefined, id: saved?.id, name: args.name, dictionaryIds: args.dictionaryIds ?? [] } };
225
+ });
226
+ writeTool(tools, "dictionary.class.delete", LANE, TOOLS["dictionary.class.delete"].scope, TOOLS["dictionary.class.delete"].description, {
227
+ ids: z.array(z.string().min(1)).min(1),
228
+ }, async (session, args) => {
229
+ const path = args.ids.length === 1 ? ENDPOINTS.dictionaryClassDelete : ENDPOINTS.dictionaryClassDeleteBatch;
230
+ const body = args.ids.length === 1 ? { id: args.ids[0] } : { ids: args.ids };
231
+ const performed = await wireCall(session, { path, method: "POST", body });
232
+ return { performed: { errcode: performed.errcode, ids: args.ids }, readback: { absent: true, ids: args.ids } };
233
+ });
234
+ }
235
+ //# sourceMappingURL=dictionary.js.map
@@ -0,0 +1,70 @@
1
+ import { assertDescriptionCarries, assertLaneContract, behaviorBinding, CONTRACT_FAMILY, CONTRACT_REFERENCE_VERSION } from "./contract-support.js";
2
+ import { LANES } from "./lane-ids.js";
3
+ /**
4
+ * environment-governance wire facts (live-proven). The read-only discovery
5
+ * surface and the canonical fingerprint are the single source for the handler
6
+ * descriptions and the coverage declarations.
7
+ */
8
+ export const ENVIRONMENT_WIRE = Object.freeze({
9
+ discoveryReads: "applicationList + applicationGroupList + applicationTree(appCode) + modelList(parentId='')",
10
+ readiness: "readiness {ready, blockers, preconditions, warnings, recovery, facts} derives from the same application/group/profile/version reads; no capability fan-out",
11
+ applicationScope: "lists every application the tenant exposes; applicationCode selects one and returns its folders/models",
12
+ portalVisibility: "group membership decides portal visibility: the portal renders NAMED groups; an app in the fallback group (code === 'all', case-insensitive) or with an unresolvable groupId is flagged portalHidden:true; move it with app.update {code, groupId}",
13
+ fingerprint: "canonical environment fingerprint (environment, origin, application, enterprise) from the authenticated session",
14
+ });
15
+ export const ENVIRONMENT_LANE_CONTRACT = Object.freeze({
16
+ lane: LANES.environment,
17
+ family: CONTRACT_FAMILY,
18
+ referenceVersion: CONTRACT_REFERENCE_VERSION,
19
+ endpoints: Object.freeze({
20
+ applicationList: "applicationList",
21
+ applicationGroupList: "applicationGroupList",
22
+ applicationTree: "applicationTree",
23
+ modelList: "modelList",
24
+ }),
25
+ wire: Object.freeze({
26
+ discoveryReads: ENVIRONMENT_WIRE.discoveryReads,
27
+ readiness: ENVIRONMENT_WIRE.readiness,
28
+ applicationScope: ENVIRONMENT_WIRE.applicationScope,
29
+ portalVisibility: ENVIRONMENT_WIRE.portalVisibility,
30
+ fingerprint: ENVIRONMENT_WIRE.fingerprint,
31
+ }),
32
+ wireBindings: Object.freeze({
33
+ discoveryReads: Object.freeze([behaviorBinding("test/discovery-group-visibility.test.ts#discover_environment exposes group membership and flags fallback-group apps", assertEnvironmentContract)]),
34
+ readiness: Object.freeze([behaviorBinding("test/ticket-04-environment-readiness.test.ts#discover_environment reports structured blockers when applications and groups are absent", assertEnvironmentContract)]),
35
+ applicationScope: Object.freeze([behaviorBinding("test/discovery-group-visibility.test.ts#discover_environment with applicationCode returns that app's models", assertEnvironmentContract)]),
36
+ portalVisibility: Object.freeze([behaviorBinding("test/discovery-group-visibility.test.ts#discover_environment fails closed: an unresolvable groupId is portalHidden", assertEnvironmentContract)]),
37
+ fingerprint: Object.freeze([behaviorBinding("test/lane-contracts.test.ts#environment lane contract pins the discovery/fingerprint wire", assertEnvironmentContract)]),
38
+ }),
39
+ evidence: Object.freeze([
40
+ "src/dbsweep.ts",
41
+ ".scratch/omc-route-wayfinder/leg2-closeout.md",
42
+ "packages/omc-cli/src/lanes/environment.ts",
43
+ ]),
44
+ tools: Object.freeze({
45
+ discover_environment: Object.freeze({
46
+ scope: "discovery fingerprint",
47
+ description: "发现应用、模型、字段与默认表单(只读)。默认列出全部应用及其门户分组归属;传入 applicationCode 时进入该应用并返回其下的模型。每个应用带 groupId/groupCode/groupName 与 portalHidden:落在兜底分组「全部」(code=all) 或分组解析不到的应用标 portalHidden:true——门户按具名分组渲染,这些应用在门户不可见,须用 app.update {code,groupId} 迁入具名分组。",
48
+ }),
49
+ "environment.fingerprint": Object.freeze({
50
+ scope: "fingerprint",
51
+ description: "Canonical environment fingerprint (environment, origin, application, enterprise).",
52
+ }),
53
+ }),
54
+ });
55
+ export function assertEnvironmentContract() {
56
+ assertLaneContract(ENVIRONMENT_LANE_CONTRACT);
57
+ assertDescriptionCarries(ENVIRONMENT_LANE_CONTRACT, "discover_environment", ["只读", "applicationCode", "模型", "分组", "portalHidden"]);
58
+ if (!ENVIRONMENT_WIRE.readiness.includes("no capability fan-out"))
59
+ throw new Error("environment contract must pin readiness as a bounded projection");
60
+ assertDescriptionCarries(ENVIRONMENT_LANE_CONTRACT, "environment.fingerprint", ["fingerprint", "environment"]);
61
+ if (!ENVIRONMENT_WIRE.discoveryReads.includes("applicationList"))
62
+ throw new Error("environment contract must pin the discovery reads");
63
+ if (!ENVIRONMENT_WIRE.applicationScope.includes("applicationCode"))
64
+ throw new Error("environment contract must pin the application scope selector");
65
+ if (!ENVIRONMENT_WIRE.portalVisibility.includes("portalHidden"))
66
+ throw new Error("environment contract must pin the portal-visibility flag");
67
+ if (!ENVIRONMENT_WIRE.fingerprint.includes("environment"))
68
+ throw new Error("environment contract must pin the fingerprint fields");
69
+ }
70
+ //# sourceMappingURL=environment-contract.js.map
@@ -0,0 +1,14 @@
1
+ import { claim, declareLaneCoverage } from "./coverage-declaration.js";
2
+ import { ENVIRONMENT_LANE_CONTRACT } from "./environment-contract.js";
3
+ const T = ENVIRONMENT_LANE_CONTRACT.tools;
4
+ export const ENVIRONMENT_COVERAGE = declareLaneCoverage(ENVIRONMENT_LANE_CONTRACT.lane, [
5
+ {
6
+ capability: "Environment governance (discovery/fingerprint/compat/dbsweep/license renewal)",
7
+ evidence: ["src/dbsweep.ts", ".scratch/omc-route-wayfinder/leg2-closeout.md"],
8
+ tools: [
9
+ claim("discover_environment", T["discover_environment"].scope),
10
+ claim("environment.fingerprint", T["environment.fingerprint"].scope),
11
+ ],
12
+ },
13
+ ]);
14
+ //# sourceMappingURL=environment-coverage.js.map
@@ -0,0 +1,163 @@
1
+ import { z } from "zod";
2
+ import { ENDPOINTS } from "../endpoints.js";
3
+ import { capabilityArg, environmentArg } from "../args.js";
4
+ import { governedRead } from "../read.js";
5
+ import { environmentWritable, requiresProductionConfirmation } from "../environment-policy.js";
6
+ import { flattenTreeRows } from "../support.js";
7
+ import { defineTool } from "../tool-types.js";
8
+ import { wireCall, wireRows } from "../wire.js";
9
+ import { ENVIRONMENT_LANE_CONTRACT } from "./environment-contract.js";
10
+ const ENVIRONMENT_LANE = ENVIRONMENT_LANE_CONTRACT.lane;
11
+ const ENVIRONMENT_TOOLS = ENVIRONMENT_LANE_CONTRACT.tools;
12
+ export function registerEnvironmentLane(tools) {
13
+ tools.push(defineTool({
14
+ name: "discover_environment",
15
+ layer: "high",
16
+ kind: "read",
17
+ lane: ENVIRONMENT_LANE,
18
+ scope: ENVIRONMENT_TOOLS["discover_environment"].scope,
19
+ description: ENVIRONMENT_TOOLS["discover_environment"].description,
20
+ inputSchema: {
21
+ environment: environmentArg,
22
+ applicationCode: z.string().optional(),
23
+ modelCode: z.string().optional(),
24
+ full: z.boolean().optional().describe("内联完整发现结果;默认返回计数与 detailFile"),
25
+ capability: capabilityArg,
26
+ },
27
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
28
+ // List every application the tenant exposes — the configured
29
+ // applicationCode is a default, never the only application. Resolve
30
+ // the active code through the same priority chain as writes:
31
+ // explicit > project-env binding > configured default > tenant's
32
+ // first application.
33
+ const appRows = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data);
34
+ // Ticket 57: group membership decides portal visibility. The portal
35
+ // renders NAMED groups; an app parked in the fallback group
36
+ // (code === "all", case-insensitive) is not rendered by that view, and
37
+ // an unresolvable groupId must fail closed rather than be assumed
38
+ // visible. The discovery read face exposes both so "choose an app" is
39
+ // an informed decision.
40
+ const groups = wireRows((await wireCall(session, { path: ENDPOINTS.applicationGroupList, query: {} })).data).map((row) => {
41
+ const code = String(row.code ?? "");
42
+ return { id: String(row.id ?? ""), code, name: row.name ?? row.groupName ?? code, isFallback: code.toLowerCase() === "all" };
43
+ });
44
+ const groupById = new Map(groups.map((group) => [group.id, group]));
45
+ const apps = appRows.map((row) => {
46
+ const groupId = row.groupId === undefined || row.groupId === null ? undefined : String(row.groupId);
47
+ const group = groupId ? groupById.get(groupId) : undefined;
48
+ return {
49
+ code: String(row.appCode ?? row.code),
50
+ name: row.appName ?? row.name,
51
+ groupId: groupId ?? null,
52
+ groupCode: group?.code ?? null,
53
+ groupName: group?.name ?? null,
54
+ portalHidden: group === undefined || group.isFallback === true,
55
+ };
56
+ });
57
+ const configured = session.profile.applicationCode?.trim() || undefined;
58
+ const explicit = typeof args.applicationCode === "string" && args.applicationCode.trim() ? args.applicationCode.trim() : undefined;
59
+ const selected = explicit ?? configured ?? apps.find((app) => typeof app.code === "string" && app.code)?.code;
60
+ const selectedApp = selected ? apps.find((app) => app.code === selected) : undefined;
61
+ const applicationResolution = explicit
62
+ ? selectedApp ? { status: "resolved", source: "explicit", requested: explicit } : { status: "requested-unresolved", source: "explicit", requested: explicit }
63
+ : configured
64
+ ? selectedApp ? { status: "resolved", source: "configured", requested: configured } : { status: "configured-unresolved", source: "configured", requested: configured }
65
+ : selectedApp ? { status: "resolved", source: "first-tenant-application", requested: selectedApp.code } : { status: "absent", source: "tenant", requested: null };
66
+ const groupStatus = groups.length === 0
67
+ ? { status: "absent" }
68
+ : groups.some((group) => !group.id) ? { status: "unresolved" } : { status: "resolved" };
69
+ const version = session.detectedVersion();
70
+ const readinessBlockers = [];
71
+ const readinessPreconditions = [];
72
+ const readinessWarnings = [];
73
+ const readinessRecovery = [];
74
+ if (applicationResolution.status === "absent") {
75
+ readinessBlockers.push({ code: "application-absent", message: "租户没有可选择的应用" });
76
+ readinessRecovery.push({ code: "app-create-prerequisite-missing", action: "先确认应用分组存在;分组缺失需由管理侧创建" });
77
+ }
78
+ else if (applicationResolution.status !== "resolved") {
79
+ readinessBlockers.push({ code: "configured-application-unresolved", requested: applicationResolution.requested, message: "配置的 applicationCode 在租户应用列表中不存在" });
80
+ readinessRecovery.push({ code: "application-selection-unresolved", action: "选择 discover_environment applications 中存在的 code,或清除错误的配置 applicationCode" });
81
+ }
82
+ if (groupStatus.status === "absent") {
83
+ readinessBlockers.push({ code: "application-groups-absent", message: "租户没有可用于应用创建的分组" });
84
+ readinessRecovery.push({ code: "app-create-prerequisite-missing", action: "由管理侧创建应用分组后重新 discover_environment" });
85
+ }
86
+ else if (groupStatus.status !== "resolved") {
87
+ readinessBlockers.push({ code: "application-groups-unresolved", message: "应用分组返回了无法解析的记录" });
88
+ }
89
+ const writable = environmentWritable(session.profile);
90
+ if (!writable)
91
+ readinessBlockers.push({ code: "environment-not-writable", message: "环境 writable=false,应用配置写入会被拒绝" });
92
+ if (!version)
93
+ readinessBlockers.push({ code: "version-evidence-missing", message: "未取得 CloudPivot 版本族证据,写入应 fail closed" });
94
+ if (requiresProductionConfirmation(session.profile))
95
+ readinessPreconditions.push({ code: "production-confirmation-required", requiredArg: "confirmProduction:true" });
96
+ readinessWarnings.push({ code: "app-publish-out-of-boundary", message: "app.create 不发布应用;需在设计器发布并核对具名分组后再做门户验证" });
97
+ const readiness = {
98
+ ready: readinessBlockers.length === 0,
99
+ blockers: readinessBlockers,
100
+ preconditions: readinessPreconditions,
101
+ warnings: readinessWarnings,
102
+ recovery: readinessRecovery,
103
+ facts: { writable, production: session.profile.production === true, versionEvidence: Boolean(version), selectedApplicationResolved: applicationResolution.status === "resolved", applicationGroupsResolved: groupStatus.status === "resolved" },
104
+ };
105
+ let foldersAndModels = [];
106
+ let models = [];
107
+ if (selectedApp) {
108
+ // Live (2026-09-17) the app tree is NESTED (Folder -> BizModel
109
+ // children); the previous top-level map dropped every model, so
110
+ // foldersAndModels showed only the folder. Flatten first (the same
111
+ // helper resolveModelTreeNode uses).
112
+ const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: selected } })).data));
113
+ foldersAndModels = tree.map((row) => ({ id: row.id, type: row.type, code: row.code, name: row.name ?? row.text }));
114
+ // The models under THIS application — derived from its tree, not the
115
+ // tenant-wide list_schema (which returned every model in the tenant
116
+ // regardless of applicationCode, contradicting the lane contract
117
+ // "applicationCode selects one and returns its folders/models").
118
+ models = tree
119
+ .filter((row) => String(row.type ?? "").toLowerCase() === "bizmodel")
120
+ .filter((row) => !args.modelCode || row.code === args.modelCode || row.schemaCode === args.modelCode)
121
+ .map((row) => ({ schemaCode: row.code ?? row.schemaCode, name: row.name }));
122
+ }
123
+ return {
124
+ applications: apps,
125
+ groups,
126
+ applicationCode: selected ?? null,
127
+ applicationResolution,
128
+ applicationGroups: groupStatus,
129
+ readiness,
130
+ foldersAndModels,
131
+ models,
132
+ };
133
+ }, {
134
+ full: args.full === true,
135
+ target: `discover_environment:${args.applicationCode ?? ""}:${args.modelCode ?? ""}`,
136
+ compact: (result) => {
137
+ const compact = { ...result };
138
+ for (const key of ["applications", "groups", "foldersAndModels", "models"]) {
139
+ const value = compact[key];
140
+ if (Array.isArray(value)) {
141
+ compact[`${key}Count`] = value.length;
142
+ delete compact[key];
143
+ }
144
+ }
145
+ return compact;
146
+ },
147
+ }),
148
+ }));
149
+ tools.push(defineTool({
150
+ name: "environment.fingerprint",
151
+ layer: "low",
152
+ kind: "read",
153
+ lane: ENVIRONMENT_LANE,
154
+ scope: ENVIRONMENT_TOOLS["environment.fingerprint"].scope,
155
+ description: ENVIRONMENT_TOOLS["environment.fingerprint"].description,
156
+ inputSchema: { environment: environmentArg, capability: capabilityArg },
157
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => ({
158
+ environment: session.environmentName,
159
+ fingerprint: session.environmentFingerprint(),
160
+ })),
161
+ }));
162
+ }
163
+ //# sourceMappingURL=environment.js.map