@cspeach/cli 1.1.48 → 1.1.49
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/level.js +1 -1
- package/dist/agent/loop.js +7 -6
- 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/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/project.js +301 -0
- package/dist/commands/team-plan.js +16 -6
- package/dist/commands/team.js +295 -68
- package/dist/doctor/checks/zcspeach.js +96 -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/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/email-template.js +56 -0
- package/dist/projects/extract-cca.js +8 -7
- package/dist/projects/index.js +0 -1
- package/dist/projects/merge-cca.js +0 -19
- 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/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 +21 -8
- package/dist/router/router-core.js +5 -3
- package/dist/skill-catalog.js +4 -7
- package/dist/tools/_team-work-path.js +53 -0
- package/dist/tools/approval.js +97 -19
- package/dist/tools/filesystem/file-edit.js +8 -1
- package/dist/tools/filesystem/file-write.js +8 -1
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +2 -2
- package/dist/tools/project/_team-tool-shared.js +5 -0
- package/dist/tools/project/cca_merge.js +1 -11
- 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/subagent/schedule_create.js +131 -0
- package/dist/tools/text-elements/tools.js +7 -1
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/update-method-intercept.js +18 -1
- package/dist/tools/write-pipeline.js +19 -5
- package/dist/ui/approval-modal.js +6 -3
- package/dist/ui/error-emitter.js +21 -0
- package/dist/ui/footer-line.js +19 -1
- package/dist/ui/header.js +41 -0
- package/dist/ui/ink-classifier-route.js +19 -0
- package/dist/ui/rich-error-box.js +9 -0
- package/dist/ui/sidebar.js +12 -0
- package/dist/ui/status-emitter.js +19 -0
- package/dist/ui/status-line.js +62 -0
- package/dist/ui/status-row.js +38 -0
- package/dist/ui/todo-panel.js +85 -0
- package/dist/ui/widgets/ask-form.js +250 -0
- package/dist/ui/widgets/ask-question-modal.js +178 -0
- package/fiori/zlas-inspection-lr/package.json +26 -0
- package/fiori/zlas-inspection-lr/ui5-local.yaml +45 -0
- package/fiori/zlas-inspection-lr/ui5.yaml +41 -0
- package/fiori/zlas-inspection-lr/webapp/Component.js +12 -0
- package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +9 -0
- package/fiori/zlas-inspection-lr/webapp/index.html +35 -0
- package/fiori/zlas-inspection-lr/webapp/manifest.json +145 -0
- package/live-check-2026-10-17-1343.txt +41 -0
- package/package.json +121 -121
- 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
package/dist/agent/level.js
CHANGED
|
@@ -26,7 +26,7 @@ export function parseLevel(raw) {
|
|
|
26
26
|
export const LEVEL_INVARIANT = `The level changes how much you explain, never what you found: findings, numbers, object names, warnings, risks, approvals, plan and safety prompts stay exact. It does not change what this person is allowed to do in SAP.`;
|
|
27
27
|
export const LEVEL_SENTENCES = Object.freeze({
|
|
28
28
|
learning: 'You\'re talking to a developer who is still building experience. Explain the why, not just the what. The first time a technical term comes up, say in a few words what it means. When you show a fix, point out the pattern behind it so they can use it again. Stay encouraging and concrete; never talk down. Just before you ask them to approve anything or answer a yes/no question, say in one or two plain sentences what will happen, what could go wrong and how it can be undone, then ask. Keep it short; don\'t lecture.',
|
|
29
|
-
functional: 'You\'re talking to
|
|
29
|
+
functional: 'You\'re talking to a developer who wants the business view first. Start with what it means for the business: which documents, amounts, users or reports are affected. Avoid technical terms unless they are needed; when one is needed, explain it in plain words. Show code only when they ask for it. If they ask you to write it up for a developer, write cspeach-out/developer-request-<object-or-topic>.md (if that name is taken, add -2, -3 and so on) with the sections Summary, What we found, Proposed change, How to test and Open questions. Just before you ask them to approve anything or answer a yes/no question, say in one or two plain sentences what will happen, what could go wrong and how it can be undone, then ask. Keep it short; don\'t lecture.',
|
|
30
30
|
});
|
|
31
31
|
// ---- pinned: level text composition ----
|
|
32
32
|
/** The text appended under a "Who you're talking to" heading; null for developer (today's behaviour). */
|
package/dist/agent/loop.js
CHANGED
|
@@ -72,6 +72,7 @@ import { loadWatchdogConfig, evaluateWatchdog } from './turn-watchdog.js';
|
|
|
72
72
|
import { maybeBuildProjectContext } from './maybe-build-project-context.js';
|
|
73
73
|
import { buildSapConnectionContext } from './sap-connection-adapter.js';
|
|
74
74
|
import { getCurrentTransport } from '../repl/current-transport.js';
|
|
75
|
+
import { projectTransport } from '../register/project-transport.js';
|
|
75
76
|
import { collectTurnAssistantText } from './turn-assistant-text.js';
|
|
76
77
|
import { checkAndMark, newGuardState } from './parallel-write-guard.js';
|
|
77
78
|
import { userPromptText } from '../session/user-prompt.js';
|
|
@@ -116,12 +117,10 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
116
117
|
'abap-upgrade-fix',
|
|
117
118
|
'abap-upgrade-verify',
|
|
118
119
|
'abap-upgrade-merge',
|
|
119
|
-
// 2026-05-09: CCA pipeline manifest envelopes. /abap-cca-merge produces
|
|
120
|
-
// a consolidated cca-assessment from N parallel consultant slices.
|
|
121
120
|
// 2026-10-06: the thin /abap-cca (1.1.31) reads the estate inventory and
|
|
122
121
|
// writes no envelope, so it is no longer a save-hook skill. An old
|
|
123
122
|
// cca-manifest in a turn is still offered through detectArtifactSkill.
|
|
124
|
-
|
|
123
|
+
// 2026-10-20: /abap-cca-merge cut (drift spec §3.6); its name aliases to abap-cca.
|
|
125
124
|
// 2026-05-10: v0.7 trio chain. /abap-modernize emits modernize-result;
|
|
126
125
|
// /abap-test emits test-coverage. Both accept cca-assessment via --from
|
|
127
126
|
// and can fan out into further chains after their own envelope saves.
|
|
@@ -408,7 +407,7 @@ export async function maybeOfferSave(p) {
|
|
|
408
407
|
// the output carried a complete csforge:plan-manifest block. The label
|
|
409
408
|
// stays the primary trigger — it is authoritative for prose-extracted
|
|
410
409
|
// artefacts with no inline marker (spec-gap / design / estimate) and keeps
|
|
411
|
-
// merge-skill provenance exact (abap-
|
|
410
|
+
// merge-skill provenance exact (abap-upgrade-merge saves as abap-upgrade-merge).
|
|
412
411
|
// When the label would NOT fire, a recognizable manifest block in the
|
|
413
412
|
// output resolves the registry skill instead (detectArtifactSkill), so a
|
|
414
413
|
// misroute can no longer silently lose the artifact.
|
|
@@ -925,6 +924,8 @@ ${imageExpansion.error}`));
|
|
|
925
924
|
const sapConnection = params.ctx.sapAlias
|
|
926
925
|
? buildSapConnectionContext(params.ctx.sapAlias, sapSystemInfo, {
|
|
927
926
|
currentTransport: getCurrentTransport() ?? null,
|
|
927
|
+
// Task 6 (project view): the project request of this folder; null outside a project.
|
|
928
|
+
projectTransport: projectTransport(params.ctx.cwd),
|
|
928
929
|
})
|
|
929
930
|
: null;
|
|
930
931
|
projectContextBlock = await maybeBuildProjectContext({
|
|
@@ -1106,7 +1107,7 @@ ${imageExpansion.error}`));
|
|
|
1106
1107
|
session.turn_seq = (session.turn_seq ?? Math.max(turnNumber - 1, session.lastAutoCompactTurn ?? 0)) + 1;
|
|
1107
1108
|
// Snapshot session.messages.length at turn start so the save hook can
|
|
1108
1109
|
// walk every assistant message added during this turn — not just the
|
|
1109
|
-
// final one. Skills like /abap-
|
|
1110
|
+
// final one. Skills like /abap-upgrade-merge emit their manifest in an early
|
|
1110
1111
|
// message before a closing ask_question widget; the final wrap-up
|
|
1111
1112
|
// message ends the turn but doesn't carry the manifest. See
|
|
1112
1113
|
// ./turn-assistant-text.ts for the full rationale.
|
|
@@ -1932,7 +1933,7 @@ ${imageExpansion.error}`));
|
|
|
1932
1933
|
// a background block on the next REPL prompt.
|
|
1933
1934
|
//
|
|
1934
1935
|
// 2026-05-12: collect text from ALL assistant messages added during
|
|
1935
|
-
// this turn, not just the final one. Skills like /abap-
|
|
1936
|
+
// this turn, not just the final one. Skills like /abap-upgrade-merge emit
|
|
1936
1937
|
// their manifest in an early message before a closing ask_question
|
|
1937
1938
|
// widget; the final wrap-up message ends the turn but doesn't
|
|
1938
1939
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
@@ -34,6 +34,8 @@ export function buildSapConnectionContext(alias, systemInfo, state = {}) {
|
|
|
34
34
|
alias,
|
|
35
35
|
system: toSummary(systemInfo),
|
|
36
36
|
currentTransport: state.currentTransport ?? null,
|
|
37
|
+
// Only in a project folder: a context without one stays as it was, key for key.
|
|
38
|
+
...(state.projectTransport ? { projectTransport: state.projectTransport } : {}),
|
|
37
39
|
activeScope: toActiveScope(state),
|
|
38
40
|
recentObjects: state.recentObjects ?? [],
|
|
39
41
|
};
|
|
@@ -47,7 +47,7 @@ export function buildReaderSystemPrompt(opts) {
|
|
|
47
47
|
* Chat lane (spec 2026-10-17 D3) — one block, the pinned chat prompt, as the
|
|
48
48
|
* proxy sends it for the `_chat` skill header. The coding-workflow preamble is
|
|
49
49
|
* NOT sent on chat turns. A non-professional style is appended to that block;
|
|
50
|
-
* a non-
|
|
50
|
+
* a non-default level (§10 D14) follows the style text.
|
|
51
51
|
*/
|
|
52
52
|
export function buildChatSystemPrompt(opts, style = DEFAULT_STYLE, level = DEFAULT_LEVEL) {
|
|
53
53
|
const marker = opts.cacheMarkers ? { cache_control: { type: 'ephemeral' } } : {};
|
|
@@ -62,7 +62,7 @@ export function buildChatSystemPrompt(opts, style = DEFAULT_STYLE, level = DEFAU
|
|
|
62
62
|
* has no SKILL.md, so it never reaches the skill source (a lookup would fail);
|
|
63
63
|
* nor does the built-in `_chat` (D3). A non-professional `style` (D9) is
|
|
64
64
|
* appended to the END of the last block — professional changes nothing, and a
|
|
65
|
-
* reader (which reports facts) never gets style. A non-
|
|
65
|
+
* reader (which reports facts) never gets style. A non-default `level`
|
|
66
66
|
* (§10 D14) follows the style text; developer changes nothing; a reader never
|
|
67
67
|
* gets it.
|
|
68
68
|
*/
|
|
@@ -21,17 +21,24 @@ export const CHAT_UNIT_TEST_REASON = 'This chat reads but never runs code on SAP
|
|
|
21
21
|
export const CHAT_DISPATCH_SKILL_REASON = 'In this chat, dispatch_skill is not available. To start a guided workflow, call hand_off with the workflow name and a clear request.';
|
|
22
22
|
/** A call carrying an approval_id in chat — approvals belong to workflows, never to chat. */
|
|
23
23
|
export const CHAT_APPROVAL_ID_REASON = 'This chat never uses an approval. To make this change, call hand_off with the right workflow and a clear request — the workflow asks before it writes.';
|
|
24
|
+
/**
|
|
25
|
+
* team_lane_move in chat (Task 4, 2026-10-20) — listed like every tool, refused
|
|
26
|
+
* here. Moving packages between people is a lead's decision with its own card;
|
|
27
|
+
* in chat the lead is given the command instead.
|
|
28
|
+
*/
|
|
29
|
+
export const CHAT_LANE_MOVE_REASON = 'In this chat, packages are not moved between people. The lead runs: cspeach team lane move --packages <names> --to <name> --reason "<why>"';
|
|
24
30
|
/**
|
|
25
31
|
* Tools refused in chat beyond the generic rule: the two smokes write SAP
|
|
26
32
|
* data over OData although requiresSap is false; sap_run_unit_test runs code
|
|
27
33
|
* that can COMMIT; dispatch_skill could queue any slash command (hand_off is
|
|
28
|
-
* chat's switch, D5).
|
|
34
|
+
* chat's switch, D5); team_lane_move writes the team project's lanes (Task 4).
|
|
29
35
|
*/
|
|
30
36
|
const CHAT_EXTRA_REFUSED = new Map([
|
|
31
37
|
['fiori_draft_smoke', CHAT_READ_ONLY_REASON],
|
|
32
38
|
['cap_link_smoke', CHAT_READ_ONLY_REASON],
|
|
33
39
|
['sap_run_unit_test', CHAT_UNIT_TEST_REASON],
|
|
34
40
|
['dispatch_skill', CHAT_DISPATCH_SKILL_REASON],
|
|
41
|
+
['team_lane_move', CHAT_LANE_MOVE_REASON],
|
|
35
42
|
]);
|
|
36
43
|
/**
|
|
37
44
|
* Chat lane refusal set (D-F): WRITE_TOOLS ∪ {request_approval} ∪ every tool
|
|
@@ -256,6 +263,11 @@ function declined(r) {
|
|
|
256
263
|
export function isLocalProjectWork(tool, args) {
|
|
257
264
|
if (tool.category === 'filesystem')
|
|
258
265
|
return true;
|
|
266
|
+
// Task 4 (2026-10-20) — every project tool writes project files behind its
|
|
267
|
+
// own card (team_lane_move) or reads only: a second lane move in one turn
|
|
268
|
+
// never gets a batch card on top of its own card. No project tool reaches SAP.
|
|
269
|
+
if (tool.category === 'project')
|
|
270
|
+
return true;
|
|
259
271
|
// 2026-10-02 — fiori_deploy mode "test" (the default) writes nothing to SAP:
|
|
260
272
|
// the backend validates the upload and keeps nothing. Not a batch write, so
|
|
261
273
|
// a deploy phase shows ONE approval (the real deploy's request_approval),
|
|
@@ -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"
|