@cspeach/cli 1.1.30 → 1.1.32
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 +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +100 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +53 -53
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/dispatch-skill.js +8 -4
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSV export on disk (step 2, Task 4; spec 2026-10-08 §3.4, D2). The
|
|
3
|
+
* tables come from register-core (`exportSet`, pure); this file writes them:
|
|
4
|
+
* each file whole, then renamed, so a sync tool never picks up half of one;
|
|
5
|
+
* CSV as UTF-8 with a BOM (Excel then reads umlauts right), CRLF line ends,
|
|
6
|
+
* RFC 4180 quoting (`csvText`).
|
|
7
|
+
*
|
|
8
|
+
* The switch is visible JSON in the team project file — `"exports": { "csv": true }`
|
|
9
|
+
* — set by `cspeach team export --always` or the journey's report card. No
|
|
10
|
+
* hidden state. Default off.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { csvText, exportSet } from '@cspeach/register-core';
|
|
14
|
+
import { HELD } from './workbooks.js';
|
|
15
|
+
import { readProjectFile, TEAM_FILE } from './store.js';
|
|
16
|
+
import { writeWhole } from './write-whole.js';
|
|
17
|
+
/** The folder the export goes into, under the reports folder (or the folder `--out` named for a report). */
|
|
18
|
+
export const EXPORTS_DIR = 'exports';
|
|
19
|
+
/** True only when the team project file says exactly `"exports": { "csv": true }`. */
|
|
20
|
+
export function exportsOn(project) {
|
|
21
|
+
const e = project.exports;
|
|
22
|
+
return !!e && typeof e === 'object' && e.csv === true;
|
|
23
|
+
}
|
|
24
|
+
/** Set the switch on or off: the whole project file, then renamed. Every other field stays as it is. */
|
|
25
|
+
export function setExports(projectDir, on) {
|
|
26
|
+
const project = readProjectFile(projectDir);
|
|
27
|
+
const had = project.exports && typeof project.exports === 'object' ? project.exports : {};
|
|
28
|
+
const next = { ...project, exports: { ...had, csv: on } };
|
|
29
|
+
writeWhole(path.join(projectDir, TEAM_FILE), `${JSON.stringify(next, null, 2)}\n`);
|
|
30
|
+
}
|
|
31
|
+
/** Turn the switch on (`team export --always`, the card's "Every time"). */
|
|
32
|
+
export const setExportsAlways = (projectDir) => setExports(projectDir, true);
|
|
33
|
+
/**
|
|
34
|
+
* Every file of the export into `dir`. Each file is tried on its own: one held
|
|
35
|
+
* open in Excel never costs the others, and the caller names it.
|
|
36
|
+
*/
|
|
37
|
+
export function writeExportFiles(register, project, dir, generatedAt) {
|
|
38
|
+
const set = exportSet(register, project, { generatedAt });
|
|
39
|
+
const files = [
|
|
40
|
+
...set.tables.map((t) => [t.file, csvText(t)]),
|
|
41
|
+
['manifest.json', `${JSON.stringify(set.manifest, null, 2)}\n`],
|
|
42
|
+
['SCHEMA.md', set.schema],
|
|
43
|
+
];
|
|
44
|
+
const saved = [];
|
|
45
|
+
const held = [];
|
|
46
|
+
for (const [name, text] of files) {
|
|
47
|
+
try {
|
|
48
|
+
writeWhole(path.join(dir, name), Buffer.from(text, 'utf8'));
|
|
49
|
+
saved.push(name);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (!HELD.has(e?.code ?? ''))
|
|
53
|
+
throw e;
|
|
54
|
+
held.push(name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { saved, held };
|
|
58
|
+
}
|
|
@@ -20,7 +20,7 @@ const basisOf = (v) => {
|
|
|
20
20
|
const w = String(v).trim().toLowerCase();
|
|
21
21
|
return w === '' || w === 'system' ? 'system' : 'file';
|
|
22
22
|
};
|
|
23
|
-
export function findingsByObject(projectDir, outboxDir) {
|
|
23
|
+
export function findingsByObject(projectDir, outboxDir, opts = {}) {
|
|
24
24
|
const project = readProjectFile(projectDir);
|
|
25
25
|
const ours = variantKey(project.atcVariant);
|
|
26
26
|
const records = [
|
|
@@ -56,13 +56,16 @@ export function findingsByObject(projectDir, outboxDir) {
|
|
|
56
56
|
// handed-over file, and it goes to a terminal and to the model's tools.
|
|
57
57
|
const message = named && typeof f.message === 'string' ? shown(f.message) : '';
|
|
58
58
|
const had = seen.rules.get(rule);
|
|
59
|
+
const line = opts.lines && named && Number.isInteger(f.line) && f.line > 0 ? f.line : undefined;
|
|
59
60
|
if (had) {
|
|
60
61
|
had.count += 1;
|
|
61
62
|
if (!had.message && message)
|
|
62
63
|
had.message = message;
|
|
64
|
+
if (line !== undefined && (had.line === undefined || line < had.line))
|
|
65
|
+
had.line = line;
|
|
63
66
|
}
|
|
64
67
|
else
|
|
65
|
-
seen.rules.set(rule, { rule, message, count: 1 });
|
|
68
|
+
seen.rules.set(rule, { rule, message, count: 1, ...(line !== undefined ? { line } : {}) });
|
|
66
69
|
}
|
|
67
70
|
byKey.set(key, list);
|
|
68
71
|
}
|
package/dist/register/measure.js
CHANGED
|
@@ -98,18 +98,169 @@ function apiNameIn(text) {
|
|
|
98
98
|
*
|
|
99
99
|
* The TITLE is read first: it is where SAP names the API, while the long text
|
|
100
100
|
* can quote source code and would otherwise hand a token like `TABLE` to the
|
|
101
|
-
* record.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
101
|
+
* record.
|
|
102
|
+
*
|
|
103
|
+
* READINESS WORDING: VERIFIED-NO-API-NAME (live, S4H, 2026-10-08). On a real
|
|
104
|
+
* ABAP_CLOUD_READINESS worklist the message TEXT is empty on every finding and
|
|
105
|
+
* the title says only "Usage of not released ABAP Platform APIs." — SAP's
|
|
106
|
+
* wording names no API (`__tests__/fixtures/atc-readiness-worklist.xml`). So
|
|
107
|
+
* this rule names nothing on a real system, and it is KEPT ONLY AS A FALLBACK
|
|
108
|
+
* for a text that ever does name one. The API of a USAGE_OF_RELEASED_APIS
|
|
109
|
+
* finding comes from the finding itself instead: its REF_OBJ_NAME /
|
|
110
|
+
* REF_OBJ_TYPE tags, or its document (`apiNamesFor`), and it only feeds
|
|
111
|
+
* `uses` — the finding keeps its check's own rule id.
|
|
105
112
|
*/
|
|
106
113
|
export function unreleasedApiRule(f) {
|
|
114
|
+
const name = unreleasedApiName(f);
|
|
115
|
+
return name ? `UNRELEASED_API:${name}` : null;
|
|
116
|
+
}
|
|
117
|
+
/** The API an unreleased-API finding names, by the same (unverified) reading as `unreleasedApiRule`. */
|
|
118
|
+
function unreleasedApiName(f) {
|
|
107
119
|
const title = f.messageTitle ?? '';
|
|
108
120
|
const body = f.messageText ?? '';
|
|
109
121
|
if (!NOT_RELEASED.test(`${title} ${body}`))
|
|
110
122
|
return null;
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
return apiNameIn(title) ?? apiNameIn(body);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A finding of SAP's "Usage of Released APIs" check (USAGE_OF_RELEASED_APIS),
|
|
127
|
+
* read by its rule id exactly the way register-core's `classifyFinding` reads
|
|
128
|
+
* it. Only such a finding's referenced object is "the API it uses": the syntax
|
|
129
|
+
* check also tags a REF_OBJ_NAME, and that names a statement.
|
|
130
|
+
*/
|
|
131
|
+
export function isReleasedApiFinding(f) {
|
|
132
|
+
return [ruleIdFromAtc(f), ...ruleAliasesFromAtc(f)]
|
|
133
|
+
.some((id) => normalizeRuleId(id).split(':')[0] === 'USAGE_OF_RELEASED_APIS');
|
|
134
|
+
}
|
|
135
|
+
const apiRef = (o) => {
|
|
136
|
+
const name = typeof o?.name === 'string' ? o.name.trim().toUpperCase() : '';
|
|
137
|
+
const type = typeof o?.type === 'string' ? o.type.trim().toUpperCase() : '';
|
|
138
|
+
return name ? { name, ...(type ? { type } : {}) } : undefined;
|
|
139
|
+
};
|
|
140
|
+
/** Finding documents one run reads, at most. The rest are left without `uses`, counted and said. */
|
|
141
|
+
export const MAX_API_DOCUMENTS = 200;
|
|
142
|
+
/** Finding documents read at the same time. */
|
|
143
|
+
export const API_DOCUMENT_CONCURRENCY = 4;
|
|
144
|
+
/**
|
|
145
|
+
* The API each USAGE_OF_RELEASED_APIS finding uses, by finding.
|
|
146
|
+
*
|
|
147
|
+
* The worklist's own REF_OBJ_NAME / REF_OBJ_TYPE tags first — they cost
|
|
148
|
+
* nothing. A finding without them has its document read (distinct links, at
|
|
149
|
+
* most `API_DOCUMENT_CONCURRENCY` at a time, at most `cap` per run). A
|
|
150
|
+
* document that fails, names nothing, or was past the cap leaves its finding
|
|
151
|
+
* without a name: no `uses`, so it stays open for B. Never a guess.
|
|
152
|
+
*/
|
|
153
|
+
async function apiNamesFor(findings, read, cap, signal) {
|
|
154
|
+
const named = new Map();
|
|
155
|
+
const needDoc = new Map();
|
|
156
|
+
let namedNone = 0;
|
|
157
|
+
let notLookedUp = 0;
|
|
158
|
+
for (const f of findings) {
|
|
159
|
+
const tag = apiRef(f.referencedObject);
|
|
160
|
+
if (tag) {
|
|
161
|
+
named.set(f, tag);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!f.documentation)
|
|
165
|
+
namedNone += 1;
|
|
166
|
+
else if (!read)
|
|
167
|
+
notLookedUp += 1;
|
|
168
|
+
else
|
|
169
|
+
needDoc.set(f.documentation, [...(needDoc.get(f.documentation) ?? []), f]);
|
|
170
|
+
}
|
|
171
|
+
const hrefs = [...needDoc.keys()];
|
|
172
|
+
const toRead = hrefs.slice(0, Math.max(0, cap));
|
|
173
|
+
const overCap = hrefs.slice(toRead.length).reduce((n, h) => n + needDoc.get(h).length, 0);
|
|
174
|
+
let unreadable = 0;
|
|
175
|
+
let documents = 0;
|
|
176
|
+
const started = Date.now();
|
|
177
|
+
let next = 0;
|
|
178
|
+
const worker = async () => {
|
|
179
|
+
while (next < toRead.length && !signal?.aborted) {
|
|
180
|
+
const href = toRead[next++];
|
|
181
|
+
documents += 1;
|
|
182
|
+
try {
|
|
183
|
+
const ref = apiRef((await read(href))?.referencedObject);
|
|
184
|
+
if (ref)
|
|
185
|
+
for (const f of needDoc.get(href))
|
|
186
|
+
named.set(f, ref);
|
|
187
|
+
else
|
|
188
|
+
namedNone += needDoc.get(href).length;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
unreadable += needDoc.get(href).length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
await Promise.all(Array.from({ length: Math.min(API_DOCUMENT_CONCURRENCY, toRead.length) }, worker));
|
|
196
|
+
// Within the cap but never asked for: the run was stopped.
|
|
197
|
+
const stopped = toRead.slice(next).reduce((n, h) => n + needDoc.get(h).length, 0);
|
|
198
|
+
return {
|
|
199
|
+
named,
|
|
200
|
+
report: {
|
|
201
|
+
findings: findings.length, named: named.size, documents, ms: documents ? Date.now() - started : 0,
|
|
202
|
+
unnamed: findings.length - named.size, overCap, unreadable, namedNone, stopped, notLookedUp,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/* ── `uses`: what SAP says about the API a finding names (step 2, F8) ────── */
|
|
207
|
+
/** API names per `ARS_W_API_STATE` read. */
|
|
208
|
+
export const ARS_CHUNK = 50;
|
|
209
|
+
/** At most this many type/state pairs are kept for one API name. */
|
|
210
|
+
export const MAX_USES = 10;
|
|
211
|
+
/**
|
|
212
|
+
* Rows asked for per ARS read. ARS holds a row per sub-object too, so a read
|
|
213
|
+
* that comes back FULL may have been cut, and the row cut off may be the one
|
|
214
|
+
* that is not stable: a full page is read as unreadable (review 3).
|
|
215
|
+
*/
|
|
216
|
+
export const ARS_MAX_ROWS = ARS_CHUNK * 20;
|
|
217
|
+
/**
|
|
218
|
+
* One batched read of `ARS_W_API_STATE` per run, by `OBJECT_NAME`, in chunks
|
|
219
|
+
* of 50 distinct names (F8). Every distinct type/state SAP returns for a name
|
|
220
|
+
* is kept, as SAP spells it, so one entry that is not stable keeps the finding
|
|
221
|
+
* open for B (register-core `stateOfUses`). A name SAP returns nothing for
|
|
222
|
+
* gets no entry: the finding then has no `uses` and stays open — honest, not
|
|
223
|
+
* guessed. A read that fails reads nothing, and the measurement is written as
|
|
224
|
+
* it always was.
|
|
225
|
+
*
|
|
226
|
+
* The names come from the USAGE_OF_RELEASED_APIS findings themselves
|
|
227
|
+
* (`apiNamesFor`) and, as a fallback, from a message text that names an API
|
|
228
|
+
* (`unreleasedApiRule`): `uses` is written only on a finding one of them named.
|
|
229
|
+
*/
|
|
230
|
+
async function apiStatesOf(sql, names) {
|
|
231
|
+
const out = new Map();
|
|
232
|
+
// A name goes into the statement only when it can hold no quote or bracket.
|
|
233
|
+
const safe = [...new Set(names)].filter((n) => SAP_NAME.test(n));
|
|
234
|
+
for (let i = 0; i < safe.length; i += ARS_CHUNK) {
|
|
235
|
+
const chunk = safe.slice(i, i + ARS_CHUNK);
|
|
236
|
+
let rows;
|
|
237
|
+
try {
|
|
238
|
+
({ rows } = await sql(`SELECT DISTINCT OBJECT_TYPE, OBJECT_NAME, RELEASE_STATE FROM ARS_W_API_STATE WHERE OBJECT_NAME IN (${chunk.map((n) => `'${n}'`).join(',')})`, ARS_MAX_ROWS));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return new Map(); // unreadable: no `uses` at all, never half of them
|
|
242
|
+
}
|
|
243
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
244
|
+
if (list.length >= ARS_MAX_ROWS)
|
|
245
|
+
return new Map(); // possibly cut: no `uses` at all, never a wrong state
|
|
246
|
+
for (const r of list) {
|
|
247
|
+
const name = String(r?.OBJECT_NAME ?? '').trim().toUpperCase();
|
|
248
|
+
const type = String(r?.OBJECT_TYPE ?? '').trim().toUpperCase();
|
|
249
|
+
const state = String(r?.RELEASE_STATE ?? '').trim().toUpperCase();
|
|
250
|
+
if (!name || !type || !state || !chunk.includes(name))
|
|
251
|
+
continue;
|
|
252
|
+
const seen = out.get(name) ?? [];
|
|
253
|
+
if (!seen.some((u) => u.type === type && u.state === state))
|
|
254
|
+
seen.push({ type, name, state });
|
|
255
|
+
out.set(name, seen);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// More pairs than a finding carries: the name gets none, never a cut list whose
|
|
259
|
+
// missing entry could be the one that is not stable (review 3).
|
|
260
|
+
for (const [name, list] of out)
|
|
261
|
+
if (list.length > MAX_USES)
|
|
262
|
+
out.delete(name);
|
|
263
|
+
return out;
|
|
113
264
|
}
|
|
114
265
|
/**
|
|
115
266
|
* SAP writes some check names as `ID / text` — `SLIN_DB / SELECT without ORDER
|
|
@@ -144,7 +295,11 @@ function halvesOf(src) {
|
|
|
144
295
|
* `coverage.absent` goes to 0, an absent-clean row renders a plain "Clean", and
|
|
145
296
|
* nothing anywhere says that nothing actually looked at the object.
|
|
146
297
|
*/
|
|
147
|
-
export function findingsToRows(measured, findings, projectType
|
|
298
|
+
export function findingsToRows(measured, findings, projectType,
|
|
299
|
+
/** What `ARS_W_API_STATE` said about each API name (`apiStatesOf`). Absent: no `uses` are written. */
|
|
300
|
+
usesByName,
|
|
301
|
+
/** The API each USAGE_OF_RELEASED_APIS finding uses (`apiNamesFor`), by finding. */
|
|
302
|
+
apiNameOf) {
|
|
148
303
|
const byKey = new Map();
|
|
149
304
|
const evidenceOf = new Map();
|
|
150
305
|
for (const o of measured) {
|
|
@@ -160,7 +315,13 @@ export function findingsToRows(measured, findings, projectType) {
|
|
|
160
315
|
if (!list)
|
|
161
316
|
continue; // a finding for an object that was not measured is never written
|
|
162
317
|
const atcRule = ruleIdFromAtc(f);
|
|
163
|
-
const
|
|
318
|
+
const apiName = projectType === 'clean-core' ? unreleasedApiName(f) : null;
|
|
319
|
+
const api = apiName ? `UNRELEASED_API:${apiName}` : null;
|
|
320
|
+
// The finding's own referenced object first; the text's name only as a fallback.
|
|
321
|
+
// When SAP said the TYPE, only ARS rows of that type are its state: ARS is read by
|
|
322
|
+
// name, and a function group SCAL is not the table SCAL (live, 2026-10-08).
|
|
323
|
+
const ref = (projectType === 'clean-core' ? apiNameOf?.get(f) : undefined) ?? (apiName ? { name: apiName } : undefined);
|
|
324
|
+
const uses = ref ? usesByName?.get(ref.name)?.filter((u) => !ref.type || u.type === ref.type) : undefined;
|
|
164
325
|
// A finding nobody can name still counts: dropping it would make the object look cleaner than it is.
|
|
165
326
|
const rule = api ?? (atcRule || UNNAMED_FINDING);
|
|
166
327
|
const aliases = [...new Set([...(api && atcRule ? [atcRule] : []), ...ruleAliasesFromAtc(f), ...halvesOf(f)])]
|
|
@@ -182,6 +343,8 @@ export function findingsToRows(measured, findings, projectType) {
|
|
|
182
343
|
...(f.line ? { line: f.line } : {}),
|
|
183
344
|
// The title only. The long text can quote source, and the record promises: no source code.
|
|
184
345
|
...(title ? { message: title.slice(0, 200) } : {}),
|
|
346
|
+
// F8: only on a finding whose API was named, and only what SAP returned for that name.
|
|
347
|
+
...(uses?.length ? { uses: uses.map((u) => ({ ...u })) } : {}),
|
|
185
348
|
});
|
|
186
349
|
}
|
|
187
350
|
return [...byKey.entries()].map(([key, list]) => {
|
|
@@ -408,7 +571,25 @@ export async function measure(input) {
|
|
|
408
571
|
};
|
|
409
572
|
if (!rowRefs.length)
|
|
410
573
|
return { ...counts, file: null, withFindings: 0, refusedRows };
|
|
411
|
-
const
|
|
574
|
+
const allFindings = [...outcome.findings, ...ownFindings];
|
|
575
|
+
// F8: the APIs the findings of measured objects use, read once for the whole run.
|
|
576
|
+
let usesByName;
|
|
577
|
+
let apiNameOf;
|
|
578
|
+
let apiNames;
|
|
579
|
+
if (input.sql && input.project.type === 'clean-core') {
|
|
580
|
+
const rowKeys = new Set(rowRefs.map((o) => objectKey(o.type, o.name)));
|
|
581
|
+
const inRows = allFindings.filter((f) => typeof f?.key === 'string' && rowKeys.has(normalizeKey(f.key)));
|
|
582
|
+
const releasedApi = inRows.filter(isReleasedApiFinding);
|
|
583
|
+
if (releasedApi.length) {
|
|
584
|
+
const got = await apiNamesFor(releasedApi, input.atc.findingDocument?.bind(input.atc), input.apiDocumentCap ?? MAX_API_DOCUMENTS, input.signal);
|
|
585
|
+
apiNameOf = got.named;
|
|
586
|
+
apiNames = got.report;
|
|
587
|
+
}
|
|
588
|
+
const names = [...[...(apiNameOf?.values() ?? [])].map((r) => r.name), ...inRows.map((f) => unreleasedApiName(f)).filter((n) => !!n)];
|
|
589
|
+
if (names.length)
|
|
590
|
+
usesByName = await apiStatesOf(input.sql, names);
|
|
591
|
+
}
|
|
592
|
+
const rows = findingsToRows(rowRefs, allFindings, input.project.type, usesByName, apiNameOf);
|
|
412
593
|
const res = writeRecord(input.joined, 'measurement', rows, input.by, {
|
|
413
594
|
...(input.now ? { now: input.now } : {}), ...(input.dir ? { dir: input.dir } : {}),
|
|
414
595
|
...(input.fileName ? { fileName: input.fileName } : {}), ...(input.rand ? { rand: input.rand } : {}),
|
|
@@ -435,5 +616,6 @@ export async function measure(input) {
|
|
|
435
616
|
return {
|
|
436
617
|
...counts, ...coverageNow(), file: res.file, refusedRows,
|
|
437
618
|
withFindings: rows.filter((r) => r.findings.length > 0).length,
|
|
619
|
+
...(apiNames ? { apiNames } : {}),
|
|
438
620
|
};
|
|
439
621
|
}
|
package/dist/register/offer.js
CHANGED
|
@@ -1,12 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The line CSPeach prints after it activated, and verified, an object of the
|
|
3
|
+
* team project this laptop has joined (spec §4.3).
|
|
4
|
+
*
|
|
5
|
+
* 1.1.31 (owner, 2026-10-07: "why would developers claim it when CSPeach is
|
|
6
|
+
* working?"): when CSPeach itself wrote the object's source in this session,
|
|
7
|
+
* the fix is recorded for the developer — the claim covers every rule open at
|
|
8
|
+
* the object's last measurement, with the transport the write used. A claim is
|
|
9
|
+
* still not proof: only the next measurement moves it to verified.
|
|
10
|
+
* An object CSPeach activated but did not write (someone changed it in Eclipse
|
|
11
|
+
* or SE80) only gets the one-line offer; the developer claims that one.
|
|
6
12
|
*/
|
|
7
13
|
import { objectKey } from '@cspeach/register-core';
|
|
8
14
|
import { activeProject, readBaseline } from './store.js';
|
|
9
|
-
|
|
15
|
+
import { findingsByObject } from './findings.js';
|
|
16
|
+
import { writeRecord } from './records.js';
|
|
17
|
+
import { regenerateWorkingList } from './workbooks.js';
|
|
18
|
+
/** The source-writing tools, and the argument that names the object each one wrote. */
|
|
19
|
+
const SOURCE_WRITES = {
|
|
20
|
+
sap_set_source: 'name',
|
|
21
|
+
sap_update_method: 'class_name',
|
|
22
|
+
sap_set_class_include: 'className',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Objects whose source CSPeach wrote successfully in this session (upper-case
|
|
26
|
+
* name → the transport the write used, '' when none is known).
|
|
27
|
+
*/
|
|
28
|
+
export function cspeachWritesThisSession(toolCalls) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
for (const c of toolCalls ?? []) {
|
|
31
|
+
if (!c.completed || c.is_error)
|
|
32
|
+
continue;
|
|
33
|
+
const field = SOURCE_WRITES[c.tool];
|
|
34
|
+
if (!field)
|
|
35
|
+
continue;
|
|
36
|
+
const args = (c.args ?? {});
|
|
37
|
+
const name = String(args[field] ?? '').trim().toUpperCase();
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
let transport = typeof args['transport'] === 'string' ? args['transport'] : '';
|
|
41
|
+
try {
|
|
42
|
+
// sap_set_source / sap_update_method report the request they used as transport_used.
|
|
43
|
+
const r = JSON.parse(c.result);
|
|
44
|
+
if (typeof r?.transport_used === 'string' && r.transport_used)
|
|
45
|
+
transport = r.transport_used;
|
|
46
|
+
else if (typeof r?.transport === 'string' && r.transport)
|
|
47
|
+
transport = r.transport;
|
|
48
|
+
}
|
|
49
|
+
catch { /* a plain-text result carries no transport */ }
|
|
50
|
+
out.set(name, transport.trim().toUpperCase());
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** Fixes this process already recorded (a write that activated, then an explicit activate, record once). */
|
|
55
|
+
const recordedThisRun = new Set();
|
|
56
|
+
/** Tests only. */
|
|
57
|
+
export function resetRecordedFixes() { recordedThisRun.clear(); }
|
|
58
|
+
export function teamFixOffers(objects, opts = {}) {
|
|
59
|
+
// A guided journey is on (journey/flow.ts): the same records, in solo words.
|
|
60
|
+
const solo = opts.wording === 'journey';
|
|
10
61
|
try {
|
|
11
62
|
const joined = activeProject();
|
|
12
63
|
if (!joined)
|
|
@@ -17,15 +68,56 @@ export function teamFixOffers(objects) {
|
|
|
17
68
|
for (const o of baseline)
|
|
18
69
|
byName.set(o.name.toUpperCase(), [...(byName.get(o.name.toUpperCase()) ?? []), o]);
|
|
19
70
|
const lines = [];
|
|
71
|
+
const done = new Set();
|
|
20
72
|
for (const o of objects) {
|
|
21
73
|
const type = String(o?.type ?? '').split('/')[0].trim().toUpperCase();
|
|
22
74
|
const name = String(o?.name ?? '').trim().toUpperCase();
|
|
23
75
|
const same = byName.get(name) ?? [];
|
|
24
76
|
const hit = byKey.get(objectKey(type, name)) ?? (same.length === 1 ? same[0] : undefined);
|
|
25
|
-
if (hit)
|
|
26
|
-
|
|
77
|
+
if (!hit)
|
|
78
|
+
continue;
|
|
79
|
+
const key = objectKey(hit.type, hit.name);
|
|
80
|
+
if (done.has(key))
|
|
81
|
+
continue;
|
|
82
|
+
done.add(key);
|
|
83
|
+
const label = `${hit.type.toUpperCase()} ${hit.name.toUpperCase()}`;
|
|
84
|
+
const transport = opts.written?.get(hit.name.toUpperCase());
|
|
85
|
+
if (transport === undefined) {
|
|
86
|
+
lines.push(solo
|
|
87
|
+
? `${label} is one of your objects, but CSPeach did not write it, so no fix was recorded.`
|
|
88
|
+
: `This object is in your team project. Mark it fixed? cspeach team fixed ${label}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const runKey = `${joined.projectId}|${key}|${transport}`;
|
|
92
|
+
if (recordedThisRun.has(runKey))
|
|
93
|
+
continue;
|
|
94
|
+
const f = findingsByObject(joined.projectDir, joined.outboxDir).get(key);
|
|
95
|
+
if (!f?.latestAt) {
|
|
96
|
+
lines.push(solo
|
|
97
|
+
? `${label} has not been checked yet, so no fix was recorded. The next check looks at it.`
|
|
98
|
+
: `${label} has not been measured yet, so no fix was recorded. Run cspeach team refresh to measure it.`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!f.latest.length) {
|
|
102
|
+
lines.push(solo
|
|
103
|
+
? `${label} had nothing open at its last check, so no fix was recorded. The next check looks at it.`
|
|
104
|
+
: `${label} had nothing open at its last measurement, so no fix was recorded. The next cspeach team refresh checks it.`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// The name this person records under in this team project, as team verbs use it.
|
|
108
|
+
const who = opts.who ?? joined.name ?? process.env['CSPEACH_AUTHOR_NAME'] ?? process.env['USER'] ?? process.env['USERNAME'] ?? 'consultant';
|
|
109
|
+
const res = writeRecord(joined, 'fix', f.latest.map((r) => ({ key, rule: r.rule, status: 'fixed', ...(transport ? { transport } : {}) })), { name: who });
|
|
110
|
+
if (!res.file)
|
|
111
|
+
continue;
|
|
112
|
+
recordedThisRun.add(runKey);
|
|
113
|
+
const by = `✓ Recorded: ${label} fixed by CSPeach${transport ? ` in ${transport}` : ''}.`;
|
|
114
|
+
lines.push(solo ? `${by} Not yet checked — the next check proves it.` : `${by} A claim is not proof — the next cspeach team refresh checks it.`);
|
|
115
|
+
try {
|
|
116
|
+
regenerateWorkingList(joined, who);
|
|
117
|
+
}
|
|
118
|
+
catch { /* the record is kept; the list catches up on the next run */ }
|
|
27
119
|
}
|
|
28
|
-
return
|
|
120
|
+
return lines;
|
|
29
121
|
}
|
|
30
122
|
catch {
|
|
31
123
|
return []; // a folder halfway through a sync must never touch an activation
|
package/dist/register/picture.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { buildRegister, narrowedLanes } from '@cspeach/register-core';
|
|
3
|
-
import { collectRecords, readBaseline, readLanes, readProjectFile } from './store.js';
|
|
3
|
+
import { collectRecords, readBaseline, readEstate, readLanes, readProjectFile } from './store.js';
|
|
4
4
|
export function registerFor(projectDir, outboxDir) {
|
|
5
5
|
const project = readProjectFile(projectDir);
|
|
6
6
|
const collected = collectRecords(projectDir);
|
|
@@ -10,7 +10,11 @@ export function registerFor(projectDir, outboxDir) {
|
|
|
10
10
|
const own = outboxDir && path.resolve(outboxDir) !== path.resolve(projectDir) ? collectRecords(outboxDir) : { records: [], unreadable: 0, files: 0 };
|
|
11
11
|
const baseline = readBaseline(projectDir);
|
|
12
12
|
const lanes = readLanes(projectDir);
|
|
13
|
-
|
|
13
|
+
// The newest discovery, when the folder holds one: the estate around the team project.
|
|
14
|
+
const estate = readEstate(projectDir)?.estate;
|
|
15
|
+
const register = buildRegister({
|
|
16
|
+
project, baseline, lanes, records: [...collected.records, ...own.records], ...(estate !== undefined ? { estate } : {}),
|
|
17
|
+
});
|
|
14
18
|
return {
|
|
15
19
|
register, files: collected.files + own.files,
|
|
16
20
|
unreadable: collected.unreadable + own.unreadable, ownFiles: own.files,
|