@cspeach/cli 1.1.14 → 1.1.15
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 +63 -0
- package/dist/cli.js +81 -27
- package/dist/commands/project.js +301 -0
- package/dist/commands/team.js +1166 -0
- package/dist/projects/save-command.js +20 -0
- package/dist/register/atc-file.js +802 -0
- package/dist/register/baseline.js +106 -0
- package/dist/register/measure.js +380 -0
- package/dist/register/records.js +159 -0
- package/dist/register/store.js +227 -0
- package/dist/repl.js +10 -2
- package/dist/rewind/candidates.js +14 -1
- package/dist/rewind/restore.js +53 -19
- package/dist/skill-catalog.js +33 -0
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/filesystem/extract-document.js +6 -0
- package/dist/tools/filesystem/extract-xlsx.js +139 -0
- package/dist/tools/filesystem/file-write.js +20 -0
- package/dist/tools/filesystem/read-document.js +15 -6
- package/dist/tools/include-snapshot.js +24 -0
- package/dist/tools/sap-write.js +19 -9
- package/dist/tools/snapshot.js +27 -0
- package/package.json +9 -6
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Register on disk (spec §3, §3a).
|
|
3
|
+
*
|
|
4
|
+
* Two places, both chosen by a person, never by the model:
|
|
5
|
+
* - the TEAM PROJECT FOLDER the lead creates: cspeach-team.json, baseline/,
|
|
6
|
+
* lanes.json, rulings/. Collected records sit under writers/.
|
|
7
|
+
* - this laptop's OUTBOX: where this machine's records are written. It mirrors
|
|
8
|
+
* the project-folder layout (writers/<id>/records/…) so that handing records
|
|
9
|
+
* in is a plain folder copy. By default it lives under ~/.cspeach; a developer
|
|
10
|
+
* may point it inside their own OneDrive-synced folder instead.
|
|
11
|
+
*
|
|
12
|
+
* CSPeach writes only to paths on this list. It never writes to a network
|
|
13
|
+
* location on its own initiative.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
20
|
+
export const TEAM_FILE = 'cspeach-team.json';
|
|
21
|
+
export const BASELINE_FILE = path.join('baseline', 'objects.json');
|
|
22
|
+
export const LANES_FILE = 'lanes.json';
|
|
23
|
+
export class RegisterError extends Error {
|
|
24
|
+
}
|
|
25
|
+
export const defaultRand = (bytes) => randomBytes(bytes).toString('hex');
|
|
26
|
+
const stateFile = () => path.join(cspeachRoot(), 'register.json');
|
|
27
|
+
export function loadState() {
|
|
28
|
+
try {
|
|
29
|
+
const s = JSON.parse(stripBom(fs.readFileSync(stateFile(), 'utf8')));
|
|
30
|
+
if (s && typeof s === 'object' && s.projects)
|
|
31
|
+
return { active: s.active ?? null, projects: s.projects };
|
|
32
|
+
}
|
|
33
|
+
catch { /* first use, or unreadable: start empty */ }
|
|
34
|
+
return { active: null, projects: {} };
|
|
35
|
+
}
|
|
36
|
+
export function saveState(state) {
|
|
37
|
+
fs.mkdirSync(cspeachRoot(), { recursive: true });
|
|
38
|
+
fs.writeFileSync(stateFile(), `${JSON.stringify(state, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
/** The team project this laptop is recording against, or null. Cheap: one small file read. */
|
|
41
|
+
export function activeProject() {
|
|
42
|
+
const s = loadState();
|
|
43
|
+
if (!s.active || !s.projects[s.active])
|
|
44
|
+
return null;
|
|
45
|
+
return { projectId: s.active, ...s.projects[s.active] };
|
|
46
|
+
}
|
|
47
|
+
export function defaultOutboxDir(projectId) {
|
|
48
|
+
return path.join(cspeachRoot(), 'register', projectId, 'outbox');
|
|
49
|
+
}
|
|
50
|
+
/** `mike-khan-7f3a`: the person plus this machine, so one file never has two writers. */
|
|
51
|
+
export function writerIdFor(name, machine = os.hostname()) {
|
|
52
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'writer';
|
|
53
|
+
return `${slug}-${createHash('sha256').update(machine.toLowerCase()).digest('hex').slice(0, 4)}`;
|
|
54
|
+
}
|
|
55
|
+
export function projectIdFor(name, rand = defaultRand) {
|
|
56
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'project';
|
|
57
|
+
return `${slug}-${rand(2)}`;
|
|
58
|
+
}
|
|
59
|
+
export const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
60
|
+
/**
|
|
61
|
+
* A byte-order mark, taken off by NUMBER. Notepad, Excel and a few sync tools
|
|
62
|
+
* put one in front of a file they save as UTF-8; `JSON.parse` then refuses the
|
|
63
|
+
* whole file. Written as a character in a regex it is invisible in the source —
|
|
64
|
+
* which is how two of these were copied around and a third was forgotten — so
|
|
65
|
+
* this is the one place that knows about it, and it names the code point.
|
|
66
|
+
*/
|
|
67
|
+
export const stripBom = (text) => (text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
68
|
+
function readJson(file) {
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = fs.readFileSync(file, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new RegisterError(`Cannot read ${file}`);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(stripBom(text));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new RegisterError(`${file} is not valid JSON`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function readProjectFile(projectDir) {
|
|
84
|
+
const p = readJson(path.join(projectDir, TEAM_FILE));
|
|
85
|
+
if (p?.kind !== 'team-project' || typeof p.projectId !== 'string' || typeof p.lead?.name !== 'string') {
|
|
86
|
+
throw new RegisterError(`${path.join(projectDir, TEAM_FILE)} is not a CSPeach team project file`);
|
|
87
|
+
}
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What SAP accepts as a check variant name. Checked by every caller BEFORE a
|
|
92
|
+
* run, not only at the moment of writing: a name that cannot be written must
|
|
93
|
+
* not be measured with either.
|
|
94
|
+
*/
|
|
95
|
+
export const VARIANT_NAME = /^[A-Z0-9_/]{1,40}$/;
|
|
96
|
+
/**
|
|
97
|
+
* The one field of the team project file that is set after `init`: which ATC
|
|
98
|
+
* check variant this team project measures with. Written whole, then renamed,
|
|
99
|
+
* like a record — a sync tool must never pick up half a team project file.
|
|
100
|
+
*/
|
|
101
|
+
export function writeProjectVariant(projectDir, variant) {
|
|
102
|
+
if (!VARIANT_NAME.test(variant))
|
|
103
|
+
throw new RegisterError(`"${variant}" is not an ATC check variant name.`);
|
|
104
|
+
const file = path.join(projectDir, TEAM_FILE);
|
|
105
|
+
const next = { ...readProjectFile(projectDir), atcVariant: variant };
|
|
106
|
+
fs.writeFileSync(`${file}.tmp`, `${JSON.stringify(next, null, 2)}\n`);
|
|
107
|
+
fs.renameSync(`${file}.tmp`, file);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One read, one sentence. `existsSync` then `readFileSync` answers only the
|
|
111
|
+
* easy failure: a folder halfway through a sync also gives EISDIR (a folder
|
|
112
|
+
* where the file goes), EBUSY and EPERM (a file the sync tool still holds, or
|
|
113
|
+
* one OneDrive has not fetched). None of those may reach a person as a stack.
|
|
114
|
+
*/
|
|
115
|
+
function readTextOrSay(file, missing) {
|
|
116
|
+
try {
|
|
117
|
+
return fs.readFileSync(file, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
if (e?.code === 'ENOENT')
|
|
121
|
+
throw new RegisterError(missing);
|
|
122
|
+
throw new RegisterError(`${file} could not be read. It may still be syncing.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function readBaseline(projectDir) {
|
|
126
|
+
const project = readProjectFile(projectDir);
|
|
127
|
+
const file = path.join(projectDir, project.baseline?.file ?? BASELINE_FILE);
|
|
128
|
+
const text = readTextOrSay(file, `The baseline file is missing: ${file}`);
|
|
129
|
+
if (project.baseline?.sha256 && sha256(text) !== project.baseline.sha256) {
|
|
130
|
+
throw new RegisterError(`${file} does not match the checksum in the team project file. It was changed or only partly copied.`);
|
|
131
|
+
}
|
|
132
|
+
// A folder halfway through a sync is the everyday reason this file is not
|
|
133
|
+
// whole JSON. A person reads a sentence about their own folder here, never a
|
|
134
|
+
// parser's stack trace about a position in a byte stream.
|
|
135
|
+
let b;
|
|
136
|
+
try {
|
|
137
|
+
b = JSON.parse(stripBom(text));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw new RegisterError(`${file} could not be read. It may still be syncing.`);
|
|
141
|
+
}
|
|
142
|
+
if (b?.kind !== 'baseline' || !Array.isArray(b.objects))
|
|
143
|
+
throw new RegisterError(`${file} is not a baseline file`);
|
|
144
|
+
// Every caller reads `o.type` and `o.name` straight away. One hand-edited or
|
|
145
|
+
// half-copied entry that is not an object would come out as a TypeError with
|
|
146
|
+
// a stack, from whichever command happened to touch it first.
|
|
147
|
+
if (b.objects.some((o) => !o || typeof o.type !== 'string' || typeof o.name !== 'string')) {
|
|
148
|
+
throw new RegisterError(`${file} could not be read. It may still be syncing.`);
|
|
149
|
+
}
|
|
150
|
+
return b.objects;
|
|
151
|
+
}
|
|
152
|
+
export function readLanes(projectDir) {
|
|
153
|
+
const file = path.join(projectDir, LANES_FILE);
|
|
154
|
+
if (!fs.existsSync(file))
|
|
155
|
+
return [];
|
|
156
|
+
const l = readJson(file);
|
|
157
|
+
return Array.isArray(l?.lanes) ? l.lanes : [];
|
|
158
|
+
}
|
|
159
|
+
export function writeLanes(projectDir, projectId, lanes) {
|
|
160
|
+
fs.writeFileSync(path.join(projectDir, LANES_FILE), `${JSON.stringify({ kind: 'lanes', formatVersion: 1, projectId, lanes }, null, 2)}\n`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A record is a short list of object names. Anything this big is a dump, a log
|
|
164
|
+
* or something that is not a record at all, and reading it would cost a
|
|
165
|
+
* gigabyte of memory before `JSON.parse` could say so.
|
|
166
|
+
*/
|
|
167
|
+
const MAX_RECORD_BYTES = 64 * 1024 * 1024;
|
|
168
|
+
/** One record file, or nothing. Counted as unreadable rather than thrown, whatever is wrong with it. */
|
|
169
|
+
function readRecord(file) {
|
|
170
|
+
try {
|
|
171
|
+
if (fs.statSync(file).size > MAX_RECORD_BYTES)
|
|
172
|
+
return { ok: false };
|
|
173
|
+
return { ok: true, value: JSON.parse(stripBom(fs.readFileSync(file, 'utf8'))) };
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// A file can be listed before the sync tool has finished writing it, or be a
|
|
177
|
+
// folder, or be held open: skip and count, never fail.
|
|
178
|
+
return { ok: false };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Every record-shaped JSON under a folder: writers/<id>/records, rulings, baseline/measurement-*.json. */
|
|
182
|
+
export function collectRecords(root) {
|
|
183
|
+
const records = [];
|
|
184
|
+
let unreadable = 0;
|
|
185
|
+
let files = 0;
|
|
186
|
+
const take = (file) => {
|
|
187
|
+
files += 1;
|
|
188
|
+
const read = readRecord(file);
|
|
189
|
+
if (read.ok)
|
|
190
|
+
records.push(read.value);
|
|
191
|
+
else
|
|
192
|
+
unreadable += 1;
|
|
193
|
+
};
|
|
194
|
+
const readDir = (dir) => {
|
|
195
|
+
let names = [];
|
|
196
|
+
try {
|
|
197
|
+
names = fs.readdirSync(dir);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
if (!name.toLowerCase().endsWith('.json'))
|
|
204
|
+
continue;
|
|
205
|
+
take(path.join(dir, name));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
let writers = [];
|
|
209
|
+
try {
|
|
210
|
+
writers = fs.readdirSync(path.join(root, 'writers'));
|
|
211
|
+
}
|
|
212
|
+
catch { /* none handed in yet */ }
|
|
213
|
+
for (const w of writers)
|
|
214
|
+
readDir(path.join(root, 'writers', w, 'records'));
|
|
215
|
+
readDir(path.join(root, 'rulings'));
|
|
216
|
+
try {
|
|
217
|
+
for (const name of fs.readdirSync(path.join(root, 'baseline'))) {
|
|
218
|
+
if (!/^measurement-.*\.json$/i.test(name))
|
|
219
|
+
continue;
|
|
220
|
+
// Measurement zero lands here, written by the lead and then synced: the
|
|
221
|
+
// same hazards as any other record, read the same way.
|
|
222
|
+
take(path.join(root, 'baseline', name));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { /* no baseline folder here (an outbox has none) */ }
|
|
226
|
+
return { records, unreadable, files };
|
|
227
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -2482,8 +2482,16 @@ export async function runRepl(opts) {
|
|
|
2482
2482
|
// user re-submits the empty pre-fill, just silently re-fill
|
|
2483
2483
|
// without re-spamming the hint line. Tracked via lastEmptyPrefill.
|
|
2484
2484
|
if (lastEmptyPrefill !== head) {
|
|
2485
|
-
|
|
2486
|
-
|
|
2485
|
+
// 2026-09-23 (Glenn's first run): "type your prompt" never said WHAT
|
|
2486
|
+
// to type, and our own skills suggest a bare next command
|
|
2487
|
+
// ("/abap-estimate next"), so the user is told to run something and
|
|
2488
|
+
// then asked an unanswerable question. Every skill now carries an
|
|
2489
|
+
// `asks` line with an example (skill-catalog.ts).
|
|
2490
|
+
const asks = SKILL_CATALOG.find((e) => `/${e.name}` === head)?.asks;
|
|
2491
|
+
console.log(chalk.dim(asks
|
|
2492
|
+
? `↪ ${head} — ${asks}`
|
|
2493
|
+
: `${'↪'} ${head} — type your prompt and press Enter`));
|
|
2494
|
+
console.log(chalk.dim(' Enter to send · /cancel to abandon'));
|
|
2487
2495
|
lastEmptyPrefill = head;
|
|
2488
2496
|
}
|
|
2489
2497
|
// 2026-05-02: bake the pre-fill into the next prompt string
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* - sap_delete_object ⇒ restorable when a pre-delete snapshot exists (the
|
|
25
25
|
* snapshot IS the prior source; the executor recreates it).
|
|
26
26
|
*/
|
|
27
|
+
import { includeSnapshotType } from '../tools/include-snapshot.js';
|
|
27
28
|
/** The five write tools whose effects can (potentially) be rewound. */
|
|
28
29
|
const WRITE_TOOLS = new Set([
|
|
29
30
|
'sap_set_source',
|
|
@@ -116,7 +117,19 @@ function extractIdentity(tool, rawArgs) {
|
|
|
116
117
|
const args = (rawArgs ?? {});
|
|
117
118
|
if (IMPLICIT_CLASS_TOOLS.has(tool)) {
|
|
118
119
|
const object = pickString(args, 'className') ?? pickString(args, 'class_name');
|
|
119
|
-
|
|
120
|
+
if (!object)
|
|
121
|
+
return null;
|
|
122
|
+
// An include write is rewound from the INCLUDE's own snapshot
|
|
123
|
+
// (type CLAS.<INCLUDE>), never from the class main source — restoring the
|
|
124
|
+
// main source would report success while the include change stayed.
|
|
125
|
+
// No includeType recorded ⇒ unknown target ⇒ not rewindable.
|
|
126
|
+
if (tool === 'sap_set_class_include') {
|
|
127
|
+
const includeType = pickString(args, 'includeType');
|
|
128
|
+
return includeType
|
|
129
|
+
? { object: object.toUpperCase(), objType: includeSnapshotType(includeType) }
|
|
130
|
+
: null;
|
|
131
|
+
}
|
|
132
|
+
return { object: object.toUpperCase(), objType: 'CLAS' };
|
|
120
133
|
}
|
|
121
134
|
const object = pickString(args, 'name') ?? pickString(args, 'object_name');
|
|
122
135
|
if (!object)
|
package/dist/rewind/restore.js
CHANGED
|
@@ -27,6 +27,7 @@ import { getEffectiveWriteMode } from '../repl/mode-cycle.js';
|
|
|
27
27
|
import { effectiveRisk } from '../approvals/risk-floor.js';
|
|
28
28
|
import { verifySyntax, verifyActive } from '../tools/verify.js';
|
|
29
29
|
import { renderDiff } from '../repl/diff-display.js';
|
|
30
|
+
import { parseIncludeSnapshotType } from '../tools/include-snapshot.js';
|
|
30
31
|
/** Compact single-line failure text for receipts / error envelopes. */
|
|
31
32
|
function shortError(err) {
|
|
32
33
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -55,6 +56,10 @@ export async function restoreSnapshot(c, deps) {
|
|
|
55
56
|
if (c.snapshotId === null) {
|
|
56
57
|
return { ok: false, steps, error: 'no snapshot to restore from' };
|
|
57
58
|
}
|
|
59
|
+
// A class-include candidate (objType CLAS.<INCLUDE>) restores that include;
|
|
60
|
+
// syntax check / activation / verify then run on the CLASS.
|
|
61
|
+
const include = parseIncludeSnapshotType(c.objType);
|
|
62
|
+
const repoType = include ? 'CLAS' : c.objType;
|
|
58
63
|
// ── Step 2: Read the snapshot source (the content we will restore) ────────
|
|
59
64
|
let snapshotSource;
|
|
60
65
|
try {
|
|
@@ -67,12 +72,23 @@ export async function restoreSnapshot(c, deps) {
|
|
|
67
72
|
// A 404 means the object was deleted this session; current = null and we
|
|
68
73
|
// will recreate it. Any OTHER read failure is NOT proof of absence — stop
|
|
69
74
|
// rather than blindly treat it as a create.
|
|
75
|
+
// Class include candidates (objType CLAS.<INCLUDE>) read and write the
|
|
76
|
+
// include itself; there is no delete-restore for an include, so a 404 stops.
|
|
70
77
|
let currentSource;
|
|
71
78
|
try {
|
|
72
|
-
|
|
79
|
+
if (include) {
|
|
80
|
+
const inc = await deps.adt.classIncludes(c.object, include);
|
|
81
|
+
if (typeof inc !== 'string') {
|
|
82
|
+
return { ok: false, steps, error: 'could not read current include source' };
|
|
83
|
+
}
|
|
84
|
+
currentSource = inc;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
currentSource = await deps.adt.getSource(c.objType, c.object);
|
|
88
|
+
}
|
|
73
89
|
}
|
|
74
90
|
catch (err) {
|
|
75
|
-
if (err instanceof SapError && err.httpStatus === 404) {
|
|
91
|
+
if (err instanceof SapError && err.httpStatus === 404 && !include) {
|
|
76
92
|
currentSource = null;
|
|
77
93
|
}
|
|
78
94
|
else {
|
|
@@ -87,7 +103,7 @@ export async function restoreSnapshot(c, deps) {
|
|
|
87
103
|
const change = {
|
|
88
104
|
op,
|
|
89
105
|
object: c.object,
|
|
90
|
-
type:
|
|
106
|
+
type: repoType,
|
|
91
107
|
diff: renderDiff(currentSource ?? '', snapshotSource, {
|
|
92
108
|
label: `${c.objType}/${c.object}`,
|
|
93
109
|
syntaxHighlight: false,
|
|
@@ -143,20 +159,38 @@ export async function restoreSnapshot(c, deps) {
|
|
|
143
159
|
return { ok: false, steps, error: `recreate failed: ${created.error ?? `HTTP ${created.httpStatus}`}` };
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
if (include) {
|
|
163
|
+
// Include restore: write the include back. setClassInclude reads its own
|
|
164
|
+
// write back and returns written:false on a discarded or refused PUT.
|
|
165
|
+
let incResult;
|
|
166
|
+
try {
|
|
167
|
+
incResult = await deps.adt.setClassInclude(c.object, include, snapshotSource, deps.transport);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
record('② written ✗');
|
|
171
|
+
return { ok: false, steps, error: `write failed: ${shortError(err)}` };
|
|
172
|
+
}
|
|
173
|
+
if (!incResult.written) {
|
|
174
|
+
record('② written ✗');
|
|
175
|
+
return { ok: false, steps, error: `write failed: ${incResult.error ?? `HTTP ${incResult.httpStatus}`}` };
|
|
176
|
+
}
|
|
153
177
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
else {
|
|
179
|
+
let writeResult;
|
|
180
|
+
try {
|
|
181
|
+
writeResult = await deps.adt.setSource(c.objType, c.object, snapshotSource, deps.transport);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
record('② written ✗');
|
|
185
|
+
return { ok: false, steps, error: `write failed: ${shortError(err)}` };
|
|
186
|
+
}
|
|
187
|
+
// setSource RETURNS (does not throw) on HTTP 4xx — check the return value,
|
|
188
|
+
// exactly as sap_set_source does.
|
|
189
|
+
if (writeResult.error || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
|
|
190
|
+
const detail = writeResult.error ?? `HTTP ${writeResult.writeStatus}`;
|
|
191
|
+
record('② written ✗');
|
|
192
|
+
return { ok: false, steps, error: `write failed: ${detail}` };
|
|
193
|
+
}
|
|
160
194
|
}
|
|
161
195
|
record('② written ✓');
|
|
162
196
|
// ── Step 7: Syntax check (Rule 10) — errors ⇒ STOP, do NOT activate ───────
|
|
@@ -167,7 +201,7 @@ export async function restoreSnapshot(c, deps) {
|
|
|
167
201
|
// WRITE HAPPENED; only the verification/activation call failed to run.
|
|
168
202
|
let syntax;
|
|
169
203
|
try {
|
|
170
|
-
syntax = await verifySyntax(deps.adt, c.object,
|
|
204
|
+
syntax = await verifySyntax(deps.adt, c.object, repoType);
|
|
171
205
|
}
|
|
172
206
|
catch (err) {
|
|
173
207
|
record(`③ syntax — check failed to run: ${shortError(err)}`);
|
|
@@ -191,7 +225,7 @@ export async function restoreSnapshot(c, deps) {
|
|
|
191
225
|
// ── Step 8: Activate + verify (Rule 10) ───────────────────────────────────
|
|
192
226
|
let activation;
|
|
193
227
|
try {
|
|
194
|
-
activation = await deps.adt.activate(
|
|
228
|
+
activation = await deps.adt.activate(repoType, c.object);
|
|
195
229
|
}
|
|
196
230
|
catch (err) {
|
|
197
231
|
record(`④ activate — call failed: ${shortError(err)}`);
|
|
@@ -217,7 +251,7 @@ export async function restoreSnapshot(c, deps) {
|
|
|
217
251
|
// past the receipt.
|
|
218
252
|
let active;
|
|
219
253
|
try {
|
|
220
|
-
active = await verifyActive(deps.adt, c.object,
|
|
254
|
+
active = await verifyActive(deps.adt, c.object, repoType);
|
|
221
255
|
}
|
|
222
256
|
catch (err) {
|
|
223
257
|
active = { active: false, verifyError: shortError(err) };
|
package/dist/skill-catalog.js
CHANGED
|
@@ -19,36 +19,42 @@ export const SKILL_CATALOG = [
|
|
|
19
19
|
category: 'Read / Analyze',
|
|
20
20
|
description: 'Impact analysis before changes — traces usage, callers, transports',
|
|
21
21
|
whenToUse: 'pick this for blast radius before a change (not /abap-explain — explain = teach the code)',
|
|
22
|
+
asks: 'which object? e.g. "ZCL_ORDER_UTILS" or "what breaks if I change ZSD_ORDER_RELEASE"',
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
name: 'abap-explain',
|
|
25
26
|
category: 'Read / Analyze',
|
|
26
27
|
description: 'Understand an object — plain-language (default) or `technical` orientation (deps, tables, risks)',
|
|
27
28
|
whenToUse: `plain teaching, or technical=fast orientation (not /abap-impact = blast radius)`,
|
|
29
|
+
asks: 'which object, plain or technical? e.g. "ZFI_DUNNING_SELECT in plain words"',
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
32
|
name: 'abap-review',
|
|
31
33
|
category: 'Read / Analyze',
|
|
32
34
|
description: 'Read-only review across lenses: quality (default), performance, cloud-readiness',
|
|
33
35
|
whenToUse: `grade source: quality (default) + performance/cloud lenses`,
|
|
36
|
+
asks: 'which object, and which lens? e.g. "ZCL_CREDIT_SVC for performance"',
|
|
34
37
|
},
|
|
35
38
|
{
|
|
36
39
|
name: 'abap-clean-core',
|
|
37
40
|
category: 'Read / Analyze',
|
|
38
41
|
description: 'Clean-core decision & defense — recommend the clean-core-correct approach, classify the level (A/B/C/D), produce citable evidence; verifies live when connected. Read-only.',
|
|
39
42
|
whenToUse: `pick this for "is this clean-core-OK?" decisions + defendable evidence (not /abap-review cloud lens — that grades code)`,
|
|
43
|
+
asks: 'which object or requirement? e.g. "can I add a field to VBAK the clean way"',
|
|
40
44
|
},
|
|
41
45
|
{
|
|
42
46
|
name: 'abap-handover',
|
|
43
47
|
category: 'Read / Analyze',
|
|
44
48
|
description: 'Documentation for one object (`object` scope) or a whole package (`package` scope)',
|
|
45
49
|
whenToUse: `object doc OR full package handover dossier`,
|
|
50
|
+
asks: 'which object or package? e.g. "package ZTEST_LAS, full handover"',
|
|
46
51
|
},
|
|
47
52
|
{
|
|
48
53
|
name: 'abap-dump',
|
|
49
54
|
category: 'Read / Analyze',
|
|
50
55
|
description: 'Diagnose ABAP short dumps (ST22) and fix them — root cause, call stack, fix with approval; ask for "triage" for ranked read-only hypotheses',
|
|
51
56
|
whenToUse: `pick this for any ST22 dump — diagnose + fix by default, say "triage" for read-only ranking (not /abap-incident — that's the full dump-to-transport pipeline)`,
|
|
57
|
+
asks: 'which dump? e.g. "the dump in ZFIN_DOC_JOURNAL this morning"',
|
|
52
58
|
},
|
|
53
59
|
// ── Modify / Fix ─────────────────────────────────────────────────────────
|
|
54
60
|
{
|
|
@@ -56,48 +62,56 @@ export const SKILL_CATALOG = [
|
|
|
56
62
|
category: 'Modify / Fix',
|
|
57
63
|
description: 'Run ATC checks against an object and auto-fix findings with safety gates',
|
|
58
64
|
whenToUse: `pick this for ad-hoc ATC fix on one object (not /abap-upgrade-fix — that's worklist-driven)`,
|
|
65
|
+
asks: 'which object or package? e.g. "ZCL_ORDER_UTILS, fix what is safe"',
|
|
59
66
|
},
|
|
60
67
|
{
|
|
61
68
|
name: 'abap-refactor',
|
|
62
69
|
category: 'Modify / Fix',
|
|
63
70
|
description: 'Refactor a Z object in place — FORM→method, SELECT *→specific fields, guard clauses, inline DATA. Same architecture, cleaner code.',
|
|
64
71
|
whenToUse: `pick this to clean up YOUR Z code, same architecture (not /abap-modernize — that rebuilds as RAP/Fiori)`,
|
|
72
|
+
asks: 'which object and what change? e.g. "ZLEGACY_FI_REPORT — add a net amount column"',
|
|
65
73
|
},
|
|
66
74
|
{
|
|
67
75
|
name: 'abap-enhance',
|
|
68
76
|
category: 'Modify / Fix',
|
|
69
77
|
description: 'Extend SAP standard via BAdI implementations, enhancement spots, user exits',
|
|
70
78
|
whenToUse: `pick this to hook into SAP-STANDARD code via BAdI/exit (not /abap-refactor — that's for YOUR Z code)`,
|
|
79
|
+
asks: 'which SAP object and what behaviour? e.g. "block goods issue when the credit check fails"',
|
|
71
80
|
},
|
|
72
81
|
{
|
|
73
82
|
name: 'abap-extend-model',
|
|
74
83
|
category: 'Modify / Fix',
|
|
75
84
|
description: 'Add a field or @UI column to an EXISTING custom CDS view / DDLX metadata extension in place — anchored insertion, snapshot, syntax-check, activate, republish-if-needed. The Fiori Elements column-add.',
|
|
76
85
|
whenToUse: `pick this to add a field/column to an EXISTING custom CDS/DDLX in place (not /abap-rap — that's a new stack; not /abap-refactor — that's ABAP code, not CDS)`,
|
|
86
|
+
asks: 'which view or BO, and what to add? e.g. "add ShippingPoint to the ZC_Delivery list"',
|
|
77
87
|
},
|
|
78
88
|
{
|
|
79
89
|
name: 'abap-incident',
|
|
80
90
|
category: 'Modify / Fix',
|
|
81
91
|
description: 'End-to-end incident pipeline — dump to fix deployed to transport in one skill',
|
|
82
92
|
whenToUse: `pick this for full ST22-to-transport pipeline with formal report (not /abap-dump — single fix only)`,
|
|
93
|
+
asks: 'the incident. e.g. "dump in ZOPEN_ORDER_REPORT, users blocked"',
|
|
83
94
|
},
|
|
84
95
|
{
|
|
85
96
|
name: 'abap-test',
|
|
86
97
|
category: 'Modify / Fix',
|
|
87
98
|
description: 'Generate ABAP Unit test classes for existing code with dependency injection',
|
|
88
99
|
whenToUse: `pick this to generate + run ABAP Unit tests (not /abap-eml — EML body only)`,
|
|
100
|
+
asks: 'which class? e.g. "unit tests for ZCL_CREDIT_SVC"',
|
|
89
101
|
},
|
|
90
102
|
{
|
|
91
103
|
name: 'abap-eml',
|
|
92
104
|
category: 'Modify / Fix',
|
|
93
105
|
description: 'Write and test EML (Entity Manipulation Language) for RAP business objects',
|
|
94
106
|
whenToUse: `pick this for EML inside a RAP context (not /abap-test — that's the test class)`,
|
|
107
|
+
asks: 'what the EML should do. e.g. "create a draft inspection and activate it"',
|
|
95
108
|
},
|
|
96
109
|
{
|
|
97
110
|
name: 'abap-segw',
|
|
98
111
|
category: 'Modify / Fix',
|
|
99
112
|
description: 'Implement SEGW OData DPC_EXT method bodies — reads generated MPC/DPC base classes. SEGW project itself created in transaction SEGW. For S/4 1909+ RAP services use /abap-rap.',
|
|
100
113
|
whenToUse: `pick this for DPC_EXT method bodies on an EXISTING SEGW project (ECC / older S/4; not /abap-rap — that's S/4 1909+ RAP; SEGW project + MPC/DPC must already exist)`,
|
|
114
|
+
asks: 'which project and operation? e.g. "GET_ENTITYSET for ZORDER_SRV"',
|
|
101
115
|
},
|
|
102
116
|
// ── Create / Generate ────────────────────────────────────────────────────
|
|
103
117
|
{
|
|
@@ -105,30 +119,35 @@ export const SKILL_CATALOG = [
|
|
|
105
119
|
category: 'Create / Generate',
|
|
106
120
|
description: 'Generate ONE ABAP class / program / include from a clear scope — asks clarifying questions first per Forge Rule 1. For a full RAP stack use /abap-rap; for DDIC only use /abap-data-model.',
|
|
107
121
|
whenToUse: `pick this for ONE class/program/include from clear scope (not /abap-rap — that's a full RAP stack; not /abap-data-model — that's DDIC only)`,
|
|
122
|
+
asks: 'what to build, or the scope. e.g. "all changes" after a design, or "a report of open orders by plant"',
|
|
108
123
|
},
|
|
109
124
|
{
|
|
110
125
|
name: 'abap-rap',
|
|
111
126
|
category: 'Create / Generate',
|
|
112
127
|
description: 'Scaffold a complete RAP stack on S/4HANA 1909+ — table, CDS interface, projection, BDEF, BP class, SRVD, SRVB, DDLX, DCL. ECC/older S/4 → use /abap-segw.',
|
|
113
128
|
whenToUse: `pick this for a full RAP stack on S/4 1909+ (10 objects, table → SRVB; not /abap-segw — that's ECC/older S/4 OData; not /abap-generate — that's one object)`,
|
|
129
|
+
asks: 'the business object. e.g. "maintenance requests with draft and an approve action"',
|
|
114
130
|
},
|
|
115
131
|
{
|
|
116
132
|
name: 'abap-fiori-build',
|
|
117
133
|
category: 'Create / Generate',
|
|
118
134
|
description: 'Build a working freestyle SAPUI5 app on the laptop from a published OData service — scaffold, pages, dialogs, chart, value help, local preview. Freestyle only, not Fiori Elements.',
|
|
119
135
|
whenToUse: `pick this for a freestyle UI5 frontend on a published OData service (not /abap-rap — that's the backend; produces local files, not ADT objects)`,
|
|
136
|
+
asks: 'which service and what UI? e.g. "list + object page on ZUI_LAS_INSPECTIONLOG_O4"',
|
|
120
137
|
},
|
|
121
138
|
{
|
|
122
139
|
name: 'abap-data-model',
|
|
123
140
|
category: 'Create / Generate',
|
|
124
141
|
description: 'Design and create DDIC objects — domains, data elements, tables, structures',
|
|
125
142
|
whenToUse: `pick this for DDIC ONLY — domain/data element/table/structure (step after /abap-design; embedded inside /abap-rap step 1 — call standalone when you don't need the rest of the stack)`,
|
|
143
|
+
asks: 'the tables and fields. e.g. "inspection results, header and items"',
|
|
126
144
|
},
|
|
127
145
|
{
|
|
128
146
|
name: 'abap-modernize',
|
|
129
147
|
category: 'Create / Generate',
|
|
130
148
|
description: 'Transform classic ABAP to the best modern architecture for the target release (ECC→SEGW, S/4→RAP)',
|
|
131
149
|
whenToUse: `pick this to rebuild a WRITE/dynpro/FM as Fiori/RAP (not /abap-upgrade-fix — that's syntax patches)`,
|
|
150
|
+
asks: 'which program? e.g. "ZLEGACY_SD_PROCESS onto a modern stack"',
|
|
132
151
|
},
|
|
133
152
|
// ── Upgrade / Migrate ────────────────────────────────────────────────────
|
|
134
153
|
{
|
|
@@ -136,42 +155,49 @@ export const SKILL_CATALOG = [
|
|
|
136
155
|
category: 'Upgrade / Migrate',
|
|
137
156
|
description: 'Custom code analysis pipeline for S/4 upgrade — whole-estate inventory + tier classification + ATC findings + report. Multi-session project with state file.',
|
|
138
157
|
whenToUse: `pick this to discover + classify the whole custom estate (not /abap-migrate — that's per object)`,
|
|
158
|
+
asks: 'which package or namespace? e.g. "Z*" or "package ZTEST_LAS"',
|
|
139
159
|
},
|
|
140
160
|
{
|
|
141
161
|
name: 'abap-upgrade-scan',
|
|
142
162
|
category: 'Upgrade / Migrate',
|
|
143
163
|
description: 'Scan custom ABAP code for S/4HANA upgrade findings via ATC readiness variant',
|
|
144
164
|
whenToUse: 'pick this to baseline ATC for one package + S/4 release (step 1 of 3 — feeds /abap-upgrade-fix)',
|
|
165
|
+
asks: 'the scope. e.g. "package ZTEST_LAS" or "Z* with the S/4 readiness variant"',
|
|
145
166
|
},
|
|
146
167
|
{
|
|
147
168
|
name: 'abap-upgrade-fix',
|
|
148
169
|
category: 'Upgrade / Migrate',
|
|
149
170
|
description: 'AI-guided upgrade remediation — fixes ATC findings one object at a time with approval gates',
|
|
150
171
|
whenToUse: 'pick this to fix the upgrade-scan baseline object-by-object (step 2 of 3 — needs /abap-upgrade-scan first)',
|
|
172
|
+
asks: 'which findings. e.g. "@upgrade-progress" or "the API findings in ZCL_ORDER_UTILS"',
|
|
151
173
|
},
|
|
152
174
|
{
|
|
153
175
|
name: 'abap-upgrade-verify',
|
|
154
176
|
category: 'Upgrade / Migrate',
|
|
155
177
|
description: 'Verify remediation results — re-runs ATC with same variant, compares before/after finding counts',
|
|
156
178
|
whenToUse: 'pick this for the customer sign-off report after fixing (step 3 of 3 — re-runs ATC vs baseline)',
|
|
179
|
+
asks: 'which scan to verify against. e.g. "@upgrade-progress from the first scan"',
|
|
157
180
|
},
|
|
158
181
|
{
|
|
159
182
|
name: 'abap-upgrade-merge',
|
|
160
183
|
category: 'Upgrade / Migrate',
|
|
161
184
|
description: 'Reconcile multiple parallel /abap-upgrade-fix progress files into one consolidated upgrade-progress',
|
|
162
185
|
whenToUse: 'pick this when a multi-dev team ran scoped fix slices in parallel — merges N progress files for /abap-upgrade-verify',
|
|
186
|
+
asks: 'which progress files. e.g. "merge the three @upgrade-progress files here"',
|
|
163
187
|
},
|
|
164
188
|
{
|
|
165
189
|
name: 'abap-cca-merge',
|
|
166
190
|
category: 'Upgrade / Migrate',
|
|
167
191
|
description: 'Reconcile multiple parallel /abap-cca assessment files into one consolidated cca-assessment',
|
|
168
192
|
whenToUse: 'pick this when multiple consultants ran scoped /abap-cca slices — merges N assessments into one team view',
|
|
193
|
+
asks: 'which assessments. e.g. "merge the ZFI and ZSD cca files"',
|
|
169
194
|
},
|
|
170
195
|
{
|
|
171
196
|
name: 'abap-migrate',
|
|
172
197
|
category: 'Upgrade / Migrate',
|
|
173
198
|
description: 'ECC to S/4HANA migration assessment — API analysis, clean-core alignment, migration path',
|
|
174
199
|
whenToUse: `pick this to pick retire/replace/refactor per object (not /abap-cca — cca discovers them)`,
|
|
200
|
+
asks: 'which objects. e.g. "package ZLEGACY, ECC to S/4"',
|
|
175
201
|
},
|
|
176
202
|
// ── Ship / Release ───────────────────────────────────────────────────────
|
|
177
203
|
{
|
|
@@ -179,18 +205,21 @@ export const SKILL_CATALOG = [
|
|
|
179
205
|
category: 'Ship / Release',
|
|
180
206
|
description: 'Transport management — create, list, validate, release transport requests',
|
|
181
207
|
whenToUse: `pick this to CREATE/LIST/RELEASE a TR (inspect-only? use /abap-transport-analysis)`,
|
|
208
|
+
asks: 'what to do. e.g. "create a transport for this session" or "release S4HK903449"',
|
|
182
209
|
},
|
|
183
210
|
{
|
|
184
211
|
name: 'abap-transport-analysis',
|
|
185
212
|
category: 'Ship / Release',
|
|
186
213
|
description: 'Pre-release transport analysis — objects, ATC scan, activation status, conflict check',
|
|
187
214
|
whenToUse: `pick this if you have a TR NUMBER and want a read-only safety scan`,
|
|
215
|
+
asks: 'which transport. e.g. "S4HK903449 before it goes to QA"',
|
|
188
216
|
},
|
|
189
217
|
{
|
|
190
218
|
name: 'abap-preflight',
|
|
191
219
|
category: 'Ship / Release',
|
|
192
220
|
description: 'Pre-release checklist — ATC, syntax, inactive objects, transport conflicts',
|
|
193
221
|
whenToUse: `pick this for a full ship-readiness package on a CHANGE SET (no TR# needed)`,
|
|
222
|
+
asks: 'what is going out. e.g. "the objects in S4HK903449"',
|
|
194
223
|
},
|
|
195
224
|
// ── Pre-Coding ───────────────────────────────────────────────────────────
|
|
196
225
|
{
|
|
@@ -198,24 +227,28 @@ export const SKILL_CATALOG = [
|
|
|
198
227
|
category: 'Pre-Coding',
|
|
199
228
|
description: 'Find missing requirements before coding — business questions, technical gaps, assumptions',
|
|
200
229
|
whenToUse: `pick this for missing business + technical questions on a vague spec (step 1 of 3 — feeds /abap-design)`,
|
|
230
|
+
asks: 'the requirement. e.g. paste the ticket text, or "@spec.pdf"',
|
|
201
231
|
},
|
|
202
232
|
{
|
|
203
233
|
name: 'abap-design',
|
|
204
234
|
category: 'Pre-Coding',
|
|
205
235
|
description: 'Solution architecture — object list, dependency sequence, patterns, transport strategy. Plans the build, does not generate code.',
|
|
206
236
|
whenToUse: `pick this for the object list + dependency sequence + pattern choices (step 2 of 3 — needs /abap-spec-gap done, feeds /abap-estimate or /abap-generate)`,
|
|
237
|
+
asks: 'the requirement, once the gaps are closed. e.g. "the inspection log from the spec above"',
|
|
207
238
|
},
|
|
208
239
|
{
|
|
209
240
|
name: 'abap-estimate',
|
|
210
241
|
category: 'Pre-Coding',
|
|
211
242
|
description: 'Effort estimation for ABAP tickets — component breakdown with hour ranges (optimistic / realistic / pessimistic) + risk adjustments',
|
|
212
243
|
whenToUse: `pick this for hour ranges + risk adjustments on a confirmed object list (step 3 of 3 — needs /abap-design first; high uncertainty without it)`,
|
|
244
|
+
asks: 'what to estimate. e.g. "the objects from the design above" or "all changes in this ticket"',
|
|
213
245
|
},
|
|
214
246
|
{
|
|
215
247
|
name: 'abap-plan',
|
|
216
248
|
category: 'Pre-Coding',
|
|
217
249
|
description: 'Multi-session project plan — phase envelope holds state across sessions; create from a goal or spec-gap file, resume one bounded phase per session',
|
|
218
250
|
whenToUse: `pick this when the work spans multiple sessions or components (not /abap-design — that's one design in one session; plan calls design per phase)`,
|
|
251
|
+
asks: 'the goal, or a plan to resume. e.g. "build the inspection log end to end"',
|
|
219
252
|
},
|
|
220
253
|
];
|
|
221
254
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED FILE — bundled skills for local mode.
|
|
2
2
|
// Source: https://manifest.cspeach.dev/v1.json
|
|
3
|
-
// Generated: 2026-09-
|
|
3
|
+
// Generated: 2026-09-23T17:28:19.737Z
|
|
4
4
|
// Manifest signature verified at build time.
|
|
5
5
|
export const BUNDLED_SKILLS = {
|
|
6
6
|
"abap-atc-fix": {
|