@cspeach/cli 1.1.18 → 1.1.20
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.
- package/README.md +3 -4
- package/dist/agent/anthropic-provider.js +30 -10
- package/dist/agent/cache-keepalive.js +162 -0
- package/dist/agent/cold-prune.js +116 -0
- package/dist/agent/loop.js +804 -159
- package/dist/agent/provider-shape.js +263 -0
- package/dist/agent/providers/ai-hub-provider.js +17 -2
- package/dist/agent/providers/byok-provider.js +33 -3
- package/dist/agent/providers/local-provider.js +12 -2
- package/dist/agent/repair-partial.js +125 -7
- package/dist/agent/summarise-via-provider.js +6 -1
- package/dist/agent/system-prompt.js +38 -0
- package/dist/agent/tool-dispatch.js +66 -21
- package/dist/agent/tool-loading-pin.js +100 -0
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +36 -6
- package/dist/commands/auto-compact.js +33 -16
- package/dist/commands/compact.js +49 -11
- package/dist/commands/config-set.js +16 -3
- package/dist/commands/config-show.js +18 -5
- package/dist/commands/cost.js +54 -35
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-audit.js +1 -0
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +70 -5
- package/dist/cost/cost-log.js +62 -2
- package/dist/cost/pricing.js +11 -6
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lib/spill-labels.js +13 -0
- package/dist/lock-contention.js +2 -2
- package/dist/models/resolve.js +93 -2
- package/dist/models/server-config.js +158 -3
- package/dist/one-shot.js +150 -45
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/image-attachments.js +15 -2
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +100 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +154 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +269 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +55 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/current-transport.js +13 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +43 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +863 -339
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/repin-model.js +18 -0
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +84 -28
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/preamble.js +75 -0
- package/dist/skills/source-manifest.js +11 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-read.js +11 -1
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/result-spill.js +238 -0
- package/dist/tools/sap-read.js +74 -15
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +39 -12
- package/dist/tools/subagent/adt-serial.js +33 -0
- package/dist/tools/subagent/agent_run.js +2 -0
- package/dist/tools/subagent/read_agent.js +178 -0
- package/dist/tools/subagent/reader-prompt.js +48 -0
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +26 -19
- package/dist/tools/tool-loading.js +255 -0
- package/dist/tools/tool-output-read.js +117 -0
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +39 -8
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +2 -1
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +65 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +12 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- 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
|
+
}
|