@cspeach/cli 1.1.29 → 1.1.31
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/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +127 -11
- package/dist/auth/auth-file.js +2 -2
- package/dist/cli.js +39 -13
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -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.js +652 -46
- package/dist/commands/whoami.js +1 -1
- package/dist/config/paths.js +15 -1
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +21 -1
- package/dist/projects/clean-core-confirm.js +192 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +82 -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 +115 -15
- 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/validate.js +2 -1
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -120
- package/dist/register/atc-file.js +9 -1
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +82 -0
- package/dist/register/measure.js +60 -1
- package/dist/register/offer.js +125 -0
- package/dist/register/picture.js +25 -0
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +57 -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 +221 -0
- package/dist/register/status-file/register-model.js +341 -0
- package/dist/register/status-file/register-parts.js +555 -0
- package/dist/register/status-file/register-status-file.js +189 -0
- package/dist/register/status-file/team-css.js +5 -0
- package/dist/register/status-file-sync.js +22 -0
- package/dist/register/status-html.js +19 -0
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +124 -0
- package/dist/register/write-whole.js +22 -0
- package/dist/register/xlsx.js +127 -0
- 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/journey-entry.js +25 -0
- package/dist/repl.js +158 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -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 +45 -45
- package/dist/tools/_filesystem-shared.js +190 -21
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- 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/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +145 -0
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +203 -5
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +2 -2
- 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/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery: the estate inventory, from fixed read-only SQL (spec §5.1–§5.3).
|
|
3
|
+
*
|
|
4
|
+
* Every statement is a fixed shape built here (DISCOVER_STATEMENTS lists them
|
|
5
|
+
* for the static rules test) and goes through the one `SqlQuery` capability —
|
|
6
|
+
* `AdtClient.sqlQuery`, which runs sap-client's read-only gate. The ADT
|
|
7
|
+
* data-preview dialect rules hold for every statement: no IN-lists, at most one
|
|
8
|
+
* LIKE per WHERE, an alias on every aggregate, never `UP TO n ROWS` (the row
|
|
9
|
+
* cap is the maxRows argument), one query per prefix and per namespace.
|
|
10
|
+
*
|
|
11
|
+
* Honesty rules:
|
|
12
|
+
* - a failed statement is recorded (`queries[]`, `ok:false`) and named under
|
|
13
|
+
* `unknowns`; discovery never throws for a SQL failure and the files are
|
|
14
|
+
* still produced;
|
|
15
|
+
* - usage has four states and "unused" only ever rests on an ACTIVE runtime
|
|
16
|
+
* source over a stated period; "last changed" is never called use;
|
|
17
|
+
* - callers are "read" only for objects the where-used pull really covered.
|
|
18
|
+
*/
|
|
19
|
+
import { CODE_OBJECT_TYPES, LOCAL_PACKAGE } from './baseline.js';
|
|
20
|
+
import { callersFromIndex, reposrcNameFor } from './xref.js';
|
|
21
|
+
import { outsideUsageWindowLine, shortUsageWindow, usageUnknownLineOf } from '@cspeach/register-core';
|
|
22
|
+
// ── Fixed values ──
|
|
23
|
+
/** Rows per keyset page (steps 5, 6, 8, 9, 10). */
|
|
24
|
+
const PAGE = 5000;
|
|
25
|
+
/** Namespaces that are SAP's or a product's, never treated as customer even with role P. */
|
|
26
|
+
const NOT_CUSTOMER_NAMESPACES = [
|
|
27
|
+
'/BI0/', '/BIC/', '/IWBEP/', '/IWFND/', '/UI2/', '/UI5/', '/SDF/', '/BOBF/', '/OSP/', '/SCWM/', '/SCMB/', '/S4PPM/',
|
|
28
|
+
'/ARBA/', '/AIF/', '/BA1/', '/CFG/', '/DMF/', '/GBT/', '/LIME/', '/MRSS/', '/NXL/', '/PLMB/', '/PLMI/', '/POSDW/',
|
|
29
|
+
'/SAPAPO/', '/SAPPO/', '/SAPSLL/', '/SAPTRX/', '/SCDL/', '/SCMTMS/', '/SPE/', '/STTP/', '/TDF/', '/CPD/', '/OPT/', '/XLSO/',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Types the where-used index records callers for. Others (a function group's
|
|
33
|
+
* callers sit under its function modules' names; domains, access controls,
|
|
34
|
+
* services, behaviour definitions, messages, transactions, forms, …) are
|
|
35
|
+
* "not read", never "no callers".
|
|
36
|
+
*/
|
|
37
|
+
const INDEXED_TYPES = new Set(['PROG', 'CLAS', 'INTF', 'TABL', 'VIEW', 'DTEL', 'TTYP', 'DDLS', 'SHLP']);
|
|
38
|
+
const NOT_INDEXED = 'The where-used index does not record callers for this type.';
|
|
39
|
+
/** Types runtime usage sources (SUSG, SCMON) can see: executed main programs. */
|
|
40
|
+
const RUNTIME_TYPES = new Set(['PROG', 'CLAS', 'FUGR']);
|
|
41
|
+
const STEP_LABELS = {
|
|
42
|
+
1: 'namespaces', 2: 'packages', 3: 'object counts', 4: 'objects per package', 5: 'local objects',
|
|
43
|
+
6: 'change dates', 7: 'batch jobs', 8: 'usage sources', 9: 'where-used', 10: 'include to program',
|
|
44
|
+
11: 'modifications', 12: 'successor data',
|
|
45
|
+
};
|
|
46
|
+
const NO_USAGE = 'No program usage is recorded on this system.';
|
|
47
|
+
const BATCH_NOTE = 'A batch run proves use. Finding no batch run proves nothing.';
|
|
48
|
+
const STOPPED = 'stopped before it finished';
|
|
49
|
+
const notPerObject = (source, period) => `Usage data exists on this system (${source}${period ? ` from ${period.from} to ${period.to}` : ''}), but CSPeach does not read it per object yet.`;
|
|
50
|
+
const PREFIX_RE = /^[A-Z/][A-Z0-9_/]{0,29}$/;
|
|
51
|
+
const NAMESPACE_RE = /^\/[A-Z0-9_]{1,10}\/$/;
|
|
52
|
+
const PACKAGE_RE = /^[A-Z0-9_/$]{1,30}$/;
|
|
53
|
+
const clean = (v) => (v ?? '').trim();
|
|
54
|
+
const num = (v) => { const n = Number(clean(v)); return Number.isFinite(n) ? n : 0; };
|
|
55
|
+
const lit = (v) => v.replace(/'/g, "''");
|
|
56
|
+
const iso = (v) => {
|
|
57
|
+
const s = clean(v);
|
|
58
|
+
if (/^\d{8}$/.test(s))
|
|
59
|
+
return s === '00000000' ? null : `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
|
60
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(s))
|
|
61
|
+
return s.slice(0, 10);
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
const isNamespace = (p) => p.startsWith('/');
|
|
65
|
+
const capped = (s) => (s.length > 300 ? `${s.slice(0, 297)}...` : s);
|
|
66
|
+
// ── Statement builders: the only SQL discovery sends ──
|
|
67
|
+
const after = (col, a, strict = false) => (a === undefined ? '' : ` AND ${col} ${strict ? '>' : '>='} '${lit(a)}'`);
|
|
68
|
+
export const SQL = {
|
|
69
|
+
namespaces: () => 'SELECT NAMESPACE, ROLE FROM TRNSPACE',
|
|
70
|
+
packages: (p) => `SELECT DEVCLASS, PARENTCL, COMPONENT, APPLICAT, DLVUNIT, CREATED_BY, CREATED_ON FROM TDEVC WHERE DEVCLASS LIKE '${p}%'`,
|
|
71
|
+
packageTexts: (p) => `SELECT DEVCLASS, SPRAS, CTEXT FROM TDEVCT WHERE SPRAS = 'E' AND DEVCLASS LIKE '${p}%'`,
|
|
72
|
+
counts: (p) => `SELECT DEVCLASS, OBJECT, COUNT(*) AS CNT FROM TADIR WHERE PGMID = 'R3TR' AND DELFLAG = ' ' AND DEVCLASS LIKE '${p}%' GROUP BY DEVCLASS, OBJECT`,
|
|
73
|
+
packageObjects: (pkg) => `SELECT OBJECT, OBJ_NAME, DEVCLASS, AUTHOR, GENFLAG, CREATED_ON FROM TADIR WHERE PGMID = 'R3TR' AND DELFLAG = ' ' AND DEVCLASS = '${pkg}'`,
|
|
74
|
+
localCount: (p) => `SELECT COUNT(*) AS CNT FROM TADIR WHERE PGMID = 'R3TR' AND DELFLAG = ' ' AND DEVCLASS = '${LOCAL_PACKAGE}' AND OBJ_NAME LIKE '${p}%'`,
|
|
75
|
+
localObjects: (p, a) => `SELECT OBJECT, OBJ_NAME, DEVCLASS, AUTHOR, GENFLAG, CREATED_ON FROM TADIR WHERE PGMID = 'R3TR' AND DELFLAG = ' ' AND DEVCLASS = '${LOCAL_PACKAGE}' AND OBJ_NAME LIKE '${p}%'${after('OBJ_NAME', a)} ORDER BY OBJ_NAME, OBJECT`,
|
|
76
|
+
changes: (p, a) => `SELECT PROGNAME, R3STATE, UNAM, UDAT, CNAM, CDAT FROM REPOSRC WHERE R3STATE = 'A' AND PROGNAME LIKE '${p}%'${after('PROGNAME', a)} ORDER BY PROGNAME`,
|
|
77
|
+
batch: (p) => `SELECT p~PROGNAME, COUNT(*) AS RUNS, MAX( o~STRTDATE ) AS LAST_RUN FROM TBTCP AS p INNER JOIN TBTCO AS o ON p~JOBNAME = o~JOBNAME AND p~JOBCOUNT = o~JOBCOUNT WHERE o~STATUS = 'F' AND p~PROGNAME LIKE '${p}%' GROUP BY p~PROGNAME`,
|
|
78
|
+
batchWindow: () => `SELECT MIN( STRTDATE ) AS FIRST_RUN, MAX( STRTDATE ) AS LAST_RUN, COUNT(*) AS CNT FROM TBTCO WHERE STATUS = 'F'`,
|
|
79
|
+
scmonConfig: () => 'SELECT CONFIG_KEY, CONFIG_VALUE FROM SCMON_CONFIG',
|
|
80
|
+
scmonData: () => 'SELECT COUNT(*) AS CNT FROM SCMON_DATA',
|
|
81
|
+
scmonSlice: () => 'SELECT MIN( STARTDATE ) AS FROM_DATE, MAX( ENDDATE ) AS TO_DATE FROM SCMON_SLICE',
|
|
82
|
+
sdfScmonConfig: () => 'SELECT COUNT(*) AS CNT FROM /SDF/SCMON_CFG',
|
|
83
|
+
sdfScmonData: () => 'SELECT COUNT(*) AS CNT FROM /SDF/SCMON_DATA',
|
|
84
|
+
susgAdmin: () => 'SELECT USGID, KIND, DATE_FROM, DATE_TO, DAYS_AVAILABLE FROM SUSG_ADMIN',
|
|
85
|
+
susgData: () => 'SELECT COUNT(*) AS CNT, MIN( LAST_USED ) AS MIN_LU, MAX( LAST_USED ) AS MAX_LU FROM SUSG_DATA',
|
|
86
|
+
susgObjects: (p, a) => `SELECT g~OBJ_TYPE, g~OBJ_NAME, g~PROGNAME, MAX( d~LAST_USED ) AS LAST_USED FROM SUSG_DATA AS d INNER JOIN SUSG_PROG AS g ON d~USGID = g~USGID AND d~TRIGID = g~PROGID WHERE g~PROGNAME LIKE '${p}%'${after('g~PROGNAME', a)} GROUP BY g~OBJ_TYPE, g~OBJ_NAME, g~PROGNAME ORDER BY g~PROGNAME, g~OBJ_TYPE, g~OBJ_NAME`,
|
|
87
|
+
scmonProg: (p, a) => `SELECT PROGID, PROGNAME, OBJECT, OBJ_NAME FROM SCMON_PROG WHERE PROGNAME LIKE '${p}%'${after('PROGNAME', a)} ORDER BY PROGNAME, PROGID`,
|
|
88
|
+
monHeader: () => 'SELECT COUNT(*) AS CNT FROM /SDF/MON_HEADER',
|
|
89
|
+
monCall: () => 'SELECT COUNT(*) AS CNT FROM /SDF/MON_CALL',
|
|
90
|
+
workload: () => 'SELECT MIN( FIRSTRECDY ) AS FROM_DATE, MAX( LASTRECDY ) AS TO_DATE, COUNT(*) AS CNT FROM SWNCMONIINDEX',
|
|
91
|
+
wbcrossgt: (p, a, strict) => `SELECT OTYPE, NAME, INCLUDE FROM WBCROSSGT WHERE NAME LIKE '${p}%'${after('NAME', a, strict)} ORDER BY NAME, INCLUDE`,
|
|
92
|
+
cross: (p, a, strict) => `SELECT TYPE, NAME, INCLUDE FROM CROSS WHERE NAME LIKE '${p}%'${after('NAME', a, strict)} ORDER BY NAME, INCLUDE`,
|
|
93
|
+
masters: (p, a) => `SELECT MASTER, INCLUDE FROM D010INC WHERE INCLUDE LIKE '${p}%'${after('INCLUDE', a)} ORDER BY INCLUDE, MASTER`,
|
|
94
|
+
modifications: (op) => `SELECT OBJ_TYPE, OBJ_NAME, SUB_TYPE, SUB_NAME, OPERATION, MOD_USER, MOD_DATE FROM SMODILOG WHERE OPERATION = '${op}'`,
|
|
95
|
+
modificationCount: (op) => `SELECT COUNT(*) AS CNT FROM SMODILOG WHERE OPERATION = '${op}'`,
|
|
96
|
+
successors: () => 'SELECT COUNT(*) AS CNT FROM ARS_W_API_STATE',
|
|
97
|
+
};
|
|
98
|
+
/** Function groups' main programs and includes do not start with the prefix (SAPLZ…, LZ…). */
|
|
99
|
+
const fugrPrefixes = (p, lead) => (isNamespace(p) ? [] : [`${lead}${p}`]);
|
|
100
|
+
/** Every statement shape for one prefix (and package / paging key), for the static rules test. */
|
|
101
|
+
export const DISCOVER_STATEMENTS = (o) => {
|
|
102
|
+
const p = o.prefix;
|
|
103
|
+
const a = o.after;
|
|
104
|
+
const out = [
|
|
105
|
+
{ step: 1, sql: SQL.namespaces() },
|
|
106
|
+
{ step: 2, sql: SQL.packages(p) },
|
|
107
|
+
{ step: 2, sql: SQL.packageTexts(p) },
|
|
108
|
+
{ step: 3, sql: SQL.counts(p) },
|
|
109
|
+
{ step: 4, sql: SQL.packageObjects(o.pkg ?? `${p}PKG`) },
|
|
110
|
+
{ step: 5, sql: SQL.localCount(p) },
|
|
111
|
+
{ step: 5, sql: SQL.localObjects(p, a) },
|
|
112
|
+
{ step: 6, sql: SQL.changes(p, a) },
|
|
113
|
+
...fugrPrefixes(p, 'SAPL').map((fp) => ({ step: 6, sql: SQL.changes(fp, a) })),
|
|
114
|
+
{ step: 7, sql: SQL.batch(p) },
|
|
115
|
+
{ step: 7, sql: SQL.batchWindow() },
|
|
116
|
+
{ step: 8, sql: SQL.scmonConfig() },
|
|
117
|
+
{ step: 8, sql: SQL.scmonData() },
|
|
118
|
+
{ step: 8, sql: SQL.scmonSlice() },
|
|
119
|
+
{ step: 8, sql: SQL.sdfScmonConfig() },
|
|
120
|
+
{ step: 8, sql: SQL.sdfScmonData() },
|
|
121
|
+
{ step: 8, sql: SQL.susgAdmin() },
|
|
122
|
+
{ step: 8, sql: SQL.susgData() },
|
|
123
|
+
{ step: 8, sql: SQL.susgObjects(p, a) },
|
|
124
|
+
{ step: 8, sql: SQL.scmonProg(p, a) },
|
|
125
|
+
...fugrPrefixes(p, 'SAPL').flatMap((fp) => [{ step: 8, sql: SQL.susgObjects(fp, a) }, { step: 8, sql: SQL.scmonProg(fp, a) }]),
|
|
126
|
+
{ step: 8, sql: SQL.monHeader() },
|
|
127
|
+
{ step: 8, sql: SQL.monCall() },
|
|
128
|
+
{ step: 8, sql: SQL.workload() },
|
|
129
|
+
{ step: 9, sql: SQL.cross(p, a) },
|
|
130
|
+
{ step: 9, sql: SQL.wbcrossgt(p, a) },
|
|
131
|
+
...(a === undefined ? [] : [{ step: 9, sql: SQL.cross(p, a, true) }, { step: 9, sql: SQL.wbcrossgt(p, a, true) }]),
|
|
132
|
+
{ step: 10, sql: SQL.masters(p, a) },
|
|
133
|
+
...fugrPrefixes(p, 'L').map((fp) => ({ step: 10, sql: SQL.masters(fp, a) })),
|
|
134
|
+
{ step: 11, sql: SQL.modifications('MOD') },
|
|
135
|
+
{ step: 11, sql: SQL.modifications('NEW') },
|
|
136
|
+
{ step: 11, sql: SQL.modifications('IMP') },
|
|
137
|
+
{ step: 11, sql: SQL.modificationCount('NOTE') },
|
|
138
|
+
{ step: 11, sql: SQL.modificationCount('ALL') },
|
|
139
|
+
{ step: 12, sql: SQL.successors() },
|
|
140
|
+
];
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
143
|
+
export async function discover(sql, opts) {
|
|
144
|
+
const now = opts.now ?? (() => new Date());
|
|
145
|
+
const takenAt = now().toISOString();
|
|
146
|
+
const queries = [];
|
|
147
|
+
const unknowns = [];
|
|
148
|
+
const aborted = () => opts.signal?.aborted === true;
|
|
149
|
+
const codeTypes = new Set(CODE_OBJECT_TYPES);
|
|
150
|
+
/** The one wrapper every statement runs through. Never throws. */
|
|
151
|
+
const run = async (step, statement, maxRows, what) => {
|
|
152
|
+
const t0 = Date.now();
|
|
153
|
+
try {
|
|
154
|
+
const res = await sql(statement, maxRows);
|
|
155
|
+
const rows = res.rows ?? [];
|
|
156
|
+
queries.push({ step, statement, rows: rows.length, ms: Date.now() - t0, ok: true });
|
|
157
|
+
return { rows };
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const error = capped(clean(err instanceof Error ? err.message : String(err)) || 'The query failed without a message.');
|
|
161
|
+
queries.push({ step, statement, rows: 0, ms: Date.now() - t0, ok: false, error });
|
|
162
|
+
unknowns.push({ what: what ?? `step ${step}: ${STEP_LABELS[step]}`, why: error });
|
|
163
|
+
return { error };
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
/** Keyset paging with `>=` on the key column; rows already seen (by `uniq`) are dropped. */
|
|
167
|
+
const paged = async (step, build, keyCol, uniq, stop, what,
|
|
168
|
+
/** One key fills a whole page: hand it over and read on past it, instead of stopping. */
|
|
169
|
+
skip) => {
|
|
170
|
+
const rows = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
let a;
|
|
173
|
+
let strict = false;
|
|
174
|
+
for (;;) {
|
|
175
|
+
const why = stop();
|
|
176
|
+
if (why)
|
|
177
|
+
return { rows, complete: false, boundary: a, stop: why };
|
|
178
|
+
const res = await run(step, build(a, strict), PAGE, what);
|
|
179
|
+
if ('error' in res)
|
|
180
|
+
return { rows, complete: false, boundary: a, stop: 'error', error: res.error };
|
|
181
|
+
let fresh = 0;
|
|
182
|
+
for (const r of res.rows) {
|
|
183
|
+
const k = uniq(r);
|
|
184
|
+
if (seen.has(k))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(k);
|
|
187
|
+
rows.push(r);
|
|
188
|
+
fresh += 1;
|
|
189
|
+
}
|
|
190
|
+
if (res.rows.length < PAGE)
|
|
191
|
+
return { rows, complete: true };
|
|
192
|
+
const last = clean(res.rows[res.rows.length - 1][keyCol]);
|
|
193
|
+
if (fresh === 0 || last === a) {
|
|
194
|
+
if (!skip || !last)
|
|
195
|
+
return { rows, complete: false, boundary: last, stop: 'stuck' };
|
|
196
|
+
skip(last);
|
|
197
|
+
a = last;
|
|
198
|
+
strict = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
a = last;
|
|
202
|
+
strict = false;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const abortOnly = () => (aborted() ? 'abort' : null);
|
|
206
|
+
const started = new Set();
|
|
207
|
+
/** Steps Ctrl-C stopped part way through: listed under unknowns like the ones never started. */
|
|
208
|
+
const cut = new Set();
|
|
209
|
+
const step = (n) => {
|
|
210
|
+
if (aborted())
|
|
211
|
+
return false;
|
|
212
|
+
started.add(n);
|
|
213
|
+
opts.onStep?.(n, STEP_LABELS[n]);
|
|
214
|
+
return true;
|
|
215
|
+
};
|
|
216
|
+
// Prefixes from options (checked, never escaped — they go into LIKE literals).
|
|
217
|
+
const prefixes = [];
|
|
218
|
+
for (const raw of opts.prefixes.length ? opts.prefixes : ['Z', 'Y']) {
|
|
219
|
+
const p = clean(raw).toUpperCase();
|
|
220
|
+
if (!PREFIX_RE.test(p)) {
|
|
221
|
+
unknowns.push({ what: `prefix ${raw}`, why: 'Not a valid name prefix; it was not read.' });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!prefixes.includes(p))
|
|
225
|
+
prefixes.push(p);
|
|
226
|
+
}
|
|
227
|
+
// ── Step 1: namespaces ──
|
|
228
|
+
let namespaces = [];
|
|
229
|
+
if (step(1)) {
|
|
230
|
+
const res = await run(1, SQL.namespaces(), 500);
|
|
231
|
+
let customer = [];
|
|
232
|
+
if ('rows' in res) {
|
|
233
|
+
let notCustomer = 0;
|
|
234
|
+
for (const r of res.rows) {
|
|
235
|
+
const ns = clean(r.NAMESPACE).toUpperCase();
|
|
236
|
+
if (!ns)
|
|
237
|
+
continue;
|
|
238
|
+
const sap = ns.startsWith('/SAP') || ns.startsWith('/1') || NOT_CUSTOMER_NAMESPACES.includes(ns);
|
|
239
|
+
// ROLE P (producer): this system develops in the namespace. C (recipient) only receives its objects.
|
|
240
|
+
if (!sap && clean(r.ROLE).toUpperCase() === 'P' && NAMESPACE_RE.test(ns))
|
|
241
|
+
customer.push(ns);
|
|
242
|
+
else
|
|
243
|
+
notCustomer += 1;
|
|
244
|
+
}
|
|
245
|
+
customer = [...new Set(customer)].sort();
|
|
246
|
+
unknowns.push({
|
|
247
|
+
what: `namespaces not treated as customer: ${notCustomer}`,
|
|
248
|
+
why: 'SAP, partner or product namespaces. Their objects are not in this inventory.',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (opts.namespaces === 'auto')
|
|
252
|
+
namespaces = customer;
|
|
253
|
+
}
|
|
254
|
+
if (opts.namespaces !== 'auto') {
|
|
255
|
+
for (const raw of opts.namespaces) {
|
|
256
|
+
const ns = clean(raw).toUpperCase();
|
|
257
|
+
if (!NAMESPACE_RE.test(ns)) {
|
|
258
|
+
unknowns.push({ what: `namespace ${raw}`, why: 'Not a valid namespace; it was not read.' });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!namespaces.includes(ns))
|
|
262
|
+
namespaces.push(ns);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// A prefix a shorter one already covers (Z and ZFI) would count its objects twice: dropped.
|
|
266
|
+
const covered = (p, all) => all.some((q) => q !== p && p.startsWith(q));
|
|
267
|
+
const both = [...prefixes, ...namespaces];
|
|
268
|
+
for (const list of [prefixes, namespaces]) {
|
|
269
|
+
for (let i = list.length - 1; i >= 0; i--)
|
|
270
|
+
if (covered(list[i], both))
|
|
271
|
+
list.splice(i, 1);
|
|
272
|
+
}
|
|
273
|
+
const allPrefixes = [...prefixes, ...namespaces];
|
|
274
|
+
const customerNamed = (name) => allPrefixes.some((p) => name.startsWith(p) || (!isNamespace(p) && name.startsWith(`SAPL${p}`)));
|
|
275
|
+
// ── Step 2: packages + texts ──
|
|
276
|
+
const pkgInfo = new Map();
|
|
277
|
+
const pkgText = new Map();
|
|
278
|
+
if (step(2)) {
|
|
279
|
+
for (const p of allPrefixes) {
|
|
280
|
+
const res = await run(2, SQL.packages(p), 10000);
|
|
281
|
+
if ('rows' in res)
|
|
282
|
+
for (const r of res.rows)
|
|
283
|
+
pkgInfo.set(clean(r.DEVCLASS).toUpperCase(), r);
|
|
284
|
+
const tx = await run(2, SQL.packageTexts(p), 10000);
|
|
285
|
+
if ('rows' in tx)
|
|
286
|
+
for (const r of tx.rows)
|
|
287
|
+
pkgText.set(clean(r.DEVCLASS).toUpperCase(), clean(r.CTEXT));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ── Step 3: counts by package and type ──
|
|
291
|
+
const byPkg = new Map();
|
|
292
|
+
const countedPrefixes = new Set();
|
|
293
|
+
if (step(3)) {
|
|
294
|
+
for (const p of allPrefixes) {
|
|
295
|
+
const res = await run(3, SQL.counts(p), 50000);
|
|
296
|
+
if (!('rows' in res))
|
|
297
|
+
continue;
|
|
298
|
+
countedPrefixes.add(p);
|
|
299
|
+
for (const r of res.rows) {
|
|
300
|
+
const pkg = clean(r.DEVCLASS).toUpperCase();
|
|
301
|
+
const t = clean(r.OBJECT).toUpperCase();
|
|
302
|
+
const m = byPkg.get(pkg) ?? {};
|
|
303
|
+
m[t] = (m[t] ?? 0) + num(r.CNT);
|
|
304
|
+
byPkg.set(pkg, m);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const packageNames = [...new Set([...pkgInfo.keys(), ...byPkg.keys()])]
|
|
309
|
+
.filter((p) => p !== LOCAL_PACKAGE).sort((a, b) => a.localeCompare(b));
|
|
310
|
+
// ── Step 4: every object, per package, with the control total ──
|
|
311
|
+
const objects = [];
|
|
312
|
+
const seenObj = new Set();
|
|
313
|
+
const genPerPkg = new Map();
|
|
314
|
+
const addObject = (r, pkg) => {
|
|
315
|
+
const type = clean(r.OBJECT).toUpperCase();
|
|
316
|
+
const name = clean(r.OBJ_NAME).toUpperCase();
|
|
317
|
+
if (!codeTypes.has(type))
|
|
318
|
+
return;
|
|
319
|
+
const key = `${type}|${name}`;
|
|
320
|
+
if (seenObj.has(key))
|
|
321
|
+
return;
|
|
322
|
+
seenObj.add(key);
|
|
323
|
+
objects.push({
|
|
324
|
+
type, name, package: pkg, author: clean(r.AUTHOR), createdOn: iso(r.CREATED_ON) ?? clean(r.CREATED_ON),
|
|
325
|
+
generated: clean(r.GENFLAG) !== '', changed: null,
|
|
326
|
+
callers: { read: false, why: 'Where-used was not read.' }, batch: null, usage: { state: 'unknown', why: NO_USAGE },
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
if (step(4)) {
|
|
330
|
+
for (const pkg of packageNames) {
|
|
331
|
+
if (aborted()) {
|
|
332
|
+
cut.add(4);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (!PACKAGE_RE.test(pkg)) {
|
|
336
|
+
unknowns.push({ what: `step 4: objects of package ${pkg}`, why: 'Not a valid package name; it was not read.' });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const counts = byPkg.get(pkg);
|
|
340
|
+
const expected = counts ? Object.values(counts).reduce((n, c) => n + c, 0) : 0;
|
|
341
|
+
const prefixCounted = allPrefixes.some((p) => pkg.startsWith(p) && countedPrefixes.has(p));
|
|
342
|
+
if (prefixCounted && expected === 0)
|
|
343
|
+
continue;
|
|
344
|
+
const res = await run(4, SQL.packageObjects(pkg), prefixCounted ? expected + 50 : 50000, `step 4: objects of package ${pkg}`);
|
|
345
|
+
if (!('rows' in res))
|
|
346
|
+
continue;
|
|
347
|
+
if (prefixCounted && res.rows.length !== expected) {
|
|
348
|
+
unknowns.push({
|
|
349
|
+
what: `step 4: objects of package ${pkg}`,
|
|
350
|
+
why: `Package ${pkg}: the system counted ${expected} objects but the pull returned ${res.rows.length}.`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
for (const r of res.rows) {
|
|
354
|
+
if (clean(r.GENFLAG))
|
|
355
|
+
genPerPkg.set(pkg, (genPerPkg.get(pkg) ?? 0) + 1);
|
|
356
|
+
addObject(r, pkg);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// ── Step 5: $TMP ──
|
|
361
|
+
const localByPrefix = {};
|
|
362
|
+
let localCode = 0;
|
|
363
|
+
if (step(5)) {
|
|
364
|
+
for (const p of allPrefixes) {
|
|
365
|
+
if (aborted()) {
|
|
366
|
+
cut.add(5);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
const cnt = await run(5, SQL.localCount(p), 10);
|
|
370
|
+
if (!('rows' in cnt))
|
|
371
|
+
continue;
|
|
372
|
+
const n = num(cnt.rows[0]?.CNT);
|
|
373
|
+
if (!n)
|
|
374
|
+
continue;
|
|
375
|
+
localByPrefix[p] = n;
|
|
376
|
+
const res = await paged(5, (a) => SQL.localObjects(p, a), 'OBJ_NAME', (r) => `${clean(r.OBJECT)}|${clean(r.OBJ_NAME)}`, abortOnly);
|
|
377
|
+
if (res.stop === 'abort')
|
|
378
|
+
cut.add(5);
|
|
379
|
+
for (const r of res.rows) {
|
|
380
|
+
const before = objects.length;
|
|
381
|
+
addObject(r, LOCAL_PACKAGE);
|
|
382
|
+
if (objects.length > before)
|
|
383
|
+
localCode += 1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Lookups over the code objects.
|
|
388
|
+
const byKey = new Map(objects.map((o) => [`${o.type}|${o.name}`, o]));
|
|
389
|
+
const byProgname = new Map();
|
|
390
|
+
for (const o of objects) {
|
|
391
|
+
const pn = reposrcNameFor(o.type, o.name);
|
|
392
|
+
if (pn)
|
|
393
|
+
byProgname.set(pn, o);
|
|
394
|
+
}
|
|
395
|
+
// ── Step 6: change dates from REPOSRC ──
|
|
396
|
+
if (step(6)) {
|
|
397
|
+
for (const p of allPrefixes) {
|
|
398
|
+
for (const lp of [p, ...fugrPrefixes(p, 'SAPL')]) {
|
|
399
|
+
if (aborted()) {
|
|
400
|
+
cut.add(6);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
const res = await paged(6, (a) => SQL.changes(lp, a), 'PROGNAME', (r) => clean(r.PROGNAME), abortOnly);
|
|
404
|
+
if (res.stop === 'abort')
|
|
405
|
+
cut.add(6);
|
|
406
|
+
for (const r of res.rows) {
|
|
407
|
+
const o = byProgname.get(clean(r.PROGNAME).toUpperCase());
|
|
408
|
+
if (!o)
|
|
409
|
+
continue;
|
|
410
|
+
const at = iso(r.UDAT) ?? iso(r.CDAT);
|
|
411
|
+
if (!at)
|
|
412
|
+
continue;
|
|
413
|
+
const by = iso(r.UDAT) ? clean(r.UNAM) : clean(r.CNAM);
|
|
414
|
+
o.changed = { at, by, source: 'REPOSRC' };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ── Step 7: batch runs ──
|
|
420
|
+
const usageSources = [];
|
|
421
|
+
let batchPeriod = null;
|
|
422
|
+
let batchSource = {
|
|
423
|
+
name: 'Batch jobs', state: 'not read', period: null, rows: null,
|
|
424
|
+
note: `${opts.jobs ? STOPPED : 'skipped on request'}. ${BATCH_NOTE}`,
|
|
425
|
+
};
|
|
426
|
+
if (opts.jobs && step(7)) {
|
|
427
|
+
const win = await run(7, SQL.batchWindow(), 10);
|
|
428
|
+
let failed = 'error' in win ? win.error : undefined;
|
|
429
|
+
if ('rows' in win) {
|
|
430
|
+
const from = iso(win.rows[0]?.FIRST_RUN);
|
|
431
|
+
const to = iso(win.rows[0]?.LAST_RUN);
|
|
432
|
+
if (from && to)
|
|
433
|
+
batchPeriod = { from, to };
|
|
434
|
+
}
|
|
435
|
+
let found = 0;
|
|
436
|
+
for (const p of allPrefixes) {
|
|
437
|
+
if (aborted()) {
|
|
438
|
+
cut.add(7);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const res = await run(7, SQL.batch(p), 50000);
|
|
442
|
+
if (!('rows' in res)) {
|
|
443
|
+
failed = failed ?? res.error;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
for (const r of res.rows) {
|
|
447
|
+
const o = byKey.get(`PROG|${clean(r.PROGNAME).toUpperCase()}`);
|
|
448
|
+
if (!o)
|
|
449
|
+
continue;
|
|
450
|
+
found += 1;
|
|
451
|
+
o.batch = { runs: num(r.RUNS), lastRun: iso(r.LAST_RUN), source: 'TBTCO/TBTCP', period: batchPeriod };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
batchSource = cut.has(7)
|
|
455
|
+
? { name: 'Batch jobs', state: 'not read', period: batchPeriod, rows: found, note: `${STOPPED}. ${BATCH_NOTE}` }
|
|
456
|
+
: failed
|
|
457
|
+
? { name: 'Batch jobs', state: 'not read', period: batchPeriod, rows: found, note: `${failed} ${BATCH_NOTE}` }
|
|
458
|
+
: { name: 'Batch jobs', state: found ? 'active' : 'empty', period: batchPeriod, rows: found, note: BATCH_NOTE };
|
|
459
|
+
}
|
|
460
|
+
let susg = { state: 'not read', period: null, error: STOPPED };
|
|
461
|
+
let scmon = { state: 'not read', period: null, error: STOPPED };
|
|
462
|
+
const susgUsed = new Map(); // object key → last used
|
|
463
|
+
const scmonUsed = new Set();
|
|
464
|
+
let susgObjectsError;
|
|
465
|
+
let scmonObjectsError;
|
|
466
|
+
const objectOf = (type, name, progname) => byKey.get(`${clean(type).toUpperCase()}|${clean(name).toUpperCase()}`) ?? byProgname.get(clean(progname).toUpperCase());
|
|
467
|
+
if (step(8)) {
|
|
468
|
+
// SUSG
|
|
469
|
+
{
|
|
470
|
+
const admin = await run(8, SQL.susgAdmin(), 1000, 'step 8: SUSG');
|
|
471
|
+
const data = await run(8, SQL.susgData(), 10, 'step 8: SUSG');
|
|
472
|
+
const err = 'error' in admin ? admin.error : 'error' in data ? data.error : undefined;
|
|
473
|
+
if (err) {
|
|
474
|
+
susg = { state: 'not read', period: null, error: err };
|
|
475
|
+
usageSources.push({ name: 'SUSG', state: 'not read', period: null, rows: null, note: err });
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
const aRows = admin.rows;
|
|
479
|
+
const rows = num(data.rows[0]?.CNT);
|
|
480
|
+
const froms = aRows.map((r) => iso(r.DATE_FROM)).filter((x) => !!x).sort();
|
|
481
|
+
const tos = aRows.map((r) => iso(r.DATE_TO)).filter((x) => !!x).sort();
|
|
482
|
+
const period = froms.length && tos.length ? { from: froms[0], to: tos[tos.length - 1] } : null;
|
|
483
|
+
const state = period && rows > 0 ? 'active' : aRows.length || rows > 0 ? 'empty' : 'off';
|
|
484
|
+
susg = { state, period };
|
|
485
|
+
usageSources.push({
|
|
486
|
+
name: 'SUSG', state, period, rows,
|
|
487
|
+
note: state === 'active' ? 'Aggregated usage over the stated period.' : state === 'empty' ? 'Set up, but holds no usage over a stated period.' : 'Not set up on this system.',
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// SCMON
|
|
492
|
+
{
|
|
493
|
+
const cfg = await run(8, SQL.scmonConfig(), 100, 'step 8: SCMON');
|
|
494
|
+
const data = await run(8, SQL.scmonData(), 10, 'step 8: SCMON');
|
|
495
|
+
const slice = await run(8, SQL.scmonSlice(), 10, 'step 8: SCMON');
|
|
496
|
+
const err = 'error' in cfg ? cfg.error : 'error' in data ? data.error : 'error' in slice ? slice.error : undefined;
|
|
497
|
+
if (err) {
|
|
498
|
+
scmon = { state: 'not read', period: null, error: err };
|
|
499
|
+
usageSources.push({ name: 'SCMON', state: 'not read', period: null, rows: null, note: err });
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const cfgRows = cfg.rows.length;
|
|
503
|
+
const rows = num(data.rows[0]?.CNT);
|
|
504
|
+
const s = slice.rows[0];
|
|
505
|
+
const from = iso(s?.FROM_DATE);
|
|
506
|
+
const to = iso(s?.TO_DATE);
|
|
507
|
+
const period = from && to ? { from, to } : null;
|
|
508
|
+
const state = cfgRows > 0 && rows > 0 && period ? 'active' : cfgRows > 0 || rows > 0 ? 'empty' : 'off';
|
|
509
|
+
scmon = { state, period };
|
|
510
|
+
usageSources.push({
|
|
511
|
+
name: 'SCMON', state, period, rows,
|
|
512
|
+
note: state === 'active' ? 'Call monitor rows over the stated period.' : state === 'empty' ? 'Set up, but holds no calls over a stated period.' : 'Not set up on this system.',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// The ST-PI copy of the call monitor: state only, it does not feed usage here.
|
|
517
|
+
{
|
|
518
|
+
const cfg = await run(8, SQL.sdfScmonConfig(), 10, 'step 8: SCMON (ST-PI copy)');
|
|
519
|
+
const data = await run(8, SQL.sdfScmonData(), 10, 'step 8: SCMON (ST-PI copy)');
|
|
520
|
+
const err = 'error' in cfg ? cfg.error : 'error' in data ? data.error : undefined;
|
|
521
|
+
if (err)
|
|
522
|
+
usageSources.push({ name: 'SCMON (ST-PI copy)', state: 'not read', period: null, rows: null, note: err });
|
|
523
|
+
else {
|
|
524
|
+
const c = num(cfg.rows[0]?.CNT);
|
|
525
|
+
const rows = num(data.rows[0]?.CNT);
|
|
526
|
+
const state = c > 0 && rows > 0 ? 'active' : c > 0 || rows > 0 ? 'empty' : 'off';
|
|
527
|
+
usageSources.push({
|
|
528
|
+
name: 'SCMON (ST-PI copy)', state, period: null, rows,
|
|
529
|
+
note: state === 'active' ? 'Holds rows. Per-object use is not read from this copy.' : state === 'empty' ? 'Set up, but holds no calls.' : 'Not set up on this system.',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// ST-PI monitor: system snapshots, never usage.
|
|
534
|
+
{
|
|
535
|
+
const header = await run(8, SQL.monHeader(), 10, 'step 8: ST-PI monitor');
|
|
536
|
+
const call = await run(8, SQL.monCall(), 10, 'step 8: ST-PI monitor');
|
|
537
|
+
const err = 'error' in header ? header.error : 'error' in call ? call.error : undefined;
|
|
538
|
+
const rows = err ? null : num(header.rows[0]?.CNT) + num(call.rows[0]?.CNT);
|
|
539
|
+
usageSources.push({
|
|
540
|
+
name: 'ST-PI monitor (not usage)', state: err ? 'not read' : rows ? 'active' : 'empty', period: null, rows,
|
|
541
|
+
note: err ?? 'System snapshots (work processes, memory). It does not record which programs ran.',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
// Workload statistics: the window only.
|
|
545
|
+
{
|
|
546
|
+
const w = await run(8, SQL.workload(), 10, 'step 8: workload statistics');
|
|
547
|
+
if ('error' in w)
|
|
548
|
+
usageSources.push({ name: 'Workload statistics', state: 'not read', period: null, rows: null, note: w.error });
|
|
549
|
+
else {
|
|
550
|
+
const from = iso(w.rows[0]?.FROM_DATE);
|
|
551
|
+
const to = iso(w.rows[0]?.TO_DATE);
|
|
552
|
+
const rows = num(w.rows[0]?.CNT);
|
|
553
|
+
usageSources.push({
|
|
554
|
+
name: 'Workload statistics', state: rows ? 'active' : 'empty', period: from && to ? { from, to } : null, rows,
|
|
555
|
+
note: from && to
|
|
556
|
+
? `Covers ${from} to ${to}. Per-program figures are stored in a form SQL cannot read, so they do not count as use here.`
|
|
557
|
+
: 'No workload window recorded. Per-program figures are stored in a form SQL cannot read.',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Per-object pulls, only from an active source — and only where the join is
|
|
562
|
+
// proven (it is not yet: see `unprovenPerObjectUsage`). Function groups run
|
|
563
|
+
// as SAPL<fg>, which LIKE '<p>%' never matches, so those prefixes are read too.
|
|
564
|
+
const usagePrefixes = allPrefixes.flatMap((p) => [p, ...fugrPrefixes(p, 'SAPL')]);
|
|
565
|
+
if (!opts.unprovenPerObjectUsage) { /* no per-object usage until the link is proven live */ }
|
|
566
|
+
else if (susg.state === 'active') {
|
|
567
|
+
for (const p of usagePrefixes) {
|
|
568
|
+
const res = await paged(8, (a) => SQL.susgObjects(p, a), 'PROGNAME', (r) => `${clean(r.PROGNAME)}|${clean(r.OBJ_TYPE)}|${clean(r.OBJ_NAME)}`, abortOnly, 'step 8: SUSG per object');
|
|
569
|
+
if (!res.complete)
|
|
570
|
+
susgObjectsError = res.error ?? STOPPED;
|
|
571
|
+
for (const r of res.rows) {
|
|
572
|
+
const o = objectOf(r.OBJ_TYPE, r.OBJ_NAME, r.PROGNAME);
|
|
573
|
+
if (!o)
|
|
574
|
+
continue;
|
|
575
|
+
const k = `${o.type}|${o.name}`;
|
|
576
|
+
const lu = iso(r.LAST_USED);
|
|
577
|
+
const prev = susgUsed.get(k);
|
|
578
|
+
susgUsed.set(k, prev && lu ? (prev > lu ? prev : lu) : (prev ?? lu));
|
|
579
|
+
}
|
|
580
|
+
if (susgObjectsError)
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (scmon.state === 'active') {
|
|
585
|
+
for (const p of usagePrefixes) {
|
|
586
|
+
const res = await paged(8, (a) => SQL.scmonProg(p, a), 'PROGNAME', (r) => `${clean(r.PROGNAME)}|${clean(r.PROGID)}`, abortOnly, 'step 8: SCMON per object');
|
|
587
|
+
if (!res.complete)
|
|
588
|
+
scmonObjectsError = res.error ?? STOPPED;
|
|
589
|
+
for (const r of res.rows) {
|
|
590
|
+
const o = objectOf(r.OBJECT, r.OBJ_NAME, r.PROGNAME);
|
|
591
|
+
if (o)
|
|
592
|
+
scmonUsed.add(`${o.type}|${o.name}`);
|
|
593
|
+
}
|
|
594
|
+
if (scmonObjectsError)
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
usageSources.push(batchSource);
|
|
600
|
+
// Usage per object (D2).
|
|
601
|
+
// The window the page's Usage tile names (its first active source). An object
|
|
602
|
+
// with nothing read about it on a system that has one says so with the
|
|
603
|
+
// window, never "no usage is recorded" under a tile that names a period.
|
|
604
|
+
// An aggregate window (workload statistics) is never read per object, and the sentence says so.
|
|
605
|
+
const unknownWhy = usageUnknownLineOf(usageSources.map((s) => ({ name: s.name, state: s.state, ...(s.period ? { period: s.period } : {}) })), takenAt) ?? NO_USAGE;
|
|
606
|
+
// "Unused" is never said from a window too short or too old to show it (rehearsal fix 2).
|
|
607
|
+
const unusedOrUnknown = (source, period) => (shortUsageWindow(period, takenAt)
|
|
608
|
+
? { state: 'unknown', why: outsideUsageWindowLine(period, takenAt) }
|
|
609
|
+
: { state: 'unused', source, period });
|
|
610
|
+
for (const o of objects) {
|
|
611
|
+
const k = `${o.type}|${o.name}`;
|
|
612
|
+
let u;
|
|
613
|
+
if (!started.has(8))
|
|
614
|
+
u = { state: 'not read', why: STOPPED };
|
|
615
|
+
else if (!opts.unprovenPerObjectUsage && (susg.state === 'active' || scmon.state === 'active')) {
|
|
616
|
+
// The source is real and active; reading it per object is not proven, so nothing is claimed either way.
|
|
617
|
+
u = { state: 'unknown', why: notPerObject(susg.state === 'active' ? 'SUSG' : 'SCMON', susg.state === 'active' ? susg.period : scmon.period) };
|
|
618
|
+
}
|
|
619
|
+
else if (susg.state === 'active') {
|
|
620
|
+
if (susgObjectsError)
|
|
621
|
+
u = { state: 'not read', why: susgObjectsError };
|
|
622
|
+
else if (susgUsed.has(k)) {
|
|
623
|
+
const lu = susgUsed.get(k);
|
|
624
|
+
u = { state: 'used', source: 'SUSG', period: susg.period, ...(lu ? { lastUsed: lu } : {}) };
|
|
625
|
+
}
|
|
626
|
+
else if (RUNTIME_TYPES.has(o.type))
|
|
627
|
+
u = unusedOrUnknown('SUSG', susg.period);
|
|
628
|
+
else
|
|
629
|
+
u = { state: 'unknown', why: 'Runtime use is not recorded for this type of object.' };
|
|
630
|
+
}
|
|
631
|
+
else if (scmon.state === 'active') {
|
|
632
|
+
if (scmonObjectsError)
|
|
633
|
+
u = { state: 'not read', why: scmonObjectsError };
|
|
634
|
+
else if (scmonUsed.has(k))
|
|
635
|
+
u = { state: 'used', source: 'SCMON', period: scmon.period };
|
|
636
|
+
else if (RUNTIME_TYPES.has(o.type))
|
|
637
|
+
u = unusedOrUnknown('SCMON', scmon.period);
|
|
638
|
+
else
|
|
639
|
+
u = { state: 'unknown', why: 'Runtime use is not recorded for this type of object.' };
|
|
640
|
+
}
|
|
641
|
+
else if (susg.state === 'not read' || scmon.state === 'not read') {
|
|
642
|
+
u = { state: 'not read', why: susg.state === 'not read' ? susg.error : scmon.error };
|
|
643
|
+
}
|
|
644
|
+
else
|
|
645
|
+
u = { state: 'unknown', why: unknownWhy };
|
|
646
|
+
if (o.batch && o.batch.runs > 0 && u.state !== 'used') {
|
|
647
|
+
u = {
|
|
648
|
+
state: 'used', source: 'batch',
|
|
649
|
+
...(o.batch.period ? { period: o.batch.period } : {}),
|
|
650
|
+
...(o.batch.lastRun ? { lastUsed: o.batch.lastRun } : {}),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
o.usage = u;
|
|
654
|
+
}
|
|
655
|
+
// ── Steps 9 + 10: where-used from the index ──
|
|
656
|
+
let xref = { read: 'not read', objectsCovered: 0, note: 'skipped on request' };
|
|
657
|
+
let xrefStopped = false; // step 9 stopped early and is already listed under unknowns
|
|
658
|
+
if (!opts.xref) {
|
|
659
|
+
for (const o of objects)
|
|
660
|
+
o.callers = { read: false, why: 'Where-used was skipped on request.' };
|
|
661
|
+
}
|
|
662
|
+
else if (!started.has(8) && aborted()) {
|
|
663
|
+
xref = { read: 'not read', objectsCovered: 0, note: STOPPED };
|
|
664
|
+
for (const o of objects)
|
|
665
|
+
o.callers = { read: false, why: 'Stopped before where-used reached this object.' };
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const deadline = now().getTime() + Math.max(0, opts.maxMinutes) * 60_000;
|
|
669
|
+
let budgetHit = false;
|
|
670
|
+
const xrefStop = () => {
|
|
671
|
+
if (aborted())
|
|
672
|
+
return 'abort';
|
|
673
|
+
if (now().getTime() > deadline) {
|
|
674
|
+
budgetHit = true;
|
|
675
|
+
return 'budget';
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
};
|
|
679
|
+
const indexRows = [];
|
|
680
|
+
/** Names with more index rows than one page holds: unread, the rest is read on past them. */
|
|
681
|
+
const tooMany = new Set();
|
|
682
|
+
const tableState = new Map(); // `${table}|${prefix}` → result (null = not started)
|
|
683
|
+
let stopReason = null;
|
|
684
|
+
if (step(9)) {
|
|
685
|
+
for (const p of allPrefixes) {
|
|
686
|
+
for (const table of ['CROSS', 'WBCROSSGT']) {
|
|
687
|
+
const k = `${table}|${p}`;
|
|
688
|
+
if (stopReason === 'abort' || stopReason === 'budget') {
|
|
689
|
+
tableState.set(k, null);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const build = table === 'CROSS' ? (a, st) => SQL.cross(p, a, st) : (a, st) => SQL.wbcrossgt(p, a, st);
|
|
693
|
+
const res = await paged(9, build, 'NAME', (r) => `${clean(r.NAME)}|${clean(r.INCLUDE)}`, xrefStop, `step 9: where-used (${table}, ${p})`, (name) => tooMany.add(name.toUpperCase().split('\\')[0]));
|
|
694
|
+
tableState.set(k, res);
|
|
695
|
+
for (const r of res.rows)
|
|
696
|
+
indexRows.push({ name: clean(r.NAME).toUpperCase(), include: clean(r.INCLUDE).toUpperCase() });
|
|
697
|
+
if (res.stop === 'abort' || res.stop === 'budget')
|
|
698
|
+
stopReason = res.stop;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
stopReason = 'abort';
|
|
704
|
+
}
|
|
705
|
+
if (stopReason !== null)
|
|
706
|
+
xrefStopped = true;
|
|
707
|
+
if (stopReason === 'abort')
|
|
708
|
+
unknowns.push({ what: 'step 9: where-used', why: STOPPED });
|
|
709
|
+
if (stopReason === 'budget' || budgetHit) {
|
|
710
|
+
unknowns.push({ what: 'step 9: where-used', why: `the time budget (${opts.maxMinutes} minutes) ran out` });
|
|
711
|
+
}
|
|
712
|
+
// Step 10: include → main program (paged the same way, same budget).
|
|
713
|
+
// D010INC first: a report include that TADIR also lists as a PROG belongs to its
|
|
714
|
+
// master. A report maps to itself only where D010INC never names it as an include.
|
|
715
|
+
const masters = new Map();
|
|
716
|
+
if (stopReason === null && step(10)) {
|
|
717
|
+
let mastersComplete = true;
|
|
718
|
+
let mastersFailed = false; // a failed page is already under unknowns (run())
|
|
719
|
+
for (const p of allPrefixes) {
|
|
720
|
+
for (const lp of [p, ...fugrPrefixes(p, 'L')]) {
|
|
721
|
+
const res = await paged(10, (a) => SQL.masters(lp, a), 'INCLUDE', (r) => `${clean(r.INCLUDE)}|${clean(r.MASTER)}`, xrefStop);
|
|
722
|
+
for (const r of res.rows) {
|
|
723
|
+
const inc = clean(r.INCLUDE).toUpperCase();
|
|
724
|
+
if (!masters.has(inc))
|
|
725
|
+
masters.set(inc, clean(r.MASTER).toUpperCase());
|
|
726
|
+
}
|
|
727
|
+
if (!res.complete) {
|
|
728
|
+
mastersComplete = false;
|
|
729
|
+
if (res.stop === 'error')
|
|
730
|
+
mastersFailed = true;
|
|
731
|
+
if (res.stop === 'abort' || res.stop === 'budget') {
|
|
732
|
+
stopReason = res.stop;
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (stopReason)
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
if (!mastersComplete && !mastersFailed) {
|
|
741
|
+
unknowns.push({
|
|
742
|
+
what: 'step 10: include to program',
|
|
743
|
+
why: stopReason === 'abort' ? STOPPED : stopReason === 'budget' ? `the time budget (${opts.maxMinutes} minutes) ran out` : 'read only in part; some callers are counted as unmapped',
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const o of objects)
|
|
748
|
+
if (o.type === 'PROG' && !masters.has(o.name))
|
|
749
|
+
masters.set(o.name, o.name);
|
|
750
|
+
const selfKey = new Map();
|
|
751
|
+
for (const o of objects)
|
|
752
|
+
if (!selfKey.has(o.name))
|
|
753
|
+
selfKey.set(o.name, `${o.type}|${o.name}`);
|
|
754
|
+
const callers = callersFromIndex(indexRows, masters, (n) => selfKey.get(n) ?? '');
|
|
755
|
+
const whyFor = (r) => {
|
|
756
|
+
if (r?.stop === 'error')
|
|
757
|
+
return r.error ?? 'The where-used query failed.';
|
|
758
|
+
if (r?.stop === 'stuck')
|
|
759
|
+
return 'Too many where-used rows for one name to page through.';
|
|
760
|
+
if (r?.stop === 'budget' || (!r && budgetHit))
|
|
761
|
+
return 'The where-used time budget ran out before this object was read.';
|
|
762
|
+
return 'Stopped before where-used reached this object.';
|
|
763
|
+
};
|
|
764
|
+
const okFor = (r, name) => {
|
|
765
|
+
if (!r)
|
|
766
|
+
return false;
|
|
767
|
+
if (r.complete)
|
|
768
|
+
return true;
|
|
769
|
+
const b = r.boundary;
|
|
770
|
+
return b !== undefined && name < b && !b.startsWith(`${name}\\`);
|
|
771
|
+
};
|
|
772
|
+
let covered = 0;
|
|
773
|
+
let indexed = 0;
|
|
774
|
+
for (const o of objects) {
|
|
775
|
+
if (!INDEXED_TYPES.has(o.type)) {
|
|
776
|
+
o.callers = { read: false, why: NOT_INDEXED };
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
indexed += 1;
|
|
780
|
+
if (tooMany.has(o.name)) {
|
|
781
|
+
o.callers = { read: false, why: 'too many index rows to read' };
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const owners = allPrefixes.filter((p) => o.name.startsWith(p));
|
|
785
|
+
let why = 'Where-used was not read for this name.';
|
|
786
|
+
let ok = false;
|
|
787
|
+
for (const p of owners) {
|
|
788
|
+
const c = tableState.get(`CROSS|${p}`);
|
|
789
|
+
const w = tableState.get(`WBCROSSGT|${p}`);
|
|
790
|
+
if (okFor(c, o.name) && okFor(w, o.name)) {
|
|
791
|
+
ok = true;
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
why = whyFor(!okFor(c, o.name) ? c : w);
|
|
795
|
+
}
|
|
796
|
+
if (ok) {
|
|
797
|
+
const c = callers.get(o.name);
|
|
798
|
+
const keys = c ? [...c.objects].sort() : [];
|
|
799
|
+
o.callers = { objects: keys.length, includesUnmapped: c?.includesUnmapped ?? 0, sample: keys.slice(0, 5), source: 'index', read: true };
|
|
800
|
+
covered += 1;
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
o.callers = { read: false, why };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const total = indexed;
|
|
807
|
+
if (covered === total)
|
|
808
|
+
xref = { read: 'complete', objectsCovered: covered, note: `where-used read for ${covered} of ${total} objects` };
|
|
809
|
+
else if (covered > 0)
|
|
810
|
+
xref = { read: 'partial', objectsCovered: covered, note: `where-used read for ${covered} of ${total} objects` };
|
|
811
|
+
else {
|
|
812
|
+
const firstErr = [...tableState.values()].find((r) => r?.stop === 'error')?.error;
|
|
813
|
+
xref = { read: 'not read', objectsCovered: 0, note: firstErr ? `where-used could not be read: ${firstErr}` : `where-used read for 0 of ${total} objects` };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// ── Step 11: modifications of SAP objects ──
|
|
817
|
+
const real = [];
|
|
818
|
+
let noteCorrections = null;
|
|
819
|
+
let otherRows = null;
|
|
820
|
+
if (step(11)) {
|
|
821
|
+
for (const op of ['MOD', 'NEW', 'IMP']) {
|
|
822
|
+
const res = await run(11, SQL.modifications(op), 50000);
|
|
823
|
+
if (!('rows' in res))
|
|
824
|
+
continue;
|
|
825
|
+
for (const r of res.rows) {
|
|
826
|
+
const objectName = clean(r.OBJ_NAME).toUpperCase();
|
|
827
|
+
if (customerNamed(objectName))
|
|
828
|
+
continue;
|
|
829
|
+
real.push({
|
|
830
|
+
objectType: clean(r.OBJ_TYPE), objectName, subType: clean(r.SUB_TYPE), subName: clean(r.SUB_NAME),
|
|
831
|
+
operation: clean(r.OPERATION), date: iso(r.MOD_DATE), user: clean(r.MOD_USER),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const note = await run(11, SQL.modificationCount('NOTE'), 10);
|
|
836
|
+
if ('rows' in note)
|
|
837
|
+
noteCorrections = num(note.rows[0]?.CNT);
|
|
838
|
+
const all = await run(11, SQL.modificationCount('ALL'), 10);
|
|
839
|
+
if ('rows' in all)
|
|
840
|
+
otherRows = num(all.rows[0]?.CNT);
|
|
841
|
+
}
|
|
842
|
+
// ── Step 12: successor data ──
|
|
843
|
+
if (step(12))
|
|
844
|
+
await run(12, SQL.successors(), 10);
|
|
845
|
+
// Unfinished steps after Ctrl-C (steps skipped on request are not unfinished).
|
|
846
|
+
for (let n = 1; n <= 12; n++) {
|
|
847
|
+
if (cut.has(n)) {
|
|
848
|
+
unknowns.push({ what: `step ${n}: ${STEP_LABELS[n]}`, why: STOPPED });
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (started.has(n))
|
|
852
|
+
continue;
|
|
853
|
+
if (n === 7 && !opts.jobs)
|
|
854
|
+
continue;
|
|
855
|
+
if ((n === 9 || n === 10) && !opts.xref)
|
|
856
|
+
continue;
|
|
857
|
+
if ((n === 9 || n === 10) && xrefStopped)
|
|
858
|
+
continue;
|
|
859
|
+
unknowns.push({ what: `step ${n}: ${STEP_LABELS[n]}`, why: STOPPED });
|
|
860
|
+
}
|
|
861
|
+
// ── Assemble ──
|
|
862
|
+
const nonCodeByType = {};
|
|
863
|
+
const lastChangedByPkg = new Map();
|
|
864
|
+
for (const o of objects) {
|
|
865
|
+
if (!o.changed)
|
|
866
|
+
continue;
|
|
867
|
+
const prev = lastChangedByPkg.get(o.package);
|
|
868
|
+
if (!prev || o.changed.at > prev.at)
|
|
869
|
+
lastChangedByPkg.set(o.package, { at: o.changed.at, by: o.changed.by });
|
|
870
|
+
}
|
|
871
|
+
const packages = packageNames.map((name) => {
|
|
872
|
+
const info = pkgInfo.get(name);
|
|
873
|
+
const counts = byPkg.get(name) ?? {};
|
|
874
|
+
let code = 0;
|
|
875
|
+
let nonCode = 0;
|
|
876
|
+
for (const [t, n] of Object.entries(counts)) {
|
|
877
|
+
if (codeTypes.has(t))
|
|
878
|
+
code += n;
|
|
879
|
+
else {
|
|
880
|
+
nonCode += n;
|
|
881
|
+
nonCodeByType[t] = (nonCodeByType[t] ?? 0) + n;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
name, text: pkgText.get(name) ?? '', parent: clean(info?.PARENTCL), component: clean(info?.DLVUNIT) || clean(info?.COMPONENT),
|
|
886
|
+
area: clean(info?.APPLICAT), objects: code + nonCode, codeObjects: code, nonCodeObjects: nonCode,
|
|
887
|
+
byType: { ...counts }, generated: genPerPkg.get(name) ?? 0, lastChanged: lastChangedByPkg.get(name) ?? null,
|
|
888
|
+
};
|
|
889
|
+
});
|
|
890
|
+
objects.sort((a, b) => a.package.localeCompare(b.package) || a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
|
891
|
+
const estate = {
|
|
892
|
+
kind: 'estate', formatVersion: 1, takenAt, by: opts.by, system: { ...(opts.system ?? {}) },
|
|
893
|
+
prefixes, namespaces, packages,
|
|
894
|
+
local: { byPrefix: localByPrefix, codeObjects: localCode },
|
|
895
|
+
nonCode: { byType: nonCodeByType },
|
|
896
|
+
usageSources,
|
|
897
|
+
modifications: { real, noteCorrections, otherRows },
|
|
898
|
+
xref, unknowns, queries,
|
|
899
|
+
};
|
|
900
|
+
return { estate, objects: { kind: 'estate-objects', formatVersion: 1, takenAt, objects } };
|
|
901
|
+
}
|