@cspeach/cli 0.9.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/intent-system-prompt.js +1 -1
  3. package/dist/agent/loop.js +228 -26
  4. package/dist/agent/providers/license-gate.js +44 -0
  5. package/dist/agent/skill-checkpoint.js +1 -1
  6. package/dist/agent/tool-dispatch.js +15 -0
  7. package/dist/approvals/canonical.js +91 -0
  8. package/dist/approvals/jwt.js +39 -2
  9. package/dist/approvals/op-labels.js +124 -0
  10. package/dist/approvals/render.js +42 -36
  11. package/dist/auth/org-anthropic-key.js +25 -0
  12. package/dist/classifier/client.js +18 -3
  13. package/dist/cli.js +15 -0
  14. package/dist/commands/compact.js +28 -2
  15. package/dist/commands/config-set.js +284 -0
  16. package/dist/commands/config-show.js +20 -0
  17. package/dist/commands/export-audit.js +43 -0
  18. package/dist/commands/help.js +5 -0
  19. package/dist/commands/login.js +31 -14
  20. package/dist/commands/plan-audit-evidence.js +266 -0
  21. package/dist/commands/plan-audit.js +692 -0
  22. package/dist/commands/plan-chain.js +671 -0
  23. package/dist/commands/plan-continue.js +179 -0
  24. package/dist/commands/plan-gate.js +154 -0
  25. package/dist/commands/plan-model-tier.js +83 -0
  26. package/dist/commands/plan-resume.js +728 -46
  27. package/dist/config/loader.js +223 -5
  28. package/dist/config/model-defaults.js +14 -0
  29. package/dist/cost/pricing.js +27 -1
  30. package/dist/doctor/checks/_http-probe.js +1 -0
  31. package/dist/doctor/checks/cert.js +14 -3
  32. package/dist/doctor/checks/sap.js +30 -8
  33. package/dist/doctor/checks/system-roles.js +41 -0
  34. package/dist/doctor/checks/zcspeach.js +19 -4
  35. package/dist/doctor/run.js +2 -0
  36. package/dist/models/resolve.js +61 -0
  37. package/dist/models/server-config.js +155 -0
  38. package/dist/one-shot.js +76 -6
  39. package/dist/projects/answer-blockers.js +137 -0
  40. package/dist/projects/extract-cca.js +111 -17
  41. package/dist/projects/extract-modernize.js +4 -2
  42. package/dist/projects/extract-plan.js +184 -37
  43. package/dist/projects/extract-spec-gap.js +34 -7
  44. package/dist/projects/extract-test-coverage.js +4 -2
  45. package/dist/projects/extract-upgrade.js +116 -23
  46. package/dist/projects/handover-md.js +195 -0
  47. package/dist/projects/index.js +5 -2
  48. package/dist/projects/merge-cca.js +292 -0
  49. package/dist/projects/merge-upgrade.js +173 -0
  50. package/dist/projects/migration.js +103 -1
  51. package/dist/projects/output-paths.js +27 -0
  52. package/dist/projects/plan-run.js +285 -27
  53. package/dist/projects/plan-schema.js +136 -3
  54. package/dist/projects/promote-command.js +25 -2
  55. package/dist/projects/promote.js +128 -0
  56. package/dist/projects/run-lease.js +157 -0
  57. package/dist/projects/save-command.js +259 -21
  58. package/dist/projects/status.js +3 -1
  59. package/dist/projects/validate.js +1 -1
  60. package/dist/projects/workspace.js +164 -20
  61. package/dist/renderer/notices.js +64 -0
  62. package/dist/renderer/progress-chatter.js +8 -0
  63. package/dist/renderer/status-footer.js +22 -12
  64. package/dist/renderer/thinking-heartbeat.js +64 -8
  65. package/dist/renderer/todo-block.js +51 -0
  66. package/dist/renderer/tool-widget.js +55 -4
  67. package/dist/renderer/tty.js +43 -4
  68. package/dist/renderer/verify-chain.js +77 -0
  69. package/dist/repl/at-picker.js +60 -7
  70. package/dist/repl/bracketed-paste.js +28 -19
  71. package/dist/repl/builtin-commands.js +42 -0
  72. package/dist/repl/current-transport.js +10 -0
  73. package/dist/repl/early-line-buffer.js +68 -0
  74. package/dist/repl/history.js +86 -0
  75. package/dist/repl/ink-stdin-guard.js +64 -0
  76. package/dist/repl/inquirer-guard.js +70 -5
  77. package/dist/repl/mode-ceiling.js +16 -0
  78. package/dist/repl/mode-cycle.js +104 -0
  79. package/dist/repl/numbered-menu.js +131 -0
  80. package/dist/repl/post-turn-status.js +26 -6
  81. package/dist/repl/rule8-detector.js +17 -2
  82. package/dist/repl/safety-confirm.js +111 -2
  83. package/dist/repl/safety-mode-state.js +19 -3
  84. package/dist/repl/slash-completer.js +5 -0
  85. package/dist/repl/slash-picker.js +10 -15
  86. package/dist/repl.js +1232 -95
  87. package/dist/rewind/candidates.js +194 -0
  88. package/dist/rewind/cli.js +137 -0
  89. package/dist/rewind/format.js +27 -0
  90. package/dist/rewind/restore.js +245 -0
  91. package/dist/router/classifier.js +150 -6
  92. package/dist/sap/capability-matrix.js +20 -0
  93. package/dist/sap/capability-matrix.json +11236 -0
  94. package/dist/sap/capability.js +146 -0
  95. package/dist/sap/connection-manager.js +19 -1
  96. package/dist/sap/onboarding.js +42 -4
  97. package/dist/session/audit-export.js +459 -0
  98. package/dist/session/context-report.js +163 -0
  99. package/dist/session/pending.js +27 -0
  100. package/dist/session/recap.js +160 -0
  101. package/dist/skill-catalog.js +51 -40
  102. package/dist/skills/bundled-skills.js +272 -1
  103. package/dist/skills/promotion-dispatch.js +23 -0
  104. package/dist/tools/_command-shared.js +36 -12
  105. package/dist/tools/_filesystem-shared.js +139 -4
  106. package/dist/tools/_flag.js +25 -0
  107. package/dist/tools/approval.js +177 -26
  108. package/dist/tools/ask-question.js +400 -7
  109. package/dist/tools/capability/tool.js +74 -0
  110. package/dist/tools/dispatch-skill.js +22 -1
  111. package/dist/tools/extend-model/anchored-insert.js +1414 -0
  112. package/dist/tools/extend-model/tool.js +340 -0
  113. package/dist/tools/filesystem/extract-document.js +57 -0
  114. package/dist/tools/filesystem/file-edit.js +12 -2
  115. package/dist/tools/filesystem/file-read.js +2 -2
  116. package/dist/tools/filesystem/file-write.js +11 -2
  117. package/dist/tools/filesystem/glob.js +11 -0
  118. package/dist/tools/filesystem/grep.js +10 -0
  119. package/dist/tools/filesystem/read-document.js +107 -0
  120. package/dist/tools/fiori/apply.js +50 -0
  121. package/dist/tools/fiori/bin.js +3 -0
  122. package/dist/tools/fiori/catalog/index.js +27 -0
  123. package/dist/tools/fiori/catalog/value-help.js +230 -0
  124. package/dist/tools/fiori/catalog/viz-chart.js +177 -0
  125. package/dist/tools/fiori/cli.js +71 -0
  126. package/dist/tools/fiori/deploy-config.js +73 -0
  127. package/dist/tools/fiori/fe-extend.js +76 -0
  128. package/dist/tools/fiori/fe-scaffold.js +71 -0
  129. package/dist/tools/fiori/floorplan-map.js +19 -0
  130. package/dist/tools/fiori/i18n.js +39 -0
  131. package/dist/tools/fiori/manifest.js +70 -0
  132. package/dist/tools/fiori/render.js +77 -0
  133. package/dist/tools/fiori/samples/data/index.json +13602 -0
  134. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  135. package/dist/tools/fiori/samples/loader.js +248 -0
  136. package/dist/tools/fiori/samples/search.js +63 -0
  137. package/dist/tools/fiori/samples/types.js +2 -0
  138. package/dist/tools/fiori/scaffold.js +39 -0
  139. package/dist/tools/fiori/smoke/assertions.js +74 -0
  140. package/dist/tools/fiori/smoke/browser.js +52 -0
  141. package/dist/tools/fiori/smoke/driver.js +89 -0
  142. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  143. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  144. package/dist/tools/fiori/tools.js +681 -0
  145. package/dist/tools/fiori/types.js +1 -0
  146. package/dist/tools/local-build.js +86 -0
  147. package/dist/tools/local-files.js +31 -0
  148. package/dist/tools/project/_merge-shared.js +68 -0
  149. package/dist/tools/project/cca_merge.js +164 -0
  150. package/dist/tools/project/playbook_get.js +1 -1
  151. package/dist/tools/project/upgrade_merge_progress.js +206 -0
  152. package/dist/tools/sap-read.js +132 -20
  153. package/dist/tools/sap-write.js +550 -21
  154. package/dist/tools/shell/shell_exec.js +41 -6
  155. package/dist/tools/snapshot.js +63 -14
  156. package/dist/tools/subagent/agent_run.js +27 -3
  157. package/dist/tools/subagent/background_run.js +17 -1
  158. package/dist/tools/todo.js +144 -0
  159. package/dist/tools/transport-resolution.js +86 -0
  160. package/dist/tools/transport.js +224 -5
  161. package/dist/tools/write-mode.js +4 -0
  162. package/dist/ui/app.js +378 -21
  163. package/dist/ui/approval-modal.js +49 -16
  164. package/dist/ui/ask-question-emitter.js +14 -0
  165. package/dist/ui/body.js +13 -0
  166. package/dist/ui/context-grid.js +108 -0
  167. package/dist/ui/footer.js +120 -27
  168. package/dist/ui/header.js +7 -0
  169. package/dist/ui/line-resolution.js +35 -8
  170. package/dist/ui/rewind-emitter.js +10 -0
  171. package/dist/ui/rewind-panel.js +81 -0
  172. package/dist/ui/sap-state-store.js +1 -0
  173. package/dist/ui/session-timeline.js +1 -0
  174. package/dist/ui/status-line.js +43 -0
  175. package/dist/ui/text-input.js +214 -0
  176. package/dist/ui/todo-emitter.js +25 -0
  177. package/dist/ui/todo-panel.js +64 -0
  178. package/dist/ui/turn-status-emitter.js +50 -4
  179. package/dist/ui/turn-status.js +18 -3
  180. package/dist/ui/widgets/ask-form.js +242 -0
  181. package/dist/ui/widgets/ask-question-modal.js +21 -8
  182. package/package.json +22 -3
  183. package/bench/README.md +0 -78
  184. package/bench/prompts/abap-document-cds.md +0 -44
  185. package/bench/prompts/abap-explain-bdef-handler.md +0 -57
  186. package/bench/prompts/abap-test-method.md +0 -42
  187. package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
  188. package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
  189. package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
  190. package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
  191. package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
  192. package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
  193. package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
  194. package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
  195. package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
