@cspeach/cli 1.1.17 → 1.1.19

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 (225) hide show
  1. package/README.md +3 -4
  2. package/dist/agent/loop.js +143 -25
  3. package/dist/agent/providers/local-provider.js +4 -1
  4. package/dist/agent/repair-partial.js +76 -19
  5. package/dist/agent/tool-dispatch.js +80 -22
  6. package/dist/agent/turn-error-ux.js +77 -26
  7. package/dist/agent/turn-stream.js +25 -3
  8. package/dist/approvals/adt-type.js +27 -0
  9. package/dist/approvals/advisory-prompt.js +48 -0
  10. package/dist/approvals/advisory-render.js +25 -11
  11. package/dist/approvals/approval-prompt.js +74 -24
  12. package/dist/approvals/jwt.js +2 -1
  13. package/dist/approvals/render.js +64 -36
  14. package/dist/approvals/risk-floor.js +53 -2
  15. package/dist/auth/api-key.js +30 -16
  16. package/dist/auth/keychain.js +0 -0
  17. package/dist/auth/me.js +25 -7
  18. package/dist/cli.js +25 -6
  19. package/dist/commands/compact.js +12 -9
  20. package/dist/commands/config-set.js +6 -2
  21. package/dist/commands/config-show.js +7 -5
  22. package/dist/commands/cost.js +41 -34
  23. package/dist/commands/help.js +88 -50
  24. package/dist/commands/login.js +28 -19
  25. package/dist/commands/logout.js +7 -3
  26. package/dist/commands/plan-chain.js +71 -77
  27. package/dist/commands/plan-continue.js +1 -0
  28. package/dist/commands/plan-resume.js +20 -12
  29. package/dist/commands/whoami.js +23 -9
  30. package/dist/config/loader.js +15 -3
  31. package/dist/cost/pricing.js +6 -5
  32. package/dist/doctor/checks/forge-rules.js +13 -3
  33. package/dist/doctor/checks/keychain.js +6 -4
  34. package/dist/doctor/run.js +101 -20
  35. package/dist/index.js +5 -2
  36. package/dist/lib/graceful-exit.js +56 -0
  37. package/dist/lib/piped-prompt.js +65 -0
  38. package/dist/lock-contention.js +2 -2
  39. package/dist/one-shot.js +135 -40
  40. package/dist/projects/answer-blockers.js +6 -1
  41. package/dist/projects/handover-md.js +15 -14
  42. package/dist/projects/plan-run.js +97 -54
  43. package/dist/projects/promote-command.js +11 -2
  44. package/dist/projects/save-command.js +3 -2
  45. package/dist/projects/status.js +2 -1
  46. package/dist/projects/yes-no.js +12 -0
  47. package/dist/renderer/abap-inline.js +18 -14
  48. package/dist/renderer/answer-trim.js +69 -0
  49. package/dist/renderer/banners.js +8 -8
  50. package/dist/renderer/brand-settled.js +19 -0
  51. package/dist/renderer/change-card.js +156 -0
  52. package/dist/renderer/checklist-format.js +130 -0
  53. package/dist/renderer/color-mode.js +45 -0
  54. package/dist/renderer/error-detail.js +104 -0
  55. package/dist/renderer/fence-state.js +46 -0
  56. package/dist/renderer/footer-line.js +96 -0
  57. package/dist/renderer/glyphs.js +25 -0
  58. package/dist/renderer/goodbye.js +38 -0
  59. package/dist/renderer/legacy-palette.js +41 -0
  60. package/dist/renderer/look.js +30 -0
  61. package/dist/renderer/markdown.js +299 -92
  62. package/dist/renderer/notice-log.js +72 -0
  63. package/dist/renderer/notice-shape.js +48 -0
  64. package/dist/renderer/notices.js +40 -8
  65. package/dist/renderer/panel-rows.js +16 -0
  66. package/dist/renderer/pipeline.js +66 -0
  67. package/dist/renderer/progress-chatter.js +23 -17
  68. package/dist/renderer/rendering-mode.js +34 -0
  69. package/dist/renderer/routing-line.js +14 -0
  70. package/dist/renderer/sanitize.js +12 -0
  71. package/dist/renderer/severity.js +2 -7
  72. package/dist/renderer/startup-lines.js +152 -0
  73. package/dist/renderer/status-footer.js +52 -34
  74. package/dist/renderer/steering-echo.js +41 -0
  75. package/dist/renderer/syntax.js +31 -12
  76. package/dist/renderer/tables.js +5 -1
  77. package/dist/renderer/theme.js +44 -0
  78. package/dist/renderer/thinking-heartbeat.js +33 -2
  79. package/dist/renderer/todo-block.js +10 -49
  80. package/dist/renderer/tool-labels.js +238 -0
  81. package/dist/renderer/tool-widget.js +245 -51
  82. package/dist/renderer/trace.js +42 -0
  83. package/dist/renderer/transcript-flow.js +132 -0
  84. package/dist/renderer/tty.js +34 -1
  85. package/dist/renderer/ui-width.js +42 -0
  86. package/dist/renderer/verify-chain.js +4 -2
  87. package/dist/renderer/widget-fallback.js +58 -65
  88. package/dist/repl/bracketed-paste.js +7 -1
  89. package/dist/repl/builtin-commands.js +19 -8
  90. package/dist/repl/credential-handover-gate.js +28 -0
  91. package/dist/repl/early-line-buffer.js +5 -2
  92. package/dist/repl/file-picker.js +54 -12
  93. package/dist/repl/ink-stdin-guard.js +66 -0
  94. package/dist/repl/inquirer-guard.js +59 -16
  95. package/dist/repl/inquirer-theme.js +27 -33
  96. package/dist/repl/paste-marker.js +19 -0
  97. package/dist/repl/plan-turn-end.js +20 -0
  98. package/dist/repl/post-turn-status.js +36 -11
  99. package/dist/repl/reroute-turn.js +21 -0
  100. package/dist/repl/reset-tty-stdin.js +44 -0
  101. package/dist/repl/restore-guard.js +22 -0
  102. package/dist/repl/resume-standalone.js +63 -0
  103. package/dist/repl/rule8-detector.js +11 -3
  104. package/dist/repl/safety-confirm.js +161 -93
  105. package/dist/repl/session-spend-line.js +8 -4
  106. package/dist/repl/themed-prompts.js +19 -0
  107. package/dist/repl/ui-look-command.js +75 -0
  108. package/dist/repl/update-method-preview-hook.js +48 -5
  109. package/dist/repl.js +795 -332
  110. package/dist/rewind/cli.js +3 -2
  111. package/dist/rewind/format.js +14 -9
  112. package/dist/rewind/restore.js +11 -0
  113. package/dist/router/classifier.js +47 -4
  114. package/dist/router/intent-extractor.js +24 -8
  115. package/dist/router/piped-routing.js +23 -0
  116. package/dist/router/resume-routing.js +16 -0
  117. package/dist/router/routing-failure.js +57 -0
  118. package/dist/sap/first-run-choice.js +1 -1
  119. package/dist/sap/onboarding.js +3 -2
  120. package/dist/sap/standalone-onboarding.js +1 -1
  121. package/dist/sap/system-info.js +4 -2
  122. package/dist/sap/unreachable.js +28 -0
  123. package/dist/sap-errors/clean-error-text.js +182 -0
  124. package/dist/session/interrupt-reason.js +39 -0
  125. package/dist/session/recap.js +36 -24
  126. package/dist/session/resume.js +104 -36
  127. package/dist/session/store.js +69 -27
  128. package/dist/session/user-prompt.js +21 -0
  129. package/dist/skill-catalog.js +7 -0
  130. package/dist/skills/bundled-skills.js +76 -69
  131. package/dist/skills/signing-public-key.js +1 -1
  132. package/dist/standards/standards-init.js +1 -1
  133. package/dist/test-helpers/answer-any.js +46 -0
  134. package/dist/tools/approval.js +27 -11
  135. package/dist/tools/ask-question.js +47 -5
  136. package/dist/tools/filesystem/file-write.js +19 -2
  137. package/dist/tools/fiori/fe-extend.js +16 -1
  138. package/dist/tools/fiori/fe-scaffold.js +105 -3
  139. package/dist/tools/fiori/html-escapes.js +28 -0
  140. package/dist/tools/fiori/metadata/audit.js +605 -0
  141. package/dist/tools/fiori/metadata/references.js +104 -0
  142. package/dist/tools/fiori/metadata/types.js +8 -0
  143. package/dist/tools/fiori/preview/app-guard.js +215 -0
  144. package/dist/tools/fiori/preview/env.js +193 -0
  145. package/dist/tools/fiori/preview/readiness.js +127 -0
  146. package/dist/tools/fiori/preview/registry.js +412 -0
  147. package/dist/tools/fiori/preview/start.js +469 -0
  148. package/dist/tools/fiori/samples/loader.js +20 -5
  149. package/dist/tools/fiori/scaffold.js +18 -6
  150. package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
  151. package/dist/tools/fiori/smoke/app-driver.js +100 -0
  152. package/dist/tools/fiori/smoke/assertions.js +161 -24
  153. package/dist/tools/fiori/smoke/audit-columns.js +21 -0
  154. package/dist/tools/fiori/smoke/batch.js +273 -0
  155. package/dist/tools/fiori/smoke/draft-safety.js +241 -0
  156. package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
  157. package/dist/tools/fiori/smoke/driver.js +171 -4
  158. package/dist/tools/fiori/smoke/evidence.js +35 -0
  159. package/dist/tools/fiori/smoke/labels-codes.js +207 -0
  160. package/dist/tools/fiori/smoke/nav-error.js +26 -0
  161. package/dist/tools/fiori/smoke/run-smoke.js +155 -12
  162. package/dist/tools/fiori/tools.js +651 -7
  163. package/dist/tools/fiori/ui5-version.js +16 -0
  164. package/dist/tools/index.js +8 -0
  165. package/dist/tools/local-build.js +6 -0
  166. package/dist/tools/sap-read.js +16 -1
  167. package/dist/tools/sap-write.js +122 -10
  168. package/dist/tools/shell/shell_exec.js +30 -12
  169. package/dist/tools/syntax-state.js +67 -0
  170. package/dist/tools/todo.js +23 -18
  171. package/dist/tools/transport-fit.js +462 -0
  172. package/dist/tools/transport-resolution.js +3 -1
  173. package/dist/tools/transport.js +33 -7
  174. package/dist/tools/verify.js +184 -8
  175. package/dist/ui/app.js +284 -146
  176. package/dist/ui/approval-modal.js +41 -15
  177. package/dist/ui/ask-answer-text.js +17 -0
  178. package/dist/ui/body.js +25 -6
  179. package/dist/ui/card-slot.js +98 -0
  180. package/dist/ui/checklist.js +9 -0
  181. package/dist/ui/coaching-picker-classic.js +8 -5
  182. package/dist/ui/command-palette.js +6 -4
  183. package/dist/ui/confirm-request.js +18 -0
  184. package/dist/ui/context-grid.js +2 -1
  185. package/dist/ui/file-palette.js +6 -4
  186. package/dist/ui/files-card-emitter.js +19 -0
  187. package/dist/ui/footer-line.js +16 -0
  188. package/dist/ui/footer.js +105 -104
  189. package/dist/ui/input-wrap.js +122 -0
  190. package/dist/ui/interrupt.js +103 -0
  191. package/dist/ui/key-burst.js +117 -0
  192. package/dist/ui/line-resolution.js +4 -3
  193. package/dist/ui/live-area.js +71 -0
  194. package/dist/ui/login-banner.js +54 -43
  195. package/dist/ui/rewind-panel.js +76 -24
  196. package/dist/ui/role-props.js +47 -0
  197. package/dist/ui/sap-state-store.js +1 -0
  198. package/dist/ui/session-timeline.js +49 -3
  199. package/dist/ui/skill-picker.js +5 -47
  200. package/dist/ui/text-input.js +128 -24
  201. package/dist/ui/todo-emitter.js +19 -0
  202. package/dist/ui/turn-status-emitter.js +28 -2
  203. package/dist/ui/turn-status.js +16 -19
  204. package/dist/ui/use-card-request.js +44 -0
  205. package/dist/ui/widgets/ask-card.js +521 -0
  206. package/dist/ui/widgets/bar-chart.js +11 -5
  207. package/dist/ui/widgets/coaching-picker.js +50 -55
  208. package/dist/ui/widgets/confirm-card.js +193 -0
  209. package/dist/ui/widgets/dep-graph.js +3 -2
  210. package/dist/ui/widgets/diff-viewer.js +6 -5
  211. package/dist/ui/widgets/files-card.js +97 -0
  212. package/dist/ui/widgets/question-card.js +2 -1
  213. package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
  214. package/dist/ui/widgets/shortcuts-sheet.js +25 -0
  215. package/dist/ui/widgets/stack-frames.js +2 -1
  216. package/dist/upgrade-check.js +39 -8
  217. package/package.json +11 -2
  218. package/test-harness/fe-draft/app.ts +188 -0
  219. package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
  220. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
  221. package/test-harness/fe-draft/routes.ts +224 -0
  222. package/test-harness/fe-draft/routes.unit.test.ts +45 -0
  223. package/test-harness/fe-draft/server.ts +225 -0
  224. package/test-harness/fe-draft/variants.ts +196 -0
  225. package/vitest.smoke-e2e.config.ts +20 -0
