@cspeach/cli 0.8.0 → 1.0.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 (141) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/intent-system-prompt.js +1 -1
  3. package/dist/agent/loop.js +330 -36
  4. package/dist/agent/providers/license-gate.js +44 -0
  5. package/dist/agent/skill-checkpoint.js +1 -1
  6. package/dist/agent/steering-queue.js +27 -0
  7. package/dist/agent/tool-dispatch.js +15 -0
  8. package/dist/approvals/canonical.js +91 -0
  9. package/dist/approvals/jwt.js +84 -7
  10. package/dist/approvals/render.js +38 -0
  11. package/dist/auth/org-anthropic-key.js +25 -0
  12. package/dist/classifier/client.js +24 -5
  13. package/dist/commands/config-set.js +95 -0
  14. package/dist/commands/login.js +31 -14
  15. package/dist/commands/plan-model-tier.js +83 -0
  16. package/dist/commands/plan-resume.js +435 -0
  17. package/dist/config/loader.js +102 -3
  18. package/dist/cost/pricing.js +14 -5
  19. package/dist/doctor/checks/_http-probe.js +1 -0
  20. package/dist/doctor/checks/cert.js +14 -3
  21. package/dist/doctor/checks/sap.js +30 -8
  22. package/dist/doctor/checks/zcspeach.js +19 -4
  23. package/dist/one-shot.js +52 -4
  24. package/dist/projects/answer-blockers.js +137 -0
  25. package/dist/projects/email-template.js +2 -0
  26. package/dist/projects/extract-cca.js +108 -16
  27. package/dist/projects/extract-modernize.js +1 -1
  28. package/dist/projects/extract-plan.js +178 -0
  29. package/dist/projects/extract-spec-gap.js +34 -7
  30. package/dist/projects/extract-test-coverage.js +1 -1
  31. package/dist/projects/extract-upgrade.js +113 -22
  32. package/dist/projects/index.js +7 -1
  33. package/dist/projects/merge-cca.js +292 -0
  34. package/dist/projects/merge-upgrade.js +173 -0
  35. package/dist/projects/migration.js +103 -1
  36. package/dist/projects/output-paths.js +27 -0
  37. package/dist/projects/plan-run.js +254 -0
  38. package/dist/projects/plan-schema.js +210 -0
  39. package/dist/projects/promote-command.js +26 -2
  40. package/dist/projects/promote.js +128 -0
  41. package/dist/projects/save-command.js +263 -20
  42. package/dist/projects/status.js +22 -0
  43. package/dist/projects/validate.js +2 -0
  44. package/dist/projects/workspace.js +164 -20
  45. package/dist/renderer/notices.js +64 -0
  46. package/dist/renderer/progress-chatter.js +8 -0
  47. package/dist/renderer/syntax.js +16 -1
  48. package/dist/renderer/thinking-heartbeat.js +13 -1
  49. package/dist/renderer/tool-widget.js +18 -4
  50. package/dist/renderer/tty.js +43 -4
  51. package/dist/renderer/verify-chain.js +77 -0
  52. package/dist/repl/at-picker.js +93 -21
  53. package/dist/repl/builtin-commands.js +37 -0
  54. package/dist/repl/early-line-buffer.js +68 -0
  55. package/dist/repl/inquirer-guard.js +70 -5
  56. package/dist/repl/numbered-menu.js +131 -0
  57. package/dist/repl/post-turn-status.js +2 -2
  58. package/dist/repl/rule8-detector.js +17 -2
  59. package/dist/repl/safety-confirm.js +111 -2
  60. package/dist/repl/safety-mode-state.js +19 -3
  61. package/dist/repl/slash-picker.js +25 -19
  62. package/dist/repl.js +470 -22
  63. package/dist/router/classifier.js +150 -6
  64. package/dist/sap/capability-matrix.js +20 -0
  65. package/dist/sap/capability-matrix.json +11236 -0
  66. package/dist/sap/capability.js +146 -0
  67. package/dist/sap/connection-manager.js +19 -1
  68. package/dist/sap/onboarding.js +42 -4
  69. package/dist/session/pending.js +27 -0
  70. package/dist/skill-catalog.js +54 -43
  71. package/dist/skills/bundled-skills.js +279 -1
  72. package/dist/skills/promotion-dispatch.js +23 -0
  73. package/dist/tools/_command-shared.js +36 -12
  74. package/dist/tools/_filesystem-shared.js +139 -4
  75. package/dist/tools/_flag.js +25 -0
  76. package/dist/tools/approval.js +64 -21
  77. package/dist/tools/ask-question.js +96 -4
  78. package/dist/tools/capability/tool.js +74 -0
  79. package/dist/tools/dispatch-skill.js +22 -1
  80. package/dist/tools/extend-model/anchored-insert.js +810 -0
  81. package/dist/tools/extend-model/tool.js +188 -0
  82. package/dist/tools/filesystem/extract-document.js +57 -0
  83. package/dist/tools/filesystem/file-edit.js +12 -2
  84. package/dist/tools/filesystem/file-read.js +2 -2
  85. package/dist/tools/filesystem/file-write.js +11 -2
  86. package/dist/tools/filesystem/glob.js +11 -0
  87. package/dist/tools/filesystem/grep.js +10 -0
  88. package/dist/tools/filesystem/read-document.js +107 -0
  89. package/dist/tools/fiori/apply.js +50 -0
  90. package/dist/tools/fiori/bin.js +3 -0
  91. package/dist/tools/fiori/catalog/index.js +27 -0
  92. package/dist/tools/fiori/catalog/value-help.js +230 -0
  93. package/dist/tools/fiori/catalog/viz-chart.js +177 -0
  94. package/dist/tools/fiori/cli.js +71 -0
  95. package/dist/tools/fiori/deploy-config.js +73 -0
  96. package/dist/tools/fiori/fe-scaffold.js +45 -0
  97. package/dist/tools/fiori/i18n.js +39 -0
  98. package/dist/tools/fiori/manifest.js +70 -0
  99. package/dist/tools/fiori/render.js +77 -0
  100. package/dist/tools/fiori/scaffold.js +39 -0
  101. package/dist/tools/fiori/tools.js +356 -0
  102. package/dist/tools/fiori/types.js +1 -0
  103. package/dist/tools/local-build.js +76 -0
  104. package/dist/tools/local-files.js +31 -0
  105. package/dist/tools/project/_merge-shared.js +68 -0
  106. package/dist/tools/project/cca_merge.js +164 -0
  107. package/dist/tools/project/playbook_get.js +1 -1
  108. package/dist/tools/project/upgrade_merge_progress.js +206 -0
  109. package/dist/tools/sap-read.js +53 -9
  110. package/dist/tools/sap-write.js +530 -21
  111. package/dist/tools/shell/shell_exec.js +41 -6
  112. package/dist/tools/snapshot.js +37 -14
  113. package/dist/tools/subagent/background_run.js +17 -1
  114. package/dist/tools/transport-resolution.js +86 -0
  115. package/dist/tools/transport.js +224 -5
  116. package/dist/tools/write-mode.js +4 -0
  117. package/dist/ui/app.js +84 -11
  118. package/dist/ui/body.js +13 -0
  119. package/dist/ui/command-palette.js +46 -10
  120. package/dist/ui/file-palette.js +44 -0
  121. package/dist/ui/footer.js +28 -11
  122. package/dist/ui/line-resolution.js +92 -0
  123. package/dist/ui/session-timeline.js +1 -0
  124. package/dist/ui/text-input.js +150 -0
  125. package/dist/ui/turn-status-emitter.js +52 -0
  126. package/dist/ui/turn-status.js +59 -0
  127. package/dist/ui/widgets/ask-question-modal.js +30 -2
  128. package/package.json +23 -4
  129. package/bench/README.md +0 -78
  130. package/bench/prompts/abap-document-cds.md +0 -44
  131. package/bench/prompts/abap-explain-bdef-handler.md +0 -57
  132. package/bench/prompts/abap-test-method.md +0 -42
  133. package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
  134. package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
  135. package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
  136. package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
  137. package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
  138. package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
  139. package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
  140. package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
  141. package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
