@cspeach/cli 0.9.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.
- package/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +209 -20
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/commands/config-set.js +95 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +148 -21
- package/dist/config/loader.js +95 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/one-shot.js +52 -4
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +130 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +1 -1
- package/dist/projects/extract-upgrade.js +113 -22
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +159 -25
- package/dist/projects/plan-schema.js +63 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +247 -20
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/tool-widget.js +18 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/builtin-commands.js +37 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +2 -2
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +301 -35
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/pending.js +27 -0
- package/dist/skill-catalog.js +48 -43
- package/dist/skills/bundled-skills.js +279 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +64 -21
- package/dist/tools/ask-question.js +96 -4
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +810 -0
- package/dist/tools/extend-model/tool.js +188 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-scaffold.js +45 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/tools.js +356 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +76 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +53 -9
- package/dist/tools/sap-write.js +530 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +37 -14
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +6 -2
- package/dist/ui/body.js +13 -0
- package/dist/ui/footer.js +20 -6
- package/dist/ui/line-resolution.js +17 -6
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/widgets/ask-question-modal.js +4 -1
- package/package.json +19 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
// cspeach-cli/src/projects/plan-run.ts
|
|
2
|
+
//
|
|
3
|
+
// Pure plan-execution helpers for /abap-plan --resume (B3, 2026-06-06).
|
|
4
|
+
// No I/O here — file reads, prompts, and the runTurn call live in
|
|
5
|
+
// commands/plan-resume.ts. Keeping these pure makes the next-phase
|
|
6
|
+
// picker, the tracker renderer, and the revision builder unit-testable
|
|
7
|
+
// without touching disk.
|
|
8
|
+
import chalk from 'chalk';
|
|
1
9
|
/** Statuses a dead session may have left behind — resumable, picked before fresh `todo`s. */
|
|
2
10
|
const IN_PROGRESS = ['designing', 'building', 'verifying'];
|
|
11
|
+
/**
|
|
12
|
+
* C1 (2026-06-11, D30 waiver) — a phase counts as DONE for DAG purposes when
|
|
13
|
+
* it is 'validated' OR 'validated-with-waiver' (exit gate not met, but the
|
|
14
|
+
* user explicitly waived it). Single predicate so eligibility, summary
|
|
15
|
+
* counting, and the "plan complete" checks can never drift apart.
|
|
16
|
+
*/
|
|
17
|
+
export function isPhaseSatisfied(s) {
|
|
18
|
+
return s === 'validated' || s === 'validated-with-waiver';
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* Phase status lives in interaction.items (store convention). Build the
|
|
5
22
|
* id→status map; a phase with no matching item defaults to 'todo' rather
|
|
@@ -23,11 +40,11 @@ export function statusesFromItems(items, phases) {
|
|
|
23
40
|
export function computeNextPhase(phases, statuses) {
|
|
24
41
|
for (const p of phases) {
|
|
25
42
|
const s = statuses[p.id] ?? 'todo';
|
|
26
|
-
if (s
|
|
43
|
+
if (isPhaseSatisfied(s) || s === 'blocked')
|
|
27
44
|
continue;
|
|
28
45
|
if (s !== 'todo' && !IN_PROGRESS.includes(s))
|
|
29
46
|
continue;
|
|
30
|
-
if (p.entryCriteria.every((dep) => statuses[dep]
|
|
47
|
+
if (p.entryCriteria.every((dep) => isPhaseSatisfied(statuses[dep])))
|
|
31
48
|
return p;
|
|
32
49
|
}
|
|
33
50
|
return null;
|
|
@@ -42,41 +59,158 @@ export function computeNextPhase(phases, statuses) {
|
|
|
42
59
|
* ▶ c1.orchestration abap-generate ← THIS SESSION
|
|
43
60
|
* ○ c1.integration abap-generate (needs c1.orchestration)
|
|
44
61
|
*/
|
|
62
|
+
// Human phase labels — a phase is named for the KIND of work it does, so a
|
|
63
|
+
// non-ABAP reader can follow the plan. The exit-gate / produced objects carry
|
|
64
|
+
// the specifics on the detail line.
|
|
65
|
+
const LAYER_LABELS = {
|
|
66
|
+
'types': 'Types',
|
|
67
|
+
'persistence': 'Tables & structures',
|
|
68
|
+
'data-model': 'CDS views',
|
|
69
|
+
'behavior': 'Behaviour & validations',
|
|
70
|
+
'service': 'Service binding',
|
|
71
|
+
'orchestration': 'Jobs & reports',
|
|
72
|
+
'integration': 'Integration',
|
|
73
|
+
'ui': 'UI',
|
|
74
|
+
};
|
|
75
|
+
const DELEGATE_LABELS = {
|
|
76
|
+
'abap-design': 'Design',
|
|
77
|
+
'abap-test': 'Tests',
|
|
78
|
+
'abap-eml': 'Behaviour logic',
|
|
79
|
+
'abap-segw': 'OData (SEGW)',
|
|
80
|
+
};
|
|
81
|
+
function phaseTitle(p) {
|
|
82
|
+
// C2 (2026-06-11, D29): an explicit seed-time title wins — the seed prose
|
|
83
|
+
// instructs short human names ("Design", "Tables", "Service"). The label
|
|
84
|
+
// maps remain the fallback for envelopes seeded before titles existed.
|
|
85
|
+
const explicit = p.title?.trim();
|
|
86
|
+
if (explicit)
|
|
87
|
+
return explicit;
|
|
88
|
+
return DELEGATE_LABELS[p.delegateTo] ?? LAYER_LABELS[p.layer] ?? tidyId(p.id);
|
|
89
|
+
}
|
|
90
|
+
function tidyId(id) {
|
|
91
|
+
const tail = id.split('.').pop() ?? id;
|
|
92
|
+
return tail.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
93
|
+
}
|
|
94
|
+
// A short plain-language hint from the exit gate: the first clause, clamped.
|
|
95
|
+
function shortGate(gate, n = 60) {
|
|
96
|
+
const first = String(gate ?? '').split(/[.;]/)[0].trim();
|
|
97
|
+
const s = first.length >= 10 ? first : String(gate ?? '').trim();
|
|
98
|
+
return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s;
|
|
99
|
+
}
|
|
100
|
+
// The detail line: what a done phase produced, else what the phase is about.
|
|
101
|
+
function phaseDetail(p, s) {
|
|
102
|
+
if (isPhaseSatisfied(s)) {
|
|
103
|
+
const objs = (p.work?.generated ?? []).join(', ');
|
|
104
|
+
return objs || shortGate(p.exitGate);
|
|
105
|
+
}
|
|
106
|
+
return shortGate(p.exitGate);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Plan-progress board — a Claude-Code-style checkbox task list. Each phase is
|
|
110
|
+
* named for the work it does (not its mechanical id), so the reader can follow
|
|
111
|
+
* along; ☒ = done (dim), ☐ = to do, the active phase is bold with a `◀ now`
|
|
112
|
+
* marker, blocked is red. Flows through chunkEmitter (Ink) and console.log
|
|
113
|
+
* (classic) alike — chalk degrades to plain text on a non-colour stream.
|
|
114
|
+
*/
|
|
45
115
|
export function renderPlanTracker(a) {
|
|
116
|
+
const PEACH = '#F5A623';
|
|
46
117
|
const lines = [];
|
|
47
118
|
const total = a.content.phases.length;
|
|
48
|
-
|
|
119
|
+
// Waived phases count as done in the header tally — the DAG treats them as
|
|
120
|
+
// satisfied; the per-line "✓* (waived)" marker carries the distinction.
|
|
121
|
+
const validated = a.content.phases.filter((p) => isPhaseSatisfied(a.statuses[p.id])).length;
|
|
49
122
|
if (a.includeHeader !== false) {
|
|
50
|
-
lines.push(`─
|
|
123
|
+
lines.push(chalk.dim(`─ ${a.title} (v${a.version}) · ${validated}/${total} done ─`));
|
|
124
|
+
lines.push('');
|
|
51
125
|
}
|
|
52
|
-
const idWidth = Math.max(...a.content.phases.map((p) => p.id.length));
|
|
53
|
-
const skillWidth = Math.max(...a.content.phases.map((p) => p.delegateTo.length));
|
|
54
126
|
for (const p of a.content.phases) {
|
|
55
127
|
const s = a.statuses[p.id] ?? 'todo';
|
|
56
128
|
const isCurrent = a.currentId != null && p.id === a.currentId;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
129
|
+
const title = phaseTitle(p);
|
|
130
|
+
const detail = phaseDetail(p, s);
|
|
131
|
+
const dash = detail ? ` — ${detail}` : '';
|
|
132
|
+
if (s === 'validated') {
|
|
133
|
+
lines.push(chalk.dim(` ☒ ${title}${dash}`));
|
|
134
|
+
}
|
|
135
|
+
else if (s === 'validated-with-waiver') {
|
|
136
|
+
// C1 (D30 waiver) — done-but-waived: the exit gate was never met; the
|
|
137
|
+
// user accepted it anyway. Distinct marker so a tracker reader can tell
|
|
138
|
+
// a verified phase from a waived one at a glance.
|
|
139
|
+
lines.push(chalk.dim(` ☒ ${title}${dash}`) + ` ${chalk.yellow('✓* (waived)')}`);
|
|
140
|
+
}
|
|
141
|
+
else if (s === 'blocked') {
|
|
142
|
+
lines.push(` ${chalk.red('☐')} ${chalk.red(title)} ${chalk.red('— blocked')}${detail ? chalk.dim(` · ${detail}`) : ''}`);
|
|
143
|
+
}
|
|
144
|
+
else if (isCurrent) {
|
|
145
|
+
lines.push(` ${chalk.hex(PEACH)('☐')} ${chalk.bold(title)}${dash} ${chalk.hex(PEACH).bold('◀ now')}`);
|
|
146
|
+
}
|
|
147
|
+
else if (IN_PROGRESS.includes(s)) {
|
|
148
|
+
lines.push(` ${chalk.hex(PEACH)('☐')} ${chalk.bold(title)}${dash} ${chalk.dim(`(${s})`)}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
lines.push(` ☐ ${title}${chalk.dim(dash)}`);
|
|
152
|
+
}
|
|
63
153
|
}
|
|
64
154
|
return lines.join('\n');
|
|
65
155
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
156
|
+
/* ── plan-complete chaining (C2, 2026-06-11, D24) ───────────────────────── */
|
|
157
|
+
/**
|
|
158
|
+
* SRVB-shaped object name. Two naming families seen live + documented in
|
|
159
|
+
* .claude/rules/rap-patterns.md:
|
|
160
|
+
* - ADT default OData suffix: ZUI_PM_MAINTREQ_O4 / ..._O2
|
|
161
|
+
* - rap-patterns convention: ZSalesOrder_UI_V4 / _UI_V2 / _API_V4 / _API_V2
|
|
162
|
+
*/
|
|
163
|
+
const SRVB_NAME_RE = /^[zy]\w*(?:_o[24]|_(?:ui|api)_v[24])$/i;
|
|
164
|
+
/**
|
|
165
|
+
* The exact service-binding name a completed plan produced, for the
|
|
166
|
+
* /abap-fiori-build hand-off. Resolution ladder (newest phase wins at
|
|
167
|
+
* each rung — a structurally-revised plan may carry several):
|
|
168
|
+
* 1. explicit `work.binding` (the seed/resume prose instructs the service
|
|
169
|
+
* phase to record it)
|
|
170
|
+
* 2. an SRVB-shaped name in a service-layer phase's `work.generated`
|
|
171
|
+
* 3. an SRVB-shaped name anywhere in `work.generated` (binding built
|
|
172
|
+
* outside a service-layer phase)
|
|
173
|
+
* Returns null when nothing matches — the caller still mentions
|
|
174
|
+
* /abap-fiori-build generically.
|
|
175
|
+
*/
|
|
176
|
+
export function findServiceBinding(phases) {
|
|
177
|
+
for (let i = phases.length - 1; i >= 0; i--) {
|
|
178
|
+
const explicit = phases[i].work?.binding?.trim();
|
|
179
|
+
if (explicit)
|
|
180
|
+
return explicit;
|
|
181
|
+
}
|
|
182
|
+
const scan = (ps) => {
|
|
183
|
+
for (let i = ps.length - 1; i >= 0; i--) {
|
|
184
|
+
const hit = (ps[i].work?.generated ?? []).find((n) => SRVB_NAME_RE.test(n.trim()));
|
|
185
|
+
if (hit)
|
|
186
|
+
return hit.trim();
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
return scan(phases.filter((p) => p.layer === 'service')) ?? scan(phases);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The lines printed when a plan has no next phase because EVERY phase is
|
|
194
|
+
* satisfied. Single construction site for both call points (preparePlanResume
|
|
195
|
+
* re-opening a finished plan, finishPlanResume completing the last phase) so
|
|
196
|
+
* the D24 fiori chain can never drift between them.
|
|
197
|
+
*
|
|
198
|
+
* A UI-less backend stack (service layer present, no ui phase) chains to
|
|
199
|
+
* /abap-fiori-build — with the EXACT binding name when resolvable. A plan
|
|
200
|
+
* that already built its UI, or never built a service, gets no chain line.
|
|
201
|
+
*/
|
|
202
|
+
export function planCompletionLines(phases) {
|
|
203
|
+
const lines = ['Plan complete — every phase validated.'];
|
|
204
|
+
const hasUi = phases.some((p) => p.layer === 'ui' || p.delegateTo === 'abap-fiori-build');
|
|
205
|
+
const hasService = phases.some((p) => p.layer === 'service');
|
|
206
|
+
if (!hasUi && hasService) {
|
|
207
|
+
const binding = findServiceBinding(phases);
|
|
208
|
+
lines.push(binding
|
|
209
|
+
? `Backend complete. Next: /abap-fiori-build — point it at ${binding}.`
|
|
210
|
+
: 'Backend complete. Next: /abap-fiori-build — point it at the published service binding.');
|
|
73
211
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (IN_PROGRESS.includes(s))
|
|
77
|
-
return `in progress (${s})`;
|
|
78
|
-
const unmet = p.entryCriteria.filter((dep) => statuses[dep] !== 'validated');
|
|
79
|
-
return unmet.length > 0 ? `(needs ${unmet.join(', ')})` : '';
|
|
212
|
+
lines.push('Consider /abap-preflight on the produced transport(s) before release.');
|
|
213
|
+
return lines;
|
|
80
214
|
}
|
|
81
215
|
/**
|
|
82
216
|
* Build the version-N+1 envelope from the original plan envelope and the
|
|
@@ -28,11 +28,17 @@ export const PLAN_LAYERS = [
|
|
|
28
28
|
*/
|
|
29
29
|
export const PLAN_DELEGATE_SKILLS = [
|
|
30
30
|
'abap-design', 'abap-data-model', 'abap-rap', 'abap-eml',
|
|
31
|
-
'abap-generate', 'abap-segw', 'abap-test',
|
|
31
|
+
'abap-generate', 'abap-segw', 'abap-test', 'abap-fiori-build',
|
|
32
|
+
'abap-extend-model',
|
|
32
33
|
];
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Keep in sync with ItemStatusByType['plan'] in types.ts.
|
|
36
|
+
* 'validated-with-waiver' (C1, 2026-06-11): the exit gate was NOT met but the
|
|
37
|
+
* user explicitly waived it — DAG eligibility treats it like 'validated'
|
|
38
|
+
* (see isPhaseSatisfied in plan-run.ts).
|
|
39
|
+
*/
|
|
34
40
|
export const PLAN_PHASE_STATUSES = [
|
|
35
|
-
'todo', 'designing', 'building', 'verifying', 'validated', 'blocked',
|
|
41
|
+
'todo', 'designing', 'building', 'verifying', 'validated', 'validated-with-waiver', 'blocked',
|
|
36
42
|
];
|
|
37
43
|
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
38
44
|
const planPhaseManifestSchema = z.object({
|
|
@@ -48,9 +54,14 @@ const planPhaseWorkSchema = z.object({
|
|
|
48
54
|
transport: z.string().optional(),
|
|
49
55
|
snapshot: z.string().optional(),
|
|
50
56
|
notes: z.string().optional(),
|
|
57
|
+
// C2 (D24): exact SRVB name for the plan-complete /abap-fiori-build chain.
|
|
58
|
+
binding: z.string().optional(),
|
|
51
59
|
}).passthrough();
|
|
52
60
|
const planPhaseSchema = z.object({
|
|
53
61
|
id: z.string().min(1),
|
|
62
|
+
// C2 (D29): optional short human name for the tracker board; old envelopes
|
|
63
|
+
// without it still parse and render via the layer/delegate label fallback.
|
|
64
|
+
title: z.string().min(1).optional(),
|
|
54
65
|
component: z.string().min(1),
|
|
55
66
|
layer: z.enum(PLAN_LAYERS),
|
|
56
67
|
entryCriteria: z.array(z.string()),
|
|
@@ -60,6 +71,41 @@ const planPhaseSchema = z.object({
|
|
|
60
71
|
approval: z.boolean().optional(),
|
|
61
72
|
work: planPhaseWorkSchema.optional(),
|
|
62
73
|
});
|
|
74
|
+
/**
|
|
75
|
+
* Validates the discovered live stack for a revision plan (RevisionTarget in
|
|
76
|
+
* types.ts). All layer fields are optional/nullable — a partial stack (e.g.
|
|
77
|
+
* a table view with no MDE) is valid. Only `anchor`, `uiKind`, and `binding`
|
|
78
|
+
* are required shape — layers is always an object but all its members are opt.
|
|
79
|
+
*/
|
|
80
|
+
const revisionTargetSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
anchor: z.string().min(1),
|
|
83
|
+
uiKind: z.enum(['fiori-elements', 'freestyle', 'none']),
|
|
84
|
+
binding: z.string().nullable(),
|
|
85
|
+
// C2 (2026-06-26): freestyle app on-disk location (see types.ts). Additive
|
|
86
|
+
// and optional; the superRefine below makes it mandatory ONLY for freestyle.
|
|
87
|
+
app: z.object({ dir: z.string().min(1) }).optional(),
|
|
88
|
+
layers: z.object({
|
|
89
|
+
table: z.string().nullable().optional(),
|
|
90
|
+
interfaceView: z.string().nullable().optional(),
|
|
91
|
+
projectionView: z.string().nullable().optional(),
|
|
92
|
+
bdef: z.string().nullable().optional(),
|
|
93
|
+
behaviorPool: z.string().nullable().optional(),
|
|
94
|
+
mde: z.string().nullable().optional(),
|
|
95
|
+
serviceDef: z.string().nullable().optional(),
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
.superRefine((rev, ctx) => {
|
|
99
|
+
// A freestyle revision must record where the app lives on disk, or the
|
|
100
|
+
// ui-phase delegation to /abap-fiori-build amend has no appDir to amend.
|
|
101
|
+
if (rev.uiKind === 'freestyle' && !rev.app) {
|
|
102
|
+
ctx.addIssue({
|
|
103
|
+
code: z.ZodIssueCode.custom,
|
|
104
|
+
path: ['app'],
|
|
105
|
+
message: 'uiKind "freestyle" requires `revision.app.dir` (the freestyle app location for amend)',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
63
109
|
// Input type is `unknown` (third param): the `.default({})` on `from` means
|
|
64
110
|
// the schema ACCEPTS input without the key while still OUTPUTTING PlanContent.
|
|
65
111
|
export const planContentSchema = z
|
|
@@ -68,6 +114,8 @@ export const planContentSchema = z
|
|
|
68
114
|
goal: z.string().min(1),
|
|
69
115
|
source: z.string(),
|
|
70
116
|
target: z.string(),
|
|
117
|
+
// Absent mode parses as undefined (treated as 'create') — back-compat.
|
|
118
|
+
mode: z.enum(['create', 'revision']).optional(),
|
|
71
119
|
}),
|
|
72
120
|
// Tolerant by lesson (2026-06-06 live smoke): the model omitted the whole
|
|
73
121
|
// `from` key for a goal-only plan and the strict schema killed the save.
|
|
@@ -82,6 +130,9 @@ export const planContentSchema = z
|
|
|
82
130
|
.optional(),
|
|
83
131
|
})
|
|
84
132
|
.default({}),
|
|
133
|
+
// Present when project.mode='revision'. Optional here; the superRefine
|
|
134
|
+
// below enforces the cross-field rule: mode='revision' requires revision.
|
|
135
|
+
revision: revisionTargetSchema.optional(),
|
|
85
136
|
phases: z.array(planPhaseSchema).min(1),
|
|
86
137
|
summary: z.object({
|
|
87
138
|
total: z.number().int().nonnegative(),
|
|
@@ -91,6 +142,15 @@ export const planContentSchema = z
|
|
|
91
142
|
}),
|
|
92
143
|
})
|
|
93
144
|
.superRefine((content, ctx) => {
|
|
145
|
+
// mode:'revision' requires a revision target map (the discovered live stack).
|
|
146
|
+
// Absent mode is treated as 'create' — this check is a no-op in that case.
|
|
147
|
+
if (content.project.mode === 'revision' && !content.revision) {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: z.ZodIssueCode.custom,
|
|
150
|
+
path: ['revision'],
|
|
151
|
+
message: 'mode "revision" requires a `revision` target map (discovered live stack)',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
94
154
|
// Phase ids must be unique, and entryCriteria may only reference phases
|
|
95
155
|
// EARLIER in the array. Bottom-up ordering by construction — this is what
|
|
96
156
|
// guarantees resume is deterministic and the dependency graph acyclic
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readProjectFile } from './status.js';
|
|
2
|
-
import { buildPromotedFromSnapshot, validatePromotionSource } from './promote.js';
|
|
2
|
+
import { buildPromotedFromSnapshot, buildReviewDecisionsBlock, buildDetailPathHint, validatePromotionSource } from './promote.js';
|
|
3
3
|
import { resolveAtTokenAsync, formatProjectFileList } from './workspace.js';
|
|
4
4
|
/**
|
|
5
5
|
* Per-skill predecessor map. v0.7 admits multi-source via array values —
|
|
@@ -92,6 +92,29 @@ export async function runPromoteCommand(args) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
const promotedFrom = await buildPromotedFromSnapshot(env);
|
|
95
|
-
|
|
95
|
+
let extendedSkillInput = buildExtendedInput(promotedFrom);
|
|
96
|
+
// Always hand the skill the source detail-file path so it reads the baseline
|
|
97
|
+
// directly (the envelope at .cspeach/ root is unreadable; glob skips .cspeach/).
|
|
98
|
+
// This is what makes `--from` actually work for upgrade-fix on any scan,
|
|
99
|
+
// triaged or not — without it the skill globs for the file and fails.
|
|
100
|
+
const detailHint = buildDetailPathHint(env);
|
|
101
|
+
if (detailHint) {
|
|
102
|
+
extendedSkillInput = `${extendedSkillInput}\n\n${detailHint}`;
|
|
103
|
+
}
|
|
104
|
+
// When the source is a triaged upgrade-scan envelope, append a dedicated
|
|
105
|
+
// "Review decisions (honor these)" block joining each finding's status with
|
|
106
|
+
// its objectName/line/rule. This drives /abap-upgrade-fix off the viewer
|
|
107
|
+
// triage instead of re-asking object-by-object. Returns null (and we append
|
|
108
|
+
// nothing) for non-upgrade-scan sources or fully-untriaged scans.
|
|
109
|
+
const reviewBlock = buildReviewDecisionsBlock(env);
|
|
110
|
+
if (reviewBlock) {
|
|
111
|
+
extendedSkillInput = `${extendedSkillInput}\n\n${reviewBlock}`;
|
|
112
|
+
}
|
|
113
|
+
// Carry the originating spec document into the handoff so the downstream
|
|
114
|
+
// skill (e.g. /abap-plan) can cite the running spec the answered gaps trace
|
|
115
|
+
// back to — the Word/PDF doc → spec-gap → plan thread stays visible.
|
|
116
|
+
if (env.artefactType === 'spec-gap' && env.content.sourceDocument) {
|
|
117
|
+
extendedSkillInput = `${extendedSkillInput}\n\nSource spec document: ${env.content.sourceDocument}`;
|
|
118
|
+
}
|
|
96
119
|
return { promotedFrom, extendedSkillInput };
|
|
97
120
|
}
|
package/dist/projects/promote.js
CHANGED
|
@@ -67,6 +67,134 @@ export async function buildPromotedFromSnapshot(source) {
|
|
|
67
67
|
snapshot: { items },
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the compact "Review decisions" block for an upgrade-scan source.
|
|
72
|
+
*
|
|
73
|
+
* The viewer lets the user triage each finding to one of
|
|
74
|
+
* `open | auto-fix | manual | suppress` (stored in `interaction.items[].status`).
|
|
75
|
+
* Those decisions must DRIVE /abap-upgrade-fix instead of the skill re-asking
|
|
76
|
+
* object-by-object. The skill cannot file_read the root `.cspeach/<name>.json`
|
|
77
|
+
* envelope (denylisted), so the statuses must reach it via the `--from`
|
|
78
|
+
* prompt injection — this block is what the CLI appends to that injection.
|
|
79
|
+
*
|
|
80
|
+
* Joins each item's status with the matching `content.findings[]` row
|
|
81
|
+
* (objectName / line / rule) and renders one aligned line per finding,
|
|
82
|
+
* e.g.:
|
|
83
|
+
* finding-001 ZFI_DUNNING_SELECT line 52 SELECT_WO_ORDER_BY → auto-fix
|
|
84
|
+
*
|
|
85
|
+
* Returns `null` when the source is not an upgrade-scan, or when every
|
|
86
|
+
* finding is still `open` (untriaged — nothing to honor; the skill falls
|
|
87
|
+
* back to its existing interactive ask for all of them).
|
|
88
|
+
*/
|
|
89
|
+
export function buildReviewDecisionsBlock(source) {
|
|
90
|
+
if (source.artefactType === 'upgrade-scan')
|
|
91
|
+
return buildUpgradeScanDecisions(source);
|
|
92
|
+
if (source.artefactType === 'cca-assessment')
|
|
93
|
+
return buildCcaDecisions(source);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Always-inject hint for any `--from` source that carries a `content.detailPath`
|
|
98
|
+
* (upgrade-scan/progress/report, cca-assessment, modernize-result, test-coverage).
|
|
99
|
+
*
|
|
100
|
+
* Why this is REQUIRED, not optional: the `.cspeach.json` envelope itself lives at
|
|
101
|
+
* the denylisted `.cspeach/` root and cannot be read by the skill, and `glob` skips
|
|
102
|
+
* `.cspeach/` — so without an explicit path the skill hunts for the baseline and
|
|
103
|
+
* fails (observed live 2026-06-16: `/abap-upgrade-fix --from` globbing endlessly).
|
|
104
|
+
* The detail file lives under `.cspeach/<domain>/`, which the filesystem carve-out
|
|
105
|
+
* makes readable — so we hand the skill that exact path to `file_read` directly.
|
|
106
|
+
*/
|
|
107
|
+
export function buildDetailPathHint(source) {
|
|
108
|
+
const detailPath = source.content?.detailPath;
|
|
109
|
+
if (!detailPath)
|
|
110
|
+
return null;
|
|
111
|
+
return [
|
|
112
|
+
'Source detail file — READ THIS DIRECTLY with file_read at the exact path below.',
|
|
113
|
+
'Do NOT glob for it, and do NOT try to read the .cspeach.json envelope (that path',
|
|
114
|
+
'is protected). The detail file holds the full per-finding data (object, line,',
|
|
115
|
+
'rule, message, successor) and is readable under .cspeach/:',
|
|
116
|
+
` ${detailPath}`,
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render a fixed-width "Review decisions (honor these)" table from already-built
|
|
121
|
+
* rows. `summary` is the one-line counts header; `countKeys` lists the decision
|
|
122
|
+
* tokens to tally (in display order). Shared by the upgrade-scan and
|
|
123
|
+
* cca-assessment branches so both render identically.
|
|
124
|
+
*/
|
|
125
|
+
function renderDecisionRows(rows, countKeys, summaryLabel) {
|
|
126
|
+
const wId = Math.max(...rows.map((r) => r.id.length));
|
|
127
|
+
const wObj = Math.max(...rows.map((r) => r.objectName.length));
|
|
128
|
+
const wLine = Math.max(...rows.map((r) => r.line.length));
|
|
129
|
+
const wRule = Math.max(...rows.map((r) => r.rule.length));
|
|
130
|
+
const counts = {};
|
|
131
|
+
for (const k of countKeys)
|
|
132
|
+
counts[k] = 0;
|
|
133
|
+
for (const r of rows)
|
|
134
|
+
counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
135
|
+
const summary = countKeys.map((k) => `${counts[k]} ${k}`).join(', ');
|
|
136
|
+
const lines = [
|
|
137
|
+
'Review decisions from the source envelope (honor these):',
|
|
138
|
+
` ${summaryLabel}: ${summary}.`,
|
|
139
|
+
'',
|
|
140
|
+
];
|
|
141
|
+
for (const r of rows) {
|
|
142
|
+
const parts = [
|
|
143
|
+
r.id.padEnd(wId),
|
|
144
|
+
r.objectName.padEnd(wObj),
|
|
145
|
+
r.line.padEnd(wLine),
|
|
146
|
+
r.rule.padEnd(wRule),
|
|
147
|
+
`→ ${r.status}`,
|
|
148
|
+
];
|
|
149
|
+
// Collapse the run of spaces from an empty (line-less) column so we never
|
|
150
|
+
// emit a stray "line undefined"; padEnd of "" already keeps alignment.
|
|
151
|
+
lines.push(` ${parts.join(' ').replace(/\s+$/, '')}`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
function buildUpgradeScanDecisions(source) {
|
|
156
|
+
const items = source.interaction.items;
|
|
157
|
+
if (items.length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
// Untriaged source (all open) → omit the block; nothing was decided.
|
|
160
|
+
if (items.every((it) => it.status === 'open'))
|
|
161
|
+
return null;
|
|
162
|
+
const findingById = new Map(source.content.findings.map((f) => [f.id, f]));
|
|
163
|
+
const rows = items.map((it) => {
|
|
164
|
+
const f = findingById.get(it.id);
|
|
165
|
+
return {
|
|
166
|
+
id: it.id,
|
|
167
|
+
objectName: f?.objectName ?? '<unknown>',
|
|
168
|
+
line: f?.line !== undefined ? `line ${f.line}` : '',
|
|
169
|
+
rule: f?.rule ?? '',
|
|
170
|
+
status: it.status,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return renderDecisionRows(rows, ['auto-fix', 'suppress', 'manual', 'open'], 'Applying your review')
|
|
174
|
+
// The upgrade-scan summary historically reads "... K still open"; keep that
|
|
175
|
+
// wording for the open bucket while the shared renderer emits "K open".
|
|
176
|
+
.replace(/(\d+) open\./, '$1 still open.');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* cca-assessment: the per-object review decision is the `classification`
|
|
180
|
+
* (keep/fix/retire/redesign) the consultant assigned in the worksheet, carried
|
|
181
|
+
* in content.classifications[]. interaction.items[].status is the triage
|
|
182
|
+
* lifecycle (open/reviewed/classified/archived), not the fix decision — so we
|
|
183
|
+
* surface the classification as the decision token the skill must honor.
|
|
184
|
+
*/
|
|
185
|
+
function buildCcaDecisions(source) {
|
|
186
|
+
const classifications = source.content.classifications;
|
|
187
|
+
if (!classifications || classifications.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
const rows = classifications.map((c) => ({
|
|
190
|
+
id: c.id,
|
|
191
|
+
objectName: c.objectName,
|
|
192
|
+
line: '',
|
|
193
|
+
rule: c.objectType ?? '',
|
|
194
|
+
status: c.classification,
|
|
195
|
+
}));
|
|
196
|
+
return renderDecisionRows(rows, ['fix', 'redesign', 'retire', 'keep'], 'Applying your assessment');
|
|
197
|
+
}
|
|
70
198
|
export function validatePromotionSource(source, expectedPredecessor) {
|
|
71
199
|
// v0.7 — accept either a single predecessor or an array (multi-source).
|
|
72
200
|
// /abap-upgrade-fix accepts both upgrade-scan AND cca-assessment, etc.
|