@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.
- package/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +330 -36
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/steering-queue.js +27 -0
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +84 -7
- package/dist/approvals/render.js +38 -0
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +24 -5
- 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 +435 -0
- package/dist/config/loader.js +102 -3
- package/dist/cost/pricing.js +14 -5
- 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/email-template.js +2 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +178 -0
- 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 +7 -1
- 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 +254 -0
- package/dist/projects/plan-schema.js +210 -0
- package/dist/projects/promote-command.js +26 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +263 -20
- package/dist/projects/status.js +22 -0
- package/dist/projects/validate.js +2 -0
- 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/syntax.js +16 -1
- package/dist/renderer/thinking-heartbeat.js +13 -1
- 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 +93 -21
- 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 +25 -19
- package/dist/repl.js +470 -22
- 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 +54 -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 +84 -11
- package/dist/ui/body.js +13 -0
- package/dist/ui/command-palette.js +46 -10
- package/dist/ui/file-palette.js +44 -0
- package/dist/ui/footer.js +28 -11
- package/dist/ui/line-resolution.js +92 -0
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/turn-status-emitter.js +52 -0
- package/dist/ui/turn-status.js +59 -0
- package/dist/ui/widgets/ask-question-modal.js +30 -2
- package/package.json +23 -4
- 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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { parsePlanContent, PLAN_PHASE_STATUSES } from './plan-schema.js';
|
|
2
|
+
import { computeNextPhase, isPhaseSatisfied } from './plan-run.js';
|
|
3
|
+
const MANIFEST_RE = /<!--\s*csforge:plan-manifest\s*\n([\s\S]*?)\n\s*-->/g;
|
|
4
|
+
// 2026-06-06 live-smoke lesson #4 (Postel): normalise model-emitted
|
|
5
|
+
// `work` blocks instead of rejecting them. Models naturally write
|
|
6
|
+
// `"transport": null` for "none" (schema fields are optional, NOT
|
|
7
|
+
// nullable) and a structured OBJECT for notes (a design phase's
|
|
8
|
+
// decision register) — both reasonable; both killed a fully correct
|
|
9
|
+
// phase write-back. null -> absent; object notes -> JSON string.
|
|
10
|
+
function normalizeWorkFields(p) {
|
|
11
|
+
const w = p?.work;
|
|
12
|
+
if (!w || typeof w !== 'object')
|
|
13
|
+
return;
|
|
14
|
+
for (const k of ['generated', 'transport', 'snapshot', 'notes', 'binding']) {
|
|
15
|
+
if (w[k] === null)
|
|
16
|
+
delete w[k];
|
|
17
|
+
}
|
|
18
|
+
if (w.notes !== undefined && typeof w.notes !== 'string') {
|
|
19
|
+
w.notes = JSON.stringify(w.notes);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Expand a compact `changed` map against the prior envelope content into a
|
|
24
|
+
* full PlanContent candidate (still Zod-validated by the caller). The
|
|
25
|
+
* dangerous failure here is silently dropping or duplicating phase content
|
|
26
|
+
* during the merge — hence: every changed key must name an existing phase,
|
|
27
|
+
* an inner `id` may not contradict its key, and the phase list is ALWAYS
|
|
28
|
+
* prior.phases mapped 1:1 (no additions, no removals, no reordering).
|
|
29
|
+
*/
|
|
30
|
+
function expandCompactContent(changedRaw, prior, rawStatuses) {
|
|
31
|
+
if (!prior) {
|
|
32
|
+
throw new Error('compact plan-manifest ("changed" without "content") is only valid on a resume turn — ' +
|
|
33
|
+
'no prior plan content is available to merge into; seed sessions must emit the full shape');
|
|
34
|
+
}
|
|
35
|
+
if (typeof changedRaw !== 'object' || changedRaw === null || Array.isArray(changedRaw)) {
|
|
36
|
+
throw new Error('plan-manifest "changed" must be an object map keyed by phase id');
|
|
37
|
+
}
|
|
38
|
+
const changed = changedRaw;
|
|
39
|
+
const knownIds = new Set(prior.phases.map((p) => p.id));
|
|
40
|
+
for (const [id, overlay] of Object.entries(changed)) {
|
|
41
|
+
if (!knownIds.has(id)) {
|
|
42
|
+
throw new Error(`plan-manifest changed["${id}"] references an unknown phase id; ` +
|
|
43
|
+
`known phases: ${[...knownIds].join(', ')}. ` +
|
|
44
|
+
'To add or remove phases, emit the full shape with "content" instead.');
|
|
45
|
+
}
|
|
46
|
+
if (typeof overlay !== 'object' || overlay === null || Array.isArray(overlay)) {
|
|
47
|
+
throw new Error(`plan-manifest changed["${id}"] must be an object carrying the phase's updated fields`);
|
|
48
|
+
}
|
|
49
|
+
const innerId = overlay.id;
|
|
50
|
+
if (innerId !== undefined && innerId !== id) {
|
|
51
|
+
throw new Error(`plan-manifest changed["${id}"] carries a mismatched "id": ${String(innerId)}`);
|
|
52
|
+
}
|
|
53
|
+
// Lesson-#4 parity for the overlay itself: models write `"work": null`
|
|
54
|
+
// for "nothing to record". null/undefined work means "keep prior work" —
|
|
55
|
+
// drop the key so the spread below doesn't clobber the prior phase's work.
|
|
56
|
+
if (overlay.work == null) {
|
|
57
|
+
delete overlay.work;
|
|
58
|
+
}
|
|
59
|
+
normalizeWorkFields(overlay);
|
|
60
|
+
}
|
|
61
|
+
// 1:1 map over the PRIOR phase list — changed entries overlay their phase,
|
|
62
|
+
// everything else passes through verbatim from the envelope.
|
|
63
|
+
const phases = prior.phases.map((p) => changed[p.id] !== undefined ? { ...p, ...changed[p.id], id: p.id } : p);
|
|
64
|
+
// Recompute summary CLI-side from the merged statuses (the compact shape
|
|
65
|
+
// does not carry one — fewer tokens, and it cannot drift from statuses).
|
|
66
|
+
// Invalid/missing status values are skipped here; the shared statuses
|
|
67
|
+
// validation loop in extractPlan throws the precise error afterwards.
|
|
68
|
+
const statuses = {};
|
|
69
|
+
for (const p of phases) {
|
|
70
|
+
const s = rawStatuses[p.id];
|
|
71
|
+
if (typeof s === 'string' && PLAN_PHASE_STATUSES.includes(s)) {
|
|
72
|
+
statuses[p.id] = s;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const next = computeNextPhase(phases, statuses);
|
|
76
|
+
const summary = {
|
|
77
|
+
total: phases.length,
|
|
78
|
+
// Waived phases (C1) count as validated here — summary.validated means
|
|
79
|
+
// "done for DAG purposes", same as the tracker header tally.
|
|
80
|
+
validated: phases.filter((p) => isPhaseSatisfied(statuses[p.id])).length,
|
|
81
|
+
blocked: phases.filter((p) => statuses[p.id] === 'blocked').length,
|
|
82
|
+
next: next?.id ?? null,
|
|
83
|
+
};
|
|
84
|
+
return { ...prior, phases, summary };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the LAST plan-manifest block out of the skill's markdown output.
|
|
88
|
+
* `priorContent` is the validated content of the envelope being resumed —
|
|
89
|
+
* required to expand the compact shape; ignored for the full shape (seed
|
|
90
|
+
* sessions pass nothing).
|
|
91
|
+
*/
|
|
92
|
+
export function extractPlan(markdown, priorContent) {
|
|
93
|
+
// Last block wins — defensive against a turn emitting more than one block.
|
|
94
|
+
const re = new RegExp(MANIFEST_RE.source, MANIFEST_RE.flags);
|
|
95
|
+
let m;
|
|
96
|
+
let last = null;
|
|
97
|
+
while ((m = re.exec(markdown)) !== null)
|
|
98
|
+
last = m;
|
|
99
|
+
if (!last) {
|
|
100
|
+
throw new Error('No <!-- csforge:plan-manifest --> block found in skill output');
|
|
101
|
+
}
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(last[1]);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
throw new Error(`plan-manifest block is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
108
|
+
}
|
|
109
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
110
|
+
throw new Error('plan-manifest block must be a JSON object');
|
|
111
|
+
}
|
|
112
|
+
const obj = parsed;
|
|
113
|
+
if (typeof obj.title !== 'string' || obj.title.trim().length === 0) {
|
|
114
|
+
throw new Error('plan-manifest is missing a non-empty "title"');
|
|
115
|
+
}
|
|
116
|
+
if (typeof obj.statuses !== 'object' || obj.statuses === null || Array.isArray(obj.statuses)) {
|
|
117
|
+
throw new Error('plan-manifest is missing the "statuses" object');
|
|
118
|
+
}
|
|
119
|
+
const rawStatuses = obj.statuses;
|
|
120
|
+
// Dual-shape manifests are the one silent-data-loss path: a model that
|
|
121
|
+
// copies stale `content` out of <plan_state> AND emits correct `changed`
|
|
122
|
+
// updates would persist the stale plan with no signal. Refuse loudly.
|
|
123
|
+
if (obj.content !== undefined && obj.changed !== undefined) {
|
|
124
|
+
throw new Error('plan-manifest must contain either "content" (full shape) or "changed" (compact shape), ' +
|
|
125
|
+
'not both — re-emit using exactly one');
|
|
126
|
+
}
|
|
127
|
+
let contentInput;
|
|
128
|
+
if (obj.content !== undefined) {
|
|
129
|
+
// FULL shape — Mode 1 create, plus the backward-compatible / structural-
|
|
130
|
+
// change path on resume.
|
|
131
|
+
// 2026-06-06 live-smoke lesson #2: the model cannot know the consumed
|
|
132
|
+
// spec-gap's real path/sha — that provenance lives CLI-side in the
|
|
133
|
+
// envelope's promotedFrom (threaded by the --from save flow). Models
|
|
134
|
+
// emit placeholders like {"path":"@equip","sha256":""}, and a junk
|
|
135
|
+
// provenance pointer must not cost the whole plan: strip it.
|
|
136
|
+
const rawContent = obj.content;
|
|
137
|
+
if (rawContent && typeof rawContent === 'object' && rawContent.from?.specGap) {
|
|
138
|
+
const sha = String(rawContent.from.specGap.sha256 ?? '');
|
|
139
|
+
if (!/^[0-9a-f]{64}$/.test(sha))
|
|
140
|
+
delete rawContent.from.specGap;
|
|
141
|
+
}
|
|
142
|
+
const rawPhases = rawContent?.phases;
|
|
143
|
+
if (Array.isArray(rawPhases)) {
|
|
144
|
+
for (const p of rawPhases)
|
|
145
|
+
normalizeWorkFields(p);
|
|
146
|
+
}
|
|
147
|
+
contentInput = obj.content;
|
|
148
|
+
}
|
|
149
|
+
else if (obj.changed !== undefined) {
|
|
150
|
+
// COMPACT shape (A2) — resume turns only; merged into the prior content.
|
|
151
|
+
contentInput = expandCompactContent(obj.changed, priorContent, rawStatuses);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw new Error('plan-manifest must carry either "content" (full shape) or "changed" (compact resume shape)');
|
|
155
|
+
}
|
|
156
|
+
const pc = parsePlanContent(contentInput);
|
|
157
|
+
if (!pc.ok) {
|
|
158
|
+
throw new Error(`plan-manifest content failed validation:\n ${pc.errors.join('\n ')}`);
|
|
159
|
+
}
|
|
160
|
+
const statuses = {};
|
|
161
|
+
for (const p of pc.content.phases) {
|
|
162
|
+
const s = rawStatuses[p.id];
|
|
163
|
+
if (typeof s !== 'string' || !PLAN_PHASE_STATUSES.includes(s)) {
|
|
164
|
+
throw new Error(`plan-manifest statuses["${p.id}"] is ${s === undefined ? 'missing' : `invalid: ${String(s)}`}; ` +
|
|
165
|
+
`allowed: ${PLAN_PHASE_STATUSES.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
statuses[p.id] = s;
|
|
168
|
+
}
|
|
169
|
+
const items = pc.content.phases.map((p) => ({
|
|
170
|
+
id: p.id,
|
|
171
|
+
status: statuses[p.id],
|
|
172
|
+
answer: null,
|
|
173
|
+
answeredBy: null,
|
|
174
|
+
answeredAt: null,
|
|
175
|
+
comments: [],
|
|
176
|
+
}));
|
|
177
|
+
return { title: obj.title.trim(), content: pc.content, items, statuses };
|
|
178
|
+
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
const TITLE_RE = /^## Spec Gap Analysis — (.+)$/m;
|
|
2
2
|
const DOMAIN_RE = /^\*\*Inferred Domain:\*\*\s*(.+?)(?:\s*\[INFERRED.*?\])?$/m;
|
|
3
3
|
const INPUT_TYPE_RE = /^\*\*Input Type:\*\*\s*(.+)$/m;
|
|
4
|
+
const SOURCE_DOC_RE = /^\*\*Source Document:\*\*\s*(.+)$/m;
|
|
4
5
|
const SECTION_HEADING_RE = /^### (.+)$/gm;
|
|
5
6
|
// Real-world model output shows two formatting variants:
|
|
6
7
|
// ' 1. [BLOCKER] Text' — leading indent (4 spaces or tab), plain severity
|
|
7
8
|
// '1. **[BLOCKER]** Text' — no indent, severity wrapped in markdown bold
|
|
8
9
|
// Tolerate both: optional leading whitespace + optional `**` around the severity bracket.
|
|
9
10
|
const GAP_LINE_RE = /^\s*(\d+)\.\s*(?:\*\*)?\[(BLOCKER|IMPORTANT|NICE TO KNOW)\](?:\*\*)?\s*(.+)$/m;
|
|
11
|
+
// An "Options:" / "Options (multi):" line under a gap — candidate answers the
|
|
12
|
+
// user can pick instead of typing. Tolerant of leading whitespace + bold.
|
|
13
|
+
const OPTIONS_LINE_RE = /^\s*(?:\*\*)?Options(\s*\(multi\))?(?:\*\*)?\s*:\s*(.+)$/i;
|
|
10
14
|
const AUDIENCE_BY_SECTION_NAME = {
|
|
11
15
|
'missing business questions': 'business',
|
|
12
16
|
'missing technical questions': 'technical',
|
|
17
|
+
// Standard-vs-Custom / fit-gap: the BA knows the standard tcode/app exists,
|
|
18
|
+
// the architect confirms the technical fit — so it's a mixed audience.
|
|
19
|
+
'fit-gap': 'mixed',
|
|
20
|
+
'standard-vs-custom': 'mixed',
|
|
21
|
+
'standard functionality': 'mixed',
|
|
13
22
|
'data assumptions to confirm': 'data',
|
|
14
23
|
'authorization questions': 'auth',
|
|
15
24
|
'authorisation questions': 'auth',
|
|
@@ -28,6 +37,9 @@ function audienceFor(sectionName) {
|
|
|
28
37
|
const KNOWN_GAP_SECTIONS = [
|
|
29
38
|
'Missing Business Questions',
|
|
30
39
|
'Missing Technical Questions',
|
|
40
|
+
'Fit-Gap', // 'Standard-vs-Custom (Fit-Gap)' — matched by substring
|
|
41
|
+
'Standard-vs-Custom',
|
|
42
|
+
'Standard Functionality',
|
|
31
43
|
'Data Assumptions to Confirm',
|
|
32
44
|
'Authorization Questions',
|
|
33
45
|
'Authorisation Questions', // British spelling — both OK
|
|
@@ -47,6 +59,7 @@ export function extractSpecGap(markdown) {
|
|
|
47
59
|
const title = titleMatch[1].trim();
|
|
48
60
|
const inputType = (INPUT_TYPE_RE.exec(markdown)?.[1] ?? 'Unknown').trim();
|
|
49
61
|
const inferredDomain = (DOMAIN_RE.exec(markdown)?.[1] ?? 'Unknown').trim();
|
|
62
|
+
const sourceDocument = SOURCE_DOC_RE.exec(markdown)?.[1].trim() || undefined;
|
|
50
63
|
const sectionStarts = [];
|
|
51
64
|
let m;
|
|
52
65
|
const reCopy = new RegExp(SECTION_HEADING_RE.source, SECTION_HEADING_RE.flags);
|
|
@@ -84,18 +97,32 @@ export function extractSpecGap(markdown) {
|
|
|
84
97
|
}
|
|
85
98
|
return {
|
|
86
99
|
title,
|
|
87
|
-
content: { inputType, inferredDomain, sections },
|
|
100
|
+
content: { inputType, inferredDomain, ...(sourceDocument ? { sourceDocument } : {}), sections },
|
|
88
101
|
items,
|
|
89
102
|
};
|
|
90
103
|
}
|
|
91
104
|
function parseGapsFromSection(sectionBody, nextId) {
|
|
92
105
|
const out = [];
|
|
93
|
-
const
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
106
|
+
const gapRe = new RegExp(GAP_LINE_RE.source); // per-line, no /m — exec on each line
|
|
107
|
+
let current = null;
|
|
108
|
+
for (const raw of sectionBody.split('\n')) {
|
|
109
|
+
const line = raw.replace(/\r$/, '');
|
|
110
|
+
const g = gapRe.exec(line);
|
|
111
|
+
if (g) {
|
|
112
|
+
current = { id: nextId(), severity: g[2], text: g[3].trim() };
|
|
113
|
+
out.push(current);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// An Options: line attaches its choices to the gap immediately above it.
|
|
117
|
+
const o = OPTIONS_LINE_RE.exec(line);
|
|
118
|
+
if (o && current) {
|
|
119
|
+
const opts = o[2].split('|').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
120
|
+
if (opts.length > 0) {
|
|
121
|
+
current.options = opts;
|
|
122
|
+
if (o[1])
|
|
123
|
+
current.optionsMultiSelect = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
return out;
|
|
101
128
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <!-- csforge:test-coverage-manifest
|
|
11
11
|
* artefact: test-coverage
|
|
12
12
|
* title: ZDPR_PAYMENTS — test coverage
|
|
13
|
-
* detail_path: .
|
|
13
|
+
* detail_path: .cspeach/tests/zdpr-payments/project.json
|
|
14
14
|
* baseline_path: <project-relative path>
|
|
15
15
|
* baseline_sha256: <hex>
|
|
16
16
|
* transport: <id or ->
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* <!-- csforge:upgrade-manifest
|
|
10
10
|
* artefact: upgrade-scan
|
|
11
11
|
* title: ZFI* — S/4HANA 2023 readiness
|
|
12
|
-
* detail_path: .
|
|
12
|
+
* detail_path: .cspeach/upgrades/zfi-readiness-2023-baseline.json
|
|
13
13
|
* atc_variant: S4HANA_READINESS_2023
|
|
14
14
|
* scope: Z* in package ZFI*
|
|
15
15
|
* total: 2847
|
|
@@ -17,12 +17,16 @@
|
|
|
17
17
|
* manual_review: 136
|
|
18
18
|
* suppressed: 26
|
|
19
19
|
* findings:
|
|
20
|
-
* finding-001 | ZFI_BP_SYNC | PROG | S4HANA_BUSINESS_PARTNER | E
|
|
21
|
-
* finding-002 | ZCL_FI_DOC | CLAS | PERF_DB_SEARCH
|
|
20
|
+
* finding-001 | ZFI_BP_SYNC | PROG | S4HANA_BUSINESS_PARTNER | E | 88 | Replace BAPI_BUPA_CREATE with I_BusinessPartner CDS
|
|
21
|
+
* finding-002 | ZCL_FI_DOC | CLAS | PERF_DB_SEARCH | W | - | Add ORDER BY PRIMARY KEY to the SELECT
|
|
22
22
|
* -->
|
|
23
23
|
*
|
|
24
24
|
* Key-value pairs are `key: value` per line. Tabular sections use a
|
|
25
25
|
* trailing colon header (`findings:`) followed by `|`-separated rows.
|
|
26
|
+
* Findings rows are 7-column: id | object | type | rule | severity | line |
|
|
27
|
+
* message. `line` is an integer or `-` (object-level, not line-specific);
|
|
28
|
+
* `message` is a one-line description incl. the fix. Legacy 5-column rows
|
|
29
|
+
* (no line/message) still parse — the extra fields are optional.
|
|
26
30
|
*
|
|
27
31
|
* Three artefact types share the same parser and dispatch by `artefact:`:
|
|
28
32
|
* - upgrade-scan → findings table
|
|
@@ -82,6 +86,13 @@ function intOr(kv, key, fallback) {
|
|
|
82
86
|
const n = Number.parseInt(v, 10);
|
|
83
87
|
return Number.isFinite(n) ? n : fallback;
|
|
84
88
|
}
|
|
89
|
+
function optionalInt(kv, key) {
|
|
90
|
+
const v = kv[key];
|
|
91
|
+
if (v === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const n = Number.parseInt(v, 10);
|
|
94
|
+
return Number.isFinite(n) ? n : undefined;
|
|
95
|
+
}
|
|
85
96
|
function buildItems(count, defaultStatus) {
|
|
86
97
|
const out = [];
|
|
87
98
|
for (let i = 0; i < count; i++) {
|
|
@@ -107,24 +118,66 @@ export function extractUpgradeScan(markdown) {
|
|
|
107
118
|
if (!kv.detail_path) {
|
|
108
119
|
throw new Error("upgrade-scan manifest missing required 'detail_path'");
|
|
109
120
|
}
|
|
110
|
-
const findings = (tables.findings ?? []).map((row, i) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
const findings = (tables.findings ?? []).map((row, i) => {
|
|
122
|
+
const finding = {
|
|
123
|
+
id: row[0] ?? `finding-${String(i + 1).padStart(3, '0')}`,
|
|
124
|
+
objectName: row[1] ?? '',
|
|
125
|
+
objectType: row[2] ?? '',
|
|
126
|
+
rule: row[3] ?? '',
|
|
127
|
+
severity: row[4] ?? '',
|
|
128
|
+
};
|
|
129
|
+
// line: cell 6 (row[5]); include only if a finite integer (skip '-', '', non-numeric).
|
|
130
|
+
if (row[5] !== undefined) {
|
|
131
|
+
const n = Number.parseInt(row[5], 10);
|
|
132
|
+
if (Number.isFinite(n))
|
|
133
|
+
finding.line = n;
|
|
134
|
+
}
|
|
135
|
+
// message: cells 7+ rejoined with ' | ' so a '|' inside the message survives.
|
|
136
|
+
if (row.length > 6) {
|
|
137
|
+
const message = row.slice(6).join(' | ').trim();
|
|
138
|
+
if (message.length > 0)
|
|
139
|
+
finding.message = message;
|
|
140
|
+
}
|
|
141
|
+
return finding;
|
|
142
|
+
});
|
|
143
|
+
const summary = {
|
|
144
|
+
total: intOr(kv, 'total', findings.length),
|
|
145
|
+
autoFixable: intOr(kv, 'auto_fixable', 0),
|
|
146
|
+
manualReview: intOr(kv, 'manual_review', 0),
|
|
147
|
+
suppressed: intOr(kv, 'suppressed', 0),
|
|
148
|
+
};
|
|
149
|
+
// ── D7 manifest integrity (2026-06-12) ──────────────────────────────────
|
|
150
|
+
// The scan manifest is chain input for /abap-upgrade-fix slicing and the
|
|
151
|
+
// merge's pending-fill. A `total` that disagrees with the rows means
|
|
152
|
+
// either the model truncated the table without marking it (D7) or
|
|
153
|
+
// miscounted (D6) — both corrupt downstream reconciliation, so fail the
|
|
154
|
+
// extract loudly (extract-plan loud-failure style).
|
|
155
|
+
const truncated = kv.findings_truncated === 'true';
|
|
156
|
+
const shown = optionalInt(kv, 'findings_shown');
|
|
157
|
+
if (truncated) {
|
|
158
|
+
if (shown !== undefined && shown !== findings.length) {
|
|
159
|
+
throw new Error(`upgrade-scan manifest contradiction: findings_shown is ${shown} but the table has ${findings.length} rows.`);
|
|
160
|
+
}
|
|
161
|
+
if (summary.total < findings.length) {
|
|
162
|
+
throw new Error(`upgrade-scan manifest contradiction: total (${summary.total}) is smaller than the ${findings.length} rows shown.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (summary.total !== findings.length) {
|
|
166
|
+
throw new Error(`upgrade-scan manifest contradiction: total is ${summary.total} but the table has ${findings.length} finding rows. `
|
|
167
|
+
+ `Counts must equal rows — if the table was capped, mark it with findings_truncated: true + findings_shown.`);
|
|
168
|
+
}
|
|
117
169
|
const content = {
|
|
118
170
|
detailPath: kv.detail_path,
|
|
119
171
|
atcVariant: kv.atc_variant ?? '',
|
|
120
172
|
scope: kv.scope ?? '',
|
|
121
|
-
summary
|
|
122
|
-
total: intOr(kv, 'total', findings.length),
|
|
123
|
-
autoFixable: intOr(kv, 'auto_fixable', 0),
|
|
124
|
-
manualReview: intOr(kv, 'manual_review', 0),
|
|
125
|
-
suppressed: intOr(kv, 'suppressed', 0),
|
|
126
|
-
},
|
|
173
|
+
summary,
|
|
127
174
|
findings,
|
|
175
|
+
...(truncated
|
|
176
|
+
? {
|
|
177
|
+
findingsTruncated: true,
|
|
178
|
+
...(shown !== undefined ? { findingsShown: shown } : {}),
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
128
181
|
};
|
|
129
182
|
// One InteractionItem per finding, default status 'open'.
|
|
130
183
|
const items = findings.map((f) => ({
|
|
@@ -154,6 +207,49 @@ export function extractUpgradeProgress(markdown) {
|
|
|
154
207
|
finding: row[2] ?? '',
|
|
155
208
|
status: row[3] ?? 'pending',
|
|
156
209
|
}));
|
|
210
|
+
const summary = {
|
|
211
|
+
fixed: intOr(kv, 'fixed', 0),
|
|
212
|
+
skipped: intOr(kv, 'skipped', 0),
|
|
213
|
+
failed: intOr(kv, 'failed', 0),
|
|
214
|
+
pending: intOr(kv, 'pending', 0),
|
|
215
|
+
};
|
|
216
|
+
// ── D6/D7 manifest integrity (2026-06-12) ───────────────────────────────
|
|
217
|
+
// Same backstop as extract-cca / the scan branch above: the progress
|
|
218
|
+
// manifest is chain input for /abap-upgrade-merge — summary counts that
|
|
219
|
+
// contradict the fixes rows corrupt the consolidated team numbers, so a
|
|
220
|
+
// contradiction fails the extract LOUDLY. Unknown statuses tally as
|
|
221
|
+
// 'pending', matching the InteractionItem coercion below.
|
|
222
|
+
const STATUSES = ['fixed', 'skipped', 'failed', 'pending'];
|
|
223
|
+
const rowTally = {
|
|
224
|
+
fixed: 0, skipped: 0, failed: 0, pending: 0,
|
|
225
|
+
};
|
|
226
|
+
for (const f of fixes) {
|
|
227
|
+
const s = STATUSES.includes(f.status)
|
|
228
|
+
? f.status
|
|
229
|
+
: 'pending';
|
|
230
|
+
rowTally[s]++;
|
|
231
|
+
}
|
|
232
|
+
const fixesTruncated = kv.fixes_truncated === 'true';
|
|
233
|
+
const fixesShown = optionalInt(kv, 'fixes_shown');
|
|
234
|
+
if (fixesTruncated) {
|
|
235
|
+
if (fixesShown !== undefined && fixesShown !== fixes.length) {
|
|
236
|
+
throw new Error(`upgrade-progress manifest contradiction: fixes_shown is ${fixesShown} but the table has ${fixes.length} rows.`);
|
|
237
|
+
}
|
|
238
|
+
for (const s of STATUSES) {
|
|
239
|
+
if (rowTally[s] > summary[s]) {
|
|
240
|
+
throw new Error(`upgrade-progress manifest contradiction: ${rowTally[s]} '${s}' rows in the table but the summary says ${s}: ${summary[s]}. `
|
|
241
|
+
+ `Counts must cover every row even when the table is truncated.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const s of STATUSES) {
|
|
247
|
+
if (rowTally[s] !== summary[s]) {
|
|
248
|
+
throw new Error(`upgrade-progress manifest contradiction: summary says ${s}: ${summary[s]} but the table has ${rowTally[s]} '${s}' rows. `
|
|
249
|
+
+ `Counts must equal rows — if the table was capped, mark it with fixes_truncated: true + fixes_shown.`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
157
253
|
const content = {
|
|
158
254
|
detailPath: kv.detail_path,
|
|
159
255
|
baselineRef: {
|
|
@@ -161,12 +257,7 @@ export function extractUpgradeProgress(markdown) {
|
|
|
161
257
|
sha256: kv.baseline_sha256 ?? '',
|
|
162
258
|
},
|
|
163
259
|
transport: kv.transport && kv.transport !== '-' ? kv.transport : null,
|
|
164
|
-
summary
|
|
165
|
-
fixed: intOr(kv, 'fixed', 0),
|
|
166
|
-
skipped: intOr(kv, 'skipped', 0),
|
|
167
|
-
failed: intOr(kv, 'failed', 0),
|
|
168
|
-
pending: intOr(kv, 'pending', 0),
|
|
169
|
-
},
|
|
260
|
+
summary,
|
|
170
261
|
fixes,
|
|
171
262
|
};
|
|
172
263
|
const items = fixes.map((f) => {
|
package/dist/projects/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
export { planContentSchema, parsePlanContent, PLAN_LAYERS, PLAN_DELEGATE_SKILLS, PLAN_PHASE_STATUSES } from './plan-schema.js';
|
|
2
|
+
export { extractPlan } from './extract-plan.js';
|
|
3
|
+
export { statusesFromItems, computeNextPhase, renderPlanTracker, buildPlanRevision, isPhaseSatisfied } from './plan-run.js';
|
|
1
4
|
export { validateEnvelope } from './validate.js';
|
|
2
5
|
export { canonicalSha256 } from './canonicalize.js';
|
|
3
6
|
export { titleSlug, shortId, projectFilename } from './filename.js';
|
|
4
|
-
export { runSaveCommand } from './save-command.js';
|
|
7
|
+
export { runSaveCommand, detectArtifactSkill, peekPlanManifestTitle, resolvePriorPlanEnvelope } from './save-command.js';
|
|
8
|
+
export { offerAnswerBlockers, openBlockers, applyBlockerAnswers } from './answer-blockers.js';
|
|
9
|
+
export { mergeCcaAssessments, renderCcaMergeManifest, parseScopePatterns, scopePatternsIntersect, CCA_MANIFEST_ROW_CAP } from './merge-cca.js';
|
|
10
|
+
export { mergeUpgradeProgress, renderUpgradeMergeManifest } from './merge-upgrade.js';
|
|
5
11
|
export { extractDesign } from './extract-design.js';
|
|
6
12
|
export { extractEstimate } from './extract-estimate.js';
|
|
7
13
|
export { buildPromotedFromSnapshot, validatePromotionSource } from './promote.js';
|