@@ -0,0 +1,52 @@
1
+ // cspeach-cli/src/ui/turn-status-emitter.ts
2
+ //
3
+ // Turn-lifecycle channel for the persistent status row (2026-06-07 —
4
+ // the CC-style "always alive while working" indicator, B5 smoke
5
+ // feedback round 3: fresh-line heartbeats were not enough).
6
+ //
7
+ // Same singleton-emitter pattern as error-emitter / approval-emitter:
8
+ // the REPL marks turn start/stop around runTurn (try/finally so a
9
+ // thrown turn can never leave the row stuck), loop.ts narrates the
10
+ // current activity, and <TurnStatus /> renders it in the dynamic Ink
11
+ // frame. In classic mode App is never mounted, the emitter has no
12
+ // listeners, and every call is a no-op.
13
+ import { EventEmitter } from 'node:events';
14
+ class TurnStatusEmitter extends EventEmitter {
15
+ state = { active: false, paused: false, label: '', startedAt: 0 };
16
+ start(label = 'thinking') {
17
+ this.state = { active: true, paused: false, label, startedAt: Date.now() };
18
+ this.emit('change', this.state);
19
+ }
20
+ /** Update the activity label; ignored when no turn is active. Clears pause —
21
+ * a real activity means the model is working again. */
22
+ activity(label) {
23
+ if (!this.state.active)
24
+ return;
25
+ this.state = { ...this.state, label, paused: false };
26
+ this.emit('change', this.state);
27
+ }
28
+ /** Blocked on user input — stop ticking and hide the row until resume(). */
29
+ pause() {
30
+ if (!this.state.active || this.state.paused)
31
+ return;
32
+ this.state = { ...this.state, paused: true };
33
+ this.emit('change', this.state);
34
+ }
35
+ /** User answered — the model is working again; bring the row back. */
36
+ resume() {
37
+ if (!this.state.active || !this.state.paused)
38
+ return;
39
+ this.state = { ...this.state, paused: false };
40
+ this.emit('change', this.state);
41
+ }
42
+ stop() {
43
+ if (!this.state.active)
44
+ return;
45
+ this.state = { active: false, paused: false, label: '', startedAt: 0 };
46
+ this.emit('change', this.state);
47
+ }
48
+ snapshot() {
49
+ return this.state;
50
+ }
51
+ }
52
+ export const turnStatusEmitter = new TurnStatusEmitter();
@@ -0,0 +1,59 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Text } from 'ink';
4
+ import { turnStatusEmitter } from './turn-status-emitter.js';
5
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ const TICK_MS = 250;
7
+ const PEACH = '#FFCBA4';
8
+ function fmtElapsed(ms) {
9
+ const s = Math.floor(ms / 1000);
10
+ if (s < 60)
11
+ return `${s}s`;
12
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
13
+ }
14
+ /**
15
+ * Persistent turn-status row (2026-06-07) — lives in the DYNAMIC Ink
16
+ * frame just above the Footer, so it repaints with the frame and
17
+ * vanishes the instant the turn ends. While a turn is in flight:
18
+ *
19
+ * ⠹ sap_create_object · 34s · ctrl+c to interrupt
20
+ *
21
+ * The label follows the loop's narration (thinking / tool name /
22
+ * writing response / save check). Its presence answers "is it still
23
+ * working?"; its absence answers "is it done?" — the ambiguity that
24
+ * burned the B5 smoke three times.
25
+ *
26
+ * Pure Ink — no raw stdout writes, no cursor escape codes (the phantom
27
+ * cursor #25 class of bug came from a raw-stdout spinner; this
28
+ * component cannot reproduce it). Guard: ink-cursor-invariant test.
29
+ */
30
+ export function TurnStatus() {
31
+ const [state, setState] = useState(turnStatusEmitter.snapshot());
32
+ const [frame, setFrame] = useState(0);
33
+ const [now, setNow] = useState(Date.now());
34
+ useEffect(() => {
35
+ const onChange = (s) => setState({ ...s });
36
+ turnStatusEmitter.on('change', onChange);
37
+ // Re-sync from the snapshot AFTER subscribing — an event fired between
38
+ // mount and this effect (e.g. a turn starting while App is still
39
+ // mounting) would otherwise be missed and the row stays blank.
40
+ setState({ ...turnStatusEmitter.snapshot() });
41
+ return () => { turnStatusEmitter.off('change', onChange); };
42
+ }, []);
43
+ useEffect(() => {
44
+ // Don't tick while paused (blocked on user input) — a live tick here
45
+ // repaints the dynamic frame every 250ms and overdraws an inquirer
46
+ // approval picker / churns the Ink ask_question modal (stacked borders,
47
+ // lost keypress). The row resumes the instant the user answers.
48
+ if (!state.active || state.paused)
49
+ return;
50
+ const interval = setInterval(() => {
51
+ setFrame((f) => (f + 1) % FRAMES.length);
52
+ setNow(Date.now());
53
+ }, TICK_MS);
54
+ return () => clearInterval(interval);
55
+ }, [state.active, state.paused]);
56
+ if (!state.active || state.paused)
57
+ return null;
58
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: PEACH, children: [FRAMES[frame], " "] }), _jsx(Text, { children: state.label }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", fmtElapsed(now - state.startedAt), " \u00B7 ctrl+c to interrupt"] })] }));
59
+ }
@@ -15,11 +15,39 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * learned the hard way that absolute overlays paint over Body and
16
16
  * can't be navigated cleanly).