@@ -17,8 +17,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
17
  */
18
18
  import { useState, useEffect } from 'react';
19
19
  import { Box, Text, useInput } from 'ink';
20
- import TextInput from 'ink-text-input';
20
+ // D29 paste fix (2026-06-12): paste-safe replacement for ink-text-input —
21
+ // pasted free-form answers were losing every chunk but the last on large
22
+ // pastes (stale-prop closure upstream). See ../text-input.tsx.
23
+ import TextInput from '../text-input.js';
21
24
  const PEACH = '#FFCBA4';
25
+ export function askHelpLine(kind, opts) {
26
+ switch (kind) {
27
+ case 'multi': return 'Space toggle · Enter confirm · Esc cancel';
28
+ case 'pick': return opts?.single ? 'Enter pick · Esc cancel' : 'Enter pick · Tab next · Esc cancel';
29
+ default: return opts?.single ? 'Enter submit · Esc cancel' : 'Tab next · Esc cancel';
30
+ }
31
+ }
22
32
  /**
23
33
  * Reclaim raw-mode stdin when an Ink modal mounts (2026-06-08).
24
34
  *
@@ -30,8 +40,11 @@ const PEACH = '#FFCBA4';
30
40
  * answer the question. Re-syncing the OS state to what Ink expects on mount
31
41
  * fixes it. Direct `process.stdin` calls (not Ink's ref-counted setRawMode)
32
42
  * because the whole problem is that Ink's count is stale.
43
+ *
44
+ * Exported (UX Wave 2 / Task 2) so <AskForm> — the v2 multi-question form —
45
+ * reuses the exact same reclaim on mount. v1 rendering is untouched.
33
46
  */
