@cspeach/cli 0.6.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 (210) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +108 -0
  3. package/dist/agent/anthropic-provider.js +59 -0
  4. package/dist/agent/llm-provider.js +1 -0
  5. package/dist/agent/loop.js +709 -0
  6. package/dist/agent/maybe-build-project-context.js +126 -0
  7. package/dist/agent/providers/ai-hub-provider.js +58 -0
  8. package/dist/agent/providers/byok-provider.js +53 -0
  9. package/dist/agent/providers/factory.js +13 -0
  10. package/dist/agent/providers/local-provider.js +125 -0
  11. package/dist/agent/repair-partial.js +31 -0
  12. package/dist/agent/retry-key.js +58 -0
  13. package/dist/agent/retry.js +17 -0
  14. package/dist/agent/sap-connection-adapter.js +82 -0
  15. package/dist/agent/skill-checkpoint.js +119 -0
  16. package/dist/agent/tool-dispatch.js +47 -0
  17. package/dist/agent/turn-assistant-text.js +49 -0
  18. package/dist/agent/turn-error-ux.js +126 -0
  19. package/dist/agent/turn-stream.js +79 -0
  20. package/dist/agent/turn-watchdog.js +71 -0
  21. package/dist/approvals/advisory-prompt.js +40 -0
  22. package/dist/approvals/advisory-render.js +38 -0
  23. package/dist/approvals/approval-prompt.js +100 -0
  24. package/dist/approvals/jwt.js +33 -0
  25. package/dist/approvals/render.js +211 -0
  26. package/dist/approvals/risk-floor.js +26 -0
  27. package/dist/auth/api-key.js +40 -0
  28. package/dist/auth/auth-file.js +59 -0
  29. package/dist/auth/device.js +8 -0
  30. package/dist/auth/me.js +19 -0
  31. package/dist/classifier/client.js +58 -0
  32. package/dist/cli-args.js +38 -0
  33. package/dist/cli.js +148 -0
  34. package/dist/commands/config-set.js +245 -0
  35. package/dist/commands/config-show.js +159 -0
  36. package/dist/commands/help.js +93 -0
  37. package/dist/commands/login.js +122 -0
  38. package/dist/commands/logout.js +17 -0
  39. package/dist/commands/project-context-impact.js +215 -0
  40. package/dist/commands/reroute.js +60 -0
  41. package/dist/commands/spec-gap-status.js +52 -0
  42. package/dist/commands/whoami.js +35 -0
  43. package/dist/config/loader.js +67 -0
  44. package/dist/config/paths.js +20 -0
  45. package/dist/doctor/checks/_http-probe.js +56 -0
  46. package/dist/doctor/checks/auth.js +15 -0
  47. package/dist/doctor/checks/cert.js +24 -0
  48. package/dist/doctor/checks/forge-rules.js +102 -0
  49. package/dist/doctor/checks/keychain-fallback.js +14 -0
  50. package/dist/doctor/checks/keychain.js +23 -0
  51. package/dist/doctor/checks/llm-mode.js +27 -0
  52. package/dist/doctor/checks/proxy.js +13 -0
  53. package/dist/doctor/checks/sap.js +33 -0
  54. package/dist/doctor/checks/skill.js +24 -0
  55. package/dist/doctor/checks/write-mode.js +34 -0
  56. package/dist/doctor/checks/zcspeach.js +76 -0
  57. package/dist/doctor/run.js +46 -0
  58. package/dist/errors/codes.js +12 -0
  59. package/dist/index.js +6 -0
  60. package/dist/lock-contention.js +22 -0
  61. package/dist/one-shot.js +104 -0
  62. package/dist/project-context/conventions.js +309 -0
  63. package/dist/project-context/detect.js +250 -0
  64. package/dist/project-context/domain/abap-cloud.js +26 -0
  65. package/dist/project-context/domain/abapgit.js +177 -0
  66. package/dist/project-context/domain/cap.js +164 -0
  67. package/dist/project-context/domain/fiori.js +326 -0
  68. package/dist/project-context/git.js +115 -0
  69. package/dist/project-context/index-files.js +235 -0
  70. package/dist/project-context/index.js +117 -0
  71. package/dist/project-context/render.js +308 -0
  72. package/dist/project-context/types.js +14 -0
  73. package/dist/projects/build.js +20 -0
  74. package/dist/projects/canonicalize.js +39 -0
  75. package/dist/projects/email-template.js +54 -0
  76. package/dist/projects/extract-cca.js +139 -0
  77. package/dist/projects/extract-design.js +107 -0
  78. package/dist/projects/extract-estimate.js +93 -0
  79. package/dist/projects/extract-modernize.js +130 -0
  80. package/dist/projects/extract-spec-gap.js +101 -0
  81. package/dist/projects/extract-test-coverage.js +137 -0
  82. package/dist/projects/extract-upgrade.js +230 -0
  83. package/dist/projects/filename.js +18 -0
  84. package/dist/projects/index.js +8 -0
  85. package/dist/projects/migration.js +111 -0
  86. package/dist/projects/promote-command.js +96 -0
  87. package/dist/projects/promote.js +107 -0
  88. package/dist/projects/save-command.js +124 -0
  89. package/dist/projects/save.js +21 -0
  90. package/dist/projects/status.js +170 -0
  91. package/dist/projects/types.js +1 -0
  92. package/dist/projects/validate.js +146 -0
  93. package/dist/projects/workspace.js +478 -0
  94. package/dist/renderer/abap-inline.js +121 -0
  95. package/dist/renderer/banners.js +39 -0
  96. package/dist/renderer/highlighters/abap.js +126 -0
  97. package/dist/renderer/highlighters/bdef.js +81 -0
  98. package/dist/renderer/highlighters/cds.js +91 -0
  99. package/dist/renderer/markdown.js +291 -0
  100. package/dist/renderer/pipeline.js +201 -0
  101. package/dist/renderer/progress-chatter.js +237 -0
  102. package/dist/renderer/question-normalizer.js +306 -0
  103. package/dist/renderer/severity.js +61 -0
  104. package/dist/renderer/status-footer.js +50 -0
  105. package/dist/renderer/syntax.js +58 -0
  106. package/dist/renderer/tables.js +55 -0
  107. package/dist/renderer/thinking-heartbeat.js +70 -0
  108. package/dist/renderer/tool-widget.js +199 -0
  109. package/dist/renderer/tty.js +66 -0
  110. package/dist/renderer/widget-extractor.js +87 -0
  111. package/dist/renderer/widget-fallback.js +78 -0
  112. package/dist/renderer/widget-schemas.js +43 -0
  113. package/dist/repl/at-completer.js +64 -0
  114. package/dist/repl/at-picker.js +122 -0
  115. package/dist/repl/bracketed-paste.js +284 -0
  116. package/dist/repl/current-transport.js +46 -0
  117. package/dist/repl/diff-display.js +41 -0
  118. package/dist/repl/file-picker.js +219 -0
  119. package/dist/repl/inquirer-guard.js +130 -0
  120. package/dist/repl/inquirer-theme.js +41 -0
  121. package/dist/repl/rule8-detector.js +99 -0
  122. package/dist/repl/safety-confirm.js +106 -0
  123. package/dist/repl/safety-mode-state.js +36 -0
  124. package/dist/repl/slash-completer.js +59 -0
  125. package/dist/repl/slash-picker.js +124 -0
  126. package/dist/repl/update-method-preview-hook.js +45 -0
  127. package/dist/repl.js +1383 -0
  128. package/dist/router/classifier.js +38 -0
  129. package/dist/router/intent-extractor.js +140 -0
  130. package/dist/router/routing-decision.js +19 -0
  131. package/dist/sap/connection-manager.js +52 -0
  132. package/dist/sap/onboarding.js +178 -0
  133. package/dist/sap/system-info.js +515 -0
  134. package/dist/session/awaiting-answer.js +73 -0
  135. package/dist/session/gc.js +28 -0
  136. package/dist/session/pending.js +37 -0
  137. package/dist/session/resume.js +77 -0
  138. package/dist/session/schema.js +20 -0
  139. package/dist/session/store.js +147 -0
  140. package/dist/session/time-ago.js +41 -0
  141. package/dist/skill-catalog.js +222 -0
  142. package/dist/skills/bundled-skills.js +1 -0
  143. package/dist/skills/canonical.js +12 -0
  144. package/dist/skills/manifest-client.js +93 -0
  145. package/dist/skills/promotion-dispatch.js +24 -0
  146. package/dist/skills/signing-public-key.js +4 -0
  147. package/dist/skills/source-bundled.js +20 -0
  148. package/dist/skills/source-managed.js +26 -0
  149. package/dist/skills/source-manifest.js +26 -0
  150. package/dist/tools/_command-shared.js +110 -0
  151. package/dist/tools/_filesystem-shared.js +81 -0
  152. package/dist/tools/_flag.js +39 -0
  153. package/dist/tools/approval.js +228 -0
  154. package/dist/tools/ask-question.js +205 -0
  155. package/dist/tools/dispatch-skill.js +81 -0
  156. package/dist/tools/filesystem/file-edit.js +140 -0
  157. package/dist/tools/filesystem/file-read.js +89 -0
  158. package/dist/tools/filesystem/file-write.js +128 -0
  159. package/dist/tools/filesystem/glob.js +177 -0
  160. package/dist/tools/filesystem/grep.js +163 -0
  161. package/dist/tools/index.js +32 -0
  162. package/dist/tools/project/convention_get.js +91 -0
  163. package/dist/tools/project/playbook_get.js +132 -0
  164. package/dist/tools/project/project_context_get.js +101 -0
  165. package/dist/tools/sap-read.js +454 -0
  166. package/dist/tools/sap-write.js +746 -0
  167. package/dist/tools/shell/shell_exec.js +209 -0
  168. package/dist/tools/snapshot.js +107 -0
  169. package/dist/tools/subagent/_background-shared.js +133 -0
  170. package/dist/tools/subagent/agent_run.js +186 -0
  171. package/dist/tools/subagent/background_run.js +143 -0
  172. package/dist/tools/subagent/monitor_emit.js +65 -0
  173. package/dist/tools/subagent/schedule_create.js +131 -0
  174. package/dist/tools/transport.js +233 -0
  175. package/dist/tools/update-method-intercept.js +119 -0
  176. package/dist/tools/verify.js +39 -0
  177. package/dist/tools/web/_web-shared.js +251 -0
  178. package/dist/tools/web/web_fetch.js +257 -0
  179. package/dist/tools/web/web_search.js +195 -0
  180. package/dist/tools/write-mode.js +22 -0
  181. package/dist/ui/app.js +95 -0
  182. package/dist/ui/approval-emitter.js +10 -0
  183. package/dist/ui/approval-modal.js +53 -0
  184. package/dist/ui/ascii-chars.js +6 -0
  185. package/dist/ui/body.js +102 -0
  186. package/dist/ui/coaching-picker-classic.js +36 -0
  187. package/dist/ui/coaching-picker-emitter.js +27 -0
  188. package/dist/ui/command-palette.js +34 -0
  189. package/dist/ui/error-emitter.js +21 -0
  190. package/dist/ui/footer.js +103 -0
  191. package/dist/ui/header.js +17 -0
  192. package/dist/ui/ink-classifier-route.js +19 -0
  193. package/dist/ui/login-banner.js +72 -0
  194. package/dist/ui/rich-error-box.js +9 -0
  195. package/dist/ui/sap-state-store.js +65 -0
  196. package/dist/ui/session-timeline.js +31 -0
  197. package/dist/ui/sidebar.js +10 -0
  198. package/dist/ui/skill-picker.js +50 -0
  199. package/dist/ui/status-row.js +12 -0
  200. package/dist/ui/widget-control.js +4 -0
  201. package/dist/ui/widgets/bar-chart.js +15 -0
  202. package/dist/ui/widgets/coaching-picker.js +41 -0
  203. package/dist/ui/widgets/component-registry.js +12 -0
  204. package/dist/ui/widgets/dep-graph.js +9 -0
  205. package/dist/ui/widgets/diff-viewer.js +11 -0
  206. package/dist/ui/widgets/question-card.js +11 -0
  207. package/dist/ui/widgets/stack-frames.js +5 -0
  208. package/dist/upgrade-check.js +28 -0
  209. package/dist/upgrade.js +13 -0
  210. package/package.json +83 -0
