@cspeach/cli 1.1.35 → 1.1.37

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.
@@ -0,0 +1,57 @@
1
+ const camel = (base) => base.toLowerCase().split('_').filter(Boolean)
2
+ .map((p) => p[0].toUpperCase() + p.slice(1)).join('');
3
+ /**
4
+ * The names both skeleton paths build. `suffix` (final review I3, spec row 7):
5
+ * a digit appended to the base when a name is taken — every derived name
6
+ * carries it, and the cuts that keep the limits are made BEFORE it, so the
7
+ * digit is never cut off. The table name itself never changes.
8
+ */
9
+ export function proposeNames(table, suffix = '') {
10
+ const t = table.trim().toUpperCase();
11
+ const plain = t.replace(/^[ZY]/, '').replace(/^_/, '');
12
+ const base = `${plain}${suffix}`;
13
+ // Draft table ≤16 chars; strip ALL trailing underscores so it never ends in '__D'.
14
+ const draft = `${t.slice(0, 14 - suffix.length).replace(/_+$/, '')}${suffix}_D`;
15
+ // Service names ≤26 (SRVB limit): shorten the service base only (spec §3: "the plan proposes shorter ones").
16
+ const svcBase = `ZUI_${base}_O4`.length <= 26 ? base : `${plain.slice(0, 19 - suffix.length).replace(/_+$/, '')}${suffix}`;
17
+ const svc = `ZUI_${svcBase}_O4`;
18
+ return {
19
+ table: t, base, entity: camel(base),
20
+ root: `ZR_${base}`, projection: `ZC_${base}`, behaviorPool: `ZBP_${base}`,
21
+ draftTable: draft, serviceDefinition: svc, serviceBinding: svc,
22
+ };
23
+ }
24
+ export function namesFitLimits(n) {
25
+ return n.draftTable.length <= 16 && n.serviceBinding.length <= 26
26
+ && [n.root, n.projection, n.behaviorPool, n.serviceDefinition].every((x) => x.length <= 30);
27
+ }
28
+ /** The 9 objects SAP's uiservice generator creates, in SAP's preview order. */
29
+ export function generatorPlannedObjects(n, pkg) {
30
+ const p = pkg.toUpperCase();
31
+ return [
32
+ { type: 'DDLS', name: n.root, package: p },
33
+ { type: 'BDEF', name: n.root, package: p },
34
+ { type: 'CLAS', name: n.behaviorPool, package: p },
35
+ { type: 'TABL', name: n.draftTable, package: p },
36
+ { type: 'DDLS', name: n.projection, package: p },
37
+ { type: 'BDEF', name: n.projection, package: p },
38
+ { type: 'DDLX', name: n.projection, package: p },
39
+ { type: 'SRVD', name: n.serviceDefinition, package: p },
40
+ { type: 'SRVB', name: n.serviceBinding, package: p },
41
+ ];
42
+ }
43
+ export function buildUiserviceContent(n, pkg, description) {
44
+ return {
45
+ metadata: { package: pkg.toUpperCase(), masterLanguage: 'EN' },
46
+ general: { referenceObjectName: n.table, description: description.slice(0, 60) },
47
+ businessObject: {
48
+ dataModelEntity: { cdsName: n.root, entityName: n.entity },
49
+ behavior: { implementationType: 'Managed', implementationClass: n.behaviorPool, draftTable: n.draftTable },
50
+ },
51
+ serviceProjection: { name: n.projection },
52
+ businessService: {
53
+ serviceDefinition: { name: n.serviceDefinition },
54
+ serviceBinding: { name: n.serviceBinding, bindingType: 'OData V4 - UI' },
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,31 @@
1
+ import { cardLine } from '../lane/card.js';
2
+ import { OBJECT_TYPE_LABELS } from '../projects/plan-objects.js';
3
+ export const SKELETON_WHO = {
4
+ 'sap-generator': "SAP's generator",
5
+ cspeach: 'CSPeach writer',
6
+ };
7
+ export function skeletonCardRequest(d, blockedReason, n, objects) {
8
+ const by = d.by ?? 'cspeach';
9
+ const facts = [
10
+ `Table: ${n.table}`,
11
+ `Why: ${d.reason}`,
12
+ ...(by === 'sap-generator'
13
+ ? objects.map((o) => `${o.name} · ${OBJECT_TYPE_LABELS[o.type] ?? o.type}`)
14
+ : [`App shape: ${n.root}, ${n.projection}, ${n.serviceBinding}`]),
15
+ 'Either way, CSPeach then adds security, labels, value helps, logic and tests, and checks them.',
16
+ ];
17
+ const choices = [{ value: 'confirm', label: `Build the skeleton with the ${SKELETON_WHO[by]}` }];
18
+ if (by === 'sap-generator')
19
+ choices.push({ value: 'switch:cspeach', label: 'Use the CSPeach writer instead' });
20
+ else if (!blockedReason)
21
+ choices.push({ value: 'switch:sap-generator', label: "Use SAP's generator instead" });
22
+ choices.push({ value: 'cancel', label: 'Cancel' });
23
+ return {
24
+ id: 'rap-skeleton',
25
+ tag: 'QUESTION',
26
+ title: 'How should the RAP skeleton be built?',
27
+ facts: facts.map(cardLine),
28
+ choices: choices.map((c) => ({ ...c, label: cardLine(c.label) })),
29
+ initial: 0,
30
+ };
31
+ }
@@ -0,0 +1,38 @@
1
+ const RAP_TIERS = new Set(['s4-2020-plus', 'unknown']);
2
+ export function generatorBlockers(req, f) {
3
+ // Row 3 variant (final review I1): no SAP at all — nothing to check, the CSPeach writer.
4
+ if (f.connected === false)
5
+ return { row: 3, reason: 'Not connected to SAP' };
6
+ if (f.generator === 'no')
7
+ return { row: 3, reason: 'This system has no RAP generator' };
8
+ if (f.generator === 'unknown')
9
+ return { row: 3, reason: "Couldn't confirm a RAP generator on this system" };
10
+ if (req.entities !== 1 || req.implementation !== 'managed' || !req.draft || req.binding !== 'odata-v4-ui') {
11
+ return { row: 4, reason: "SAP's generator supports only one table, managed, with draft, V4 UI" };
12
+ }
13
+ // Row 4 variant (final review M2): the generator writes to a transport; a $ package has none.
14
+ if (f.localPackage)
15
+ return { row: 4, reason: "SAP's generator needs a transport, and a local package has none" };
16
+ if (f.tableExists && f.tableHasTimestamps === false)
17
+ return { row: 5, reason: "This table lacks the fields SAP's generator needs" };
18
+ if (f.tableExists && f.uiserviceOffered === false)
19
+ return { row: 6, reason: "SAP's generator isn't offered for this table" };
20
+ if (!f.namesFit)
21
+ return { row: 7, reason: "These names are too long for SAP's generator" };
22
+ if (f.tableExists && f.validation && !f.validation.ok) {
23
+ return { row: 7, reason: `SAP's generator check failed: ${f.validation.text || 'no reason given'}` };
24
+ }
25
+ return null;
26
+ }
27
+ export function decideSkeleton(req, f, override) {
28
+ if (!RAP_TIERS.has(f.tier))
29
+ return { by: null, row: 2, reason: 'RAP needs S/4HANA 2020 or later', overridden: false };
30
+ const blocked = generatorBlockers(req, f);
31
+ if (override === 'cspeach')
32
+ return { by: 'cspeach', row: 1, reason: 'Chosen by you', overridden: true };
33
+ if (override === 'sap-generator' && !blocked)
34
+ return { by: 'sap-generator', row: 1, reason: 'Chosen by you', overridden: true };
35
+ if (blocked)
36
+ return { by: 'cspeach', row: blocked.row, reason: blocked.reason, overridden: false };
37
+ return { by: 'sap-generator', row: 8, reason: "Fits SAP's generator: built in seconds, then finished by CSPeach", overridden: false };
38
+ }
@@ -0,0 +1,21 @@
1
+ // cspeach-cli/src/rap/skeleton-session.ts
2
+ //
3
+ // RAP generator plan, Task 8 fix round 1 — the session's latest skeleton
4
+ // decision. rap_skeleton_resolve records the {path, sha256} it just wrote; the
5
+ // plan save then accepts only that decision, so a plan cannot cite an older
6
+ // sha-valid file (e.g. one from before an owner override). Keyed on the session
7
+ // object in a WeakMap, like the app flow (lane/app-flow.ts): two sessions never
8
+ // see each other's decision and nothing outlives its session.
9
+ const latest = new WeakMap();
10
+ /** Record the decision rap_skeleton_resolve just wrote (replaces any earlier one). */
11
+ export function setLatestSkeletonDecision(session, ref) {
12
+ latest.set(session, { path: ref.path, sha256: ref.sha256 });
13
+ }
14
+ /** A frozen copy of the session's latest decision, or undefined when none was made. */
15
+ export function getLatestSkeletonDecision(session) {
16
+ const r = latest.get(session);
17
+ return r ? Object.freeze({ ...r }) : undefined;
18
+ }
19
+ export function clearLatestSkeletonDecision(session) {
20
+ latest.delete(session);
21
+ }
@@ -0,0 +1,40 @@
1
+ // cspeach-cli/src/rap/skeleton-store.ts
2
+ //
3
+ // RAP generator plan, Task 6 — the skeleton decision file, written only by
4
+ // rap_skeleton_resolve, read by the plan-save check and sap_generate_objects.
5
+ // sha-pinned like the lane decision (lane/store.ts): a hand edit is refused.
6
+ import { createHash, randomBytes } from 'node:crypto';
7
+ import { promises as fs } from 'node:fs';
8
+ import * as path from 'node:path';
9
+ const DIR = '.cspeach/lane';
10
+ const sha = (s) => createHash('sha256').update(s).digest('hex');
11
+ export async function writeSkeletonDecision(laneRoot, rec) {
12
+ const full = { kind: 'rap-skeleton', version: 1, ...rec, createdAt: new Date().toISOString() };
13
+ const body = `${JSON.stringify(full, null, 2)}\n`;
14
+ const day = full.createdAt.slice(0, 10).replace(/-/g, '');
15
+ const rel = `${DIR}/skeleton-${day}-${randomBytes(4).toString('hex')}.json`;
16
+ await fs.mkdir(path.join(laneRoot, DIR), { recursive: true });
17
+ await fs.writeFile(path.join(laneRoot, rel), body, { encoding: 'utf8', flag: 'wx' });
18
+ return { path: rel, sha256: sha(body) };
19
+ }
20
+ export async function readSkeletonDecision(laneRoot, relPath, sha256) {
21
+ const norm = String(relPath ?? '').replace(/\\/g, '/');
22
+ if (!/^\.cspeach\/lane\/skeleton-\d{8}-[0-9a-f]{8}\.json$/.test(norm)) {
23
+ return { ok: false, message: 'The skeleton decision path is not a CSPeach decision file. Run /abap-plan again.' };
24
+ }
25
+ let body;
26
+ try {
27
+ body = await fs.readFile(path.join(laneRoot, norm), 'utf8');
28
+ }
29
+ catch {
30
+ return { ok: false, message: 'The skeleton decision file is missing. Run /abap-plan again.' };
31
+ }
32
+ if (sha(body) !== sha256) {
33
+ return { ok: false, message: 'The skeleton decision file changed after it was saved. Run /abap-plan again.' };
34
+ }
35
+ const rec = JSON.parse(body);
36
+ if (rec.kind !== 'rap-skeleton' || rec.version !== 1) {
37
+ return { ok: false, message: 'The skeleton decision file is not readable by this CSPeach version. Run /abap-plan again.' };
38
+ }
39
+ return { ok: true, rec };
40
+ }
@@ -160,6 +160,9 @@ const RULES = {
160
160
  sap_link: (a) => ({ verb: 'SAP link', object: str(a['action']) ?? '' }),
161
161
  cap_link_smoke: (a) => ({ verb: 'S/4 link check', object: str(a['s4']?.['entitySet']) ?? '' }),
162
162
  app_lane_resolve: (a) => ({ verb: 'Chose lane', object: shortLeft(str(a['specPath']) ?? '') }),
163
+ rap_skeleton_resolve: (a) => ({ verb: 'Chose skeleton', object: str(a['table']) ?? '' }),
164
+ sap_generate_objects: () => ({ verb: 'Ran SAP generator', object: 'RAP skeleton' }),
165
+ rap_finish_check: (a) => ({ verb: 'Checked finish', object: str(a['root']) ?? '' }),
163
166
  team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
164
167
  rules_propose: () => ({ verb: 'Check a proposed decision rule', object: 'nothing is saved' }),
165
168
  team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
@@ -258,7 +261,7 @@ const PRESENT = {
258
261
  Structure: 'Reading structure of', 'Change log': 'Reading change log of', 'Read dumps': 'Reading dumps',
259
262
  'API state': 'Checking API state of', 'ABAP docs': 'Reading ABAP docs for', 'Read includes': 'Reading includes of',
260
263
  Compared: 'Comparing', 'Previewed change': 'Previewing change to', 'Ran unit tests': 'Running unit tests on',
261
- 'OData test': 'Testing OData', 'Service catalog': 'Reading service catalog', 'Import metadata': 'Importing metadata', 'SAP link': 'SAP link', 'S/4 link check': 'Checking S/4 link', 'Chose lane': 'Choosing build lane', 'Searched BAdIs': 'Searching BAdIs', 'Listed snapshots': 'Listing snapshots',
264
+ 'OData test': 'Testing OData', 'Service catalog': 'Reading service catalog', 'Import metadata': 'Importing metadata', 'SAP link': 'SAP link', 'S/4 link check': 'Checking S/4 link', 'Chose lane': 'Choosing build lane', 'Chose skeleton': 'Choosing RAP skeleton builder', 'Ran SAP generator': 'Running SAP generator', 'Checked finish': 'Checking finish items', 'Searched BAdIs': 'Searching BAdIs', 'Listed snapshots': 'Listing snapshots',
262
265
  'Read snapshot': 'Reading snapshot of', 'Cleaned snapshots': 'Cleaning snapshots',
263
266
  'Created transport': 'Creating transport', 'Transport for': 'Finding transport for',
264
267
  'Listed transports': 'Listing transports', Released: 'Releasing', 'Created domain': 'Creating domain',
@@ -32,12 +32,13 @@ export function setRule8BatchApprovedProvider(fn) {
32
32
  }
33
33
  /**
34
34
  * True for a tool that cannot run without an approval id from request_approval
35
- * (its schema requires `approval_id`): each such call is already approved one
35
+ * (its schema requires `approval_id`, or `approval_ids` for a multi-object write): each such call is already approved one
36
36
  * by one. Shell, file and other tools without an id are never in this set.
37
37
  */
38
38
  export function toolSpendsApprovalId(tool) {
39
39
  const req = tool.input_schema?.required;
40
- return Array.isArray(req) && req.includes('approval_id');
40
+ // RAP generator final review M4 — sap_generate_objects spends one id per object (`approval_ids`).
41
+ return Array.isArray(req) && (req.includes('approval_id') || req.includes('approval_ids'));
41
42
  }
42
43
  /**
43
44
  * Returns true on the 2nd (and later) mutating call of the turn — the gate
package/dist/repl.js CHANGED
@@ -172,6 +172,9 @@ import './tools/cap/metadata-import.js';
172
172
  import './tools/cap/sap-link-tool.js';
173
173
  import './tools/cap/link-smoke.js';
174
174
  import './tools/lane/app-lane-resolve.js';
175
+ import './tools/rap/skeleton-resolve.js';
176
+ import './tools/rap/generate-objects.js';
177
+ import './tools/rap/finish-check.js';
175
178
  import './tools/extend-model/tool.js';
176
179
  import './tools/text-elements/tools.js';
177
180
  import './tools/capability/tool.js';
@@ -1980,7 +1983,7 @@ export async function runRepl(opts) {
1980
1983
  // Task 7 — plan-phase subagent read-only enforcement: set for EVERY
1981
1984
  // plan-resume phase turn regardless of mode (guarded AND step);
1982
1985
  // agent_run consults the flag and forces its read-only tool filter.
1983
- setPlanPhaseActive(true, planPhaseCtx.envelope.id);
1986
+ setPlanPhaseActive(true, planPhaseCtx.envelope.id, planPhaseCtx.path);
1984
1987
  // Task 10 — the writes declaration of the phase EXECUTING this turn
1985
1988
  // (fail-safe: undefined ⇒ true, the deviation backstop never fires).
1986
1989
  // request_approval consults it in guarded mode to deny undeclared
@@ -3998,7 +4001,7 @@ export async function runRepl(opts) {
3998
4001
  const { setPlanPhaseActive, setGuardedRunActive, setCurrentPhaseWrites, getAndResetPlanDeviation } = await import('./commands/plan-gate.js');
3999
4002
  // Task 7 — plan-phase subagent read-only enforcement (classic mirror):
4000
4003
  // set for EVERY plan-resume phase turn regardless of mode.
4001
- setPlanPhaseActive(true, planPhaseCtx.envelope.id);
4004
+ setPlanPhaseActive(true, planPhaseCtx.envelope.id, planPhaseCtx.path);
4002
4005
  // Task 10 — executing phase's writes declaration for the deviation
4003
4006
  // backstop + stale-flag discard (classic mirror; see Ink path note).
4004
4007
  setCurrentPhaseWrites(planPhaseCtx.nextPhaseWrites ?? true);
@@ -28,6 +28,7 @@ import { effectiveRisk } from '../approvals/risk-floor.js';
28
28
  import { verifySyntax, verifyActive } from '../tools/verify.js';
29
29
  import { renderDiff } from '../repl/diff-display.js';
30
30
  import { parseIncludeSnapshotType } from '../tools/include-snapshot.js';
31
+ import { hasManualLocks } from '../sap/session-end.js';
31
32
  /** Compact single-line failure text for receipts / error envelopes. */
32
33
  function shortError(err) {
33
34
  const msg = err instanceof Error ? err.message : String(err);
@@ -148,7 +149,8 @@ export async function restoreSnapshot(c, deps) {
148
149
  if (currentSource === null) {
149
150
  let created;
150
151
  try {
151
- created = await deps.adt.createObject(c.objType, c.object, c.package ?? '', 'Restored via CSPeach rewind', deps.transport);
152
+ // Review fix round 1, I-1: keep the stateful session while a sap_lock is held.
153
+ created = await deps.adt.createObject(c.objType, c.object, c.package ?? '', 'Restored via CSPeach rewind', deps.transport, { keepSession: hasManualLocks() });
152
154
  }
153
155
  catch (err) {
154
156
  record('② written ✗');
@@ -0,0 +1,21 @@
1
+ const cache = new Map();
2
+ export async function getGeneratorFact(adt, alias) {
3
+ const hit = cache.get(alias);
4
+ if (hit)
5
+ return hit;
6
+ let r;
7
+ try {
8
+ r = await adt.hasRepositoryGenerators();
9
+ }
10
+ catch {
11
+ return 'unknown';
12
+ }
13
+ if (r === null)
14
+ return 'unknown';
15
+ const fact = r ? 'yes' : 'no';
16
+ cache.set(alias, fact);
17
+ return fact;
18
+ }
19
+ export function resetGeneratorFactCache() {
20
+ cache.clear();
21
+ }
@@ -26,6 +26,11 @@ export function noteManualLock() {
26
26
  export function noteManualUnlock() {
27
27
  manualLocks = Math.max(0, manualLocks - 1);
28
28
  }
29
+ /** True while a `sap_lock` handle is held (review fix round 1, I-1): sap-client's
30
+ * createObject must then keep the stateful session (extras.keepSession). */
31
+ export function hasManualLocks() {
32
+ return manualLocks > 0;
33
+ }
29
34
  export function __resetManualLocksForTests() {
30
35
  manualLocks = 0;
31
36
  }
@@ -59,6 +59,9 @@ const WRITE_TOOLS = new Set([
59
59
  // wrong silence.
60
60
  'sap_transport_create',
61
61
  'sap_transport_release',
62
+ // Final review I2 — SAP's RAP generator (tools/rap/generate-objects.ts) creates
63
+ // the skeleton objects in one call; its row names them.
64
+ 'sap_generate_objects',
62
65
  ]);
63
66
  /**
64
67
  * Non-mutating tools that are interaction/plumbing, not object reads — excluded
@@ -189,6 +192,19 @@ function writeObject(tool, args, result) {
189
192
  const inc = str(args.includeType);
190
193
  return inc ? `${cls} (${inc})` : cls;
191
194
  }
195
+ case 'sap_generate_objects': {
196
+ // The generated objects live in the RESULT; a failed call names its decision file.
197
+ let r = {};
198
+ try {
199
+ r = asRecord(JSON.parse(result ?? ''));
200
+ }
201
+ catch { /* r stays {} */ }
202
+ const names = Array.isArray(r.objects) ? r.objects.map((o) => str(asRecord(o).name)).filter(Boolean) : [];
203
+ if (names.length)
204
+ return names.join(', ');
205
+ const d = str(args.decisionPath);
206
+ return d ? `RAP skeleton (${d})` : 'RAP skeleton';
207
+ }
192
208
  case 'sap_activate': {
193
209
  if (Array.isArray(args.objects)) {
194
210
  const names = args.objects.map((o) => str(asRecord(o).name) ?? '?');
@@ -362,7 +378,9 @@ export function buildAuditReport(input) {
362
378
  L('| --- | --- | --- | :---: | --- | --- |');
363
379
  for (const c of writes) {
364
380
  const args = asRecord(c.args);
365
- const approvalId = str(args.approval_id);
381
+ // sap_generate_objects carries one approval per object (approval_ids); pair the first.
382
+ const approvalId = str(args.approval_id)
383
+ ?? (Array.isArray(args.approval_ids) ? str(args.approval_ids[0]) : undefined);
366
384
  const rec = findApproval(approvalId, approvals);
367
385
  const key = snapshotKey(c.tool, args);
368
386
  const snap = key ? newestSnapshotBefore(snapIndex.get(key), Date.parse(tsOf(c))) : undefined;
@@ -0,0 +1,118 @@
1
+ // cspeach-cli/src/tools/rap/finish-check.ts
2
+ //
3
+ // RAP generator plan, Task 10 — `rap_finish_check` (spec 2026-10-14 §6).
4
+ // Reads the app's live sources (root + projection DDLS, root BDEF, behavior
5
+ // pool CCIMP, projection DDLX, table, access controls) and reports the Finish
6
+ // items F1–F5 item by item, so the model cannot claim them done.
7
+ //
8
+ // A read that fails is never a pass:
9
+ // - a 404 on the DDLX is the F4 problem "No metadata extension for <projection>";
10
+ // - any other failed read (a 404 on the DDLS, BDEF, class include or table,
11
+ // or any non-404 error) is a tool error naming the object.
12
+ import { registerTool } from '../index.js';
13
+ import { checkFinish, FINISH_TITLES } from '../../rap/finish-checks.js';
14
+ const fail = (m) => ({ content: `error: ${m}`, is_error: true });
15
+ const NAME_RE = /^[A-Za-z0-9_/]{1,40}$/;
16
+ class ReadFailed extends Error {
17
+ }
18
+ function httpStatus(e) {
19
+ const s = e?.httpStatus;
20
+ return typeof s === 'number' ? s : undefined;
21
+ }
22
+ /** Reads one object; a 404 returns undefined only when `allow404`, every other failure throws ReadFailed. */
23
+ async function read(what, fn, allow404 = false) {
24
+ try {
25
+ return await fn();
26
+ }
27
+ catch (e) {
28
+ const status = httpStatus(e);
29
+ if (status === 404 && allow404)
30
+ return undefined;
31
+ if (status === 404)
32
+ throw new ReadFailed(`${what} not found. Build the app first, then run the finish check.`);
33
+ const why = status !== undefined ? `HTTP ${status}` : (e instanceof Error ? e.message : String(e));
34
+ throw new ReadFailed(`could not read ${what} (${why}). Nothing was checked.`);
35
+ }
36
+ }
37
+ export function formatFinish(items) {
38
+ const passed = items.filter((i) => i.ok).length;
39
+ return [
40
+ `Finish check: ${passed} of ${items.length} passed`,
41
+ ...items.map((i) => `${i.id} ${FINISH_TITLES[i.id]} — ${i.ok ? 'passed' : `needs work: ${i.problems.join('; ')}`}`),
42
+ ].join('\n');
43
+ }
44
+ export async function rapFinishCheckHandler(args, ctx) {
45
+ const names = {};
46
+ for (const k of ['root', 'projection', 'behaviorPool', 'entity']) {
47
+ const v = args[k];
48
+ if (typeof v !== 'string' || !NAME_RE.test(v.trim()))
49
+ return fail(`${k} is required (an object name)`);
50
+ names[k] = k === 'entity' ? v.trim() : v.trim().toUpperCase();
51
+ }
52
+ const { root, projection, behaviorPool, entity } = names;
53
+ let table;
54
+ if (args.table !== undefined && args.table !== null && args.table !== '') {
55
+ if (typeof args.table !== 'string' || !NAME_RE.test(args.table.trim()))
56
+ return fail('table must be a table name');
57
+ table = args.table.trim().toUpperCase();
58
+ }
59
+ let authObject;
60
+ if (args.authObject !== undefined && args.authObject !== null && args.authObject !== '') {
61
+ if (typeof args.authObject !== 'string' || !/^[A-Za-z0-9_]{1,10}$/.test(args.authObject.trim())) {
62
+ return fail('authObject must be an authorization object name');
63
+ }
64
+ authObject = args.authObject.trim().toUpperCase();
65
+ }
66
+ let codedFields = [];
67
+ if (args.codedFields !== undefined && args.codedFields !== null) {
68
+ if (!Array.isArray(args.codedFields) || !args.codedFields.every((f) => typeof f === 'string' && /^\w+$/.test(f.trim()))) {
69
+ return fail('codedFields must be a list of element names');
70
+ }
71
+ codedFields = args.codedFields.map((f) => f.trim());
72
+ }
73
+ const adt = ctx.adt;
74
+ let src;
75
+ try {
76
+ const rootDdls = (await read(`CDS view ${root}`, () => adt.getSource('DDLS', root)));
77
+ const projectionDdls = (await read(`CDS view ${projection}`, () => adt.getSource('DDLS', projection)));
78
+ const rootBdef = (await read(`behavior definition ${root}`, () => adt.getSource('BDEF', root)));
79
+ const ccimp = String((await read(`local types of ${behaviorPool}`, () => adt.classIncludes(behaviorPool, 'locals_imp'))));
80
+ const ddlx = await read(`metadata extension ${projection}`, () => adt.getSource('DDLX', projection), true);
81
+ const tableSrc = table ? await read(`table ${table}`, () => adt.getSource('TABL', table)) : undefined;
82
+ const dclsNames = [];
83
+ for (const n of [root, projection]) {
84
+ const hits = (await read(`access controls for ${n}`, () => adt.searchObject(n, 'DCLS', 10)));
85
+ if (hits.some((h) => h.name.toUpperCase() === n && /^DCLS(\/|$)/i.test(h.type)))
86
+ dclsNames.push(n);
87
+ }
88
+ src = { rootDdls, projectionDdls, rootBdef, ccimp, dclsNames, ...(ddlx !== undefined ? { ddlx } : {}), ...(tableSrc !== undefined ? { table: tableSrc } : {}) };
89
+ }
90
+ catch (e) {
91
+ if (e instanceof ReadFailed)
92
+ return fail(e.message);
93
+ throw e;
94
+ }
95
+ return { content: formatFinish(checkFinish(src, { root, projection, entity, authObject, codedFields })) };
96
+ }
97
+ registerTool({
98
+ name: 'rap_finish_check',
99
+ description: 'Check the Finish items F1–F5 of a RAP app against its live sources (authorization, access control, '
100
+ + 'read-only admin fields, labels, coded fields). Read-only. Run it after the Finish step and before the UI. '
101
+ + 'A failed item lists what to fix; fix only those and run it again.',
102
+ isMutating: false,
103
+ requiresSap: true,
104
+ input_schema: {
105
+ type: 'object',
106
+ properties: {
107
+ root: { type: 'string', description: 'Root interface view (ZR_…); also the root behavior definition' },
108
+ projection: { type: 'string', description: 'Projection view (ZC_…); its metadata extension has the same name' },
109
+ behaviorPool: { type: 'string', description: 'Behavior pool class (ZBP_…)' },
110
+ entity: { type: 'string', description: 'The BDEF entity alias, e.g. SbsBook2 (the app title must not be this)' },
111
+ table: { type: 'string', description: 'Persistence table, to check a UUID key uses managed numbering' },
112
+ authObject: { type: 'string', description: 'Authorization object from the spec; the auth method must check it' },
113
+ codedFields: { type: 'array', items: { type: 'string' }, description: 'Elements with a fixed code list (status, type, currency …)' },
114
+ },
115
+ required: ['root', 'projection', 'behaviorPool', 'entity'],
116
+ },
117
+ handler: (args, ctx) => rapFinishCheckHandler(args, ctx),
118
+ });