@cspeach/cli 1.1.48 → 1.1.50
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/dist/agent/cold-prune.js +1 -1
- package/dist/agent/conversation-prompt.js +52 -0
- package/dist/agent/level.js +1 -1
- package/dist/agent/loop.js +90 -8
- package/dist/agent/oversized-results.js +204 -0
- package/dist/agent/sap-connection-adapter.js +2 -0
- package/dist/agent/system-prompt.js +2 -2
- package/dist/agent/tool-dispatch.js +13 -1
- package/dist/agent/turn-error-ux.js +14 -0
- package/dist/approvals/op-labels.js +49 -10
- package/dist/approvals/render.js +14 -23
- package/dist/approvals/risk-floor.js +196 -19
- package/dist/approvals/visible-changes.js +35 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/plan-continue-gate.js +11 -0
- package/dist/commands/team-plan.js +16 -6
- package/dist/commands/team.js +295 -68
- package/dist/cost/cost-log.js +4 -0
- package/dist/cost/system-block.js +25 -0
- package/dist/journey/chain.js +489 -61
- package/dist/journey/clean-core.js +61 -36
- package/dist/journey/flow.js +75 -2
- package/dist/journey/people.js +37 -0
- package/dist/journey/rebuild.js +7 -6
- package/dist/journey/run.js +129 -42
- package/dist/journey/trigger.js +16 -10
- package/dist/models/server-config.js +13 -0
- package/dist/one-shot.js +1 -1
- package/dist/project-context/render.js +7 -0
- package/dist/projects/clean-core-confirm.js +166 -44
- package/dist/projects/extract-cca.js +8 -7
- package/dist/projects/index.js +0 -1
- package/dist/projects/merge-upgrade.js +2 -2
- package/dist/projects/project-folder.js +2 -1
- package/dist/projects/save-command.js +51 -14
- package/dist/projects/workspace.js +6 -3
- package/dist/register/decision-rules.js +2 -1
- package/dist/register/export-files.js +18 -0
- package/dist/register/picture.js +3 -3
- package/dist/register/project-transport.js +125 -0
- package/dist/register/records.js +25 -13
- package/dist/register/rules-edit.js +5 -0
- package/dist/register/status-file/progress-chart.js +17 -6
- package/dist/register/status-file/register-map.js +8 -5
- package/dist/register/status-file/register-model.js +55 -36
- package/dist/register/status-file/register-parts.js +97 -82
- package/dist/register/status-file/register-status-file.js +16 -7
- package/dist/register/status-file/report-model.js +293 -205
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-html.js +11 -4
- package/dist/register/store.js +2 -2
- package/dist/register/workbooks.js +24 -13
- package/dist/renderer/footer-line.js +10 -2
- package/dist/renderer/tool-labels.js +2 -2
- package/dist/repl/builtin-commands.js +1 -1
- package/dist/repl/continue-skill.js +5 -0
- package/dist/repl/level-command.js +6 -15
- package/dist/repl.js +37 -14
- package/dist/router/router-core.js +5 -3
- package/dist/session/fresh-start.js +15 -0
- package/dist/skill-catalog.js +4 -7
- package/dist/tools/_filesystem-shared.js +7 -0
- package/dist/tools/_team-work-path.js +53 -0
- package/dist/tools/approval.js +97 -19
- package/dist/tools/cut-markers.js +47 -0
- package/dist/tools/filesystem/extract-document.js +50 -2
- package/dist/tools/filesystem/file-edit.js +8 -1
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +8 -1
- package/dist/tools/filesystem/read-document.js +20 -4
- package/dist/tools/project/_merge-shared.js +2 -2
- package/dist/tools/project/_team-tool-shared.js +5 -0
- package/dist/tools/project/rules_propose.js +2 -1
- package/dist/tools/project/team_findings.js +81 -28
- package/dist/tools/project/team_lane_move.js +127 -0
- package/dist/tools/project/upgrade_merge_progress.js +8 -4
- package/dist/tools/text-elements/tools.js +7 -1
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/tool-output-read.js +5 -1
- package/dist/tools/update-method-intercept.js +18 -1
- package/dist/tools/write-pipeline.js +24 -5
- package/dist/ui/approval-modal.js +6 -3
- package/dist/ui/footer-line.js +19 -1
- package/package.json +3 -3
- package/dist/projects/merge-cca.js +0 -311
- package/dist/tools/project/cca_merge.js +0 -174
- package/test-harness/fe-draft/app.ts +0 -188
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +0 -461
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +0 -51
- package/test-harness/fe-draft/routes.ts +0 -224
- package/test-harness/fe-draft/routes.unit.test.ts +0 -45
- package/test-harness/fe-draft/server.ts +0 -225
- package/test-harness/fe-draft/variants.ts +0 -196
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { typeCode } from './risk-floor.js';
|
|
2
2
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
3
|
+
import { wrapRows } from '../renderer/wrap-rows.js';
|
|
4
|
+
import { shownUpTo } from '../register/atc-file.js';
|
|
5
|
+
import { transportForWrite } from '../register/project-transport.js';
|
|
3
6
|
/**
|
|
4
7
|
* Human-readable operation + object labels for the approval surfaces.
|
|
5
8
|
*
|
|
@@ -89,6 +92,32 @@ export function affectsLine(change) {
|
|
|
89
92
|
const t = sanitizeControlChars(String(change.affects ?? '')).trim();
|
|
90
93
|
return t ? `Affects: ${t}` : null;
|
|
91
94
|
}
|
|
95
|
+
/** Task 7 — the longest behaviour text a card shows, and the most rows it takes. */
|
|
96
|
+
const BEHAVIOUR_MAX = 240;
|
|
97
|
+
/** Words that mean "nothing changes": no row. */
|
|
98
|
+
const NOTHING_CHANGES = /^(none|no change|n\/a)\.?$/i;
|
|
99
|
+
export const BEHAVIOUR_MAX_ROWS = 3;
|
|
100
|
+
/**
|
|
101
|
+
* Task 7 — "Behaviour: <text>" for a change that says what callers or users
|
|
102
|
+
* will see differently; null when absent, empty, or 'none' / 'no change' /
|
|
103
|
+
* 'n/a'. The text is the model's: escape sequences and control characters
|
|
104
|
+
* go, whitespace collapses, cut at 240 code points.
|
|
105
|
+
*/
|
|
106
|
+
export function behaviourText(change) {
|
|
107
|
+
// shownUpTo drops whole escape sequences first (a lone sanitizeControlChars
|
|
108
|
+
// pass would leave their '[31m' tails), then every control character.
|
|
109
|
+
// The cut counts code points, so it never leaves half a surrogate pair.
|
|
110
|
+
const flat = Array.from(shownUpTo(String(change.behaviour ?? ''), Number.POSITIVE_INFINITY));
|
|
111
|
+
const t = flat.length > BEHAVIOUR_MAX ? `${flat.slice(0, BEHAVIOUR_MAX).join('')}…` : flat.join('');
|
|
112
|
+
if (!t || NOTHING_CHANGES.test(t))
|
|
113
|
+
return null;
|
|
114
|
+
return `Behaviour: ${t}`;
|
|
115
|
+
}
|
|
116
|
+
/** Task 7 — the behaviour text as the rows it is drawn on at `width` cells (at most 3); [] when none. */
|
|
117
|
+
export function behaviourLines(change, width) {
|
|
118
|
+
const t = behaviourText(change);
|
|
119
|
+
return t ? wrapRows(t, width, BEHAVIOUR_MAX_ROWS) : [];
|
|
120
|
+
}
|
|
92
121
|
/** Part 3 D12 — the longest request description a release card shows. */
|
|
93
122
|
const RELEASE_DESCRIPTION_MAX = 60;
|
|
94
123
|
const RELEASE_LAST_LINE = 'Cannot be undone. Once released, the request can move to the other systems on the transport route.';
|
|
@@ -136,17 +165,27 @@ export function releaseLines(change) {
|
|
|
136
165
|
* show a Transport segment.
|
|
137
166
|
* When nothing is resolvable, return undefined and the caller OMITS the
|
|
138
167
|
* segment — no "new (session)" promise (a $TMP write never creates one).
|
|
168
|
+
*
|
|
169
|
+
* Task 8 (B11) — two more facts, both absent → today's result:
|
|
170
|
+
* - `owning`: the request that already holds the object (request_approval's
|
|
171
|
+
* lookup). A modify/delete is forced into it by resolveWriteTransport, so
|
|
172
|
+
* it wins and says why: "(object already locked here)" — never worded as
|
|
173
|
+
* if the person chose it.
|
|
174
|
+
* - `project`: the request of the project joined in this folder. Every
|
|
175
|
+
* write tool uses it when the change names none (before the session
|
|
176
|
+
* transport), so it shows as "<id> (project)" — one order for card and write:
|
|
177
|
+
* owning lock (modify/delete) ?? explicit ?? project ?? session.
|
|
178
|
+
*
|
|
179
|
+
* Task 6 (project view): the choice itself is `transportForWrite`
|
|
180
|
+
* (register/project-transport.ts), the one the write tools call too. This
|
|
181
|
+
* card hands it the session request for a create only. 'resolve' writes
|
|
182
|
+
* (set_source, update_method, delete) never use the session request; the
|
|
183
|
+
* class-include, message and text-elements writes do fall back to it when no
|
|
184
|
+
* project request is set, and their modify card does not show it — today's
|
|
185
|
+
* card, kept byte for byte.
|
|
139
186
|
*/
|
|
140
|
-
export function displayTransport(op, explicit, session) {
|
|
141
|
-
|
|
142
|
-
case 'create':
|
|
143
|
-
return explicit ?? session ?? undefined;
|
|
144
|
-
case 'modify':
|
|
145
|
-
case 'delete':
|
|
146
|
-
return explicit;
|
|
147
|
-
default: // activate / release / run
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
187
|
+
export function displayTransport(op, explicit, session, owning, project) {
|
|
188
|
+
return transportForWrite({ op, explicit, owning, project, session: op === 'create' ? session : undefined }).label;
|
|
150
189
|
}
|
|
151
190
|
/**
|
|
152
191
|
* A create/delete "diff" is really a source dump (all-adds, or plain source).
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { riskReasonText, shownRiskReasons } from './risk-floor.js';
|
|
4
|
-
import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines } from './op-labels.js';
|
|
4
|
+
import { verbLine, viewAffordance, toSourceLines, affectsLine, releaseLines, behaviourText, behaviourLines } from './op-labels.js';
|
|
5
|
+
import { uiWidth } from '../renderer/ui-width.js';
|
|
5
6
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
6
7
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
7
8
|
import { withInquirer } from '../repl/inquirer-guard.js';
|
|
@@ -11,6 +12,7 @@ import { emitTrace } from '../renderer/trace.js';
|
|
|
11
12
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
12
13
|
import { paint } from '../renderer/theme.js';
|
|
13
14
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
15
|
+
import { collapseImplicitActivates, looksLikeUnifiedDiff } from './visible-changes.js';
|
|
14
16
|
import { withSpinnerSuspended } from '../renderer/tool-widget.js';
|
|
15
17
|
// Piece 2 §1.1 — the brand accent comes from the theme role, not a legacy hex.
|
|
16
18
|
const peach = (s) => paint('brand', s);
|
|
@@ -73,19 +75,7 @@ function colorizeDiff(diff) {
|
|
|
73
75
|
* compressed. This is what fixes the "what 2 changes?" complaint when the
|
|
74
76
|
* model declares modify+activate of the same object.
|
|
75
77
|
*/
|
|
76
|
-
|
|
77
|
-
const writeKeys = new Set();
|
|
78
|
-
for (const c of changes) {
|
|
79
|
-
if (c.op === 'modify' || c.op === 'create') {
|
|
80
|
-
writeKeys.add(`${c.type}:${c.object}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return changes.filter((c) => {
|
|
84
|
-
if (c.op !== 'activate')
|
|
85
|
-
return true;
|
|
86
|
-
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
78
|
+
// collapseImplicitActivates lives in ./visible-changes.ts (Task 9: request_approval uses it too).
|
|
89
79
|
export async function renderPlanGate(summary, changes, transport, effectiveRisk, system) {
|
|
90
80
|
const visibleChanges = collapseImplicitActivates(changes);
|
|
91
81
|
const visibleCount = visibleChanges.length;
|
|
@@ -122,7 +112,9 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk,
|
|
|
122
112
|
// "Apply all" mints it without its per-change card.
|
|
123
113
|
const steps = visibleChanges.map((c, i) => {
|
|
124
114
|
const affects = affectsLine(c);
|
|
125
|
-
|
|
115
|
+
// Task 7 — "Behaviour: …" is one fact (the card wraps it) and one row of the budget.
|
|
116
|
+
const behaviour = behaviourText(c);
|
|
117
|
+
return [`${i + 1}. ${verbLine(c)}`, ...(affects ? [` ${affects}`] : []), ...(behaviour ? [` ${behaviour}`] : []), ...releaseLines(c).map((l) => ` ${l}`)];
|
|
126
118
|
});
|
|
127
119
|
// Fix round 1 (owner ruling) — a release step is ALWAYS on the card, with
|
|
128
120
|
// its fact lines, wherever it sits: its rows have their own budget and
|
|
@@ -202,6 +194,9 @@ async function classicPlanGate(summary, visibleChanges, transport, effectiveRisk
|
|
|
202
194
|
const affects = affectsLine(c);
|
|
203
195
|
if (affects)
|
|
204
196
|
console.log(` ${chalk.dim(affects)}`);
|
|
197
|
+
// Task 7 — what callers or users will see differently, under its line.
|
|
198
|
+
for (const l of behaviourLines(c, uiWidth() - 11))
|
|
199
|
+
console.log(` ${chalk.dim(l)}`);
|
|
205
200
|
// Part 3 D12 / F-13 — a release row's facts, under its line.
|
|
206
201
|
for (const l of releaseLines(c))
|
|
207
202
|
console.log(` ${chalk.dim(l)}`);
|
|
@@ -240,8 +235,10 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
240
235
|
const affects = affectsLine(change);
|
|
241
236
|
// Part 3 D12 — a release card's facts, under the verb line.
|
|
242
237
|
const release = releaseLines(change).map((l) => `\n ${chalk.dim(l)}`).join('');
|
|
238
|
+
// Task 7 — "Behaviour: …" rows, after Affects and before the release lines.
|
|
239
|
+
const behaviour = behaviourLines(change, uiWidth() - 2).map((l) => `\n ${chalk.dim(l)}`).join('');
|
|
243
240
|
const choice = await withInquirer(() => select({
|
|
244
|
-
message: `${objLabel} — apply?${affects ? `\n ${chalk.dim(affects)}` : ''}${release}`,
|
|
241
|
+
message: `${objLabel} — apply?${affects ? `\n ${chalk.dim(affects)}` : ''}${behaviour}${release}`,
|
|
245
242
|
default: 'apply',
|
|
246
243
|
theme: inquirerTheme,
|
|
247
244
|
choices,
|
|
@@ -305,13 +302,7 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
305
302
|
* option doesn't return useless dim grey text when the model passed a
|
|
306
303
|
* one-paragraph description in the `diff` field.
|
|
307
304
|
*/
|
|
308
|
-
|
|
309
|
-
return s.split('\n').some((line) => {
|
|
310
|
-
if (line.startsWith('+++') || line.startsWith('---'))
|
|
311
|
-
return false;
|
|
312
|
-
return line.startsWith('+') || line.startsWith('-') || line.startsWith('@@');
|
|
313
|
-
});
|
|
314
|
-
}
|
|
305
|
+
// looksLikeUnifiedDiff lives in ./visible-changes.ts (next to collapseImplicitActivates).
|
|
315
306
|
/**
|
|
316
307
|
* v0.3 dispatcher: routes to Ink modal (via approvalEmitter) when running
|
|
317
308
|
* under Ink, else delegates to the existing inquirer-based flow.
|
|
@@ -6,15 +6,159 @@
|
|
|
6
6
|
export function typeCode(type) {
|
|
7
7
|
return String(type ?? '').trim().toUpperCase().split('/')[0].trim();
|
|
8
8
|
}
|
|
9
|
+
const DECLARATION_OPENER = /^\s*(CLASS-)?METHODS\b/i;
|
|
10
|
+
/** A line that may be a parameter of a declaration whose opener is outside the hunk. */
|
|
11
|
+
const PARAMETER_LIKE = /\b(TYPE|LIKE)\b/i;
|
|
12
|
+
const PARAMETER_KEYWORD = /^\s*(IMPORTING|EXPORTING|CHANGING|RETURNING|RAISING)\b/i;
|
|
13
|
+
const LOCAL_DECLARATION = /^\s*(DATA|FIELD-SYMBOLS|TYPES|CONSTANTS|STATICS)\b/i;
|
|
14
|
+
/**
|
|
15
|
+
* A context line shaped like one parameter: a name (or VALUE(…) /
|
|
16
|
+
* REFERENCE(…)) right before TYPE or LIKE. Two words before TYPE (`RAISE
|
|
17
|
+
* EXCEPTION TYPE`, `DATA lv TYPE`) are not this shape.
|
|
18
|
+
*/
|
|
19
|
+
const PARAMETER_LINE = /^\s*(!?[a-z_/][\w/]*|(VALUE|REFERENCE)\(\s*!?[\w/]+\s*\))\s+(TYPE|LIKE)\b/i;
|
|
20
|
+
/** A bare identifier list ending in `,` or `.` — e.g. exception classes under RAISING. */
|
|
21
|
+
const BARE_LIST = /^\s*[a-z_/][\w/]*(\s*,\s*[a-z_/][\w/]*)*\s*[,.]\s*$/i;
|
|
22
|
+
/** Single-word statements that are never part of a signature. */
|
|
23
|
+
const BARE_STATEMENT = /^\s*(END\w*|ELSE|RETURN|EXIT|CONTINUE|CLEANUP)\s*\.\s*$/i;
|
|
24
|
+
/**
|
|
25
|
+
* Task 7 (R2) — a statement that opens or ends a processing block. Its
|
|
26
|
+
* statement holds no signature (METHOD, FUNCTION, MODULE, END…) — FORM is the
|
|
27
|
+
* one exception: its USING / CHANGING / TABLES / RAISING parameters follow it,
|
|
28
|
+
* so a FORM settles the hunk only once its statement has ended in `.`.
|
|
29
|
+
* METHODS does not match (the `\b` stops at METHOD).
|
|
30
|
+
*/
|
|
31
|
+
const BLOCK_STATEMENT = /^\s*(METHOD|ENDMETHOD|FORM|ENDFORM|FUNCTION|ENDFUNCTION|MODULE|ENDMODULE)\b/i;
|
|
32
|
+
const FORM_STATEMENT = /^\s*FORM\b/i;
|
|
33
|
+
/** Fix round 1 — a parameter named like a keyword (`method TYPE string`, `form LIKE x`). */
|
|
34
|
+
const TYPED_AFTER_FIRST_WORD = /^\s*\S+\s+(TYPE|LIKE)\b/i;
|
|
35
|
+
/**
|
|
36
|
+
* Task 7 (R2) — the state a hunk header's text (between or after the `@@`
|
|
37
|
+
* marks) or a context line sets, when it is a block statement: 'out' (the
|
|
38
|
+
* lines after it are body), 'in' for a FORM that goes on (its parameters
|
|
39
|
+
* follow), undefined when it is not a block statement. Fix round 1 — a line
|
|
40
|
+
* shaped like a parameter is never a block statement.
|
|
41
|
+
*/
|
|
42
|
+
function blockState(code) {
|
|
43
|
+
if (!BLOCK_STATEMENT.test(code))
|
|
44
|
+
return undefined;
|
|
45
|
+
if (PARAMETER_LINE.test(code) || TYPED_AFTER_FIRST_WORD.test(code))
|
|
46
|
+
return undefined;
|
|
47
|
+
if (FORM_STATEMENT.test(code) && !code.trimEnd().endsWith('.'))
|
|
48
|
+
return 'in';
|
|
49
|
+
return 'out';
|
|
50
|
+
}
|
|
51
|
+
/** The text of a hunk header: `@@ -1,2 +1,2 @@ METHOD x.` or `@@ METHOD x. @@` → `METHOD x.`. */
|
|
52
|
+
function hunkHeaderText(raw) {
|
|
53
|
+
const m = /^@@(.*?)@@(.*)$/.exec(raw);
|
|
54
|
+
if (!m)
|
|
55
|
+
return raw.replace(/^@@/, '');
|
|
56
|
+
const inner = m[1].replace(/^\s*-\d+(,\d+)?\s+\+\d+(,\d+)?\s*$/, '');
|
|
57
|
+
return (m[2].trim() !== '' ? m[2] : inner);
|
|
58
|
+
}
|
|
59
|
+
/** The code part of an ABAP line: a `*` line is all comment; a `"` starts a trailing comment. */
|
|
60
|
+
function codeOf(line) {
|
|
61
|
+
if (line.startsWith('*'))
|
|
62
|
+
return '';
|
|
63
|
+
return line.replace(/".*$/, '');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Task 7 (B10) — might a unified diff change a method signature? This is a
|
|
67
|
+
* risk-floor input: it errs toward true (over-raising is accepted) and it
|
|
68
|
+
* can only ever raise risk, never lower it. True when:
|
|
69
|
+
* - any changed (`+`/`-`, not `+++`/`---`) METHODS / CLASS-METHODS line —
|
|
70
|
+
* an added, removed or renamed method, with or without parameters;
|
|
71
|
+
* - any changed line with code inside a declaration that a context
|
|
72
|
+
* `METHODS x` line opened (until its code ends in `.`; comments do not
|
|
73
|
+
* count) — a parameter, a type, an exception class on its own line;
|
|
74
|
+
* - from a hunk start until a context line ending a statement settles it
|
|
75
|
+
* (e.g. `METHOD x.`), a changed line that looks like part of a
|
|
76
|
+
* declaration whose opener sits outside the hunk's context: TYPE or LIKE
|
|
77
|
+
* (or a leading IMPORTING … RAISING), no `=`, not a local DATA,
|
|
78
|
+
* FIELD-SYMBOLS, TYPES, CONSTANTS or STATICS; or a bare identifier list
|
|
79
|
+
* ending in `,` or `.` (exception classes), not END… / RETURN / EXIT;
|
|
80
|
+
* - in that hunk-start state, an unchanged IMPORTING … RAISING line or an
|
|
81
|
+
* unchanged `name TYPE x` parameter line opens the declaration, so the
|
|
82
|
+
* changed lines after it count as above.
|
|
83
|
+
* A body line after `METHOD x.` (`CALL FUNCTION … EXPORTING`, `DATA … TYPE`)
|
|
84
|
+
* stays false.
|
|
85
|
+
* Task 7 (R2) — an unchanged METHOD / FORM / FUNCTION / MODULE / END… line
|
|
86
|
+
* (not one shaped like a parameter) settles the state to 'out' before any
|
|
87
|
+
* other rule reads it: the lines after it are body. A FORM whose statement
|
|
88
|
+
* goes on (no `.`) sets 'in' instead: its USING / CHANGING lines are its
|
|
89
|
+
* signature. A changed FORM line is a signature change.
|
|
90
|
+
* Fix round 1 — a hunk header is model text, so it never sets 'out'. A
|
|
91
|
+
* header naming a block (`@@ METHOD to_internal. @@`,
|
|
92
|
+
* `@@ -1,2 +1,2 @@ METHOD x.`) sets 'hint': only the bare hunk-start guess
|
|
93
|
+
* on a changed line is off, so a bare `EXPORTING` of a CALL FUNCTION is not
|
|
94
|
+
* taken for a parameter list. Unchanged IMPORTING … RAISING, `name TYPE x`
|
|
95
|
+
* and METHODS lines still open a declaration. Known gap (pinned in a test):
|
|
96
|
+
* changed parameter lines alone, with no context, under such a header.
|
|
97
|
+
*/
|
|
98
|
+
export function signatureChange(diff) {
|
|
99
|
+
if (!diff)
|
|
100
|
+
return false;
|
|
101
|
+
let state = 'unknown';
|
|
102
|
+
for (const raw of diff.split('\n')) {
|
|
103
|
+
if (raw.startsWith('+++') || raw.startsWith('---'))
|
|
104
|
+
continue;
|
|
105
|
+
if (raw.startsWith('@@')) {
|
|
106
|
+
// Task 7 (R2) / fix round 1 — a header naming the block
|
|
107
|
+
// (`@@ METHOD to_internal. @@`) hints that the hunk starts in its body.
|
|
108
|
+
// It is model text: a hint, never 'out'.
|
|
109
|
+
// A FORM header never hints: its USING lines are its signature, so the
|
|
110
|
+
// hunk-start guess stays on (as before Task 7).
|
|
111
|
+
const text = codeOf(hunkHeaderText(raw).trim());
|
|
112
|
+
const block = blockState(text);
|
|
113
|
+
state = block === 'in' ? 'in' : block === 'out' && !FORM_STATEMENT.test(text) ? 'hint' : 'unknown';
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const changed = raw.startsWith('+') || raw.startsWith('-');
|
|
117
|
+
const code = codeOf(/^[+\- ]/.test(raw) ? raw.slice(1) : raw);
|
|
118
|
+
// Task 7 (R2) — an unchanged block statement settles the state before any
|
|
119
|
+
// other rule reads it. A changed one goes through every rule below.
|
|
120
|
+
if (!changed) {
|
|
121
|
+
const block = blockState(code);
|
|
122
|
+
if (block) {
|
|
123
|
+
state = block;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// A changed FORM line is a changed FORM signature (or name), like a changed METHODS line.
|
|
128
|
+
if (changed && FORM_STATEMENT.test(code))
|
|
129
|
+
return true;
|
|
130
|
+
if (DECLARATION_OPENER.test(code)) {
|
|
131
|
+
if (changed)
|
|
132
|
+
return true;
|
|
133
|
+
state = 'in';
|
|
134
|
+
}
|
|
135
|
+
// Round 3 — at a hunk start, an unchanged parameter line (IMPORTING …
|
|
136
|
+
// RAISING, or `name TYPE x`) means the hunk starts inside a declaration.
|
|
137
|
+
if (!changed && (state === 'unknown' || state === 'hint') && !code.includes('=') && !LOCAL_DECLARATION.test(code)
|
|
138
|
+
&& (PARAMETER_KEYWORD.test(code) || PARAMETER_LINE.test(code)))
|
|
139
|
+
state = 'in';
|
|
140
|
+
if (changed && state === 'in' && code.trim() !== '')
|
|
141
|
+
return true;
|
|
142
|
+
if (changed && state === 'unknown' && !code.includes('=') && !LOCAL_DECLARATION.test(code)
|
|
143
|
+
&& (PARAMETER_LIKE.test(code) || PARAMETER_KEYWORD.test(code)
|
|
144
|
+
|| (BARE_LIST.test(code) && !BARE_STATEMENT.test(code))))
|
|
145
|
+
return true;
|
|
146
|
+
if (code.trimEnd().endsWith('.') && (state === 'in' || !changed))
|
|
147
|
+
state = 'out';
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
9
151
|
export function effectiveRisk(declared, changes, ctx) {
|
|
10
|
-
let floor = declared;
|
|
11
|
-
const raisedBy = [];
|
|
12
152
|
const rank = (r) => ({ low: 0, medium: 1, high: 2 }[r]);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
153
|
+
// Task 7 (R2) — every rule that applies is noted, in rule order. The shown
|
|
154
|
+
// reasons are each rule that on its own justifies the final level (at or
|
|
155
|
+
// above it, and above what the model declared), plus the reasons that are
|
|
156
|
+
// always shown. Before, only the first rule to cross a level was kept, so
|
|
157
|
+
// a body-only change read "may alter a method signature" when activation
|
|
158
|
+
// was the true reason.
|
|
159
|
+
const applied = [];
|
|
160
|
+
const raise = (r, label, always = false) => {
|
|
161
|
+
applied.push({ r, label, always });
|
|
18
162
|
};
|
|
19
163
|
if (ctx.autoApprovePolicy === 'never')
|
|
20
164
|
raise('high', 'non_prod_override');
|
|
@@ -29,19 +173,20 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
29
173
|
const badNames = new Set(ctx.syntaxErrorObjects.map((k) => k.slice(k.lastIndexOf(':') + 1).toUpperCase()));
|
|
30
174
|
const hit = (c) => badNames.has(c.object.trim().split(/\s+/)[0].toUpperCase());
|
|
31
175
|
if (changes.some(hit)) {
|
|
32
|
-
raise('high', 'syntax_errors');
|
|
33
176
|
// Final review A M-4 — the reason is always shown, even when the floor
|
|
34
177
|
// was already high (policy never, or declared high).
|
|
35
|
-
|
|
36
|
-
raisedBy.push('syntax_errors');
|
|
178
|
+
raise('high', 'syntax_errors', true);
|
|
37
179
|
}
|
|
38
180
|
}
|
|
39
181
|
// Part 2b D7 — a BAdI implementation runs inside a standard SAP process:
|
|
40
182
|
// high (never auto-approved), and the reason is always shown.
|
|
41
183
|
if (changes.some((c) => typeCode(c.type) === 'ENHO')) {
|
|
42
|
-
raise('high', 'standard_process');
|
|
43
|
-
|
|
44
|
-
|
|
184
|
+
raise('high', 'standard_process', true);
|
|
185
|
+
}
|
|
186
|
+
// Task 7 (B10) — a changed method signature on a class or interface can
|
|
187
|
+
// break its callers: medium, and the reason is always shown.
|
|
188
|
+
if (changes.some((c) => c.op === 'modify' && ['CLAS', 'INTF'].includes(typeCode(c.type)) && signatureChange(c.diff))) {
|
|
189
|
+
raise('medium', 'signature_change', true);
|
|
45
190
|
}
|
|
46
191
|
if (changes.some((c) => c.op === 'delete'))
|
|
47
192
|
raise('medium', 'delete_op');
|
|
@@ -51,14 +196,10 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
51
196
|
// Part 3 D2 — a release cannot be undone, and a run executes code on the
|
|
52
197
|
// system: both are high (never auto-approved), and the reason is always shown.
|
|
53
198
|
if (changes.some((c) => c.op === 'release')) {
|
|
54
|
-
raise('high', 'transport_release');
|
|
55
|
-
if (!raisedBy.includes('transport_release'))
|
|
56
|
-
raisedBy.push('transport_release');
|
|
199
|
+
raise('high', 'transport_release', true);
|
|
57
200
|
}
|
|
58
201
|
if (changes.some((c) => c.op === 'run')) {
|
|
59
|
-
raise('high', 'code_run');
|
|
60
|
-
if (!raisedBy.includes('code_run'))
|
|
61
|
-
raisedBy.push('code_run');
|
|
202
|
+
raise('high', 'code_run', true);
|
|
62
203
|
}
|
|
63
204
|
if (changes.some((c) => c.op === 'activate'))
|
|
64
205
|
raise('medium', 'activation');
|
|
@@ -70,6 +211,26 @@ export function effectiveRisk(declared, changes, ctx) {
|
|
|
70
211
|
raise('medium', 'escalated_in_session');
|
|
71
212
|
if (ctx.prodNamespaces && changes.some((c) => ctx.prodNamespaces.some((p) => c.object.startsWith(p))))
|
|
72
213
|
raise('medium', 'prod_namespace');
|
|
214
|
+
// The level is the highest rule (never below what the model declared).
|
|
215
|
+
let floor = declared;
|
|
216
|
+
for (const a of applied)
|
|
217
|
+
if (rank(a.r) > rank(floor))
|
|
218
|
+
floor = a.r;
|
|
219
|
+
const raisedBy = [];
|
|
220
|
+
for (const a of applied) {
|
|
221
|
+
const justifies = rank(a.r) >= rank(floor) && rank(a.r) > rank(declared);
|
|
222
|
+
if ((justifies || a.always) && !raisedBy.includes(a.label))
|
|
223
|
+
raisedBy.push(a.label);
|
|
224
|
+
}
|
|
225
|
+
// Task 7 (R2) / fix round 1 — when activation and a signature change are
|
|
226
|
+
// both shown, activation leads (it is the sure reason; the signature one is
|
|
227
|
+
// a reading of the diff). Otherwise the rule order stands.
|
|
228
|
+
const sig = raisedBy.indexOf('signature_change');
|
|
229
|
+
const act = raisedBy.indexOf('activation');
|
|
230
|
+
if (sig >= 0 && act > sig) {
|
|
231
|
+
raisedBy.splice(act, 1);
|
|
232
|
+
raisedBy.splice(sig, 0, 'activation');
|
|
233
|
+
}
|
|
73
234
|
return { level: floor, raisedBy };
|
|
74
235
|
}
|
|
75
236
|
/** Plain words for each risk reason code — the codes stay in audit data only. */
|
|
@@ -85,7 +246,23 @@ const RISK_REASON_TEXT = {
|
|
|
85
246
|
syntax_errors: 'the last syntax check of this object found errors',
|
|
86
247
|
standard_process: 'this code runs inside a standard SAP process (a BAdI implementation)',
|
|
87
248
|
code_run: 'this runs code on the SAP system',
|
|
249
|
+
signature_change: 'the change may alter a method signature, so callers may be affected',
|
|
88
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* Task 7 (R2) — one sentence on every request_approval result (and once in
|
|
253
|
+
* the journey fix block): the risk line is CSPeach's reading of the diff, so
|
|
254
|
+
* the model relays it rather than telling the person it is wrong.
|
|
255
|
+
*/
|
|
256
|
+
export const RISK_NOTE = "The risk line is CSPeach's reading of the diff; relay it, and if you think it is off, say why.";
|
|
257
|
+
/**
|
|
258
|
+
* Task 7 (R2) — the risk fields of every request_approval result:
|
|
259
|
+
* effective_risk and raised_by as before (older readers), plus risk { level,
|
|
260
|
+
* reasons } in plain words and the note.
|
|
261
|
+
*/
|
|
262
|
+
export function riskResultFields(eff) {
|
|
263
|
+
const reasons = eff.raisedBy.map(riskReasonText); // round 8 item 4: plain words, never codes
|
|
264
|
+
return { effective_risk: eff.level, raised_by: reasons, risk: { level: eff.level, reasons: [...reasons] }, note: RISK_NOTE };
|
|
265
|
+
}
|
|
89
266
|
/** Round 8 item 4 — every code the floor can raise (each has plain words above). */
|
|
90
267
|
export const RISK_REASON_CODES = Object.keys(RISK_REASON_TEXT);
|
|
91
268
|
/** Round 4 item 8a — never show an internal reason code raw. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse the changes list to what a person sees — `activate` is the
|
|
3
|
+
* implicit follow-on of a `modify`/`create` on the same object, so the
|
|
4
|
+
* activate row is hidden when its sibling write is already in the list. The
|
|
5
|
+
* underlying `changes[]` (which drives approval-id minting) is unchanged.
|
|
6
|
+
* This is what fixes the "what 2 changes?" complaint when the model declares
|
|
7
|
+
* modify+activate of the same object. Used by the plan gate's display
|
|
8
|
+
* (render.ts) and, Task 9 (C8), by request_approval to decide whether the
|
|
9
|
+
* plan gate opens at all (only for more than one visible change).
|
|
10
|
+
*/
|
|
11
|
+
export function collapseImplicitActivates(changes) {
|
|
12
|
+
const writeKeys = new Set();
|
|
13
|
+
for (const c of changes) {
|
|
14
|
+
if (c.op === 'modify' || c.op === 'create') {
|
|
15
|
+
writeKeys.add(`${c.type}:${c.object}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return changes.filter((c) => {
|
|
19
|
+
if (c.op !== 'activate')
|
|
20
|
+
return true;
|
|
21
|
+
return !writeKeys.has(`${c.type}:${c.object}`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Heuristic: a real unified diff has at least one line starting with `+`,
|
|
26
|
+
* `-`, or `@@`. A summary or change description won't. Used to gate
|
|
27
|
+
* colorizeDiff vs the "model didn't supply a diff" warning (render.ts).
|
|
28
|
+
*/
|
|
29
|
+
export function looksLikeUnifiedDiff(s) {
|
|
30
|
+
return s.split('\n').some((line) => {
|
|
31
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
32
|
+
return false;
|
|
33
|
+
return line.startsWith('+') || line.startsWith('-') || line.startsWith('@@');
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -63,7 +63,7 @@ export const HELP_GROUPS = [
|
|
|
63
63
|
{ name: '/ui [auto|ink]', description: fixed('Change the UI mode (applies after a relaunch)') },
|
|
64
64
|
{ name: '/ui look [panels|legacy]', description: fixed('Switch between the panels and legacy looks') },
|
|
65
65
|
{ name: '/style [professional|conversational|humor|blunt]', description: fixed('Change the talking style — words only; findings and warnings never change') },
|
|
66
|
-
{ name: '/level [developer|learning|functional]', description: fixed('
|
|
66
|
+
{ name: '/level [developer|learning|functional]', description: fixed('How CSPeach explains things — never what it finds or what it does') },
|
|
67
67
|
] },
|
|
68
68
|
];
|
|
69
69
|
/** Chat lane (spec 2026-10-17 D8): the Session group's /new reads "Fresh start"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marker a turn continues from, or null: only an /abap-plan turn that is
|
|
3
|
+
* not already a plan resume, with a plan step in flight. /new clears the
|
|
4
|
+
* marker (session/fresh-start.ts), so "continue" after /new never takes this
|
|
5
|
+
* path.
|
|
6
|
+
*/
|
|
7
|
+
export function planContinuationMarker(session, skill, planResume) {
|
|
8
|
+
if (skill !== 'abap-plan' || planResume !== null)
|
|
9
|
+
return null;
|
|
10
|
+
return session.plan_in_flight ?? null;
|
|
11
|
+
}
|
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
* Every change is one append-only record in the lead's `plan/` folder, written
|
|
12
12
|
* whole then renamed. Lanes stay patterns in `lanes.json`; everything per object
|
|
13
13
|
* is a plan record, and the register folds them on read. Nothing here edits a
|
|
14
|
-
* record or reaches SAP, and none of it is reachable from a model tool
|
|
14
|
+
* record or reaches SAP, and none of it is reachable from a model tool (the
|
|
15
|
+
* lane-move record is written by moveLanes in team.ts, behind the person's card
|
|
16
|
+
* when team_lane_move runs it).
|
|
15
17
|
*/
|
|
16
18
|
import fs from 'node:fs';
|
|
17
19
|
import path from 'node:path';
|
|
18
20
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { buildRegister, DEFAULT_WEIGHTS,
|
|
21
|
+
import { buildRegister, DEFAULT_WEIGHTS, objectKey, proposeLanes, utcWords, } from '@cspeach/register-core';
|
|
20
22
|
import { RegisterError, collectRecords, readBaseline, readLanes, readProjectFile, writeLanes, } from '../register/store.js';
|
|
21
23
|
import { diffPlan, readPlanSheet } from '../register/plan-import.js';
|
|
22
24
|
import { writeLeadRecord } from '../register/records.js';
|
|
23
25
|
import { registerFor } from '../register/picture.js';
|
|
26
|
+
import { projectViewFor } from '../register/status-html.js';
|
|
24
27
|
import { shown, shownUpTo } from '../register/atc-file.js';
|
|
25
28
|
import { flags, heldReportLine, joinedOrThrow, list, requireLead, whoAmI, writeReports } from './team.js';
|
|
26
29
|
const MAX_REASON = 300;
|
|
@@ -157,11 +160,16 @@ export async function laneProposal(args, deps) {
|
|
|
157
160
|
deps.print(` Written to the plan folder: ${files.join(', ')}`);
|
|
158
161
|
return 0;
|
|
159
162
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
163
|
+
/**
|
|
164
|
+
* `lane add|remove|move`: the history beside the lanes file. Returns the line
|
|
165
|
+
* to print. A move also names the packages that moved (`moved`) and the
|
|
166
|
+
* lanes they left (`from`).
|
|
167
|
+
*/
|
|
168
|
+
export function laneChangeRecord(deps, change, before, after, id, reason, moved, from) {
|
|
162
169
|
const lead = asLead(deps);
|
|
163
170
|
const side = (l) => (l ? { owner: l.owner, packages: l.packages, namePatterns: l.namePatterns } : null);
|
|
164
|
-
const
|
|
171
|
+
const lane = { id, change, ...(moved ? { moved: [...moved] } : {}), ...(from ? { from: from.map((f) => ({ id: f.id, owner: f.owner })) } : {}), before: side(before), after: side(after) };
|
|
172
|
+
const file = writePlan(lead, { action: 'lane', lane, ...(reason ? { reason } : {}) }, [], deps);
|
|
165
173
|
return writtenLine(file);
|
|
166
174
|
}
|
|
167
175
|
// ── focus ───────────────────────────────────────────────────────────────────
|
|
@@ -225,7 +233,9 @@ export function focusCommand(args, deps) {
|
|
|
225
233
|
}
|
|
226
234
|
else {
|
|
227
235
|
const lane = laneOrThrow(readLanes(lead.projectDir), opts.lane);
|
|
228
|
-
|
|
236
|
+
// Owner decision (final review): the lane's OWN objects, the ones its counts cover (the view's lane scope).
|
|
237
|
+
const view = projectViewFor(register, readProjectFile(lead.projectDir));
|
|
238
|
+
keys = (view.lanes.find((l) => l.scope.kind === 'lane' && l.scope.id === lane.id)?.objects ?? []).map((v) => v.key);
|
|
229
239
|
selector = { lane: lane.id };
|
|
230
240
|
}
|
|
231
241
|
if (!keys.length)
|