@@ -0,0 +1,107 @@
1
+ const TITLE_RE = /^## ABAP Design — (.+)$/m;
2
+ const OBJECTS_HEADING_RE = /^### Objects to Create\s*$/m;
3
+ const DEPENDENCY_HEADING_RE = /^### Dependency Sequence\s*$/m;
4
+ const NEXT_HEADING_RE = /^###\s/m;
5
+ function sliceSection(markdown, headingRe) {
6
+ const m = headingRe.exec(markdown);
7
+ if (!m)
8
+ return null;
9
+ const start = m.index + m[0].length;
10
+ const rest = markdown.slice(start);
11
+ const next = NEXT_HEADING_RE.exec(rest);
12
+ return next ? rest.slice(0, next.index) : rest;
13
+ }
14
+ function parseObjectsTable(section) {
15
+ const lines = section.split(/\r?\n/);
16
+ const rows = [];
17
+ for (const raw of lines) {
18
+ const line = raw.trim();
19
+ if (!line.startsWith('|'))
20
+ continue;
21
+ if (/^\|\s*-+\s*\|/.test(line))
22
+ continue;
23
+ if (/^\|\s*#\s*\|/.test(line))
24
+ continue;
25
+ const cells = line.split('|').slice(1, -1).map((c) => c.trim());
26
+ if (cells.length < 5)
27
+ continue;
28
+ const numStr = cells[0];
29
+ if (!/^\d+$/.test(numStr))
30
+ continue;
31
+ rows.push({
32
+ num: Number(numStr),
33
+ name: cells[1],
34
+ type: cells[2],
35
+ package: cells[4],
36
+ });
37
+ }
38
+ return rows;
39
+ }
40
+ function parseDependencySequence(section, nameToId) {
41
+ const out = new Map();
42
+ if (!section)
43
+ return out;
44
+ // Tolerate em-dash (—), en-dash (–), double hyphen (--), or single hyphen (-).
45
+ const itemRe = /^\d+\.\s+([A-Za-z0-9_]+)\s*(?:\([^)]*\))?\s*(?:[—–]|--?)\s*(.+)$/gm;
46
+ let m;
47
+ while ((m = itemRe.exec(section)) !== null) {
48
+ const name = m[1];
49
+ const tail = m[2];
50
+ const deps = [];
51
+ if (!/no dependencies/i.test(tail)) {
52
+ const depMatch = /depends on\s+(.+)$/i.exec(tail);
53
+ if (depMatch) {
54
+ const depList = depMatch[1].split(/\s*,\s*(?:and\s+)?|\s+and\s+/).map((s) => s.trim()).filter(Boolean);
55
+ for (const depName of depList) {
56
+ const clean = depName.replace(/\s*\([^)]*\)\s*$/, '').replace(/[.,;]+$/, '').trim();
57
+ const depId = nameToId.get(clean);
58
+ if (depId)
59
+ deps.push(depId);
60
+ }
61
+ }
62
+ }
63
+ out.set(name, deps);
64
+ }
65
+ return out;
66
+ }
67
+ export function extractDesign(markdown) {
68
+ const titleMatch = TITLE_RE.exec(markdown);
69
+ if (!titleMatch) {
70
+ throw new Error('Input is not a design output (no "## ABAP Design — ..." heading found)');
71
+ }
72
+ const title = titleMatch[1].trim();
73
+ const objectsSection = sliceSection(markdown, OBJECTS_HEADING_RE);
74
+ if (!objectsSection) {
75
+ throw new Error('No "### Objects to Create" section found');
76
+ }
77
+ const rows = parseObjectsTable(objectsSection);
78
+ if (rows.length === 0) {
79
+ throw new Error('Objects to Create table is empty');
80
+ }
81
+ // Assign sequential ids obj-001..obj-N preserving table order
82
+ const nameToId = new Map();
83
+ const idForRow = rows.map((r, i) => {
84
+ const id = `obj-${String(i + 1).padStart(3, '0')}`;
85
+ nameToId.set(r.name, id);
86
+ return id;
87
+ });
88
+ const depsByName = parseDependencySequence(sliceSection(markdown, DEPENDENCY_HEADING_RE) ?? '', nameToId);
89
+ const content = {
90
+ objects: rows.map((r, i) => ({
91
+ id: idForRow[i],
92
+ name: r.name,
93
+ type: r.type,
94
+ package: r.package,
95
+ dependsOn: depsByName.get(r.name) ?? [],
96
+ })),
97
+ };
98
+ const items = content.objects.map((o) => ({
99
+ id: o.id,
100
+ status: 'keep',
101
+ answer: null,
102
+ answeredBy: null,
103
+ answeredAt: null,
104
+ comments: [],
105
+ }));
106
+ return { title, content, items };
107
+ }
@@ -0,0 +1,93 @@
1
+ const TITLE_RE = /^## Effort Estimate — (.+)$/m;
2
+ const COMPONENTS_HEADING_RE = /^### Component Breakdown\s*$/m;
3
+ const NEXT_HEADING_RE = /^###\s/m;
4
+ function sliceSection(markdown, headingRe) {
5
+ const m = headingRe.exec(markdown);
6
+ if (!m)
7
+ return null;
8
+ const start = m.index + m[0].length;
9
+ const rest = markdown.slice(start);
10
+ const next = NEXT_HEADING_RE.exec(rest);
11
+ return next ? rest.slice(0, next.index) : rest;
12
+ }
13
+ // Parse a numeric cell, tolerating "**44**" / "5" / "1.5" / "0.5".
14
+ function parseHours(cell) {
15
+ const cleaned = cell.replace(/\*+/g, '').trim();
16
+ if (!/^-?\d+(\.\d+)?$/.test(cleaned))
17
+ return null;
18
+ return Number(cleaned);
19
+ }
20
+ function parseComponentsTable(section) {
21
+ const lines = section.split(/\r?\n/);
22
+ const rows = [];
23
+ for (const raw of lines) {
24
+ const line = raw.trim();
25
+ if (!line.startsWith('|'))
26
+ continue;
27
+ if (/^\|\s*-+\s*\|/.test(line))
28
+ continue;
29
+ if (/^\|\s*#\s*\|/.test(line))
30
+ continue;
31
+ const cells = line.split('|').slice(1, -1).map((c) => c.trim());
32
+ // Expected cell count: 7 (#, Component, Objects, Opt, Realistic, Pess, Notes)
33
+ if (cells.length < 7)
34
+ continue;
35
+ const numStr = cells[0];
36
+ if (!/^\d+$/.test(numStr))
37
+ continue; // skips "TOTAL" row (empty # cell)
38
+ // Belt-and-braces: also skip rows whose Component cell looks like a TOTAL
39
+ // header even if it somehow has a numeric # column (e.g. malformed input).
40
+ if (/^\*?\*?\s*total/i.test(cells[1]))
41
+ continue;
42
+ const hOpt = parseHours(cells[3]);
43
+ const hReal = parseHours(cells[4]);
44
+ const hPess = parseHours(cells[5]);
45
+ if (hOpt === null || hReal === null || hPess === null)
46
+ continue;
47
+ const notes = cells[6].trim();
48
+ const risks = notes ? notes.split(/;\s*/).map((s) => s.trim()).filter(Boolean) : [];
49
+ rows.push({
50
+ num: Number(numStr),
51
+ name: cells[1],
52
+ hoursOptimistic: hOpt,
53
+ hoursRealistic: hReal,
54
+ hoursPessimistic: hPess,
55
+ risks,
56
+ });
57
+ }
58
+ return rows;
59
+ }
60
+ export function extractEstimate(markdown) {
61
+ const titleMatch = TITLE_RE.exec(markdown);
62
+ if (!titleMatch) {
63
+ throw new Error('Input is not an estimate output (no "## Effort Estimate — ..." heading found)');
64
+ }
65
+ const title = titleMatch[1].trim();
66
+ const section = sliceSection(markdown, COMPONENTS_HEADING_RE);
67
+ if (!section) {
68
+ throw new Error('No "### Component Breakdown" section found');
69
+ }
70
+ const rows = parseComponentsTable(section);
71
+ if (rows.length === 0) {
72
+ throw new Error('Component Breakdown table has no data rows');
73
+ }
74
+ const content = {
75
+ components: rows.map((r, i) => ({
76
+ id: `cmp-${String(i + 1).padStart(3, '0')}`,
77
+ name: r.name,
78
+ hoursOptimistic: r.hoursOptimistic,
79
+ hoursRealistic: r.hoursRealistic,
80
+ hoursPessimistic: r.hoursPessimistic,
81
+ risks: r.risks,
82
+ })),
83
+ };
84
+ const items = content.components.map((c) => ({
85
+ id: c.id,
86
+ status: 'confirmed',
87
+ answer: null,
88
+ answeredBy: null,
89
+ answeredAt: null,
90
+ comments: [],
91
+ }));
92
+ return { title, content, items };
93
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Parser for the `<!-- csforge:modernize-manifest ... -->` block emitted
3
+ * by /abap-modernize at the end of a run. v0.7 — one of the trio of
4
+ * downstream artefacts a cca-assessment fans out into.
5
+ *
6
+ * Format mirrors extract-upgrade.ts / extract-cca.ts. The grammar is
7
+ * key:value pairs + optional indented `|`-separated tables under a
8
+ * trailing-colon header. Hidden in human renderers (HTML comment),
9
+ * structured enough for a deterministic extractor.
10
+ *
11
+ * <!-- csforge:modernize-manifest
12
+ * artefact: modernize-result
13
+ * title: ZDPR_PAYMENTS — modernization
14
+ * detail_path: .abapforge/modernize/zdpr-payments/project.json
15
+ * baseline_path: <project-relative path>
16
+ * baseline_sha256: <hex>
17
+ * transport: <id or ->
18
+ * applied: 1
19
+ * skipped: 0
20
+ * failed: 0
21
+ * pending: 0
22
+ * modernizations:
23
+ * item-001 | ZBDC_VENDOR_PAYMENTS_CLOUD | FUNC | bdc-against-f53 | fm-based-posting | applied
24
+ * item-002 | ZPAYMENTS | PROG | select-from-bsig | cds-i_journalentryitem | pending
25
+ * -->
26
+ *
27
+ * Parsing is permissive — missing optional fields produce defaults; a
28
+ * missing manifest block throws so callers know to surface the error.
29
+ */
30
+ const MANIFEST_RE = /<!--\s*csforge:modernize-manifest\s*\n([\s\S]*?)\n\s*-->/;
31
+ function parseManifestBlock(markdown) {
32
+ const m = MANIFEST_RE.exec(markdown);
33
+ if (!m) {
34
+ throw new Error('No <!-- csforge:modernize-manifest --> block found in skill output');
35
+ }
36
+ const body = m[1] ?? '';
37
+ const kv = {};
38
+ const tables = {};
39
+ let currentTable = null;
40
+ for (const rawLine of body.split('\n')) {
41
+ const line = rawLine.replace(/\r$/, '');
42
+ if (line.trim().length === 0) {
43
+ currentTable = null;
44
+ continue;
45
+ }
46
+ if (currentTable !== null && /^\s+/.test(line)) {
47
+ const cells = line.trim().split('|').map((c) => c.trim());
48
+ if (cells.length > 0) {
49
+ if (!tables[currentTable])
50
+ tables[currentTable] = [];
51
+ tables[currentTable].push(cells);
52
+ }
53
+ continue;
54
+ }
55
+ const colon = line.indexOf(':');
56
+ if (colon === -1)
57
+ continue;
58
+ const key = line.slice(0, colon).trim();
59
+ const value = line.slice(colon + 1).trim();
60
+ if (value.length === 0) {
61
+ currentTable = key;
62
+ tables[key] = [];
63
+ }
64
+ else {
65
+ kv[key] = value;
66
+ currentTable = null;
67
+ }
68
+ }
69
+ return { kv, tables };
70
+ }
71
+ function intOr(kv, key, fallback) {
72
+ const v = kv[key];
73
+ if (v === undefined)
74
+ return fallback;
75
+ const n = Number.parseInt(v, 10);
76
+ return Number.isFinite(n) ? n : fallback;
77
+ }
78
+ const ALLOWED_STATUSES = [
79
+ 'applied', 'skipped', 'failed', 'pending',
80
+ ];
81
+ function asStatus(raw) {
82
+ const v = raw.toLowerCase();
83
+ return ALLOWED_STATUSES.includes(v)
84
+ ? v
85
+ : 'pending';
86
+ }
87
+ export function extractModernizeResult(markdown) {
88
+ const { kv, tables } = parseManifestBlock(markdown);
89
+ if (kv.artefact !== 'modernize-result') {
90
+ throw new Error(`Manifest artefact mismatch: expected 'modernize-result', got '${kv.artefact}'`);
91
+ }
92
+ if (!kv.detail_path) {
93
+ throw new Error("modernize-result manifest missing required 'detail_path'");
94
+ }
95
+ if (!kv.baseline_path) {
96
+ throw new Error("modernize-result manifest missing required 'baseline_path'");
97
+ }
98
+ const modernizations = (tables.modernizations ?? []).map((row, i) => ({
99
+ id: row[0] ?? `item-${String(i + 1).padStart(3, '0')}`,
100
+ objectName: row[1] ?? '',
101
+ objectType: row[2] ?? '',
102
+ fromArchitecture: row[3] ?? '',
103
+ toArchitecture: row[4] ?? '',
104
+ status: asStatus(row[5] ?? 'pending'),
105
+ }));
106
+ const content = {
107
+ detailPath: kv.detail_path,
108
+ baselineRef: {
109
+ path: kv.baseline_path,
110
+ sha256: kv.baseline_sha256 ?? '',
111
+ },
112
+ transport: kv.transport && kv.transport !== '-' ? kv.transport : null,
113
+ summary: {
114
+ applied: intOr(kv, 'applied', 0),
115
+ skipped: intOr(kv, 'skipped', 0),
116
+ failed: intOr(kv, 'failed', 0),
117
+ pending: intOr(kv, 'pending', 0),
118
+ },
119
+ modernizations,
120
+ };
121
+ const items = modernizations.map((m) => ({
122
+ id: m.id,
123
+ status: m.status,
124
+ answer: null,
125
+ answeredBy: null,
126
+ answeredAt: null,
127
+ comments: [],
128
+ }));
129
+ return { title: kv.title ?? 'Modernization Result', content, items };
130
+ }
@@ -0,0 +1,101 @@
1
+ const TITLE_RE = /^## Spec Gap Analysis — (.+)$/m;
2
+ const DOMAIN_RE = /^\*\*Inferred Domain:\*\*\s*(.+?)(?:\s*\[INFERRED.*?\])?$/m;
3
+ const INPUT_TYPE_RE = /^\*\*Input Type:\*\*\s*(.+)$/m;
4
+ const SECTION_HEADING_RE = /^### (.+)$/gm;
5
+ // Real-world model output shows two formatting variants:
6
+ // ' 1. [BLOCKER] Text' — leading indent (4 spaces or tab), plain severity
7
+ // '1. **[BLOCKER]** Text' — no indent, severity wrapped in markdown bold
8
+ // Tolerate both: optional leading whitespace + optional `**` around the severity bracket.
9
+ const GAP_LINE_RE = /^\s*(\d+)\.\s*(?:\*\*)?\[(BLOCKER|IMPORTANT|NICE TO KNOW)\](?:\*\*)?\s*(.+)$/m;
10
+ const AUDIENCE_BY_SECTION_NAME = {
11
+ 'missing business questions': 'business',
12
+ 'missing technical questions': 'technical',
13
+ 'data assumptions to confirm': 'data',
14
+ 'authorization questions': 'auth',
15
+ 'authorisation questions': 'auth',
16
+ 'edge cases not covered': 'business',
17
+ 'transport and release concerns': 'transport',
18
+ 'clean core / api concerns': 'clean-core',
19
+ };
20
+ function audienceFor(sectionName) {
21
+ const key = sectionName.toLowerCase().trim();
22
+ for (const [k, v] of Object.entries(AUDIENCE_BY_SECTION_NAME)) {
23
+ if (key.includes(k))
24
+ return v;
25
+ }
26
+ return 'unknown';
27
+ }
28
+ const KNOWN_GAP_SECTIONS = [
29
+ 'Missing Business Questions',
30
+ 'Missing Technical Questions',
31
+ 'Data Assumptions to Confirm',
32
+ 'Authorization Questions',
33
+ 'Authorisation Questions', // British spelling — both OK
34
+ 'Edge Cases Not Covered',
35
+ 'Transport and Release Concerns',
36
+ 'Clean Core / API Concerns',
37
+ ];
38
+ const SKIP_SECTIONS = [
39
+ 'Suggested Test Scenarios', // is a table, not a gap list
40
+ 'Recommended Next Steps', // is a paragraph
41
+ ];
42
+ export function extractSpecGap(markdown) {
43
+ const titleMatch = TITLE_RE.exec(markdown);
44
+ if (!titleMatch) {
45
+ throw new Error('Input is not a spec-gap output (no "## Spec Gap Analysis — ..." heading found)');
46
+ }
47
+ const title = titleMatch[1].trim();
48
+ const inputType = (INPUT_TYPE_RE.exec(markdown)?.[1] ?? 'Unknown').trim();
49
+ const inferredDomain = (DOMAIN_RE.exec(markdown)?.[1] ?? 'Unknown').trim();
50
+ const sectionStarts = [];
51
+ let m;
52
+ const reCopy = new RegExp(SECTION_HEADING_RE.source, SECTION_HEADING_RE.flags);
53
+ while ((m = reCopy.exec(markdown)) !== null) {
54
+ sectionStarts.push({ name: m[1].trim(), index: m.index });
55
+ }
56
+ if (sectionStarts.length === 0) {
57
+ throw new Error('Input is not a spec-gap output (no "### " sections found)');
58
+ }
59
+ const sections = [];
60
+ const items = [];
61
+ let gapCounter = 1;
62
+ for (let i = 0; i < sectionStarts.length; i++) {
63
+ const cur = sectionStarts[i];
64
+ const next = sectionStarts[i + 1];
65
+ if (SKIP_SECTIONS.some((s) => cur.name.toLowerCase().includes(s.toLowerCase())))
66
+ continue;
67
+ if (!KNOWN_GAP_SECTIONS.some((s) => cur.name.toLowerCase().includes(s.toLowerCase())))
68
+ continue;
69
+ const body = markdown.slice(cur.index, next ? next.index : markdown.length);
70
+ const gaps = parseGapsFromSection(body, () => `gap-${String(gapCounter++).padStart(3, '0')}`);
71
+ if (gaps.length > 0) {
72
+ sections.push({ name: cur.name, audience: audienceFor(cur.name), gaps });
73
+ for (const g of gaps) {
74
+ items.push({
75
+ id: g.id,
76
+ status: 'open',
77
+ answer: null,
78
+ answeredBy: null,
79
+ answeredAt: null,
80
+ comments: [],
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ title,
87
+ content: { inputType, inferredDomain, sections },
88
+ items,
89
+ };
90
+ }
91
+ function parseGapsFromSection(sectionBody, nextId) {
92
+ const out = [];
93
+ const re = new RegExp(GAP_LINE_RE.source, 'gm');
94
+ let m;
95
+ while ((m = re.exec(sectionBody)) !== null) {
96
+ const severity = m[2];
97
+ const text = m[3].trim();
98
+ out.push({ id: nextId(), severity, text });
99
+ }
100
+ return out;
101
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Parser for the `<!-- csforge:test-coverage-manifest ... -->` block
3
+ * emitted by /abap-test at the end of a run. v0.7 — one of the trio of
4
+ * downstream artefacts a cca-assessment fans out into.
5
+ *
6
+ * coverage_pct_before / coverage_pct_after are optional — SAP's coverage
7
+ * tool (SCMP) isn't always available and the skill works without it.
8
+ * Empty / "-" values parse to null.
9
+ *
10
+ * <!-- csforge:test-coverage-manifest
11
+ * artefact: test-coverage
12
+ * title: ZDPR_PAYMENTS — test coverage
13
+ * detail_path: .abapforge/tests/zdpr-payments/project.json
14
+ * baseline_path: <project-relative path>
15
+ * baseline_sha256: <hex>
16
+ * transport: <id or ->
17
+ * test_classes_generated: 14
18
+ * methods_added: 87
19
+ * coverage_pct_before: 12
20
+ * coverage_pct_after: 78
21
+ * skipped: 0
22
+ * failed: 0
23
+ * tests:
24
+ * item-001 | ZPAYMENTS | PROG | ZPAYMENTS_TEST | 6 | generated
25
+ * item-002 | ZPAYMENTS_OLD | PROG | ZPAYMENTS_OLD_TEST | 4 | generated
26
+ * -->
27
+ */
28
+ const MANIFEST_RE = /<!--\s*csforge:test-coverage-manifest\s*\n([\s\S]*?)\n\s*-->/;
29
+ function parseManifestBlock(markdown) {
30
+ const m = MANIFEST_RE.exec(markdown);
31
+ if (!m) {
32
+ throw new Error('No <!-- csforge:test-coverage-manifest --> block found in skill output');
33
+ }
34
+ const body = m[1] ?? '';
35
+ const kv = {};
36
+ const tables = {};
37
+ let currentTable = null;
38
+ for (const rawLine of body.split('\n')) {
39
+ const line = rawLine.replace(/\r$/, '');
40
+ if (line.trim().length === 0) {
41
+ currentTable = null;
42
+ continue;
43
+ }
44
+ if (currentTable !== null && /^\s+/.test(line)) {
45
+ const cells = line.trim().split('|').map((c) => c.trim());
46
+ if (cells.length > 0) {
47
+ if (!tables[currentTable])
48
+ tables[currentTable] = [];
49
+ tables[currentTable].push(cells);
50
+ }
51
+ continue;
52
+ }
53
+ const colon = line.indexOf(':');
54
+ if (colon === -1)
55
+ continue;
56
+ const key = line.slice(0, colon).trim();
57
+ const value = line.slice(colon + 1).trim();
58
+ if (value.length === 0) {
59
+ currentTable = key;
60
+ tables[key] = [];
61
+ }
62
+ else {
63
+ kv[key] = value;
64
+ currentTable = null;
65
+ }
66
+ }
67
+ return { kv, tables };
68
+ }
69
+ function intOr(kv, key, fallback) {
70
+ const v = kv[key];
71
+ if (v === undefined)
72
+ return fallback;
73
+ const n = Number.parseInt(v, 10);
74
+ return Number.isFinite(n) ? n : fallback;
75
+ }
76
+ function intOrNull(kv, key) {
77
+ const v = kv[key];
78
+ if (v === undefined || v === '' || v === '-')
79
+ return null;
80
+ const n = Number.parseInt(v, 10);
81
+ return Number.isFinite(n) ? n : null;
82
+ }
83
+ const ALLOWED_STATUSES = [
84
+ 'generated', 'skipped', 'failed', 'pending',
85
+ ];
86
+ function asStatus(raw) {
87
+ const v = raw.toLowerCase();
88
+ return ALLOWED_STATUSES.includes(v)
89
+ ? v
90
+ : 'pending';
91
+ }
92
+ export function extractTestCoverage(markdown) {
93
+ const { kv, tables } = parseManifestBlock(markdown);
94
+ if (kv.artefact !== 'test-coverage') {
95
+ throw new Error(`Manifest artefact mismatch: expected 'test-coverage', got '${kv.artefact}'`);
96
+ }
97
+ if (!kv.detail_path) {
98
+ throw new Error("test-coverage manifest missing required 'detail_path'");
99
+ }
100
+ if (!kv.baseline_path) {
101
+ throw new Error("test-coverage manifest missing required 'baseline_path'");
102
+ }
103
+ const tests = (tables.tests ?? []).map((row, i) => ({
104
+ id: row[0] ?? `item-${String(i + 1).padStart(3, '0')}`,
105
+ targetObjectName: row[1] ?? '',
106
+ targetObjectType: row[2] ?? '',
107
+ testClassName: row[3] ?? '',
108
+ methodCount: Number.parseInt(row[4] ?? '0', 10) || 0,
109
+ status: asStatus(row[5] ?? 'pending'),
110
+ }));
111
+ const content = {
112
+ detailPath: kv.detail_path,
113
+ baselineRef: {
114
+ path: kv.baseline_path,
115
+ sha256: kv.baseline_sha256 ?? '',
116
+ },
117
+ transport: kv.transport && kv.transport !== '-' ? kv.transport : null,
118
+ summary: {
119
+ testClassesGenerated: intOr(kv, 'test_classes_generated', tests.filter((t) => t.status === 'generated').length),
120
+ methodsAdded: intOr(kv, 'methods_added', 0),
121
+ coveragePctBefore: intOrNull(kv, 'coverage_pct_before'),
122
+ coveragePctAfter: intOrNull(kv, 'coverage_pct_after'),
123
+ skipped: intOr(kv, 'skipped', 0),
124
+ failed: intOr(kv, 'failed', 0),
125
+ },
126
+ tests,
127
+ };
128
+ const items = tests.map((t) => ({
129
+ id: t.id,
130
+ status: t.status,
131
+ answer: null,
132
+ answeredBy: null,
133
+ answeredAt: null,
134
+ comments: [],
135
+ }));
136
+ return { title: kv.title ?? 'Test Coverage Report', content, items };
137
+ }