34
- function useReclaimRawStdin() {
47
+ export function useReclaimRawStdin() {
35
48
  useEffect(() => {
36
49
  const stdin = process.stdin;
37
50
  if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
@@ -63,7 +76,7 @@ function TextInputForm({ params, onResolve }) {
63
76
  onResolve({ answer: null, cancelled: true });
64
77
  }
65
78
  }, { isActive: true });
66
- return (_jsxs(Box, { borderStyle: "round", borderColor: PEACH, flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: PEACH, children: '> ' }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: (v) => {
79
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: PEACH, children: '> ' }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: (v) => {
67
80
  const trimmed = v.trim();
68
81
  if (trimmed.length === 0) {
69
82
  onResolve({ answer: null, cancelled: true });
@@ -71,7 +84,7 @@ function TextInputForm({ params, onResolve }) {
71
84
  else {
72
85
  onResolve({ answer: trimmed });
73
86
  }
74
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" }) })] }));
87
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: askHelpLine('freeText', { single: true }) }) })] }));
75
88
  }
76
89
  // ---------------------------------------------------------------------------
77
90
  // Single choice (kind = 'choice')
@@ -97,10 +110,10 @@ function ChoiceForm({ params, onResolve }) {
97
110
  onResolve({ answer: chosen.value });
98
111
  }
99
112
  }, { isActive: true });
100
- return (_jsxs(Box, { borderStyle: "round", borderColor: PEACH, flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.choices.map((c, i) => {
113
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.choices.map((c, i) => {
101
114
  const isCursor = i === cursor;
102
115
  return (_jsxs(Text, { color: isCursor ? PEACH : undefined, bold: isCursor, children: [isCursor ? '▸ ' : ' ', c.label] }, c.value));
103
- }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
116
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: askHelpLine('pick', { single: true }) }) })] }));
104
117
  }
105
118
  // ---------------------------------------------------------------------------
106
119
  // Multi choice (kind = 'multi') — space to toggle, Enter to submit
@@ -147,11 +160,11 @@ function MultiForm({ params, onResolve }) {
147
160
  }
148
161
  }
149
162
  }, { isActive: true });