@@ -0,0 +1,605 @@
1
+ /**
2
+ * L1-1 — `auditMetadata`: machine truth about one entity set of a published
3
+ * V4 service. Pure: no network, no filesystem. The tool handler (tools.ts)
4
+ * does the fetching and hands the documents in.
5
+ *
6
+ * What the real documents taught us (fixtures/CAPTURE.md) and this file obeys:
7
+ * - Vocabulary aliases are `SAP__common`, `SAP__UI`, … never `Common.`/`UI.`.
8
+ * Nothing here matches a term by text: the EDMX parser resolves each
9
+ * document's own aliases, and the annotation converter re-keys everything
10
+ * under its default aliases (`annotations.Common.Label`, `annotations.UI.LineItem`).
11
+ * - Draft action names are fully qualified, and `DraftRoot`'s property ORDER
12
+ * differs between documents → read by property name, never by position.
13
+ * - A RAP V4 main document carries `ValueListReferences`, not `ValueList`.
14
+ * The mapping is `SAP__common.ValueListMapping` in the REFERENCED document,
15
+ * targeted at `SAP__ParentService.<EntityType>/<Property>` — which the parser
16
+ * unaliases to the parent's namespace, i.e. the main entity type's own
17
+ * fully-qualified name.
18
+ *
19
+ * One named function per concern — resolveLabel, resolveText, resolveValueHelp,
20
+ * isCoded, readDraft, readLineItem, readCapabilities, buildFlags — so the
21
+ * `coded` exclusions can be found without reading the whole file.
22
+ */
23
+ import { parse, merge } from '@sap-ux/edmx-parser';
24
+ import { convert } from '@sap-ux/annotation-converter';
25
+ import { resolveReferenceUrl } from './references.js';
26
+ const COMMON = 'com.sap.vocabularies.Common.v1';
27
+ const DRAFT_BOOKKEEPING = new Set(['IsActiveEntity', 'HasDraftEntity', 'HasActiveEntity']);
28
+ // ---------------------------------------------------------------------------
29
+ // Small value readers. The converter hands some primitives back as boxed
30
+ // String / Boolean objects (so they can carry nested annotations).
31
+ // ---------------------------------------------------------------------------
32
+ function str(v) {
33
+ if (v === undefined || v === null)
34
+ return undefined;
35
+ if (typeof v === 'string')
36
+ return v;
37
+ if (v instanceof String)
38
+ return v.valueOf();
39
+ if (typeof v === 'object' && typeof v.path === 'string')
40
+ return v.path;
41
+ if (typeof v === 'object' && typeof v.value === 'string')
42
+ return v.value;
43
+ return undefined;
44
+ }
45
+ /** Literal false (plain or boxed) → false; absent → `dflt`; anything else (true, a path) → true. */
46
+ function flag(v, dflt) {
47
+ if (v === undefined || v === null)
48
+ return dflt;
49
+ if (v === false)
50
+ return false;
51
+ if (v instanceof Boolean)
52
+ return v.valueOf();
53
+ if (v === 'false')
54
+ return false;
55
+ return true;
56
+ }
57
+ /** Every value of a UI term on the set and the type, qualified or not (`LineItem`, `LineItem#Q`). */
58
+ function uiTerms(set, et, term) {
59
+ const out = [];
60
+ for (const holder of [et?.annotations?.UI, set?.annotations?.UI]) {
61
+ if (!holder)
62
+ continue;
63
+ for (const key of Object.keys(holder)) {
64
+ if (key === '_keys')
65
+ continue;
66
+ if (key === term)
67
+ out.push({ value: holder[key] });
68
+ else if (key.startsWith(`${term}#`))
69
+ out.push({ qualifier: key.slice(term.length + 1), value: holder[key] });
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ function simplePath(v) {
75
+ const p = str(v?.path !== undefined ? v.path : v?.value);
76
+ return p && !p.includes('/') ? p : undefined;
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Entity set selection
80
+ // ---------------------------------------------------------------------------
81
+ function pickEntitySet(converted, wanted) {
82
+ const sets = converted.entitySets ?? [];
83
+ if (wanted) {
84
+ const hit = sets.find((s) => s.name === wanted);
85
+ if (!hit) {
86
+ throw new Error(`entity set "${wanted}" not found — the service has: ${sets.map((s) => s.name).join(', ') || '(none)'}`);
87
+ }
88
+ return hit;
89
+ }
90
+ // The brief's default is the first set with a UI.LineItem. Two of the three
91
+ // captured services carry NO UI annotations at all (no metadata extension),
92
+ // and `I_DraftAdministrativeData` is listed first — so the fallback is the
93
+ // draft root, then the first set that is not draft bookkeeping.
94
+ const withLineItem = sets.find((s) => uiTerms(s, s.entityType, 'LineItem').length > 0);
95
+ const draftRoot = sets.find((s) => s.annotations?.Common?.DraftRoot || s.entityType?.annotations?.Common?.DraftRoot);
96
+ const business = sets.find((s) => !/DraftAdministrativeData/.test(String(s.entityType?.name ?? s.name)));
97
+ const chosen = withLineItem ?? draftRoot ?? business ?? sets[0];
98
+ if (!chosen)
99
+ throw new Error('the service $metadata declares no entity set');
100
+ return chosen;
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Line item, field groups, selection fields, header — and "rendered"
104
+ // ---------------------------------------------------------------------------
105
+ function columnKind(type) {
106
+ const short = type?.slice(type.lastIndexOf('.') + 1);
107
+ switch (short) {
108
+ case 'DataField':
109
+ case 'DataFieldForAction':
110
+ case 'DataFieldForAnnotation':
111
+ case 'DataFieldWithUrl':
112
+ return short;
113
+ default:
114
+ return 'other';
115
+ }
116
+ }
117
+ function readLineItem(set, et) {
118
+ const li = et?.annotations?.UI?.LineItem ?? set?.annotations?.UI?.LineItem ?? [];
119
+ return li.map((rec, position) => {
120
+ const col = { kind: columnKind(str(rec?.$Type)), position };
121
+ const property = simplePath(rec?.Value);
122
+ if (property)
123
+ col.property = property;
124
+ const label = str(rec?.Label);
125
+ if (label !== undefined)
126
+ col.label = label;
127
+ return col;
128
+ });
129
+ }
130
+ function readFieldGroups(set, et) {
131
+ return uiTerms(set, et, 'FieldGroup').map(({ qualifier, value }) => {
132
+ const fg = {
133
+ properties: (value?.Data ?? []).map((d) => simplePath(d?.Value)).filter((p) => !!p),
134
+ };
135
+ if (qualifier !== undefined)
136
+ fg.qualifier = qualifier;
137
+ const label = str(value?.Label);
138
+ if (label !== undefined)
139
+ fg.label = label;
140
+ return fg;
141
+ });
142
+ }
143
+ function readSelectionFields(set, et) {
144
+ const sf = uiTerms(set, et, 'SelectionFields').find((t) => t.qualifier === undefined)?.value ?? [];
145
+ return sf.map((x) => simplePath(x)).filter((p) => !!p);
146
+ }
147
+ function readHeaderInfo(set, et) {
148
+ const h = et?.annotations?.UI?.HeaderInfo ?? set?.annotations?.UI?.HeaderInfo;
149
+ if (!h)
150
+ return undefined;
151
+ const out = {};
152
+ const typeName = str(h.TypeName);
153
+ const typeNamePlural = str(h.TypeNamePlural);
154
+ const title = simplePath(h.Title?.Value) ?? str(h.Title?.Value);
155
+ const description = simplePath(h.Description?.Value) ?? str(h.Description?.Value);
156
+ if (typeName !== undefined)
157
+ out.typeName = typeName;
158
+ if (typeNamePlural !== undefined)
159
+ out.typeNamePlural = typeNamePlural;
160
+ if (title !== undefined)
161
+ out.title = title;
162
+ if (description !== undefined)
163
+ out.description = description;
164
+ return out;
165
+ }
166
+ /** DataField-like records of every LineItem / FieldGroup / Identification, qualified or not. */
167
+ function renderingDataFields(set, et) {
168
+ const out = [];
169
+ for (const t of uiTerms(set, et, 'LineItem'))
170
+ out.push(...(t.value ?? []));
171
+ for (const t of uiTerms(set, et, 'Identification'))
172
+ out.push(...(t.value ?? []));
173
+ for (const t of uiTerms(set, et, 'FieldGroup'))
174
+ out.push(...(t.value?.Data ?? []));
175
+ return out;
176
+ }
177
+ function renderedProperties(set, et) {
178
+ const out = new Set();
179
+ for (const df of renderingDataFields(set, et)) {
180
+ const p = simplePath(df?.Value);
181
+ if (p)
182
+ out.add(p);
183
+ }
184
+ for (const t of uiTerms(set, et, 'SelectionFields')) {
185
+ for (const x of (t.value ?? [])) {
186
+ const p = simplePath(x);
187
+ if (p)
188
+ out.add(p);
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // Labels and texts
195
+ // ---------------------------------------------------------------------------
196
+ function resolveLabel(prop, dataFields) {
197
+ const common = str(prop?.annotations?.Common?.Label);
198
+ if (common !== undefined && common !== '')
199
+ return { label: common, labelSource: 'Common.Label' };
200
+ for (const df of dataFields) {
201
+ if (simplePath(df?.Value) !== prop.name)
202
+ continue;
203
+ const l = str(df?.Label);
204
+ if (l !== undefined && l !== '')
205
+ return { label: l, labelSource: 'DataField.Label' };
206
+ }
207
+ return { labelSource: 'none' };
208
+ }
209
+ function arrangementOf(v) {
210
+ const s = str(v);
211
+ if (!s)
212
+ return undefined;
213
+ const member = s.slice(s.lastIndexOf('/') + 1);
214
+ return ['TextFirst', 'TextLast', 'TextOnly', 'TextSeparate'].find((m) => m === member);
215
+ }
216
+ function resolveText(prop, et) {
217
+ const text = prop?.annotations?.Common?.Text;
218
+ const path = str(text);
219
+ if (!path)
220
+ return undefined;
221
+ const arrangement = arrangementOf(text?.annotations?.UI?.TextArrangement) ??
222
+ arrangementOf(prop?.annotations?.UI?.TextArrangement) ??
223
+ arrangementOf(et?.annotations?.UI?.TextArrangement) ??
224
+ 'TextFirst';
225
+ return { path, arrangement };
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Value helps
229
+ // ---------------------------------------------------------------------------
230
+ const PARAM_KINDS = {
231
+ ValueListParameterIn: 'In',
232
+ ValueListParameterOut: 'Out',
233
+ ValueListParameterInOut: 'InOut',
234
+ ValueListParameterDisplayOnly: 'DisplayOnly',
235
+ ValueListParameterFilterOnly: 'FilterOnly',
236
+ ValueListParameterConstant: 'Constant',
237
+ };
238
+ function param(type, localProperty, valueListProperty, constant) {
239
+ const kind = type ? PARAM_KINDS[type.slice(type.lastIndexOf('.') + 1)] : undefined;
240
+ if (!kind || valueListProperty === undefined)
241
+ return undefined;
242
+ const p = { kind, valueListProperty };
243
+ if (localProperty !== undefined && kind !== 'Constant' && kind !== 'DisplayOnly')
244
+ p.localProperty = localProperty;
245
+ if (constant !== undefined)
246
+ p.constant = constant;
247
+ return p;
248
+ }
249
+ /** A converted (typed) Common.ValueList / ValueListMapping record → Mapping. */
250
+ function mappingFromConverted(rec) {
251
+ const collectionPath = str(rec?.CollectionPath);
252
+ if (!collectionPath)
253
+ return undefined;
254
+ const parameters = (rec?.Parameters ?? [])
255
+ .map((p) => param(str(p?.$Type), str(p?.LocalDataProperty), str(p?.ValueListProperty), str(p?.Constant)))
256
+ .filter((p) => !!p);
257
+ return { collectionPath, parameters };
258
+ }
259
+ const rawScalar = (v) => v && v.type && typeof v[v.type] === 'string' ? v[v.type] : undefined;
260
+ const rawProps = (rec) => new Map((rec?.propertyValues ?? []).map((pv) => [pv.name, pv.value]));
261
+ /** A raw (parser-level) ValueListMapping record, from a referenced document → Mapping. */
262
+ function mappingFromRaw(rec) {
263
+ const pv = rawProps(rec);
264
+ const collectionPath = rawScalar(pv.get('CollectionPath'));
265
+ if (!collectionPath)
266
+ return undefined;
267
+ const params = pv.get('Parameters');
268
+ const items = (params && Array.isArray(params['Collection']) ? params['Collection'] : []);
269
+ const parameters = items
270
+ .map((it) => {
271
+ const p = rawProps(it);
272
+ return param(it?.type, rawScalar(p.get('LocalDataProperty')), rawScalar(p.get('ValueListProperty')), rawScalar(p.get('Constant')));
273
+ })
274
+ .filter((p) => !!p);
275
+ return { collectionPath, parameters };
276
+ }
277
+ /**
278
+ * In a referenced value-help document: the ValueListMapping (or ValueList)
279
+ * annotated on OUR property. The document declares our namespace under the
280
+ * `SAP__ParentService` alias; the parser has already unaliased the target, so
281
+ * it reads `<main namespace>.<EntityType>/<Property>`.
282
+ */
283
+ function mappingInReferencedDoc(xml, target) {
284
+ let raw;
285
+ try {
286
+ raw = parse(xml, 'valueHelp');
287
+ }
288
+ catch {
289
+ return { fixed: false };
290
+ }
291
+ let mapping;
292
+ let fixed = false;
293
+ for (const list of Object.values(raw.schema.annotations)) {
294
+ for (const t of list) {
295
+ if (t.target !== target)
296
+ continue;
297
+ for (const ann of t.annotations) {
298
+ if (!mapping && (ann.term === `${COMMON}.ValueListMapping` || ann.term === `${COMMON}.ValueList`)) {
299
+ mapping = mappingFromRaw(ann.record);
300
+ }
301
+ if (ann.term === `${COMMON}.ValueListWithFixedValues`) {
302
+ fixed = ann.value === undefined ? true : rawScalar(ann.value) !== 'false' && ann.value?.Bool !== false;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return { mapping, fixed };
308
+ }
309
+ function toValueHelp(propName, mapping, kind, fixedValues, ref) {
310
+ const keyish = mapping.parameters.filter((p) => p.kind === 'Out' || p.kind === 'InOut');
311
+ const key = keyish.find((p) => p.localProperty === propName) ?? keyish[0] ?? mapping.parameters[0];
312
+ const text = mapping.parameters.find((p) => p.kind === 'DisplayOnly') ??
313
+ keyish.find((p) => p !== key && p.localProperty !== propName);
314
+ const vh = {
315
+ kind: fixedValues ? 'fixed' : kind,
316
+ collectionPath: mapping.collectionPath,
317
+ keyProperty: key?.valueListProperty ?? '',
318
+ parameters: mapping.parameters,
319
+ fixedValues,
320
+ };
321
+ if (ref) {
322
+ vh.referenceUrl = ref.referenceUrl;
323
+ vh.resolvedPath = ref.resolvedPath;
324
+ }
325
+ if (text)
326
+ vh.textProperty = text.valueListProperty;
327
+ return vh;
328
+ }
329
+ function resolveValueHelp(prop, et, input) {
330
+ const common = prop?.annotations?.Common ?? {};
331
+ const fixedHere = flag(common.ValueListWithFixedValues, false);
332
+ const inlineRec = common.ValueList ??
333
+ common.ValueListMapping ??
334
+ Object.keys(common)
335
+ .filter((k) => k.startsWith('ValueList#') || k.startsWith('ValueListMapping#'))
336
+ .map((k) => common[k])[0];
337
+ if (inlineRec) {
338
+ const mapping = mappingFromConverted(inlineRec);
339
+ if (mapping)
340
+ return { declared: true, valueHelp: toValueHelp(prop.name, mapping, 'inline', fixedHere) };
341
+ }
342
+ const urls = (common.ValueListReferences ?? []).map(str).filter((u) => !!u);
343
+ if (urls.length === 0)
344
+ return { declared: !!inlineRec };
345
+ const target = `${et.fullyQualifiedName}/${prop.name}`;
346
+ let firstProblem;
347
+ for (const url of urls) {
348
+ const doc = input.referenceDocs.get(url);
349
+ if (doc === undefined) {
350
+ const status = input.referenceFailures?.get(url);
351
+ firstProblem ??= { url, status: status ?? 'not fetched' };
352
+ continue;
353
+ }
354
+ const { mapping, fixed } = mappingInReferencedDoc(doc, target);
355
+ if (!mapping) {
356
+ firstProblem ??= { url, status: 'no mapping' };
357
+ continue;
358
+ }
359
+ return {
360
+ declared: true,
361
+ valueHelp: toValueHelp(prop.name, mapping, 'references', fixedHere || fixed, {
362
+ referenceUrl: url,
363
+ resolvedPath: resolveReferenceUrl(input.servicePath, url),
364
+ }),
365
+ };
366
+ }
367
+ return { declared: true, unreachable: firstProblem };
368
+ }
369
+ /** Every `Criticality` path anywhere under the entity's UI annotations, plus property-level UI.Criticality. */
370
+ function collectCriticalityTargets(set, et) {
371
+ const out = new Set();
372
+ const seen = new Set();
373
+ const walk = (v, depth) => {
374
+ if (!v || typeof v !== 'object' || seen.has(v) || depth > 8)
375
+ return;
376
+ seen.add(v);
377
+ if (Array.isArray(v)) {
378
+ for (const x of v)
379
+ walk(x, depth + 1);
380
+ return;
381
+ }
382
+ for (const [k, x] of Object.entries(v)) {
383
+ if (k === 'Criticality') {
384
+ const p = simplePath(x);
385
+ if (p)
386
+ out.add(p);
387
+ continue;
388
+ }
389
+ if (k.startsWith('$') || k === 'fullyQualifiedName' || k === 'CollectionPathTarget')
390
+ continue;
391
+ walk(x, depth + 1);
392
+ }
393
+ };
394
+ walk(et?.annotations?.UI, 0);
395
+ walk(set?.annotations?.UI, 0);
396
+ for (const p of (et?.entityProperties ?? [])) {
397
+ const c = simplePath(p?.annotations?.UI?.Criticality);
398
+ if (c)
399
+ out.add(c);
400
+ }
401
+ return out;
402
+ }
403
+ function collectUnitTargets(et) {
404
+ const out = new Set();
405
+ for (const p of (et?.entityProperties ?? [])) {
406
+ for (const term of ['Unit', 'ISOCurrency']) {
407
+ const t = simplePath(p?.annotations?.Measures?.[term]);
408
+ if (t)
409
+ out.add(t);
410
+ }
411
+ }
412
+ return out;
413
+ }
414
+ function isCoded(prop, audit, hasValueHelp, ctx) {
415
+ // Shape: a short string code, or a small integer that has a value help.
416
+ const shortString = audit.type === 'Edm.String' && audit.maxLength !== undefined && audit.maxLength >= 1 && audit.maxLength <= 4;
417
+ const smallInt = (audit.type === 'Edm.Byte' || audit.type === 'Edm.Int16') && hasValueHelp;
418
+ if (!(shortString || smallInt))
419
+ return false;
420
+ if (!audit.rendered)
421
+ return false;
422
+ // Exclusions.
423
+ if (flag(prop?.annotations?.UI?.Hidden, false))
424
+ return false; // UI.Hidden (literal or dynamic)
425
+ if (ctx.criticalityTargets.has(prop.name))
426
+ return false; // another field's Criticality
427
+ const common = prop?.annotations?.Common ?? {};
428
+ if (flag(common.IsUnit, false) || flag(common.IsCurrency, false))
429
+ return false; // Common.IsUnit / IsCurrency
430
+ if (ctx.unitTargets.has(prop.name))
431
+ return false; // Measures.Unit / ISOCurrency target
432
+ if (flag(common.IsLanguageIdentifier, false))
433
+ return false; // Common.IsLanguageIdentifier
434
+ if (audit.isKey)
435
+ return false; // keys
436
+ if (DRAFT_BOOKKEEPING.has(prop.name) || ctx.draftAdminEntity)
437
+ return false; // draft bookkeeping
438
+ return true;
439
+ }
440
+ // ---------------------------------------------------------------------------
441
+ // Draft and capabilities
442
+ // ---------------------------------------------------------------------------
443
+ function readDraft(set, et) {
444
+ const root = set?.annotations?.Common?.DraftRoot ?? et?.annotations?.Common?.DraftRoot;
445
+ const node = set?.annotations?.Common?.DraftNode ?? et?.annotations?.Common?.DraftNode;
446
+ const rec = root ?? node;
447
+ const actions = {};
448
+ if (rec) {
449
+ // By property NAME: DraftRoot's order differs between real documents.
450
+ const edit = str(rec.EditAction);
451
+ const activate = str(rec.ActivationAction);
452
+ const prepare = str(rec.PreparationAction);
453
+ const discard = str(rec.DiscardAction);
454
+ if (edit)
455
+ actions.edit = edit;
456
+ if (activate)
457
+ actions.activate = activate;
458
+ if (prepare)
459
+ actions.prepare = prepare;
460
+ if (discard)
461
+ actions.discard = discard;
462
+ }
463
+ return { root: !!root, actions };
464
+ }
465
+ function readCapabilities(set) {
466
+ const cap = set?.annotations?.Capabilities ?? {};
467
+ return {
468
+ insertable: flag(cap.InsertRestrictions?.Insertable, true),
469
+ updatable: flag(cap.UpdateRestrictions?.Updatable, true),
470
+ deletable: flag(cap.DeleteRestrictions?.Deletable, true),
471
+ searchable: flag(cap.SearchRestrictions?.Searchable, true),
472
+ };
473
+ }
474
+ // ---------------------------------------------------------------------------
475
+ // Flags
476
+ // ---------------------------------------------------------------------------
477
+ function unreachableFix(property, u) {
478
+ const outcome = typeof u.status === 'number'
479
+ ? `HTTP ${u.status}`
480
+ : u.status === 'no mapping'
481
+ ? `HTTP 200, but it carries no ValueList for this field`
482
+ : 'not fetched';
483
+ return `Value-help service for \`${property}\` is not reachable (\`${u.url}\` → ${outcome}) — republish the binding.`;
484
+ }
485
+ function buildFlags(props, valueHelps) {
486
+ const flags = [];
487
+ for (const p of props) {
488
+ const vh = valueHelps.get(p.name);
489
+ if (p.rendered && p.labelSource === 'none') {
490
+ flags.push({
491
+ property: p.name,
492
+ code: 'no-label',
493
+ fix: `Property \`${p.name}\` is rendered but has no label. Add \`@EndUserText.label\` in the CDS view, ` +
494
+ 'or a `Label` in the metadata extension.',
495
+ });
496
+ }
497
+ if (p.coded && !p.text) {
498
+ flags.push({
499
+ property: p.name,
500
+ code: 'coded-without-text',
501
+ fix: `Coded field \`${p.name}\` has no \`Common.Text\`. Add a text/value-help view and ` +
502
+ '`@ObjectModel.text.element` plus `@UI.textArrangement: #TEXT_ONLY`.',
503
+ });
504
+ }
505
+ // A declared-but-unreachable value help is reported as unreachable, not missing.
506
+ if (p.coded && !p.valueHelp && !vh?.declared) {
507
+ flags.push({
508
+ property: p.name,
509
+ code: 'coded-without-value-help',
510
+ fix: `Coded field \`${p.name}\` has no value help. Add \`@Consumption.valueHelpDefinition\` ` +
511
+ 'pointing at the code-list view.',
512
+ });
513
+ }
514
+ if (vh?.unreachable) {
515
+ flags.push({ property: p.name, code: 'value-help-unreachable', fix: unreachableFix(p.name, vh.unreachable) });
516
+ }
517
+ }
518
+ return flags;
519
+ }
520
+ // ---------------------------------------------------------------------------
521
+ // Entry point
522
+ // ---------------------------------------------------------------------------
523
+ /** Pure. No network, no filesystem. */
524
+ export function auditMetadata(input) {
525
+ const main = parse(input.mainXml, 'serviceFile');
526
+ const raw = input.localAnnotationsXml ? merge(main, parse(input.localAnnotationsXml, 'localAnnotations')) : main;
527
+ const converted = convert(raw);
528
+ const set = pickEntitySet(converted, input.entitySet);
529
+ const et = set.entityType;
530
+ const keys = new Set((et?.keys ?? []).map((k) => k.name));
531
+ const rendered = renderedProperties(set, et);
532
+ const dataFields = renderingDataFields(set, et);
533
+ const ctx = {
534
+ criticalityTargets: collectCriticalityTargets(set, et),
535
+ unitTargets: collectUnitTargets(et),
536
+ draftAdminEntity: /DraftAdministrativeData/.test(String(et?.name ?? '')),
537
+ };
538
+ const valueHelps = new Map();
539
+ const properties = (et?.entityProperties ?? []).map((prop) => {
540
+ const vh = resolveValueHelp(prop, et, input);
541
+ valueHelps.set(prop.name, vh);
542
+ const base = {
543
+ name: prop.name,
544
+ type: String(prop.type),
545
+ nullable: prop.nullable !== false,
546
+ isKey: keys.has(prop.name),
547
+ rendered: rendered.has(prop.name),
548
+ labelSource: 'none',
549
+ coded: false,
550
+ };
551
+ if (typeof prop.maxLength === 'number')
552
+ base.maxLength = prop.maxLength;
553
+ // L1-5b: present only when true, so a property without either annotation
554
+ // audits exactly as before. A dynamic FieldControl (a Path) is not static
555
+ // "mandatory".
556
+ if (flag(prop?.annotations?.Core?.Computed, false))
557
+ base.computed = true;
558
+ const fc = prop?.annotations?.Common?.FieldControl;
559
+ if (typeof fc === 'string' ? /\/Mandatory$/.test(fc) : /\/Mandatory$/.test(String(fc?.valueOf?.() ?? ''))) {
560
+ base.mandatory = true;
561
+ }
562
+ const { label, labelSource } = resolveLabel(prop, dataFields);
563
+ base.labelSource = labelSource;
564
+ if (label !== undefined)
565
+ base.label = label;
566
+ const text = resolveText(prop, et);
567
+ if (text)
568
+ base.text = text;
569
+ if (vh.valueHelp)
570
+ base.valueHelp = vh.valueHelp;
571
+ base.coded = isCoded(prop, base, vh.declared, ctx);
572
+ return base;
573
+ });
574
+ // Every reference the caller fetched (or failed to), in document order.
575
+ const fetchedReferences = [];
576
+ const seen = new Set();
577
+ const record = (url, status) => {
578
+ if (seen.has(url))
579
+ return;
580
+ seen.add(url);
581
+ fetchedReferences.push({ url, resolvedPath: resolveReferenceUrl(input.servicePath, url), status });
582
+ };
583
+ for (const url of input.referenceDocs.keys())
584
+ record(url, 200);
585
+ for (const [url, status] of input.referenceFailures ?? [])
586
+ record(url, status);
587
+ const audit = {
588
+ servicePath: input.servicePath,
589
+ entitySet: set.name,
590
+ entityType: et?.name,
591
+ language: input.language ?? 'EN',
592
+ properties,
593
+ draft: readDraft(set, et),
594
+ lineItem: readLineItem(set, et),
595
+ selectionFields: readSelectionFields(set, et),
596
+ fieldGroups: readFieldGroups(set, et),
597
+ capabilities: readCapabilities(set),
598
+ flags: buildFlags(properties, valueHelps),
599
+ fetchedReferences,
600
+ };
601
+ const headerInfo = readHeaderInfo(set, et);
602
+ if (headerInfo)
603
+ audit.headerInfo = headerInfo;
604
+ return audit;
605
+ }