@cspeach/cli 1.1.48 → 1.1.50
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/dist/agent/cold-prune.js +1 -1
- package/dist/agent/conversation-prompt.js +52 -0
- package/dist/agent/level.js +1 -1
- package/dist/agent/loop.js +90 -8
- package/dist/agent/oversized-results.js +204 -0
- package/dist/agent/sap-connection-adapter.js +2 -0
- package/dist/agent/system-prompt.js +2 -2
- package/dist/agent/tool-dispatch.js +13 -1
- package/dist/agent/turn-error-ux.js +14 -0
- package/dist/approvals/op-labels.js +49 -10
- package/dist/approvals/render.js +14 -23
- package/dist/approvals/risk-floor.js +196 -19
- package/dist/approvals/visible-changes.js +35 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/plan-continue-gate.js +11 -0
- package/dist/commands/team-plan.js +16 -6
- package/dist/commands/team.js +295 -68
- package/dist/cost/cost-log.js +4 -0
- package/dist/cost/system-block.js +25 -0
- package/dist/journey/chain.js +489 -61
- package/dist/journey/clean-core.js +61 -36
- package/dist/journey/flow.js +75 -2
- package/dist/journey/people.js +37 -0
- package/dist/journey/rebuild.js +7 -6
- package/dist/journey/run.js +129 -42
- package/dist/journey/trigger.js +16 -10
- package/dist/models/server-config.js +13 -0
- package/dist/one-shot.js +1 -1
- package/dist/project-context/render.js +7 -0
- package/dist/projects/clean-core-confirm.js +166 -44
- package/dist/projects/extract-cca.js +8 -7
- package/dist/projects/index.js +0 -1
- package/dist/projects/merge-upgrade.js +2 -2
- package/dist/projects/project-folder.js +2 -1
- package/dist/projects/save-command.js +51 -14
- package/dist/projects/workspace.js +6 -3
- package/dist/register/decision-rules.js +2 -1
- package/dist/register/export-files.js +18 -0
- package/dist/register/picture.js +3 -3
- package/dist/register/project-transport.js +125 -0
- package/dist/register/records.js +25 -13
- package/dist/register/rules-edit.js +5 -0
- package/dist/register/status-file/progress-chart.js +17 -6
- package/dist/register/status-file/register-map.js +8 -5
- package/dist/register/status-file/register-model.js +55 -36
- package/dist/register/status-file/register-parts.js +97 -82
- package/dist/register/status-file/register-status-file.js +16 -7
- package/dist/register/status-file/report-model.js +293 -205
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-html.js +11 -4
- package/dist/register/store.js +2 -2
- package/dist/register/workbooks.js +24 -13
- package/dist/renderer/footer-line.js +10 -2
- package/dist/renderer/tool-labels.js +2 -2
- package/dist/repl/builtin-commands.js +1 -1
- package/dist/repl/continue-skill.js +5 -0
- package/dist/repl/level-command.js +6 -15
- package/dist/repl.js +37 -14
- package/dist/router/router-core.js +5 -3
- package/dist/session/fresh-start.js +15 -0
- package/dist/skill-catalog.js +4 -7
- package/dist/tools/_filesystem-shared.js +7 -0
- package/dist/tools/_team-work-path.js +53 -0
- package/dist/tools/approval.js +97 -19
- package/dist/tools/cut-markers.js +47 -0
- package/dist/tools/filesystem/extract-document.js +50 -2
- package/dist/tools/filesystem/file-edit.js +8 -1
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +8 -1
- package/dist/tools/filesystem/read-document.js +20 -4
- package/dist/tools/project/_merge-shared.js +2 -2
- package/dist/tools/project/_team-tool-shared.js +5 -0
- package/dist/tools/project/rules_propose.js +2 -1
- package/dist/tools/project/team_findings.js +81 -28
- package/dist/tools/project/team_lane_move.js +127 -0
- package/dist/tools/project/upgrade_merge_progress.js +8 -4
- package/dist/tools/text-elements/tools.js +7 -1
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/tool-output-read.js +5 -1
- package/dist/tools/update-method-intercept.js +18 -1
- package/dist/tools/write-pipeline.js +24 -5
- package/dist/ui/approval-modal.js +6 -3
- package/dist/ui/footer-line.js +19 -1
- package/package.json +3 -3
- package/dist/projects/merge-cca.js +0 -311
- package/dist/tools/project/cca_merge.js +0 -174
- package/test-harness/fe-draft/app.ts +0 -188
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +0 -461
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +0 -51
- package/test-harness/fe-draft/routes.ts +0 -224
- package/test-harness/fe-draft/routes.unit.test.ts +0 -45
- package/test-harness/fe-draft/server.ts +0 -225
- package/test-harness/fe-draft/variants.ts +0 -196
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic merge of N parallel /abap-cca assessment slices into ONE
|
|
3
|
-
* consolidated cca-assessment (the /abap-cca-merge skill's engine).
|
|
4
|
-
*
|
|
5
|
-
* WHY CODE, NOT PROSE: battery scenario S4 (docs/research/battery/
|
|
6
|
-
* s4-cca-scorecard.md, defects D6/D7) proved the model cannot keep
|
|
7
|
-
* manifest counts consistent with manifest rows when reconciling by
|
|
8
|
-
* hand — `retire: 0` next to 7 retire rows, 27/104 rows silently
|
|
9
|
-
* dropped. This module does the arithmetic; the model presents the
|
|
10
|
-
* result and adds judgment commentary, nothing else.
|
|
11
|
-
*
|
|
12
|
-
* Rules implemented (mirrors .claude/skills/abap-cca-merge/SKILL.md):
|
|
13
|
-
* - identity: same projectId across inputs, or opts.allowCrossProject
|
|
14
|
-
* - atcVariant must match across inputs (hard error — classifications
|
|
15
|
-
* produced under different ATC variants are not comparable)
|
|
16
|
-
* - scope-overlap detection: intersecting scope patterns AND duplicate
|
|
17
|
-
* object names across inputs both produce warnings
|
|
18
|
-
* - per-object union keyed by objectName|objectType (item ids are
|
|
19
|
-
* PER-INPUT local — item-001 exists in every slice — so ids cannot
|
|
20
|
-
* be the union key; merged rows are re-numbered)
|
|
21
|
-
* - conflict rule: most-restrictive wins (retire > redesign > fix >
|
|
22
|
-
* keep), recorded BOTH as a warning AND on the merged row
|
|
23
|
-
* (`conflict: { values: {label: cls}, rule: 'most-restrictive' }`)
|
|
24
|
-
* - counts RECOMPUTED from merged rows — never trusted from inputs
|
|
25
|
-
* - truncation-aware: an input marked classificationsTruncated yields
|
|
26
|
-
* a warning that the merge covers manifest rows only and the detail
|
|
27
|
-
* file must be consulted for full coverage
|
|
28
|
-
*/
|
|
29
|
-
import { OUTPUT_ROOT } from './output-paths.js';
|
|
30
|
-
import { isWindowsReservedName } from '../tools/_filesystem-shared.js';
|
|
31
|
-
/** Higher = more restrictive. Conflict resolution picks the max. */
|
|
32
|
-
const RESTRICTIVENESS = {
|
|
33
|
-
retire: 3,
|
|
34
|
-
redesign: 2,
|
|
35
|
-
fix: 1,
|
|
36
|
-
keep: 0,
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Manifest row-priority sort (matches abap-cca SKILL.md: "redesign first,
|
|
40
|
-
* then fix, then retire, then keep, ordered by confidence DESC within
|
|
41
|
-
* each group"). This is WORK priority, not restrictiveness.
|
|
42
|
-
*/
|
|
43
|
-
const ROW_PRIORITY = {
|
|
44
|
-
redesign: 0,
|
|
45
|
-
fix: 1,
|
|
46
|
-
retire: 2,
|
|
47
|
-
keep: 3,
|
|
48
|
-
};
|
|
49
|
-
const CONFIDENCE_RANK = {
|
|
50
|
-
high: 0,
|
|
51
|
-
medium: 1,
|
|
52
|
-
low: 2,
|
|
53
|
-
};
|
|
54
|
-
/* ── scope expression helpers ───────────────────────────────────────────── */
|
|
55
|
-
/**
|
|
56
|
-
* Split a scope expression like "ZFI* + ZSD*" or "ZFI*, ZSD*" into
|
|
57
|
-
* individual patterns. Exported for tests.
|
|
58
|
-
*/
|
|
59
|
-
export function parseScopePatterns(scope) {
|
|
60
|
-
return scope
|
|
61
|
-
.split(/[+,]/)
|
|
62
|
-
.map((s) => s.trim())
|
|
63
|
-
.filter((s) => s.length > 0);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Conservative intersection check for the prefix-glob patterns the
|
|
67
|
-
* --scope flag accepts (`ZFI*`, exact names). Two prefix globs intersect
|
|
68
|
-
* when one stem is a prefix of the other (`ZF*` ⊇ `ZFI*`). Patterns we
|
|
69
|
-
* cannot reason about (internal `*`) only match when identical —
|
|
70
|
-
* deliberately under-reports rather than spamming false overlaps.
|
|
71
|
-
* Exported for tests.
|
|
72
|
-
*/
|
|
73
|
-
export function scopePatternsIntersect(a, b) {
|
|
74
|
-
const A = a.trim().toUpperCase();
|
|
75
|
-
const B = b.trim().toUpperCase();
|
|
76
|
-
if (A === B)
|
|
77
|
-
return true;
|
|
78
|
-
const aStar = A.endsWith('*') && !A.slice(0, -1).includes('*');
|
|
79
|
-
const bStar = B.endsWith('*') && !B.slice(0, -1).includes('*');
|
|
80
|
-
const aStem = aStar ? A.slice(0, -1) : A;
|
|
81
|
-
const bStem = bStar ? B.slice(0, -1) : B;
|
|
82
|
-
if (A.includes('*') && !aStar)
|
|
83
|
-
return false; // unparseable — identical-only
|
|
84
|
-
if (B.includes('*') && !bStar)
|
|
85
|
-
return false;
|
|
86
|
-
if (aStar && bStar)
|
|
87
|
-
return aStem.startsWith(bStem) || bStem.startsWith(aStem);
|
|
88
|
-
if (aStar)
|
|
89
|
-
return B.startsWith(aStem);
|
|
90
|
-
if (bStar)
|
|
91
|
-
return A.startsWith(bStem);
|
|
92
|
-
return false; // two distinct exact names
|
|
93
|
-
}
|
|
94
|
-
/* ── merge ──────────────────────────────────────────────────────────────── */
|
|
95
|
-
function todayUtc() {
|
|
96
|
-
return new Date().toISOString().slice(0, 10);
|
|
97
|
-
}
|
|
98
|
-
/** A folder/file-name slug: `^[A-Za-z0-9._-]+$`, no `..`, not `.`, no device name. */
|
|
99
|
-
export function assertMergeSlug(label, value) {
|
|
100
|
-
const ok = typeof value === 'string'
|
|
101
|
-
&& value.length > 0 && value.length <= 100
|
|
102
|
-
&& /^[A-Za-z0-9._-]+$/.test(value)
|
|
103
|
-
&& !value.includes('..') && value !== '.'
|
|
104
|
-
&& !/[. ]$/.test(value)
|
|
105
|
-
&& !isWindowsReservedName(value);
|
|
106
|
-
if (!ok) {
|
|
107
|
-
throw new Error(`${label} ${JSON.stringify(value)} cannot name a folder — use letters, digits, ".", "_" and "-" only `
|
|
108
|
-
+ '(no "..", no device names such as CON or NUL).');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
export function mergeCcaAssessments(inputs, opts = {}) {
|
|
112
|
-
if (inputs.length < 2) {
|
|
113
|
-
throw new Error(`cca merge requires at least 2 inputs (got ${inputs.length}) — a single assessment needs no merge`);
|
|
114
|
-
}
|
|
115
|
-
const warnings = [];
|
|
116
|
-
const first = inputs[0];
|
|
117
|
-
// ── identity validation ──
|
|
118
|
-
for (const inp of inputs.slice(1)) {
|
|
119
|
-
if (inp.content.projectId !== first.content.projectId && !opts.allowCrossProject) {
|
|
120
|
-
throw new Error(`projectId mismatch: '${inp.label}' is project '${inp.content.projectId}' but '${first.label}' is '${first.content.projectId}'. `
|
|
121
|
-
+ `Merging different projects silently corrupts the estate view — pass allowCrossProject only for disjoint-scope slices of the same system.`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
for (const inp of inputs.slice(1)) {
|
|
125
|
-
if (inp.content.atcVariant !== first.content.atcVariant) {
|
|
126
|
-
throw new Error(`atcVariant mismatch: '${inp.label}' was classified under '${inp.content.atcVariant}' but '${first.label}' under '${first.content.atcVariant}'. `
|
|
127
|
-
+ `Classifications from different ATC variants are not comparable — re-run one slice with the matching variant.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// ── scope-overlap detection (warning, not blocker) ──
|
|
131
|
-
for (let i = 0; i < inputs.length; i++) {
|
|
132
|
-
for (let j = i + 1; j < inputs.length; j++) {
|
|
133
|
-
const a = inputs[i];
|
|
134
|
-
const b = inputs[j];
|
|
135
|
-
for (const pa of parseScopePatterns(a.content.scope)) {
|
|
136
|
-
for (const pb of parseScopePatterns(b.content.scope)) {
|
|
137
|
-
if (scopePatternsIntersect(pa, pb)) {
|
|
138
|
-
warnings.push(`SCOPE OVERLAP: '${a.label}' scope '${pa}' intersects '${b.label}' scope '${pb}' — slices were not disjoint; duplicate objects are reconciled most-restrictive.`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// ── truncation awareness ──
|
|
145
|
-
for (const inp of inputs) {
|
|
146
|
-
if (inp.content.classificationsTruncated) {
|
|
147
|
-
const shown = inp.content.classificationsShown ?? inp.content.classifications.length;
|
|
148
|
-
const total = inp.content.classificationsTotal ?? '?';
|
|
149
|
-
warnings.push(`TRUNCATED INPUT: '${inp.label}' manifest carries only ${shown} of ${total} classifications — `
|
|
150
|
-
+ `this merge covers the manifest rows only. For full coverage merge from the detail file at ${inp.content.detailPath}.`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// ── per-object union (key: objectName|objectType) ──
|
|
154
|
-
const byKey = new Map();
|
|
155
|
-
const keyOrder = [];
|
|
156
|
-
for (const inp of inputs) {
|
|
157
|
-
for (const row of inp.content.classifications) {
|
|
158
|
-
const key = `${row.objectName.toUpperCase()}|${row.objectType.toUpperCase()}`;
|
|
159
|
-
if (!byKey.has(key)) {
|
|
160
|
-
byKey.set(key, []);
|
|
161
|
-
keyOrder.push(key);
|
|
162
|
-
}
|
|
163
|
-
byKey.get(key).push({
|
|
164
|
-
label: inp.label,
|
|
165
|
-
classification: row.classification,
|
|
166
|
-
confidence: row.confidence,
|
|
167
|
-
objectName: row.objectName,
|
|
168
|
-
objectType: row.objectType,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const mergedRows = [];
|
|
173
|
-
for (const key of keyOrder) {
|
|
174
|
-
const sources = byKey.get(key);
|
|
175
|
-
const base = sources[0];
|
|
176
|
-
const distinct = new Set(sources.map((s) => s.classification));
|
|
177
|
-
if (sources.length > 1 && distinct.size === 1) {
|
|
178
|
-
warnings.push(`DUPLICATE OBJECT: ${base.objectName} (${base.objectType}) appears in ${sources.length} inputs `
|
|
179
|
-
+ `(${sources.map((s) => s.label).join(', ')}) with the same classification '${base.classification}' — overlapping slices, merged into one row.`);
|
|
180
|
-
}
|
|
181
|
-
if (distinct.size > 1) {
|
|
182
|
-
// conflict — most-restrictive wins
|
|
183
|
-
const winner = sources.reduce((best, s) => RESTRICTIVENESS[s.classification] > RESTRICTIVENESS[best.classification] ? s : best);
|
|
184
|
-
const values = {};
|
|
185
|
-
for (const s of sources)
|
|
186
|
-
values[s.label] = s.classification;
|
|
187
|
-
warnings.push(`CLASSIFICATION CONFLICT: ${base.objectName} (${base.objectType}) — `
|
|
188
|
-
+ sources.map((s) => `${s.label}: ${s.classification} [${s.confidence}]`).join('; ')
|
|
189
|
-
+ ` → resolved: ${winner.classification} (most-restrictive rule). Review with the consultants who classified before remediation planning.`);
|
|
190
|
-
mergedRows.push({
|
|
191
|
-
id: '', // re-numbered after sort
|
|
192
|
-
objectName: base.objectName,
|
|
193
|
-
objectType: base.objectType,
|
|
194
|
-
classification: winner.classification,
|
|
195
|
-
confidence: winner.confidence,
|
|
196
|
-
conflict: { values, rule: 'most-restrictive' },
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
mergedRows.push({
|
|
201
|
-
id: '',
|
|
202
|
-
objectName: base.objectName,
|
|
203
|
-
objectType: base.objectType,
|
|
204
|
-
classification: base.classification,
|
|
205
|
-
confidence: base.confidence,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// ── manifest row-priority sort + re-number ──
|
|
210
|
-
mergedRows.sort((a, b) => {
|
|
211
|
-
const p = ROW_PRIORITY[a.classification] - ROW_PRIORITY[b.classification];
|
|
212
|
-
if (p !== 0)
|
|
213
|
-
return p;
|
|
214
|
-
const c = CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence];
|
|
215
|
-
if (c !== 0)
|
|
216
|
-
return c;
|
|
217
|
-
return a.objectName.localeCompare(b.objectName);
|
|
218
|
-
});
|
|
219
|
-
mergedRows.forEach((row, i) => {
|
|
220
|
-
row.id = `item-${String(i + 1).padStart(3, '0')}`;
|
|
221
|
-
});
|
|
222
|
-
// ── counts RECOMPUTED from rows — never trusted from inputs ──
|
|
223
|
-
const tally = { keep: 0, fix: 0, retire: 0, redesign: 0 };
|
|
224
|
-
for (const row of mergedRows)
|
|
225
|
-
tally[row.classification]++;
|
|
226
|
-
// uncategorized objects have no classification rows, so they cannot be
|
|
227
|
-
// recomputed — summed across inputs (slices are disjoint by contract;
|
|
228
|
-
// overlap is already warned about above).
|
|
229
|
-
const uncategorized = inputs.reduce((sum, inp) => sum + (inp.content.summary.uncategorized || 0), 0);
|
|
230
|
-
const date = opts.mergedDate ?? todayUtc();
|
|
231
|
-
// SEC (review round 2): projectId comes from INSIDE an envelope (model-
|
|
232
|
-
// writable) and becomes a folder name under .cspeach/cca/projects/. Only a
|
|
233
|
-
// plain slug may do that — never `../`, a drive, a stream or a device name.
|
|
234
|
-
assertMergeSlug('projectId', first.content.projectId);
|
|
235
|
-
assertMergeSlug('merged date', date);
|
|
236
|
-
const mergedProjectId = `${first.content.projectId}_merged_${date}`;
|
|
237
|
-
const scopes = [];
|
|
238
|
-
for (const inp of inputs) {
|
|
239
|
-
const s = inp.content.scope.trim();
|
|
240
|
-
if (s.length > 0 && !scopes.includes(s))
|
|
241
|
-
scopes.push(s);
|
|
242
|
-
}
|
|
243
|
-
const merged = {
|
|
244
|
-
detailPath: `${OUTPUT_ROOT}/cca/projects/${mergedProjectId}/project.json`,
|
|
245
|
-
projectId: mergedProjectId,
|
|
246
|
-
scope: scopes.join(', '),
|
|
247
|
-
atcVariant: first.content.atcVariant,
|
|
248
|
-
summary: {
|
|
249
|
-
totalObjects: mergedRows.length + uncategorized,
|
|
250
|
-
keep: tally.keep,
|
|
251
|
-
fix: tally.fix,
|
|
252
|
-
retire: tally.retire,
|
|
253
|
-
redesign: tally.redesign,
|
|
254
|
-
uncategorized,
|
|
255
|
-
},
|
|
256
|
-
classifications: mergedRows,
|
|
257
|
-
};
|
|
258
|
-
return { merged, warnings };
|
|
259
|
-
}
|
|
260
|
-
/* ── manifest rendering ─────────────────────────────────────────────────── */
|
|
261
|
-
/** Manifest table cap — full rows live in the merged detail data. */
|
|
262
|
-
export const CCA_MANIFEST_ROW_CAP = 50;
|
|
263
|
-
function renderConflictCell(conflict) {
|
|
264
|
-
const pairs = Object.entries(conflict.values)
|
|
265
|
-
.map(([label, cls]) => `${label.replace(/[|;:]/g, '_')}:${cls}`)
|
|
266
|
-
.join(';');
|
|
267
|
-
return `conflict=${pairs}`;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Render the EXACT `<!-- csforge:cca-manifest ... -->` block for a merged
|
|
271
|
-
* assessment. The model must paste this VERBATIM — every number is
|
|
272
|
-
* computed here, and extract-cca.ts re-validates counts-vs-rows on save,
|
|
273
|
-
* so a hand-edited block fails loudly instead of saving silently wrong.
|
|
274
|
-
*
|
|
275
|
-
* `omitDetailPath` is for the detail-write-failed path in cca_merge:
|
|
276
|
-
* a manifest must never point at a file that does not exist, so the
|
|
277
|
-
* line is dropped (the extractor then refuses the save loudly).
|
|
278
|
-
*/
|
|
279
|
-
export function renderCcaMergeManifest(merged, title, opts = {}) {
|
|
280
|
-
const rows = merged.classifications.slice(0, CCA_MANIFEST_ROW_CAP);
|
|
281
|
-
const truncated = merged.classifications.length > CCA_MANIFEST_ROW_CAP;
|
|
282
|
-
const lines = [
|
|
283
|
-
'<!-- csforge:cca-manifest',
|
|
284
|
-
'artefact: cca-assessment',
|
|
285
|
-
`title: ${title}`,
|
|
286
|
-
...(opts.omitDetailPath ? [] : [`detail_path: ${merged.detailPath}`]),
|
|
287
|
-
`project_id: ${merged.projectId}`,
|
|
288
|
-
`scope: ${merged.scope}`,
|
|
289
|
-
`atc_variant: ${merged.atcVariant}`,
|
|
290
|
-
`total_objects: ${merged.summary.totalObjects}`,
|
|
291
|
-
`keep: ${merged.summary.keep}`,
|
|
292
|
-
`fix: ${merged.summary.fix}`,
|
|
293
|
-
`retire: ${merged.summary.retire}`,
|
|
294
|
-
`redesign: ${merged.summary.redesign}`,
|
|
295
|
-
`uncategorized: ${merged.summary.uncategorized}`,
|
|
296
|
-
];
|
|
297
|
-
if (truncated) {
|
|
298
|
-
lines.push('classifications_truncated: true');
|
|
299
|
-
lines.push(`classifications_shown: ${rows.length}`);
|
|
300
|
-
lines.push(`classifications_total: ${merged.classifications.length}`);
|
|
301
|
-
}
|
|
302
|
-
lines.push('classifications:');
|
|
303
|
-
for (const row of rows) {
|
|
304
|
-
const cells = [row.id, row.objectName, row.objectType, row.classification, row.confidence];
|
|
305
|
-
if (row.conflict)
|
|
306
|
-
cells.push(renderConflictCell(row.conflict));
|
|
307
|
-
lines.push(` ${cells.join(' | ')}`);
|
|
308
|
-
}
|
|
309
|
-
lines.push('-->');
|
|
310
|
-
return lines.join('\n');
|
|
311
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cca_merge — deterministic reconciliation of N parallel /abap-cca
|
|
3
|
-
* assessment envelopes (the /abap-cca-merge skill's execution seam).
|
|
4
|
-
*
|
|
5
|
-
* DESIGN (2026-06-12, team-chain): the skill used to ask the MODEL to
|
|
6
|
-
* reconcile envelopes by hand; battery S4 (D6/D7) proved models can't
|
|
7
|
-
* keep counts consistent with rows. This tool runs
|
|
8
|
-
* src/projects/merge-cca.ts and returns the merged content, warnings,
|
|
9
|
-
* AND a pre-rendered manifest block the model pastes VERBATIM. The
|
|
10
|
-
* normal save hook then persists the envelope — persistence
|
|
11
|
-
* deliberately stays on the one existing save path (confirm prompt,
|
|
12
|
-
* workspace resolution, validation, version semantics) instead of the
|
|
13
|
-
* tool minting envelopes through a second door. A merge is a NEW
|
|
14
|
-
* artefact superseding its inputs (new identity is correct — there is
|
|
15
|
-
* no prior chain to extend, unlike the plan-revision case in
|
|
16
|
-
* save-command.ts), and the extractor's counts-vs-rows validation acts
|
|
17
|
-
* as a tamper backstop if the model edits the pasted block.
|
|
18
|
-
*
|
|
19
|
-
* NOT flag-gated: the abap-cca-merge skill depends on this tool; gating
|
|
20
|
-
* it off would silently break the multi-consultant chain.
|
|
21
|
-
*
|
|
22
|
-
* Stays isMutating: false despite writing the merged detail file: the
|
|
23
|
-
* write is the merge's own local artifact under .cspeach/ (derived
|
|
24
|
-
* purely from envelopes the user supplied), the same trust level as the
|
|
25
|
-
* save hook persisting the envelope — no SAP or user-file mutation.
|
|
26
|
-
*/
|
|
27
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
28
|
-
import { dirname, isAbsolute, join } from 'node:path';
|
|
29
|
-
import { registerTool } from '../index.js';
|
|
30
|
-
import { mergeCcaAssessments, renderCcaMergeManifest, } from '../../projects/merge-cca.js';
|
|
31
|
-
import { loadEnvelope, MANIFEST_PASTE_INSTRUCTIONS } from './_merge-shared.js';
|
|
32
|
-
import { assertWritable } from '../_filesystem-shared.js';
|
|
33
|
-
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamWorkTarget } from '../_team-work-path.js';
|
|
34
|
-
export async function ccaMergeHandler(args, ctx) {
|
|
35
|
-
if (!Array.isArray(args.inputs) || args.inputs.some((i) => typeof i !== 'string')) {
|
|
36
|
-
return { content: 'error: inputs must be an array of envelope paths / @-tokens.', is_error: true };
|
|
37
|
-
}
|
|
38
|
-
const tokens = args.inputs;
|
|
39
|
-
if (tokens.length === 1) {
|
|
40
|
-
return {
|
|
41
|
-
content: 'error: only one input supplied — a single assessment needs no merge. Use the file directly.',
|
|
42
|
-
is_error: true,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (tokens.length < 2) {
|
|
46
|
-
return { content: 'error: at least 2 input envelopes are required.', is_error: true };
|
|
47
|
-
}
|
|
48
|
-
if (args.allow_cross_project !== undefined && typeof args.allow_cross_project !== 'boolean') {
|
|
49
|
-
return { content: 'error: allow_cross_project must be a boolean.', is_error: true };
|
|
50
|
-
}
|
|
51
|
-
// load + type-check every input before merging — fail with the precise file
|
|
52
|
-
const inputs = [];
|
|
53
|
-
const perInput = [];
|
|
54
|
-
for (const token of tokens) {
|
|
55
|
-
let resolved;
|
|
56
|
-
try {
|
|
57
|
-
resolved = await loadEnvelope(token, ctx.cwd, 'cca-assessment');
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
return { content: `error: ${e instanceof Error ? e.message : String(e)}`, is_error: true };
|
|
61
|
-
}
|
|
62
|
-
const content = resolved.envelope.content;
|
|
63
|
-
inputs.push({ label: resolved.label, content });
|
|
64
|
-
perInput.push({
|
|
65
|
-
file: resolved.path,
|
|
66
|
-
label: resolved.label,
|
|
67
|
-
project_id: content.projectId,
|
|
68
|
-
scope: content.scope,
|
|
69
|
-
total_objects: content.summary.totalObjects,
|
|
70
|
-
keep: content.summary.keep,
|
|
71
|
-
fix: content.summary.fix,
|
|
72
|
-
retire: content.summary.retire,
|
|
73
|
-
redesign: content.summary.redesign,
|
|
74
|
-
uncategorized: content.summary.uncategorized,
|
|
75
|
-
truncated: content.classificationsTruncated === true,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
let merged;
|
|
79
|
-
let warnings;
|
|
80
|
-
try {
|
|
81
|
-
({ merged, warnings } = mergeCcaAssessments(inputs, {
|
|
82
|
-
allowCrossProject: args.allow_cross_project === true,
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
return { content: `error: ${e instanceof Error ? e.message : String(e)}`, is_error: true };
|
|
87
|
-
}
|
|
88
|
-
const title = `${inputs[0].content.projectId} — Merged (${inputs.length} slices)`;
|
|
89
|
-
// ── write the merged detail file BEFORE emitting the manifest ──────────
|
|
90
|
-
// The manifest's detail_path must point at a REAL file (the rule this
|
|
91
|
-
// branch added for /abap-cca emitters applies to the merge too). The
|
|
92
|
-
// manifest table caps at 50 rows; this file always carries every merged
|
|
93
|
-
// row. Minimal documented shape (a merge supersedes its inputs, so this
|
|
94
|
-
// is a fresh consolidated detail, not a phase-state project.json):
|
|
95
|
-
// {
|
|
96
|
-
// generatedAt: ISO timestamp of the merge
|
|
97
|
-
// projectId / scope / atcVariant / summary: as in the manifest
|
|
98
|
-
// objects: ALL merged classification rows (uncapped)
|
|
99
|
-
// mergedFrom: the input envelopes [{ file, label }]
|
|
100
|
-
// }
|
|
101
|
-
let detailWritten = true;
|
|
102
|
-
// Task 15 — in a team folder the detail file is a working file: the seat's.
|
|
103
|
-
const t = teamWorkTarget(ctx.cwd, merged.detailPath);
|
|
104
|
-
const detailAbs = isAbsolute(t.path) ? t.path : join(t.root, t.path);
|
|
105
|
-
try {
|
|
106
|
-
// SEC (review round 2): the real landing path must stay in the project and
|
|
107
|
-
// not be protected (a junctioned .cspeach/<domain> -> .git is refused).
|
|
108
|
-
const realDetail = await assertWritable(detailAbs, t.root);
|
|
109
|
-
// Final fix wave, item 3 — never into the shared .cspeach of a team folder.
|
|
110
|
-
if (pointsIntoSharedCspeach(ctx.cwd, realDetail))
|
|
111
|
-
throw new Error(SHARED_CSPEACH_REFUSAL);
|
|
112
|
-
mkdirSync(dirname(detailAbs), { recursive: true });
|
|
113
|
-
writeFileSync(detailAbs, JSON.stringify({
|
|
114
|
-
generatedAt: new Date().toISOString(),
|
|
115
|
-
projectId: merged.projectId,
|
|
116
|
-
scope: merged.scope,
|
|
117
|
-
atcVariant: merged.atcVariant,
|
|
118
|
-
summary: merged.summary,
|
|
119
|
-
objects: merged.classifications,
|
|
120
|
-
mergedFrom: perInput.map((p) => ({ file: p.file, label: p.label })),
|
|
121
|
-
}, null, 2), 'utf8');
|
|
122
|
-
}
|
|
123
|
-
catch (e) {
|
|
124
|
-
detailWritten = false;
|
|
125
|
-
warnings.push(`DETAIL WRITE FAILED: could not write the merged detail file '${merged.detailPath}' `
|
|
126
|
-
+ `(${e instanceof Error ? e.message : String(e)}). The manifest block omits detail_path — `
|
|
127
|
-
+ `a manifest must never point at a file that does not exist — so the save will refuse it. `
|
|
128
|
-
+ `Fix the path/permissions and re-run the merge.`);
|
|
129
|
-
}
|
|
130
|
-
const manifestBlock = renderCcaMergeManifest(merged, title, { omitDetailPath: !detailWritten });
|
|
131
|
-
return {
|
|
132
|
-
content: JSON.stringify({
|
|
133
|
-
merged_summary: merged.summary,
|
|
134
|
-
merged_project_id: merged.projectId,
|
|
135
|
-
merged_scope: merged.scope,
|
|
136
|
-
atc_variant: merged.atcVariant,
|
|
137
|
-
...(detailWritten ? { detail_path: merged.detailPath } : {}),
|
|
138
|
-
per_input: perInput,
|
|
139
|
-
warnings,
|
|
140
|
-
// full merged rows — the manifest block caps at 50; nothing is lost
|
|
141
|
-
// from the transcript even for large estates.
|
|
142
|
-
merged_classifications: merged.classifications,
|
|
143
|
-
manifest_block: manifestBlock,
|
|
144
|
-
instructions: MANIFEST_PASTE_INSTRUCTIONS,
|
|
145
|
-
}, null, 2),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
registerTool({
|
|
149
|
-
name: 'cca_merge',
|
|
150
|
-
description: 'Deterministically merge 2+ parallel /abap-cca assessment envelopes (.cspeach.json) into one consolidated '
|
|
151
|
-
+ 'cca-assessment. Validates project identity and ATC variant, detects scope overlaps and classification '
|
|
152
|
-
+ 'conflicts (most-restrictive wins: retire > redesign > fix > keep), recomputes all counts from the merged '
|
|
153
|
-
+ 'rows, writes the full merged rows to a new detail file under .cspeach/cca/projects/, and returns warnings '
|
|
154
|
-
+ 'plus a ready-made csforge:cca-manifest block to paste verbatim. '
|
|
155
|
-
+ 'Inputs accept absolute/relative paths or bare workspace @-tokens.',
|
|
156
|
-
isMutating: false,
|
|
157
|
-
category: 'project',
|
|
158
|
-
input_schema: {
|
|
159
|
-
type: 'object',
|
|
160
|
-
properties: {
|
|
161
|
-
inputs: {
|
|
162
|
-
type: 'array',
|
|
163
|
-
items: { type: 'string' },
|
|
164
|
-
description: '2+ cca-assessment envelope references (path or @-token), one per consultant slice.',
|
|
165
|
-
},
|
|
166
|
-
allow_cross_project: {
|
|
167
|
-
type: 'boolean',
|
|
168
|
-
description: 'Accept inputs with different projectId (disjoint --scope slices of the same system). Default false.',
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
required: ['inputs'],
|
|
172
|
-
},
|
|
173
|
-
handler: (args, ctx) => ccaMergeHandler(args, ctx),
|
|
174
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// cspeach-cli/test-harness/fe-draft/app.ts
|
|
2
|
-
//
|
|
3
|
-
// L1-2b — the apps the offline harness serves, built with the PRODUCT's own
|
|
4
|
-
// tools (fiori_scaffold_fe, fiori_fe_extend handlers) so a scaffold regression
|
|
5
|
-
// shows up here, not in a sitting.
|
|
6
|
-
//
|
|
7
|
-
// good — an LROP app over the captured draft service, exactly as
|
|
8
|
-
// fiori_scaffold_fe writes it, plus one harmless custom table
|
|
9
|
-
// action ("Ping") so the good app has something to press.
|
|
10
|
-
// broken — the same app whose custom table action ("Boom") is wired to a
|
|
11
|
-
// handler that THROWS. A smoke that runs the press exercise must
|
|
12
|
-
// fail `exercises` on it.
|
|
13
|
-
// brokenPress — the same app with the custom action exactly as
|
|
14
|
-
// fiori_fe_extend writes it when asked for `eventHandler: true`:
|
|
15
|
-
// `"press": ""`. Pressing it makes UI5 try to load "/.js".
|
|
16
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
17
|
-
import * as path from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { fioriScaffoldFeHandler, feExtendHandler } from '../../src/tools/fiori/tools.js';
|
|
20
|
-
import type { ToolContext } from '../../src/tools/index.js';
|
|
21
|
-
import { openUrlFromStartScript } from './routes.js';
|
|
22
|
-
|
|
23
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
|
-
/** The captured draft service (ZUI_LAS_INSPECTIONLOG_O4) — read in place by the mockserver. */
|
|
26
|
-
export const DRAFT_METADATA = path.resolve(
|
|
27
|
-
HERE,
|
|
28
|
-
'../../src/tools/fiori/__tests__/fixtures/metadata/v4-draft-main.xml',
|
|
29
|
-
);
|
|
30
|
-
export const DRAFT_SERVICE_PATH =
|
|
31
|
-
'/sap/opu/odata4/sap/zui_las_inspectionlog_o4/srvd/sap/zui_las_inspectionlog_o4/0001/';
|
|
32
|
-
export const DRAFT_MAIN_ENTITY = 'InspectionLog';
|
|
33
|
-
/** Fully-qualified draft actions, as `SAP__common.DraftRoot` names them in the capture. */
|
|
34
|
-
export const DRAFT_NS = 'com.sap.gateway.srvd.zui_las_inspectionlog_o4.v0001';
|
|
35
|
-
|
|
36
|
-
/** The action button id suffix FE V4 renders for a manifest custom action. */
|
|
37
|
-
export const customActionSelector = (name: string): string => `[id$="::CustomAction::${name}"]`;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Fixed active rows for the draft root. Invented values, US sample names; user
|
|
41
|
-
* fields are MJOHNSON (the fixture convention). Status / Severity are the raw
|
|
42
|
-
* one-letter codes the captured service really carries with no text — the shape
|
|
43
|
-
* L1-3's no-raw-codes check has to catch.
|
|
44
|
-
*/
|
|
45
|
-
const row = (n: number, equipment: string, inspector: string, severity: string, status: string) => ({
|
|
46
|
-
InspectionUUID: `00000000-0000-4000-8000-00000000000${n}`,
|
|
47
|
-
EquipmentName: equipment,
|
|
48
|
-
InspectionDate: '2026-09-0' + n,
|
|
49
|
-
Inspector: inspector,
|
|
50
|
-
Severity: severity,
|
|
51
|
-
SeverityCriticality: severity === 'H' ? 1 : severity === 'M' ? 2 : 3,
|
|
52
|
-
Status: status,
|
|
53
|
-
Note: '',
|
|
54
|
-
CreatedBy: 'MJOHNSON',
|
|
55
|
-
LocalLastChangedBy: 'MJOHNSON',
|
|
56
|
-
IsActiveEntity: true,
|
|
57
|
-
HasActiveEntity: false,
|
|
58
|
-
HasDraftEntity: false,
|
|
59
|
-
});
|
|
60
|
-
export const INSPECTION_ROWS = [
|
|
61
|
-
row(1, 'Pump P-100', 'Mike Johnson', 'H', 'O'),
|
|
62
|
-
row(2, 'Valve V-200', 'Sarah Davis', 'M', 'C'),
|
|
63
|
-
row(3, 'Motor M-300', 'David Carter', 'L', 'O'),
|
|
64
|
-
];
|
|
65
|
-
export const HARNESS_MOCK_ROWS = { [DRAFT_MAIN_ENTITY]: INSPECTION_ROWS };
|
|
66
|
-
|
|
67
|
-
export const BROKEN_MARKER = 'CSP-HARNESS-BROKEN: the Boom handler threw';
|
|
68
|
-
|
|
69
|
-
function ctxFor(root: string): ToolContext {
|
|
70
|
-
return {
|
|
71
|
-
adt: {} as ToolContext['adt'],
|
|
72
|
-
sapAlias: 'HARNESS',
|
|
73
|
-
session: {} as ToolContext['session'],
|
|
74
|
-
cwd: root,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function must(r: { content: string; is_error?: boolean }, what: string): Promise<void> {
|
|
79
|
-
if (r.is_error) throw new Error(`${what} failed: ${r.content}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface HarnessApp {
|
|
83
|
-
/** Absolute app dir (holds webapp/). */
|
|
84
|
-
dir: string;
|
|
85
|
-
appId: string;
|
|
86
|
-
/** The page the scaffold's `npm start` opens (FLP sandbox + intent), app-relative. */
|
|
87
|
-
startEntry: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The FLP sandbox entry `npm start` opens for this app. */
|
|
91
|
-
export const flpEntry = (app: HarnessApp): string => app.startEntry;
|
|
92
|
-
|
|
93
|
-
/** Scaffold the LROP app through the fiori_scaffold_fe handler. `ui5Version` undefined = the product default. */
|
|
94
|
-
async function scaffold(root: string, rel: string, appId: string, ui5Version?: string): Promise<HarnessApp> {
|
|
95
|
-
await must(
|
|
96
|
-
await fioriScaffoldFeHandler(
|
|
97
|
-
{
|
|
98
|
-
basePath: rel,
|
|
99
|
-
appId,
|
|
100
|
-
appTitle: 'Inspection Log (harness)',
|
|
101
|
-
template: 'lrop',
|
|
102
|
-
serviceUrl: 'http://localhost',
|
|
103
|
-
servicePath: DRAFT_SERVICE_PATH,
|
|
104
|
-
mainEntity: DRAFT_MAIN_ENTITY,
|
|
105
|
-
...(ui5Version ? { ui5Version } : {}),
|
|
106
|
-
},
|
|
107
|
-
ctxFor(root),
|
|
108
|
-
),
|
|
109
|
-
'fiori_scaffold_fe',
|
|
110
|
-
);
|
|
111
|
-
const dir = path.join(root, rel);
|
|
112
|
-
const pkg = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'));
|
|
113
|
-
return { dir, appId, startEntry: openUrlFromStartScript(pkg.scripts.start) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Add a custom table action through the fiori_fe_extend handler. */
|
|
117
|
-
async function addTableAction(root: string, rel: string, name: string, settings: Record<string, unknown>): Promise<void> {
|
|
118
|
-
await must(
|
|
119
|
-
await feExtendHandler(
|
|
120
|
-
{
|
|
121
|
-
basePath: rel,
|
|
122
|
-
op: 'custom-action',
|
|
123
|
-
params: {
|
|
124
|
-
name,
|
|
125
|
-
target: { page: `${DRAFT_MAIN_ENTITY}List`, control: '@com.sap.vocabularies.UI.v1.LineItem' },
|
|
126
|
-
settings: { text: name, ...settings },
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
ctxFor(root),
|
|
130
|
-
),
|
|
131
|
-
'fiori_fe_extend custom-action',
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Point a custom action's `press` at `ext/<Name>.js#onPress` and write that
|
|
137
|
-
* module with a known body (the good app's harmless handler, the broken app's
|
|
138
|
-
* throwing one), replacing the stub fiori_fe_extend generated.
|
|
139
|
-
*/
|
|
140
|
-
async function wireHandler(app: HarnessApp, name: string, body: string): Promise<void> {
|
|
141
|
-
const manifestPath = path.join(app.dir, 'webapp', 'manifest.json');
|
|
142
|
-
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
143
|
-
const action =
|
|
144
|
-
manifest['sap.ui5'].routing.targets[`${DRAFT_MAIN_ENTITY}List`].options.settings.controlConfiguration[
|
|
145
|
-
'@com.sap.vocabularies.UI.v1.LineItem'
|
|
146
|
-
].actions[name];
|
|
147
|
-
action.press = `${app.appId}.ext.${name}.onPress`;
|
|
148
|
-
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
149
|
-
await mkdir(path.join(app.dir, 'webapp', 'ext'), { recursive: true });
|
|
150
|
-
await writeFile(
|
|
151
|
-
path.join(app.dir, 'webapp', 'ext', `${name}.js`),
|
|
152
|
-
`sap.ui.define([], function () {\n 'use strict';\n return { onPress: function () { ${body} } };\n});\n`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** The good app: scaffold as the product writes it, plus a harmless "Ping" action. */
|
|
157
|
-
export async function buildGoodApp(root: string, ui5Version?: string): Promise<HarnessApp> {
|
|
158
|
-
const app = await scaffold(root, 'good', 'z.harness.good', ui5Version);
|
|
159
|
-
await addTableAction(root, 'good', 'Ping', {});
|
|
160
|
-
await wireHandler(app, 'Ping', 'return;');
|
|
161
|
-
return app;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** The broken app: the "Boom" action's handler throws. */
|
|
165
|
-
export async function buildBrokenApp(root: string, ui5Version?: string): Promise<HarnessApp> {
|
|
166
|
-
const app = await scaffold(root, 'broken', 'z.harness.broken', ui5Version);
|
|
167
|
-
await addTableAction(root, 'broken', 'Boom', {});
|
|
168
|
-
await wireHandler(app, 'Boom', `throw new Error(${JSON.stringify(BROKEN_MARKER)});`);
|
|
169
|
-
return app;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* The broken-press app: a custom action whose press names a handler module that
|
|
174
|
-
* does not exist (a renamed or deleted ext file). fiori_fe_extend no longer
|
|
175
|
-
* writes an empty press — it always generates a handler — so the realistic
|
|
176
|
-
* broken case is a dangling reference.
|
|
177
|
-
*/
|
|
178
|
-
export async function buildBrokenPressApp(root: string, ui5Version?: string): Promise<HarnessApp> {
|
|
179
|
-
const app = await scaffold(root, 'broken-press', 'z.harness.brokenpress', ui5Version);
|
|
180
|
-
await addTableAction(root, 'broken-press', 'Boom', {});
|
|
181
|
-
const manifestPath = path.join(app.dir, 'webapp', 'manifest.json');
|
|
182
|
-
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
183
|
-
manifest['sap.ui5'].routing.targets[`${DRAFT_MAIN_ENTITY}List`].options.settings.controlConfiguration[
|
|
184
|
-
'@com.sap.vocabularies.UI.v1.LineItem'
|
|
185
|
-
].actions.Boom.press = `${app.appId}.ext.missing.Missing.onPress`;
|
|
186
|
-
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
187
|
-
return app;
|
|
188
|
-
}
|