150
- return (_jsxs(Box, { borderStyle: "round", borderColor: PEACH, flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.choices.map((c, i) => {
163
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: params.question }), params.context && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: params.context }) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.choices.map((c, i) => {
151
164
  const isCursor = i === cursor;
152
165
  const isSelected = selected.has(c.value);
153
166
  const mark = isSelected ? '◉' : '○';
154
167
  const prefix = isCursor ? '▸ ' : ' ';
155
168
  return (_jsxs(Text, { color: isCursor ? PEACH : undefined, bold: isCursor, children: [prefix, mark, " ", c.label] }, c.value));
156
- }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \u00B7 Space toggle \u00B7 Enter submit \u00B7 Esc cancel" }) })] }));
169
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: askHelpLine('multi') }) })] }));
157
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "0.9.0",
3
+ "version": "1.1.0",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
@@ -41,42 +41,61 @@
41
41
  "smoke:all": "pnpm zcspeach:smoke && pnpm adt:smoke",
42
42
  "zcspeach:smoke": "tsx scripts/zcspeach-smoke.ts",
43
43
  "bench:models": "tsx scripts/bench-models.ts",
44
- "prepare": "husky"
44
+ "bench:audit": "tsx scripts/bench-audit-task.ts",
45
+ "fiori": "tsx src/tools/fiori/bin.ts",
46
+ "prepare": "husky",
47
+ "prepublishOnly": "node -e \"const d=(require('./package.json').dependencies||{})['@cspeach/sap-client']||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: @cspeach/sap-client is a local link ('+d+') — point it at the published version before npm publish'); process.exit(1)}\""
45
48
  },
46
49
  "engines": {
47
50
  "node": ">=20.0.0"
48
51
  },
49
52
  "dependencies": {
50
53
  "@anthropic-ai/sdk": "^0.90.0",
51
- "@cspeach/sap-client": "^0.1.3",
54
+ "@cspeach/sap-client": "^0.1.10",
52
55
  "@iarna/toml": "^2.2.5",
53
56
  "@inkjs/ui": "^2.0.0",
54
57
  "@inquirer/prompts": "^7.0.0",
55
58
  "@noble/ed25519": "2.1.0",
56
59
  "@noble/hashes": "1.4.0",
60
+ "@sap-ux/fe-fpm-writer": "0.43.7",
61
+ "@sap-ux/fiori-elements-writer": "^3.0.39",
62
+ "@sap-ux/fiori-freestyle-writer": "3.0.12",
57
63
  "chalk": "^5.3.0",
58
64
  "cli-highlight": "^2.1.11",
59
65
  "cli-table3": "^0.6.5",
60
66
  "clipboardy": "^5.3.1",
67
+ "cross-spawn": "^7.0.6",
61
68
  "diff": "^9.0.0",
69
+ "ejs": "3.1.10",
62
70
  "fuse.js": "^7.0.0",
63
71
  "ink": "^5.0.0",
64
72
  "ink-text-input": "^6.0.0",
65
73
  "jose": "^5.9.0",
66
74
  "keytar": "^7.9.0",
75
+ "mammoth": "^1.8.0",
67
76
  "marked": "^11.2.0",
68
77
  "marked-terminal": "^6.2.0",
78
+ "mem-fs": "2.1.0",
79
+ "mem-fs-editor": "9.4.0",
69
80
  "node-machine-id": "^1.1.12",
70
81
  "openai": "4.67.3",
82
+ "puppeteer-core": "24.10.2",
71
83
  "react": "^18.3.0",
84
+ "turndown": "^7.2.0",
85
+ "unpdf": "^0.12.1",
72
86
  "zod": "^3.23.0"
73
87
  },