17
17
  */
18
- import { useState } from 'react';
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
+ /**
26
+ * Reclaim raw-mode stdin when an Ink modal mounts (2026-06-08).
27
+ *
28
+ * The approval plan-gate uses `@inquirer/prompts`, which toggles the
29
+ * terminal's raw mode (and pauses stdin) out from under Ink while it runs.
30
+ * When it releases, Ink's INTERNAL bookkeeping still thinks raw mode is on,
31
+ * so it does not re-assert it — and a modal mounted afterwards (this one)
32
+ * receives no keypresses: arrow keys and Enter are dead, the user cannot
33
+ * answer the question. Re-syncing the OS state to what Ink expects on mount
34
+ * fixes it. Direct `process.stdin` calls (not Ink's ref-counted setRawMode)
35
+ * because the whole problem is that Ink's count is stale.
36
+ */
37
+ function useReclaimRawStdin() {
38
+ useEffect(() => {
39
+ const stdin = process.stdin;
40
+ if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
41
+ try {
42
+ stdin.setRawMode(true);
43
+ }
44
+ catch { /* not a TTY — ignore */ }
45
+ stdin.resume();
46
+ }
47
+ }, []);
48
+ }
22
49
  export function AskQuestionModal({ params, onResolve }) {
50
+ useReclaimRawStdin();
23
51
  if (params.kind === 'text' || params.choices.length === 0) {
24
52
  return _jsx(TextInputForm, { params: params, onResolve: onResolve });
25
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
@@ -41,47 +41,66 @@
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
+ "fiori": "tsx src/tools/fiori/bin.ts",
45
+ "prepare": "husky",
46
+ "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
47
  },
46
48
  "engines": {
47
49
  "node": ">=20.0.0"
48
50
  },
49
51
  "dependencies": {
50
52
  "@anthropic-ai/sdk": "^0.90.0",
51
- "@cspeach/sap-client": "^0.1.3",
53
+ "@cspeach/sap-client": "^0.1.10",
52
54
  "@iarna/toml": "^2.2.5",
53
55
  "@inkjs/ui": "^2.0.0",
54
56
  "@inquirer/prompts": "^7.0.0",
55
57
  "@noble/ed25519": "2.1.0",
56
58
  "@noble/hashes": "1.4.0",
59
+ "@sap-ux/fiori-elements-writer": "^3.0.39",
60
+ "@sap-ux/fiori-freestyle-writer": "3.0.12",
57
61
  "chalk": "^5.3.0",
58
62
  "cli-highlight": "^2.1.11",
59
63
  "cli-table3": "^0.6.5",
60
64
  "clipboardy": "^5.3.1",
65
+ "cross-spawn": "^7.0.6",
61
66
  "diff": "^9.0.0",
67
+ "ejs": "3.1.10",
62
68
  "fuse.js": "^7.0.0",
63
69
  "ink": "^5.0.0",
64
70
  "ink-text-input": "^6.0.0",
65
71
  "jose": "^5.9.0",
66
72
  "keytar": "^7.9.0",
73
+ "mammoth": "^1.8.0",
67
74
  "marked": "^11.2.0",
68
75
  "marked-terminal": "^6.2.0",
76
+ "mem-fs": "2.1.0",
77
+ "mem-fs-editor": "9.4.0",
69
78
  "node-machine-id": "^1.1.12",
70
79
  "openai": "4.67.3",
71
80
  "react": "^18.3.0",
81
+ "turndown": "^7.2.0",
82
+ "unpdf": "^0.12.1",
72
83
  "zod": "^3.23.0"
73
84
  },
74
85
  "devDependencies": {
86
+ "@types/cross-spawn": "^6.0.6",
75
87
  "@types/diff": "^7.0.2",
88
+ "@types/ejs": "^3.1.5",
76
89
  "@types/node": "^20.17.0",
77
90
  "@types/react": "^18.3.0",
91
+ "@types/turndown": "^5.0.5",
92
+ "docx": "^9.0.2",
78
93
  "husky": "^9.1.7",
79
94
  "ink-testing-library": "^4.0.0",
95
+ "pdf-lib": "^1.17.1",
80
96
  "tsx": "^4.19.0",
81
97
  "typescript": "^5.7.0",
82
98
  "vitest": "^2.1.0"
83
99
  },
84
100
  "pnpm": {
85
- "onlyBuiltDependencies": ["keytar", "esbuild"]
101
+ "onlyBuiltDependencies": [
102
+ "keytar",
103
+ "esbuild"
104
+ ]
86
105
  }
87
106
  }
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.