@cspeach/cli 1.1.18 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +58 -21
- 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 +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- 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-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 +15 -3
- package/dist/cost/pricing.js +6 -5
- 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/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- 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 +96 -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 +152 -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 +238 -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 +42 -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/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 +36 -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 +795 -332
- 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/resume.js +104 -36
- package/dist/session/store.js +69 -27
- 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/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-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/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- 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 +1 -0
- 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 +28 -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 +11 -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,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transport-fit — which open requests can take an object of a given package
|
|
3
|
+
* (fix round 3, item 9e; fix round 4, item 3).
|
|
4
|
+
*
|
|
5
|
+
* Live defect: sap_transport_list returned 20 open requests with EMPTY
|
|
6
|
+
* description / target (the list endpoint does not fill them on this system),
|
|
7
|
+
* so the model offered S4HK903346 (no target) and S4HK903342 for a ZTEST_LAS
|
|
8
|
+
* object and SAP refused both: "CTS_WBO_API/048: Request … does not have
|
|
9
|
+
* transport target CL5". It also offered S4HK903729, which belongs to other
|
|
10
|
+
* work — its description was invisible to it.
|
|
11
|
+
*
|
|
12
|
+
* What the SAP data allows (all read-only, data preview):
|
|
13
|
+
* - E070.TARSYSTEM / TRFUNCTION / TRSTATUS — each request's target, type, status;
|
|
14
|
+
* - E07T.AS4TEXT — each request's description;
|
|
15
|
+
* - TDEVC.PDEVCLASS — the package's transport layer;
|
|
16
|
+
* - TCERELE (VERSION, INTSYS, TRANSLAYER → CONSYS) — the layer's
|
|
17
|
+
* consolidation target, restricted to the ACTIVE route version
|
|
18
|
+
* (TCEVERS ACTIVE = 'A') and this system (the request-number prefix).
|
|
19
|
+
* Round 4: round 3 read TCETRAL.CONSYS — TCETRAL has only VERSION +
|
|
20
|
+
* TRANSLAYER (verified live on S4H), so there was never a verdict.
|
|
21
|
+
* A request fits the package when it is a workbench request (K) whose target
|
|
22
|
+
* equals that consolidation target. When the package target, the request's
|
|
23
|
+
* target or its type is unknown, NO verdict is given — an unfit request is
|
|
24
|
+
* never marked as fitting.
|
|
25
|
+
*
|
|
26
|
+
* Data preview rejects a query line over 255 characters, so the IN-lists are
|
|
27
|
+
* chunked (round 4: ~20 requests in one query failed and every field came
|
|
28
|
+
* back empty). Strictly best-effort: a failed lookup leaves those fields as
|
|
29
|
+
* SAP returned them, gives no verdict, and writes a CSPEACH_DEBUG line.
|
|
30
|
+
*/
|
|
31
|
+
const TRKORR = /^[A-Z0-9]{1,20}$/;
|
|
32
|
+
const PACKAGE = /^[A-Z0-9_/$]{1,30}$/;
|
|
33
|
+
const REQUEST_SID = /^([A-Z0-9]{3})K\d+$/;
|
|
34
|
+
/** Data preview refuses a query line over 255 chars; stay well under it. */
|
|
35
|
+
export const MAX_QUERY_CHARS = 230;
|
|
36
|
+
function debug(msg) {
|
|
37
|
+
if (process.env['CSPEACH_DEBUG'] !== '1')
|
|
38
|
+
return;
|
|
39
|
+
process.stderr.write(`[transport-fit] ${msg}\n`);
|
|
40
|
+
}
|
|
41
|
+
/** Rows of one query, or null when it failed (logged, never thrown). */
|
|
42
|
+
async function rows(adt, sql, max, what) {
|
|
43
|
+
try {
|
|
44
|
+
return (await adt.sqlQuery(sql, max)).rows ?? [];
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
48
|
+
debug(`${what} lookup failed, no ranking from it: ${why.slice(0, 200)}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Build `prefix 'A', 'B' suffix` queries, each at most MAX_QUERY_CHARS long. */
|
|
53
|
+
export function chunkInList(prefix, suffix, keys) {
|
|
54
|
+
const build = (ks) => `${prefix}${ks.map((k) => `'${k}'`).join(', ')}${suffix}`;
|
|
55
|
+
const out = [];
|
|
56
|
+
let cur = [];
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
if (cur.length > 0 && build([...cur, k]).length > MAX_QUERY_CHARS) {
|
|
59
|
+
out.push({ sql: build(cur), keys: cur });
|
|
60
|
+
cur = [];
|
|
61
|
+
}
|
|
62
|
+
cur.push(k);
|
|
63
|
+
}
|
|
64
|
+
if (cur.length > 0)
|
|
65
|
+
out.push({ sql: build(cur), keys: cur });
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** sap-client caps any data-preview query at this many rows. */
|
|
69
|
+
const CLIENT_ROW_CAP = 5000;
|
|
70
|
+
/** Chunks run at most this many at a time (round 5). */
|
|
71
|
+
const CHUNK_CONCURRENCY = 4;
|
|
72
|
+
/** Run a chunked IN-list query; returns the rows and the keys whose chunk failed. */
|
|
73
|
+
async function chunkedLookup(adt, prefix, suffix, keys, perKey, what, maxChunks = Infinity) {
|
|
74
|
+
const all = chunkInList(prefix, suffix, keys);
|
|
75
|
+
// Round 5b — cap the lookups: keys past the cap are not read (no verdict).
|
|
76
|
+
const chunks = all.slice(0, maxChunks);
|
|
77
|
+
const skipped = all.slice(chunks.length);
|
|
78
|
+
if (skipped.length > 0)
|
|
79
|
+
debug(`${what}: ${skipped.length} lookup(s) past the cap skipped, those requests get no verdict`);
|
|
80
|
+
const results = new Array(chunks.length).fill(null);
|
|
81
|
+
let next = 0;
|
|
82
|
+
const worker = async () => {
|
|
83
|
+
while (next < chunks.length) {
|
|
84
|
+
const i = next++;
|
|
85
|
+
const c = chunks[i];
|
|
86
|
+
// Round 5b — ask for one row more than the budget: getting it back (or
|
|
87
|
+
// hitting the client-wide cap) means rows may be missing — not read.
|
|
88
|
+
const max = c.keys.length * perKey;
|
|
89
|
+
const r = await rows(adt, c.sql, max + 1, what);
|
|
90
|
+
const capped = r !== null && (r.length > max || r.length >= CLIENT_ROW_CAP);
|
|
91
|
+
if (capped)
|
|
92
|
+
debug(`${what} lookup hit its row cap (${max}), those requests get no verdict`);
|
|
93
|
+
results[i] = capped ? null : r;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
await Promise.all(Array.from({ length: Math.min(CHUNK_CONCURRENCY, chunks.length) }, worker));
|
|
97
|
+
const failed = new Set();
|
|
98
|
+
const out = [];
|
|
99
|
+
results.forEach((r, i) => {
|
|
100
|
+
if (r)
|
|
101
|
+
out.push(...r);
|
|
102
|
+
else
|
|
103
|
+
chunks[i].keys.forEach((k) => failed.add(k));
|
|
104
|
+
});
|
|
105
|
+
for (const c of skipped)
|
|
106
|
+
c.keys.forEach((k) => failed.add(k));
|
|
107
|
+
return { rows: out, failed };
|
|
108
|
+
}
|
|
109
|
+
/** Run a chunked IN-list query; a failed chunk contributes no rows. */
|
|
110
|
+
async function chunkedRows(adt, prefix, suffix, keys, perKey, what) {
|
|
111
|
+
return (await chunkedLookup(adt, prefix, suffix, keys, perKey, what)).rows;
|
|
112
|
+
}
|
|
113
|
+
/** LIMU piece types that belong to a class: the class name is the first 30 characters. */
|
|
114
|
+
const CLASS_PIECES = new Set(['METH', 'CINC', 'CLSD', 'CPUB', 'CPRO', 'CPRI', 'CDEF', 'CPAK']);
|
|
115
|
+
const OBJ_NAME = /^[A-Z0-9_/$]{1,40}$/;
|
|
116
|
+
/** Round 5b — at most this many TADIR lookups per list; the rest get no verdict. */
|
|
117
|
+
export const MAX_TADIR_CHUNKS = 12;
|
|
118
|
+
/** Round 5b — how many (newest) candidate requests have their contents read. */
|
|
119
|
+
export const MAX_CONTENT_REQUESTS = 12;
|
|
120
|
+
/** A valid function-group name (optional /NAMESPACE/, at most 26 characters after it). */
|
|
121
|
+
const FUGR_NAME = /^(\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_]{1,26}$/;
|
|
122
|
+
/**
|
|
123
|
+
* The function group of a function-group program name (SAPL<fg> or L<fg>xxx),
|
|
124
|
+
* if it can be one. Round 5c — only a valid FUGR name is ever derived; it is
|
|
125
|
+
* used only when TADIR returns that FUGR and has no PROG row for the name.
|
|
126
|
+
*/
|
|
127
|
+
function fugrOfInclude(name) {
|
|
128
|
+
const m = /^(\/[A-Z0-9_]+\/)?(?:SAPL([A-Z0-9_]+)|L([A-Z0-9_]+)[A-Z0-9$]{3})$/.exec(name);
|
|
129
|
+
if (!m)
|
|
130
|
+
return undefined;
|
|
131
|
+
const fg = `${m[1] ?? ''}${m[2] ?? m[3]}`;
|
|
132
|
+
return FUGR_NAME.test(fg) ? fg : undefined;
|
|
133
|
+
}
|
|
134
|
+
/** Round 5c — LIMU pieces whose R3TR owner has the same name. */
|
|
135
|
+
const SAME_NAME_OWNER = { TABD: 'TABL', DTED: 'DTEL', DOMD: 'DOMA', VIED: 'VIEW', TTYD: 'TTYP', SHLD: 'SHLP', ENQD: 'ENQU' };
|
|
136
|
+
/** The leading field of a fixed-width E071 key (padded to `width`), or its first token. */
|
|
137
|
+
function keyHead(raw, width) {
|
|
138
|
+
return (raw.length > width ? raw.slice(0, width) : raw.split(/\s+/)[0] ?? '').trim();
|
|
139
|
+
}
|
|
140
|
+
/** The class of a class-pool include name (ZCL_X=====CCIMP, ZCL_X=====CM001), if it is one. */
|
|
141
|
+
function classOfInclude(name) {
|
|
142
|
+
const m = /^([^=]+)=+[A-Z0-9]{2,5}$/.exec(name);
|
|
143
|
+
return m ? m[1] : undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How an E071 row maps to a package lookup; 'ignore' for rows that are not
|
|
147
|
+
* package work (CORR entries, documentation); null when it cannot be told —
|
|
148
|
+
* round 5b: such a row makes the request "not read" (never called clean).
|
|
149
|
+
*/
|
|
150
|
+
function resolveRow(r) {
|
|
151
|
+
const pgmid = (r['PGMID'] ?? '').trim();
|
|
152
|
+
const object = (r['OBJECT'] ?? '').trim();
|
|
153
|
+
const raw = r['OBJ_NAME'] ?? '';
|
|
154
|
+
if (pgmid === 'CORR' || object === 'DOCU')
|
|
155
|
+
return 'ignore';
|
|
156
|
+
let res = null;
|
|
157
|
+
if (pgmid === 'R3TR')
|
|
158
|
+
res = { via: 'tadir', object, name: raw.trim() };
|
|
159
|
+
else if (pgmid === 'LIMU') {
|
|
160
|
+
if (CLASS_PIECES.has(object))
|
|
161
|
+
res = { via: 'tadir', object: 'CLAS', name: raw.slice(0, 30).replace(/[= ]+$/, '') };
|
|
162
|
+
else if (SAME_NAME_OWNER[object])
|
|
163
|
+
res = { via: 'tadir', object: SAME_NAME_OWNER[object], name: raw.trim() };
|
|
164
|
+
else if (object === 'INDX')
|
|
165
|
+
res = { via: 'tadir', object: 'TABL', name: keyHead(raw, 30) };
|
|
166
|
+
else if (object === 'MESS') {
|
|
167
|
+
const name = raw.length > 20 ? raw.slice(0, 20).trim() : raw.trim().replace(/\d{3}$/, '').trim();
|
|
168
|
+
res = { via: 'tadir', object: 'MSAG', name };
|
|
169
|
+
}
|
|
170
|
+
else if (object === 'DYNP' || object === 'CUAD') {
|
|
171
|
+
const prog = object === 'DYNP' ? keyHead(raw, 40) : raw.trim();
|
|
172
|
+
res = { via: 'tadir', object: 'PROG', name: prog, fugr: fugrOfInclude(prog) };
|
|
173
|
+
}
|
|
174
|
+
else if (object === 'REPS' || object === 'REPT') {
|
|
175
|
+
const name = raw.trim();
|
|
176
|
+
const cls = classOfInclude(name);
|
|
177
|
+
// Round 5c — '=' is allowed only here, parsed into the class name; the
|
|
178
|
+
// value that reaches SQL is the class name and must pass OBJ_NAME.
|
|
179
|
+
res = cls ? { via: 'tadir', object: 'CLAS', name: cls } : { via: 'tadir', object: 'PROG', name, fugr: fugrOfInclude(name) };
|
|
180
|
+
}
|
|
181
|
+
else if (object === 'FUNC')
|
|
182
|
+
res = { via: 'func', name: raw.trim() };
|
|
183
|
+
}
|
|
184
|
+
if (!res || !OBJ_NAME.test(res.name))
|
|
185
|
+
return null;
|
|
186
|
+
return res;
|
|
187
|
+
}
|
|
188
|
+
async function requestPackages(adt, requests) {
|
|
189
|
+
const out = new Map(requests.map((r) => [r, { pkgs: new Set(), complete: true }]));
|
|
190
|
+
if (requests.length === 0)
|
|
191
|
+
return out;
|
|
192
|
+
const rank = new Map(requests.map((r, i) => [r, i]));
|
|
193
|
+
const notRead = (request, why) => {
|
|
194
|
+
const h = out.get(request);
|
|
195
|
+
if (!h)
|
|
196
|
+
return;
|
|
197
|
+
if (h.complete)
|
|
198
|
+
debug(`${request}: ${why}, not called clean`);
|
|
199
|
+
h.complete = false;
|
|
200
|
+
};
|
|
201
|
+
const tasks = await chunkedLookup(adt, 'SELECT TRKORR, STRKORR FROM E070 WHERE STRKORR IN (', ')', requests, 20, 'E070 tasks');
|
|
202
|
+
const owner = new Map(requests.map((r) => [r, r]));
|
|
203
|
+
for (const t of tasks.rows) {
|
|
204
|
+
const task = (t['TRKORR'] ?? '').trim();
|
|
205
|
+
const parent = (t['STRKORR'] ?? '').trim();
|
|
206
|
+
if (TRKORR.test(task) && out.has(parent))
|
|
207
|
+
owner.set(task, parent);
|
|
208
|
+
}
|
|
209
|
+
for (const r of tasks.failed)
|
|
210
|
+
notRead(r, 'tasks not read');
|
|
211
|
+
const e071 = await chunkedLookup(adt, 'SELECT TRKORR, PGMID, OBJECT, OBJ_NAME FROM E071 WHERE TRKORR IN (', ')', [...owner.keys()], 200, 'E071');
|
|
212
|
+
for (const k of e071.failed)
|
|
213
|
+
notRead(owner.get(k), 'objects not read');
|
|
214
|
+
const items = [];
|
|
215
|
+
for (const r of e071.rows) {
|
|
216
|
+
const request = owner.get((r['TRKORR'] ?? '').trim());
|
|
217
|
+
if (!request)
|
|
218
|
+
continue;
|
|
219
|
+
const res = resolveRow(r);
|
|
220
|
+
if (res === 'ignore')
|
|
221
|
+
continue;
|
|
222
|
+
if (!res) {
|
|
223
|
+
notRead(request, `object ${(r['PGMID'] ?? '').trim()} ${(r['OBJECT'] ?? '').trim()} cannot be resolved`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
items.push({ request, ...res });
|
|
227
|
+
}
|
|
228
|
+
items.sort((a, b) => rank.get(a.request) - rank.get(b.request));
|
|
229
|
+
// Function modules → their function group.
|
|
230
|
+
const funcs = [...new Set(items.filter((i) => i.via === 'func').map((i) => i.name))];
|
|
231
|
+
const fm = await chunkedLookup(adt, 'SELECT FUNCNAME, AREA FROM ENLFDIR WHERE FUNCNAME IN (', ')', funcs, 1, 'ENLFDIR', MAX_TADIR_CHUNKS);
|
|
232
|
+
const groupOf = new Map(fm.rows.map((r) => [(r['FUNCNAME'] ?? '').trim(), (r['AREA'] ?? '').trim()]));
|
|
233
|
+
const lookups = [];
|
|
234
|
+
for (const i of items) {
|
|
235
|
+
if (i.via === 'func') {
|
|
236
|
+
const fg = groupOf.get(i.name);
|
|
237
|
+
if (!fg || !OBJ_NAME.test(fg)) {
|
|
238
|
+
notRead(i.request, `function module ${i.name} not resolved`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
lookups.push({ request: i.request, name: i.name, keys: [{ object: 'FUGR', name: fg }] });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const keys = [{ object: i.object, name: i.name }];
|
|
245
|
+
if (i.fugr && OBJ_NAME.test(i.fugr))
|
|
246
|
+
keys.push({ object: 'FUGR', name: i.fugr });
|
|
247
|
+
lookups.push({ request: i.request, name: i.name, keys });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const names = [...new Set(lookups.flatMap((l) => l.keys.map((k) => k.name)))];
|
|
251
|
+
const tadir = await chunkedLookup(adt, "SELECT OBJECT, OBJ_NAME, DEVCLASS FROM TADIR WHERE PGMID = 'R3TR' AND OBJ_NAME IN (", ')', names, 10, 'TADIR', MAX_TADIR_CHUNKS);
|
|
252
|
+
const pkgOf = new Map(tadir.rows.map((r) => [`${(r['OBJECT'] ?? '').trim()} ${(r['OBJ_NAME'] ?? '').trim()}`, (r['DEVCLASS'] ?? '').trim()]));
|
|
253
|
+
for (const l of lookups) {
|
|
254
|
+
const pkg = l.keys.map((k) => pkgOf.get(`${k.object} ${k.name}`)).find((p) => !!p);
|
|
255
|
+
if (pkg) {
|
|
256
|
+
out.get(l.request).pkgs.add(pkg.toUpperCase());
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (l.keys.some((k) => tadir.failed.has(k.name)))
|
|
260
|
+
notRead(l.request, `package of ${l.name} not read`);
|
|
261
|
+
else
|
|
262
|
+
notRead(l.request, `${l.name} has no TADIR entry`);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/** The package's consolidation target, or undefined when unknown / ambiguous. */
|
|
267
|
+
async function packageTarget(adt, pkg, sid) {
|
|
268
|
+
const name = pkg.toUpperCase().trim();
|
|
269
|
+
if (!PACKAGE.test(name))
|
|
270
|
+
return undefined;
|
|
271
|
+
const dev = await rows(adt, `SELECT DEVCLASS, PDEVCLASS FROM TDEVC WHERE DEVCLASS = '${name}'`, 1, 'TDEVC');
|
|
272
|
+
const layer = (dev?.[0]?.['PDEVCLASS'] ?? '').trim();
|
|
273
|
+
if (!layer || !PACKAGE.test(layer)) {
|
|
274
|
+
if (dev)
|
|
275
|
+
debug(`package ${name} has no transport layer, no ranking`);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
let routes = await rows(adt, `SELECT VERSION, INTSYS, CONSYS FROM TCERELE WHERE TRANSLAYER = '${layer}'`, 50, 'TCERELE');
|
|
279
|
+
if (!routes || routes.length === 0) {
|
|
280
|
+
if (routes)
|
|
281
|
+
debug(`layer ${layer} has no consolidation route, no ranking`);
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const active = await rows(adt, `SELECT VERSION FROM TCEVERS WHERE ACTIVE = 'A'`, 5, 'TCEVERS');
|
|
285
|
+
// Review M6 — the read failed: a route of an inactive version could pass as
|
|
286
|
+
// the target, so give no verdict (an empty answer still means "unfiltered").
|
|
287
|
+
if (active === null)
|
|
288
|
+
return undefined;
|
|
289
|
+
const versions = new Set((active ?? []).map((r) => (r['VERSION'] ?? '').trim()).filter(Boolean));
|
|
290
|
+
if (versions.size > 0)
|
|
291
|
+
routes = routes.filter((r) => versions.has((r['VERSION'] ?? '').trim()));
|
|
292
|
+
if (sid) {
|
|
293
|
+
const mine = routes.filter((r) => (r['INTSYS'] ?? '').trim() === sid);
|
|
294
|
+
if (mine.length > 0)
|
|
295
|
+
routes = mine;
|
|
296
|
+
}
|
|
297
|
+
const targets = [...new Set(routes.map((r) => (r['CONSYS'] ?? '').trim()).filter((t) => t.length > 0))];
|
|
298
|
+
if (targets.length !== 1)
|
|
299
|
+
debug(`layer ${layer} routes to ${targets.length} targets, no ranking`);
|
|
300
|
+
return targets.length === 1 ? targets[0] : undefined;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* E070-TRFUNCTION request codes (re-review 4). The list endpoint's
|
|
304
|
+
* `tm:type` may carry other text (e.g. "workbench"); only a real TRFUNCTION
|
|
305
|
+
* code counts as a known type — anything else is unknown.
|
|
306
|
+
*/
|
|
307
|
+
const TRFUNCTION_CODES = new Set(['K', 'W', 'T', 'S', 'R', 'X', 'Q', 'C', 'O', 'E', 'D', 'M', 'G']);
|
|
308
|
+
function trfunction(type) {
|
|
309
|
+
const c = (type ?? '').trim().toUpperCase();
|
|
310
|
+
return TRFUNCTION_CODES.has(c) ? c : undefined;
|
|
311
|
+
}
|
|
312
|
+
/** The request numbers' system prefix (S4HK903731 → S4H) when they all agree. */
|
|
313
|
+
function systemId(numbers) {
|
|
314
|
+
const ids = new Set();
|
|
315
|
+
for (const n of numbers) {
|
|
316
|
+
const m = REQUEST_SID.exec(n);
|
|
317
|
+
if (m)
|
|
318
|
+
ids.add(m[1]);
|
|
319
|
+
}
|
|
320
|
+
return ids.size === 1 ? [...ids][0] : undefined;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Round 5b — at most `limit` data-preview queries in flight across a whole
|
|
324
|
+
* enrichment (the per-lookup concurrency alone let parallel lookups stack).
|
|
325
|
+
*/
|
|
326
|
+
function limited(adt, limit) {
|
|
327
|
+
let active = 0;
|
|
328
|
+
const waiting = [];
|
|
329
|
+
const acquire = async () => {
|
|
330
|
+
if (active < limit) {
|
|
331
|
+
active++;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await new Promise((r) => waiting.push(r));
|
|
335
|
+
};
|
|
336
|
+
const release = () => {
|
|
337
|
+
const next = waiting.shift();
|
|
338
|
+
if (next)
|
|
339
|
+
next();
|
|
340
|
+
else
|
|
341
|
+
active--;
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
async sqlQuery(query, maxRows) {
|
|
345
|
+
await acquire();
|
|
346
|
+
try {
|
|
347
|
+
return await adt.sqlQuery(query, maxRows);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
release();
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export async function enrichTransportList(rawAdt, transports, pkg) {
|
|
356
|
+
const adt = limited(rawAdt, CHUNK_CONCURRENCY);
|
|
357
|
+
const out = transports.map((t) => ({ ...t }));
|
|
358
|
+
const numbers = out.map((t) => t.transportNumber).filter((n) => TRKORR.test(n));
|
|
359
|
+
/** Requests whose E070 row was read — their target is known (even when empty). */
|
|
360
|
+
const e070Known = new Set();
|
|
361
|
+
if (numbers.length > 0) {
|
|
362
|
+
// Review I4 — English text first; any language only for requests with no
|
|
363
|
+
// English text (a request may carry EN and DE rows).
|
|
364
|
+
const [e070, e07tEn] = await Promise.all([
|
|
365
|
+
chunkedRows(adt, 'SELECT TRKORR, TARSYSTEM, TRFUNCTION, TRSTATUS FROM E070 WHERE TRKORR IN (', ')', numbers, 1, 'E070'),
|
|
366
|
+
chunkedRows(adt, 'SELECT TRKORR, AS4TEXT FROM E07T WHERE TRKORR IN (', ") AND LANGU = 'E'", numbers, 1, 'E07T'),
|
|
367
|
+
]);
|
|
368
|
+
const head = new Map(e070.map((r) => [r['TRKORR'] ?? '', r]));
|
|
369
|
+
const text = new Map();
|
|
370
|
+
const addTexts = (rs) => {
|
|
371
|
+
for (const r of rs) {
|
|
372
|
+
const k = r['TRKORR'] ?? '';
|
|
373
|
+
const v = (r['AS4TEXT'] ?? '').trim();
|
|
374
|
+
if (v && !text.has(k))
|
|
375
|
+
text.set(k, v);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
addTexts(e07tEn);
|
|
379
|
+
const missing = numbers.filter((n) => !text.has(n));
|
|
380
|
+
if (missing.length > 0) {
|
|
381
|
+
addTexts(await chunkedRows(adt, 'SELECT TRKORR, AS4TEXT FROM E07T WHERE TRKORR IN (', ')', missing, 3, 'E07T'));
|
|
382
|
+
}
|
|
383
|
+
for (const t of out) {
|
|
384
|
+
const h = head.get(t.transportNumber);
|
|
385
|
+
if (h) {
|
|
386
|
+
e070Known.add(t.transportNumber);
|
|
387
|
+
const code = (h['TRFUNCTION'] ?? '').trim();
|
|
388
|
+
if (!t.target)
|
|
389
|
+
t.target = (h['TARSYSTEM'] ?? '').trim();
|
|
390
|
+
// A real TRFUNCTION code beats list text such as "workbench".
|
|
391
|
+
if (!trfunction(t.type) && code)
|
|
392
|
+
t.type = code;
|
|
393
|
+
if (!t.status)
|
|
394
|
+
t.status = (h['TRSTATUS'] ?? '').trim();
|
|
395
|
+
}
|
|
396
|
+
if (!t.description)
|
|
397
|
+
t.description = text.get(t.transportNumber) ?? t.description;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!pkg)
|
|
401
|
+
return { transports: out };
|
|
402
|
+
const target = await packageTarget(adt, pkg, systemId(numbers));
|
|
403
|
+
if (!target)
|
|
404
|
+
return { transports: out };
|
|
405
|
+
// Review I4 — only a workbench request (TRFUNCTION 'K') can take a
|
|
406
|
+
// repository object; a customizing ('W') or transport-of-copies ('T')
|
|
407
|
+
// request on the right target is refused by SAP. Round 4 — verdicts only on
|
|
408
|
+
// what is known: a known wrong type or a known wrong target is unfit; a K
|
|
409
|
+
// request on the package's target fits; anything else gets no verdict.
|
|
410
|
+
for (const t of out) {
|
|
411
|
+
const code = trfunction(t.type);
|
|
412
|
+
const tgt = (t.target ?? '').trim();
|
|
413
|
+
const targetKnown = tgt.length > 0 || e070Known.has(t.transportNumber);
|
|
414
|
+
if (code !== undefined && code !== 'K')
|
|
415
|
+
t.fits_package = false;
|
|
416
|
+
else if (targetKnown && tgt !== target)
|
|
417
|
+
t.fits_package = false;
|
|
418
|
+
else if (code === 'K' && targetKnown)
|
|
419
|
+
t.fits_package = true;
|
|
420
|
+
else
|
|
421
|
+
delete t.fits_package;
|
|
422
|
+
}
|
|
423
|
+
// Round 5 — "belongs to other work": what the candidates already hold.
|
|
424
|
+
const pkgName = pkg.toUpperCase().trim();
|
|
425
|
+
// Round 5b — only the newest MAX_CONTENT_REQUESTS candidates are read (the
|
|
426
|
+
// pick is among them); older ones get no verdict on their contents.
|
|
427
|
+
const candidates = out.filter((t) => t.fits_package !== false && TRKORR.test(t.transportNumber))
|
|
428
|
+
.map((t) => t.transportNumber).slice(0, MAX_CONTENT_REQUESTS);
|
|
429
|
+
const held = await requestPackages(adt, candidates);
|
|
430
|
+
for (const t of out) {
|
|
431
|
+
const h = held.get(t.transportNumber);
|
|
432
|
+
const others = h ? [...h.pkgs].filter((p) => p !== pkgName).sort() : [];
|
|
433
|
+
if (others.length > 0)
|
|
434
|
+
t.other_packages = others;
|
|
435
|
+
}
|
|
436
|
+
// Stable: clean fitting first, then no verdict, then other work, then
|
|
437
|
+
// unfit; SAP's order kept inside each group.
|
|
438
|
+
const other = (t) => (t.other_packages?.length ?? 0) > 0;
|
|
439
|
+
// Round 5b — among fitting requests, those whose objects were all read
|
|
440
|
+
// (clean) come before those that could not be read.
|
|
441
|
+
const read = (t) => held.get(t.transportNumber)?.complete === true;
|
|
442
|
+
for (const t of out) {
|
|
443
|
+
if (t.fits_package === false)
|
|
444
|
+
continue;
|
|
445
|
+
// Review r7 M-4 — 'clean' only on a request known to fit; a request whose
|
|
446
|
+
// fit is unknown is never offered for reuse.
|
|
447
|
+
t.contents = other(t) ? 'other_work' : (read(t) && t.fits_package === true) ? 'clean' : 'not_checked';
|
|
448
|
+
}
|
|
449
|
+
const ranked = [
|
|
450
|
+
...out.filter((t) => t.fits_package === true && !other(t) && read(t)),
|
|
451
|
+
...out.filter((t) => t.fits_package === true && !other(t) && !read(t)),
|
|
452
|
+
...out.filter((t) => t.fits_package === undefined && !other(t)),
|
|
453
|
+
...out.filter((t) => t.fits_package !== false && other(t)),
|
|
454
|
+
...out.filter((t) => t.fits_package === false),
|
|
455
|
+
];
|
|
456
|
+
// The suggested pick: the first fitting request whose objects were read
|
|
457
|
+
// and all belong to this package (or that holds none).
|
|
458
|
+
const pick = ranked.find((t) => t.fits_package === true && !other(t) && read(t));
|
|
459
|
+
if (pick)
|
|
460
|
+
pick.suggested = true;
|
|
461
|
+
return { transports: ranked, packageTarget: target };
|
|
462
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* older mocks without the method) must NEVER block the write.
|
|
22
22
|
*/
|
|
23
23
|
import { wasTransportCreatedThisSession } from './transport.js';
|
|
24
|
+
import { describeError } from '../sap-errors/clean-error-text.js';
|
|
24
25
|
/**
|
|
25
26
|
* Resolve the transport for a write against the owning-transport ledger.
|
|
26
27
|
*
|
|
@@ -49,7 +50,8 @@ export async function resolveWriteTransport(ctx, objectType, objectName, supplie
|
|
|
49
50
|
// conflicting-override case below).
|
|
50
51
|
ctx.chunkEmitter?.emit('info', `owning-transport lookup failed for ${objectType} ${objectName} — `
|
|
51
52
|
+ `proceeding with ${supplied ?? 'no transport'} `
|
|
52
|
-
|
|
53
|
+
// describeError: an ICM HTML error page must not leak tags/CSS here.
|
|
54
|
+
+ `(${describeError(err)})`);
|
|
53
55
|
return { transport: supplied };
|
|
54
56
|
}
|
|
55
57
|
if (owning?.transport) {
|
package/dist/tools/transport.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ERR } from '../errors/codes.js';
|
|
|
17
17
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
18
18
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
19
19
|
import { setCurrentTransport } from '../repl/current-transport.js';
|
|
20
|
+
import { enrichTransportList } from './transport-fit.js';
|
|
20
21
|
// ── Session ledger: transports CREATED by sap_transport_create this session ──
|
|
21
22
|
//
|
|
22
23
|
// Orphan-hint support (third junk transport this month, 2026-06-12 — D15/D20
|
|
@@ -57,7 +58,7 @@ registerTool({
|
|
|
57
58
|
+ 'the object is free. '
|
|
58
59
|
+ 'Ask the user for the transport description using `ask_question` — DO NOT '
|
|
59
60
|
+ 'generate a marketing-style description yourself. When asking, SUGGEST the '
|
|
60
|
-
+ 'Rule 9 session format "
|
|
61
|
+
+ 'Rule 9 session format "<YYYY-MM-DD>: <work summary>" (no product name) as the default; '
|
|
61
62
|
+ 'the user may override. Use the user\'s exact answer as the `description` '
|
|
62
63
|
+ 'argument AND in your `request_approval` reason (identical strings prevent the '
|
|
63
64
|
+ 'approval mismatch failure mode observed live 2026-05-07). '
|
|
@@ -221,11 +222,12 @@ registerTool({
|
|
|
221
222
|
// Rule 9 description format (D20: "Magic fix" accepted verbatim) —
|
|
222
223
|
// non-blocking note when the description doesn't follow the session
|
|
223
224
|
// format. The transport IS created; the user's wording always wins.
|
|
224
|
-
|
|
225
|
+
// Any leading label (a customer's own prefix) is fine; no product name is suggested.
|
|
226
|
+
const rule9Format = /^(?:\S.* )?\d{4}-\d{2}-\d{2}: .+/;
|
|
225
227
|
const today = new Date().toISOString().slice(0, 10);
|
|
226
228
|
const rule9Suggestion = rule9Format.test(args.description)
|
|
227
229
|
? undefined
|
|
228
|
-
: `Description does not follow the Rule 9 session format. Suggested: "
|
|
230
|
+
: `Description does not follow the Rule 9 session format. Suggested: "${today}: <work summary>". `
|
|
229
231
|
+ 'Non-blocking — keep the user\'s wording if they chose it deliberately.';
|
|
230
232
|
return {
|
|
231
233
|
content: JSON.stringify({
|
|
@@ -330,7 +332,20 @@ registerTool({
|
|
|
330
332
|
registerTool({
|
|
331
333
|
name: 'sap_transport_list',
|
|
332
334
|
description: 'List SAP CTS transport requests. '
|
|
333
|
-
+ 'Optionally filter by owner (user) or status (modifiable | released).'
|
|
335
|
+
+ 'Optionally filter by owner (user) or status (modifiable | released). '
|
|
336
|
+
+ 'When choosing a transport for a write, pass `package`: each request then carries '
|
|
337
|
+
+ '`fits_package` (true = its target matches the transport target of the package) and the '
|
|
338
|
+
+ 'fitting ones come first. Offer the user only requests that fit — SAP refuses the others '
|
|
339
|
+
+ '("does not have transport target …"). A request with `other_packages` already holds '
|
|
340
|
+
+ 'objects of other packages (other work): it ranks lower and must never be pre-selected. '
|
|
341
|
+
+ 'Each request that may fit carries `contents`: "clean" (its objects were read and all belong '
|
|
342
|
+
+ 'to this package, or it holds none), "other_work" (it holds other packages\' objects), or '
|
|
343
|
+
+ '"not_checked" (its objects were not read). `suggested: true` is the tool\'s pick and is '
|
|
344
|
+
+ 'authoritative: present that request first, as the recommendation. Never pre-select or '
|
|
345
|
+
+ 'recommend a request with contents "other_work" or "not_checked" for reuse; list a '
|
|
346
|
+
+ '"not_checked" one as "not checked", never as a clean fit. A description that seems to name '
|
|
347
|
+
+ 'other work may only lower a request in your answer; it never overrides `suggested: true`. '
|
|
348
|
+
+ 'When no request is suggested, offer to create a new transport.',
|
|
334
349
|
isMutating: false,
|
|
335
350
|
requiresSap: true,
|
|
336
351
|
input_schema: {
|
|
@@ -338,25 +353,35 @@ registerTool({
|
|
|
338
353
|
properties: {
|
|
339
354
|
owner: {
|
|
340
355
|
type: 'string',
|
|
341
|
-
description: '
|
|
356
|
+
description: 'Owner of the requests: the exact SAP user ID (e.g. MJOHNSON), not a name or part of one. Omit it to list the connected user\'s own requests.',
|
|
342
357
|
},
|
|
343
358
|
status: {
|
|
344
359
|
type: 'string',
|
|
345
360
|
enum: ['modifiable', 'released'],
|
|
346
361
|
description: 'Filter by status. Defaults to "modifiable".',
|
|
347
362
|
},
|
|
363
|
+
package: {
|
|
364
|
+
type: 'string',
|
|
365
|
+
description: 'The package the object will be written to (e.g. ZTEST_LAS). Marks which requests can take it (fits_package).',
|
|
366
|
+
},
|
|
348
367
|
},
|
|
349
368
|
},
|
|
350
369
|
handler: async (args, ctx) => {
|
|
351
370
|
// transportList(user?, status?)
|
|
352
371
|
try {
|
|
353
372
|
const result = await ctx.adt.transportList(args.owner, args.status);
|
|
373
|
+
// Fix round 3 (item 9e) — fill the empty target / description from
|
|
374
|
+
// E070/E07T and, given a package, mark + rank the requests that fit.
|
|
375
|
+
const enriched = await enrichTransportList(ctx.adt, result.transports, typeof args.package === 'string' && args.package.trim() ? args.package : undefined);
|
|
354
376
|
return {
|
|
355
377
|
content: JSON.stringify({
|
|
356
378
|
filter_user: result.filterUser || null,
|
|
357
379
|
filter_status: result.filterStatus,
|
|
358
|
-
count:
|
|
359
|
-
|
|
380
|
+
count: enriched.transports.length,
|
|
381
|
+
...(enriched.packageTarget ? { package: String(args.package).toUpperCase(), package_target: enriched.packageTarget } : {}),
|
|
382
|
+
transports: enriched.transports,
|
|
383
|
+
// Round 5 item 3 — sap-client says when the list may be incomplete.
|
|
384
|
+
...(result.note ? { note: result.note } : {}),
|
|
360
385
|
}),
|
|
361
386
|
};
|
|
362
387
|
}
|
|
@@ -414,6 +439,7 @@ registerTool({
|
|
|
414
439
|
what_will_happen: `Releases ${args.transport}; objects move to QA queue, transport becomes immutable.`,
|
|
415
440
|
rollback_available: false,
|
|
416
441
|
transport: args.transport,
|
|
442
|
+
args: { ...args },
|
|
417
443
|
});
|
|
418
444
|
if (!r.confirmed) {
|
|
419
445
|
return {
|