74
88
  "devDependencies": {
89
+ "@types/cross-spawn": "^6.0.6",
75
90
  "@types/diff": "^7.0.2",
91
+ "@types/ejs": "^3.1.5",
76
92
  "@types/node": "^20.17.0",
77
93
  "@types/react": "^18.3.0",
94
+ "@types/turndown": "^5.0.5",
95
+ "docx": "^9.0.2",
78
96
  "husky": "^9.1.7",
79
97
  "ink-testing-library": "^4.0.0",
98
+ "pdf-lib": "^1.17.1",
80
99
  "tsx": "^4.19.0",
81
100
  "typescript": "^5.7.0",
82
101
  "vitest": "^2.1.0"
package/bench/README.md DELETED
@@ -1,78 +0,0 @@
1
- # Model bench
2
-
3
- Side-by-side outputs from Opus, Sonnet, and Haiku on the same ABAP prompt so
4
- you can decide per-skill which model carries the load before changing any
5
- default.
6
-
7
- ## Run
8
-
9
- ```bash
10
- pnpm bench:models
11
- pnpm bench:models --prompt abap-explain-bdef-handler
12
- pnpm bench:models --models claude-opus-4-7,claude-sonnet-4-6
13
- ```
14
-
15
- Routes through whichever LLM mode `cspeach config` is in — managed (proxy),
16
- BYOK, AI Hub, or local. In managed mode the proxy injects the **real skill
17
- body from KV** as the system prompt, so each bench call is end-to-end
18
- identical to what a real `/abap-explain` invocation would do — just on a
19
- different model.
20
-
21
- ## Prompts
22
-
23
- Drop new prompts in `prompts/<name>.md`. Format:
24
-
25
- ```markdown
26
- ---
27
- skill: abap-explain # required — must be a real CSPeach skill name
28
- description: short label
29
- ---
30
-
31
- ## user
32
- [the user-side prompt the model receives]
33
- ```
34
-
35
- Optionally include a `## system` section — it's **only** used in non-managed
36
- modes (BYOK, AI Hub, local) where there's no proxy to inject a skill body.
37
- In managed mode `## system` is ignored.
38
-
39
- ## Results
40
-
41
- `results/<prompt-name>/<model>.md` — full assistant text plus latency, tokens,
42
- and a USD estimate (using the same pricing table the cost footer uses).
43
-
44
- Read both files side by side. If Sonnet looks like a write-off on a given
45
- skill, keep the skill on Opus. If it looks "as good or better," that skill
46
- graduates down on the next config refactor.
47
-
48
- ## Cost note
49
-
50
- Each prompt × model call is one full skill turn through the proxy. The proxy
51
- loads the full skill body as system prompt, so input is in the 5-15k token
52
- range. A full 3-prompt × 3-model bench is roughly $0.30–$0.80 depending on
53
- output verbosity. (Haiku columns will be much cheaper than Opus.)
54
-
55
- ## Verdict log
56
-
57
- Findings from each bench run, oldest first. Each entry should name the run
58
- date, the prompts that were benched, and the model decision per skill — so
59
- the config refactor (`resolveModelForSkill` + `[models.skills]`) can be
60
- driven from evidence, not guesses.
61
-
62
- ### 2026-05-18 — initial 3-prompt run
63
-
64
- Prompts: `abap-explain-bdef-handler`, `abap-document-cds`, `abap-test-method`.
65
-
66
- | Skill | Sonnet 4.6 | Haiku 4.5 | Decision |
67
- |---|---|---|---|
68
- | `abap-explain` | matches Opus, catches `IN LOCAL MODE` non-retrigger nuance | typo `cl_abap_behav_handler` (wrong class name) | **Sonnet OK; Haiku unsafe** |
69
- | `abap-document` | matches Opus + adds client-handling + `@VDM.viewType` value | invented `DSOD` authority object; misread fuzzinessThreshold direction | **Sonnet OK; Haiku unsafe** |
70
- | `abap-test` | comparable rigor; asserts exception **textid** (Opus only `assert_bound`) | clean code; coverage-estimate prose shaky | **Sonnet OK; Haiku borderline** |
71
-
72
- Pattern: SAP/ABAP corpus is thinner than Python/JS — Haiku hallucinates SAP
73
- terms on multi-paragraph synthesis. Sonnet does not. Do NOT route any
74
- ABAP-content skill to Haiku based on this evidence.
75
-
76
- Skills NOT yet benched (kept on Opus by default): `abap-generate`, `-rap`,
77
- `-modernize`, `-design`, `-refactor`, `-review`, `-cca`, anything else
78
- producing new code or architectural decisions. Bench before routing.
@@ -1,44 +0,0 @@
1
- ---
2
- skill: abap-document
3
- description: Document a CDS interface view with associations + currency annotation
4
- ---
5
-
6
- ## user
7
-
8
- Please document this CDS view:
9
-
10
- ```cds
11
- @AccessControl.authorizationCheck: #CHECK
12
- @EndUserText.label: 'Sales Order — Interface View'
13
- @Search.searchable: true
14
- define view entity ZI_SalesOrder
15
- as select from zsales_order as Header
16
-
17
- association [0..1] to ZI_Customer as _Customer on $projection.CustomerID = _Customer.CustomerID
18
- association [0..*] to ZI_SalesOrderItem as _SalesOrderItem on $projection.SalesOrderID = _SalesOrderItem.SalesOrderID
19
- association [0..1] to I_Currency as _Currency on $projection.CurrencyCode = _Currency.Currency
20
-
21
- {
22
- key Header.sales_order_id as SalesOrderID,
23
- Header.customer_id as CustomerID,
24
- Header.order_date as OrderDate,
25
-
26
- @Search.defaultSearchElement: true
27
- @Search.fuzzinessThreshold: 0.8
28
- @ObjectModel.text.element: [ '_Customer.CustomerName' ]
29
- Header.customer_id as CustomerIDForSearch,
30
-
31
- @Semantics.amount.currencyCode: 'CurrencyCode'
32
- Header.net_amount as NetAmount,
33
- Header.currency_code as CurrencyCode,
34
-
35
- Header.created_at as CreatedAt,
36
- Header.created_by as CreatedBy,
37
- Header.last_changed_at as LastChangedAt,
38
- Header.last_changed_by as LastChangedBy,
39
-
40
- _Customer,
41
- _SalesOrderItem,
42
- _Currency
43
- }
44
- ```
@@ -1,57 +0,0 @@
1
- ---
2
- skill: abap-explain
3
- description: Explain a managed RAP behavior pool with a validation + determination
4
- ---
5
-
6
- ## user
7
-
8
- Please explain this behavior pool:
9
-
10
- ```abap
11
- CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
12
- PRIVATE SECTION.
13
- METHODS validateorderdate FOR VALIDATE ON SAVE
14
- IMPORTING keys FOR SalesOrder~validateorderdate.
15
- METHODS determinenetamount FOR DETERMINE ON MODIFY
16
- IMPORTING keys FOR SalesOrder~determinenetamount.
17
- ENDCLASS.
18
-
19
- CLASS lhc_salesorder IMPLEMENTATION.
20
- METHOD validateorderdate.
21
- READ ENTITIES OF zr_salesorder IN LOCAL MODE
22
- ENTITY SalesOrder FIELDS ( OrderDate ) WITH CORRESPONDING #( keys )
23
- RESULT DATA(lt_orders).
24
-
25
- LOOP AT lt_orders INTO DATA(ls_order).
26
- IF ls_order-OrderDate < cl_abap_context_info=>get_system_date( ).
27
- APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
28
- APPEND VALUE #(
29
- %tky = ls_order-%tky
30
- %msg = new_message_with_text(
31
- severity = if_abap_behv_message=>severity-error
32
- text = 'Order date must be today or in the future' )
33
- ) TO reported-salesorder.
34
- ENDIF.
35
- ENDLOOP.
36
- ENDMETHOD.
37
-
38
- METHOD determinenetamount.
39
- READ ENTITIES OF zr_salesorder IN LOCAL MODE
40
- ENTITY SalesOrder FIELDS ( Quantity UnitPrice ) WITH CORRESPONDING #( keys )
41
- RESULT DATA(lt_orders).
42
-
43
- DATA lt_update TYPE TABLE FOR UPDATE zr_salesorder\\SalesOrder.
44
- LOOP AT lt_orders INTO DATA(ls_order).
45
- APPEND VALUE #(
46
- %tky = ls_order-%tky
47
- NetAmount = ls_order-Quantity * ls_order-UnitPrice
48
- %control-NetAmount = if_abap_behv=>mk-on
49
- ) TO lt_update.
50
- ENDLOOP.
51
-
52
- MODIFY ENTITIES OF zr_salesorder IN LOCAL MODE
53
- ENTITY SalesOrder UPDATE FIELDS ( NetAmount )
54
- WITH lt_update.
55
- ENDMETHOD.
56
- ENDCLASS.
57
- ```
@@ -1,42 +0,0 @@
1
- ---
2
- skill: abap-test
3
- description: Generate an ABAP Unit test class for a small business rule method
4
- ---
5
-
6
- ## user
7
-
8
- Please write an ABAP Unit test class for this method:
9
-
10
- ```abap
11
- CLASS zcl_discount_calculator DEFINITION
12
- PUBLIC FINAL CREATE PUBLIC.
13
-
14
- PUBLIC SECTION.
15
- "! Returns the discount percentage (0..30) for a given net amount.
16
- "! < 1000 → 0%
17
- "! < 5000 → 5%
18
- "! < 10000 → 10%
19
- "! >= 10000 → 15%
20
- "! Raises zcx_discount_error on negative input.
21
- METHODS calculate_discount
22
- IMPORTING iv_net_amount TYPE p LENGTH 13 DECIMALS 2
23
- RETURNING VALUE(rv_discount) TYPE p LENGTH 3 DECIMALS 2
24
- RAISING zcx_discount_error.
25
- ENDCLASS.
26
-
27
- CLASS zcl_discount_calculator IMPLEMENTATION.
28
- METHOD calculate_discount.
29
- IF iv_net_amount < 0.
30
- RAISE EXCEPTION TYPE zcx_discount_error
31
- EXPORTING textid = zcx_discount_error=>negative_input.
32
- ENDIF.
33
-
34
- rv_discount = COND #(
35
- WHEN iv_net_amount < 1000 THEN '0.00'
36
- WHEN iv_net_amount < 5000 THEN '5.00'
37
- WHEN iv_net_amount < 10000 THEN '10.00'
38
- ELSE '15.00'
39
- ).
40
- ENDMETHOD.
41
- ENDCLASS.
42
- ```
@@ -1,189 +0,0 @@
1
- # abap-document-cds — claude-haiku-4-5
2
-
3
- - **skill:** abap-document
4
- - **description:** Document a CDS interface view with associations + currency annotation
5
- - **latency:** 26816 ms
6
- - **input tokens:** 2,688
7
- - **output tokens:** 2,623
8
- - **cost:** $0.0158
9
-
10
- ---
11
-
12
- # abap-document — ZI_SalesOrder
13
-
14
- ## TL;DR
15
-
16
- **Headline:** Interface view for sales order header data with customer and item associations, read-only with authorization check.
17
-
18
- **Top 3 (priority order):**
19
- 1. **Authorization enforced:** `#CHECK` mode requires `DSOD` and sales order read authority at runtime — verify user roles before consuming downstream.
20
- 2. **Fuzzy search enabled on CustomerID:** `fuzzinessThreshold: 0.8` will match partial/misspelled customer names; test with typical fat-finger errors in UAT.
21
- 3. **No filtering logic:** View exposes all rows from `zsales_order` table — upstream consumers must apply their own `WHERE` clauses; no automatic date range or status filtering.
22
-
23
- **Verdict:** Safe foundation view for consumption in analytics, APIs, or reporting. Confirm downstream apps implement business rule filtering (e.g., active orders only, date ranges).
24
-
25
- ---
26
-
27
- ## ZI_SalesOrder (CDS VIEW ENTITY)
28
-
29
- ### Overview
30
-
31
- **Package:** (not yet read — see below)
32
- **Type:** CDS View Entity (Interface Layer)
33
- **Annotations:** Authorization check enabled, search enabled, end-user label "Sales Order — Interface View"
34
-
35
- **Summary:**
36
- Exposes sales order header data from the custom table `zsales_order` with associations to customer master, line items, and SAP currency data. Designed as a read-only interface view for analytics, reporting, and API consumption.
37
-
38
- ---
39
-
40
- ### Purpose
41
-
42
- This view serves as the **interface layer** for sales order headers in the custom data model. It:
43
-
44
- 1. **Presents domain data cleanly** — maps technical table column names (`sales_order_id`, `customer_id`) to business-readable field names (`SalesOrderID`, `CustomerID`).
45
- 2. **Enforces authorization** — restricts read access to users with appropriate sales order query authority.
46
- 3. **Enables semantic search** — makes customer name searchable via fuzzy matching (0.8 threshold = allow 20% typos).
47
- 4. **Links related business entities** — connects to customer master, line items, and currency metadata via associations (no data duplication, navigation-based joins).
48
- 5. **Supports consumption layers** — designed for downstream consumption by reporting tools, OData APIs, or analytical views.
49
-
50
- ---
51
-
52
- ### Interface
53
-
54
- **Key Field:**
55
- - `SalesOrderID` (Header.sales_order_id) — unique identifier for the sales order
56
-
57
- **Header-Level Fields:**
58
-
59
- | Field Name | Source | Type | Purpose |
60
- |------------|--------|------|---------|
61
- | SalesOrderID | Header.sales_order_id | key | Sales order unique identifier |
62
- | CustomerID | Header.customer_id | string | References customer master |
63
- | OrderDate | Header.order_date | date | Date order was placed |
64
- | CustomerIDForSearch | Header.customer_id | string | Fuzzy-searchable copy for customer lookup |
65
- | NetAmount | Header.net_amount | decimal | Order value (currency-aware via annotation) |
66
- | CurrencyCode | Header.currency_code | string | ISO currency code |
67
- | CreatedAt | Header.created_at | timestamp | Audit: creation timestamp |
68
- | CreatedBy | Header.created_by | user | Audit: creator user |
69
- | LastChangedAt | Header.last_changed_at | timestamp | Audit: last change timestamp |
70
- | LastChangedBy | Header.last_changed_by | user | Audit: last change user |
71
-
72
- **Associations (Navigation):**
73
-
74
- | Association | Target View | Cardinality | Join Condition | Purpose |
75
- |-------------|-------------|-------------|-----------------|---------|
76
- | `_Customer` | ZI_Customer | 0..1 | CustomerID | Navigate to customer master data |
77
- | `_SalesOrderItem` | ZI_SalesOrderItem | 0..* | SalesOrderID | Navigate to line items for this order |
78
- | `_Currency` | I_Currency (SAP standard) | 0..1 | CurrencyCode | Navigate to SAP currency details |
79
-
80
- **Authorization & Search:**
81
- - Authorization check: `#CHECK` — runtime check required (user must have query authority for sales order).
82
- - Default search element: `CustomerIDForSearch` with fuzzy matching (80% match threshold).
83
- - Searchable: all marked fields indexed for full-text search.
84
-
85
- ---
86
-
87
- ### Logic Flow
88
-
89
- **Data Origin & Transformation:**
90
-
91
- 1. **Source:** Read all rows from custom table `zsales_order` (Header alias).
92
- 2. **Projection:** Map technical columns to semantic field names:
93
- - `sales_order_id` → `SalesOrderID` (key)
94
- - `customer_id` → `CustomerID`
95
- - `order_date` → `OrderDate`
96
- - `net_amount` → `NetAmount` (with currency semantic)
97
- - `currency_code` → `CurrencyCode`
98
- - Audit fields (created/changed by/at) mapped as-is.
99
- 3. **Duplicate CustomerID:** `CustomerIDForSearch` is a copy of `CustomerID` explicitly marked for full-text search (allows UI to search by customer name via the association).
100
- 4. **Associations:** Three navigation paths added (not joins — lazy-loaded):
101
- - To customer master for customer details.
102
- - To line items for order composition.
103
- - To SAP currency master for exchange rates, decimals, etc.
104
- 5. **Authorization Gate:** View enforces `#CHECK` at query time — SAP will validate user authority before returning rows.
105
-
106
- **No filtering logic:** View is a "raw interface" — all rows from the source table are exposed. Upstream consumers must filter by date, status, or other business rules.
107
-
108
- ---
109
-
110
- ### Data Access
111
-
112
- | Table/View | Operation | Purpose |
113
- |------------|-----------|---------|
114
- | `zsales_order` (custom) | **READ** | Source table for all header fields (sales_order_id, customer_id, order_date, net_amount, currency_code, audit fields) |
115
- | `ZI_Customer` (custom view) | **READ** (via association) | Customer master lookup; enriches orders with customer details |
116
- | `ZI_SalesOrderItem` (custom view) | **READ** (via association) | Line items for the order; enables per-item navigation |
117
- | `I_Currency` (SAP standard) | **READ** (via association) | SAP currency master; provides conversion rates, decimal places |
118
-
119
- **No writes:** This is a read-only interface view. No INSERT, UPDATE, or DELETE allowed.
120
-
121
- ---
122
-
123
- ### Dependencies
124
-
125
- **Called by (Upstream Consumers):**
126
- Unknown without further analysis — use `sap_usage_references` to identify (e.g., analytics views, OData services, reports using this view).
127
-
128
- **Calls (Dependencies):**
129
- - `zsales_order` — custom table (no option to change this view without understanding the source table schema).
130
- - `ZI_Customer` — custom customer interface view (ensure it is available and stable).
131
- - `ZI_SalesOrderItem` — custom sales order item interface view (ensure it exists).
132
- - `I_Currency` (SAP standard) — standard SAP currency master (no custom risk; always available post-installation).
133
-
134
- ---
135
-
136
- ### Authorization & Security
137
-
138
- **Authorization Check:** `#CHECK` mode
139
- - When a user queries this view, SAP **enforces** a dynamic authorization check at runtime.
140
- - The specific check depends on SAP's authority object mapping for CDS views (typically `DSOD` → Data Source Object Definition or equivalent).
141
- - **Action required:** Verify that user roles define query access for this view; otherwise, queries will fail with "Authorization error."
142
-
143
- **Search & Indexing:**
144
- - `CustomerIDForSearch` with `fuzzinessThreshold: 0.8` allows typo-tolerant search (e.g., "APLE" matches "APPLE").
145
- - Useful for user-facing search UIs; may return false positives in strict filter scenarios.
146
-
147
- ---
148
-
149
- ### Data Model & Semantics
150
-
151
- **Semantic Annotations:**
152
- - `@Semantics.amount.currencyCode: 'CurrencyCode'` on `NetAmount` — tells consuming systems that this field is a currency amount and which field holds the currency code. Tools like SAP Analytics Cloud will format it correctly.
153
- - `@ObjectModel.text.element: [ '_Customer.CustomerName' ]` on `CustomerIDForSearch` — tells UI frameworks to display the customer **name** (not ID) when showing this field.
154
-
155
- **Searchability:**
156
- - View is marked `@Search.searchable: true` — all fields with `@Search.defaultSearchElement: true` are indexed.
157
- - Only `CustomerIDForSearch` is explicitly indexed; other fields are not full-text searchable by default.
158
-
159
- ---
160
-
161
- ### Notes & Considerations
162
-
163
- 1. **No status filtering:** View exposes all orders regardless of status (open, closed, cancelled). If you need only active orders, add a downstream view or consuming WHERE clause.
164
-
165
- 2. **Audit trail:** `CreatedBy`, `LastChangedBy`, `CreatedAt`, `LastChangedAt` are available for compliance; confirm these are populated in the source table `zsales_order`.
166
-
167
- 3. **Association multiplicity:**
168
- - Customer is 0..1 (every order may not have a customer — nullable FK?). Verify business rule: must a sales order always have a customer?
169
- - Line items are 0..* (orders with no items are allowed). Confirm with business if this is intentional.
170
- - Currency is 0..1 (may not exist in SAP currency master for custom codes). Test with unusual currency codes.
171
-
172
- 4. **Search on CustomerID not CustomerName:** The view exposes `CustomerID` for search but links to `_Customer.CustomerName`. If users expect to search by customer **name**, ensure the downstream OData/UI binding is configured to search on `CustomerIDForSearch` and display `_Customer.CustomerName`.
173
-
174
- 5. **No performance optimizations:** View is a simple 1:1 projection. For large `zsales_order` tables (millions of rows), consider:
175
- - Adding a semantic partitioning hint in the view definition.
176
- - Creating secondary indexes on frequently filtered columns (e.g., `OrderDate`, `CustomerID`).
177
- - Testing query response times with large datasets.
178
-
179
- 6. **Missing fields?** Confirm with business that all required order header fields are in the projection. If fields exist in `zsales_order` but not exposed here, add them or create a second view variant.
180
-
181
- ---
182
-
183
- ## Next Steps
184
-
185
- 1. **Trace consumers:** Run `sap_usage_references` to identify which views, APIs, and reports consume this view.
186
- 2. **Verify source table:** Run `sap_get_source` on `zsales_order` to confirm the schema matches the projection.
187
- 3. **Check related views:** Retrieve `ZI_Customer` and `ZI_SalesOrderItem` to ensure the associations are compatible (key cardinality, join conditions).
188
- 4. **Authorization testing:** Create a test user with appropriate authority and verify they can query this view without "Authorization failed" errors.
189
- 5. **Search testing:** Test fuzzy search with intentional typos (e.g., customer name "ACME Corp" vs. search "ACME Crp") to confirm the 0.8 threshold behavior.