@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
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
|
-
import { provenForTarget as provenForTargetCore } from '@cspeach/register-core';
|
|
28
|
+
import { projectView, provenForTarget as provenForTargetCore } from '@cspeach/register-core';
|
|
29
29
|
import { DISCOVERY_DIR, TEAM_FILE, activeProject, readBaseline, readEstate, readProjectFile } from '../register/store.js';
|
|
30
30
|
import { registerFor } from '../register/picture.js';
|
|
31
31
|
import { reportFileNames } from '../register/workbooks.js';
|
|
@@ -36,22 +36,37 @@ const STAGE_STEP = {
|
|
|
36
36
|
INVENTORY: 1, START: 2, JOIN: 2, DECIDE: 3, FIX: 4, PROVE: 5, REPORT: 6,
|
|
37
37
|
};
|
|
38
38
|
export const stepOf = (stage) => STAGE_STEP[stage];
|
|
39
|
-
/** States that need nothing more from the person. */
|
|
40
|
-
const SETTLED = new Set(['clean', 'verified', 'retired']);
|
|
41
39
|
/** A fix was claimed and a later check still found it (or found more). */
|
|
42
40
|
const FAILED_CHECK = new Set(['did-not-hold', 'regressed']);
|
|
43
41
|
const same = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
44
42
|
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
45
43
|
const size = (o) => (o.openFindings === null ? Number.MAX_SAFE_INTEGER : o.openFindings);
|
|
46
44
|
const smallestFirst = (a, b) => size(a) - size(b) || byName(a, b);
|
|
45
|
+
/**
|
|
46
|
+
* The project view of a register when the caller holds no project file (a
|
|
47
|
+
* test, a fact read that has none): the same parts — they are read from the
|
|
48
|
+
* register alone — with `lead` as the lead's name. The journey reads its
|
|
49
|
+
* lists from the view `readJourneyFacts` built; this is only the fallback.
|
|
50
|
+
*/
|
|
51
|
+
export function journeyView(register, lead = 'the lead') {
|
|
52
|
+
return projectView(register, { kind: 'team-project', formatVersion: 1, projectId: register.projectId, name: '', type: 'clean-core', lead: { name: lead } });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* My objects whose part is one of `parts`, in register order (project view
|
|
56
|
+
* Task 5: the journey lists are filters over `view.me(me)`, never a rule of
|
|
57
|
+
* their own). Register objects, so the cards read what they always read.
|
|
58
|
+
*/
|
|
59
|
+
function mineIn(register, me, view, keep) {
|
|
60
|
+
const byKey = new Map(register.objects.map((o) => [o.key, o]));
|
|
61
|
+
return view.me(me).objects.filter(keep).map((o) => byKey.get(o.key)).filter((o) => !!o);
|
|
62
|
+
}
|
|
47
63
|
/** My objects: in a lane I own, in scope, not retired. */
|
|
48
|
-
export function myObjects(register, me) {
|
|
49
|
-
|
|
50
|
-
return register.objects.filter((o) => o.lane !== null && lanes.has(o.lane) && o.inScope && o.state !== 'retired');
|
|
64
|
+
export function myObjects(register, me, view = journeyView(register)) {
|
|
65
|
+
return mineIn(register, me, view, (o) => o.part !== 'retired' && o.part !== 'out-of-scope');
|
|
51
66
|
}
|
|
52
|
-
/** My objects that still need a decision, in register order. */
|
|
53
|
-
export function toDecide(register, me, skip = {}) {
|
|
54
|
-
return
|
|
67
|
+
/** My objects that still need a decision, in register order: not decided yet, or skipped by a view that is not this session's. */
|
|
68
|
+
export function toDecide(register, me, skip = {}, view = journeyView(register)) {
|
|
69
|
+
return mineIn(register, me, view, (o) => (o.part === 'undecided' || o.part === 'skipped') && !skip.decide?.has(o.key));
|
|
55
70
|
}
|
|
56
71
|
/** A triage is shown at most this many objects at a time (design §2: pages of 40). */
|
|
57
72
|
export const PAGE_SIZE = 40;
|
|
@@ -60,23 +75,22 @@ export const PAGE_SIZE = 40;
|
|
|
60
75
|
* the first one (the triage is queued for that one area), in register order,
|
|
61
76
|
* at most PAGE_SIZE.
|
|
62
77
|
*/
|
|
63
|
-
export function decidePage(register, me, skip = {}) {
|
|
64
|
-
const left = toDecide(register, me, skip);
|
|
78
|
+
export function decidePage(register, me, skip = {}, view = journeyView(register)) {
|
|
79
|
+
const left = toDecide(register, me, skip, view);
|
|
65
80
|
const lane = left[0]?.lane;
|
|
66
81
|
return left.filter((o) => o.lane === lane).slice(0, PAGE_SIZE);
|
|
67
82
|
}
|
|
68
83
|
/**
|
|
69
|
-
* My objects decided "fix" that need a fix: first the
|
|
70
|
-
*
|
|
71
|
-
* regressed). Each group smallest first (fewest open
|
|
84
|
+
* My objects decided "fix" that need a fix: first the view's "to fix" part,
|
|
85
|
+
* then its "fix failed the check" part (a fix decision only: a kept object
|
|
86
|
+
* that regressed is not a fix job). Each group smallest first (fewest open
|
|
87
|
+
* findings, never measured last). A fix proven for its own target, or
|
|
88
|
+
* claimed again since its check, is in another part and never listed here.
|
|
72
89
|
*/
|
|
73
|
-
export function toFix(register, me, skip = {}) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// A failed fix already claimed again waits for its check (toProve), not for another fix.
|
|
78
|
-
const retry = mine.filter((o) => FAILED_CHECK.has(o.state) && !claimedSinceCheck(o));
|
|
79
|
-
const fresh = mine.filter((o) => !FAILED_CHECK.has(o.state) && !o.fixes.some((f) => f.status === 'fixed'));
|
|
90
|
+
export function toFix(register, me, skip = {}, view = journeyView(register)) {
|
|
91
|
+
const left = (o) => !skip.fix?.has(o.key);
|
|
92
|
+
const fresh = mineIn(register, me, view, (o) => o.part === 'to-fix' && left(o));
|
|
93
|
+
const retry = mineIn(register, me, view, (o) => o.part === 'failed-check' && o.decision.what === 'fix' && left(o));
|
|
80
94
|
return [...fresh.sort(smallestFirst), ...retry.sort(smallestFirst)];
|
|
81
95
|
}
|
|
82
96
|
/**
|
|
@@ -102,12 +116,12 @@ export function provenForTarget(o) {
|
|
|
102
116
|
/** True when this object is in `toFix` because its fix failed the check. */
|
|
103
117
|
export const fixFailedCheck = (o) => FAILED_CHECK.has(o.state);
|
|
104
118
|
/** My objects planned for a rebuild beside the core — handled separately, never in the fix loop. */
|
|
105
|
-
export function toRedesign(register, me) {
|
|
106
|
-
return
|
|
119
|
+
export function toRedesign(register, me, view = journeyView(register)) {
|
|
120
|
+
return mineIn(register, me, view, (o) => o.part === 'rebuild').sort(byName);
|
|
107
121
|
}
|
|
108
122
|
/** My objects whose fix is claimed and not measured since — a failed fix claimed again included. */
|
|
109
|
-
export function toProve(register, me) {
|
|
110
|
-
return
|
|
123
|
+
export function toProve(register, me, view = journeyView(register)) {
|
|
124
|
+
return mineIn(register, me, view, (o) => o.part === 'awaiting-check');
|
|
111
125
|
}
|
|
112
126
|
/** The newest thing the register knows of, in ms; null when nothing is recorded. */
|
|
113
127
|
export function newestRecordAt(register) {
|
|
@@ -151,11 +165,12 @@ export function stageOf(f) {
|
|
|
151
165
|
if (!r.lanes.some((l) => same(l.owner, f.me)) && !same(f.folder.lead, f.me))
|
|
152
166
|
return 'UNOWNED';
|
|
153
167
|
const skip = f.skip ?? {};
|
|
154
|
-
|
|
168
|
+
const view = f.view ?? journeyView(r, f.folder.lead);
|
|
169
|
+
if (toDecide(r, f.me, skip, view).length)
|
|
155
170
|
return 'DECIDE';
|
|
156
|
-
if (toFix(r, f.me, skip).length)
|
|
171
|
+
if (toFix(r, f.me, skip, view).length)
|
|
157
172
|
return 'FIX';
|
|
158
|
-
if (!skip.prove && toProve(r, f.me).length)
|
|
173
|
+
if (!skip.prove && toProve(r, f.me, view).length)
|
|
159
174
|
return 'PROVE';
|
|
160
175
|
if (!skip.report) {
|
|
161
176
|
const newest = newestRecordAt(r);
|
|
@@ -166,14 +181,22 @@ export function stageOf(f) {
|
|
|
166
181
|
}
|
|
167
182
|
/** Where the folder's project stands for this laptop. Never throws. */
|
|
168
183
|
export function folderKind(cwd, me) {
|
|
184
|
+
return folderRead(cwd, me).folder;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* `folderKind`, plus the project file it read. The journey reuses that one read:
|
|
188
|
+
* a project file that goes unreadable halfway through a sync must read as
|
|
189
|
+
* "project file", never as some other failure on a second read.
|
|
190
|
+
*/
|
|
191
|
+
function folderRead(cwd, me) {
|
|
169
192
|
if (!fs.existsSync(path.join(cwd, TEAM_FILE)))
|
|
170
|
-
return { kind: 'none' };
|
|
193
|
+
return { folder: { kind: 'none' } };
|
|
171
194
|
let project;
|
|
172
195
|
try {
|
|
173
196
|
project = readProjectFile(cwd);
|
|
174
197
|
}
|
|
175
198
|
catch {
|
|
176
|
-
return { kind: 'unreadable', reason: 'project-file' };
|
|
199
|
+
return { folder: { kind: 'unreadable', reason: 'project-file' } };
|
|
177
200
|
}
|
|
178
201
|
const who = { lead: project.lead.name, name: project.name };
|
|
179
202
|
let joined;
|
|
@@ -184,8 +207,8 @@ export function folderKind(cwd, me) {
|
|
|
184
207
|
joined = null;
|
|
185
208
|
}
|
|
186
209
|
if (joined && joined.projectId === project.projectId && samePath(joined.projectDir, cwd))
|
|
187
|
-
return { kind: 'joined', ...who };
|
|
188
|
-
return same(project.lead.name, me) ? { kind: 'mine-not-joined', ...who } : { kind: 'someone-else', ...who };
|
|
210
|
+
return { folder: { kind: 'joined', ...who }, project };
|
|
211
|
+
return { folder: same(project.lead.name, me) ? { kind: 'mine-not-joined', ...who } : { kind: 'someone-else', ...who }, project };
|
|
189
212
|
}
|
|
190
213
|
/** mtime of the status page the report step writes, or null. */
|
|
191
214
|
export function reportsAt(cwd, projectName) {
|
|
@@ -211,21 +234,23 @@ export function readJourneyFacts(cwd, me, skip) {
|
|
|
211
234
|
catch {
|
|
212
235
|
hasDiscovery = false;
|
|
213
236
|
}
|
|
214
|
-
const folder =
|
|
237
|
+
const { folder, project } = folderRead(cwd, me);
|
|
215
238
|
const facts = { hasDiscovery, folder, me, reportsAt: null, ...(skip ? { skip } : {}) };
|
|
216
|
-
if (folder.kind !== 'joined')
|
|
239
|
+
if (folder.kind !== 'joined' || !project)
|
|
217
240
|
return facts;
|
|
218
241
|
try {
|
|
219
242
|
// The object list first, on its own: its failures (missing, garbled,
|
|
220
243
|
// checksum) are the everyday mid-sync case and get their own word.
|
|
221
|
-
readBaseline(cwd);
|
|
244
|
+
readBaseline(cwd, project);
|
|
222
245
|
}
|
|
223
246
|
catch {
|
|
224
247
|
return { ...facts, folder: { kind: 'unreadable', reason: 'object-list' } };
|
|
225
248
|
}
|
|
226
249
|
try {
|
|
227
250
|
const joined = activeProject();
|
|
228
|
-
|
|
251
|
+
// The project file folderRead already read (joined means its folder is this one).
|
|
252
|
+
facts.register = registerFor(joined.projectDir, joined.outboxDir, project).register;
|
|
253
|
+
facts.view = projectView(facts.register, project, skip?.decide ? { skipped: skip.decide } : {});
|
|
229
254
|
facts.reportsAt = reportsAt(cwd, folder.name);
|
|
230
255
|
}
|
|
231
256
|
catch {
|
package/dist/journey/flow.js
CHANGED
|
@@ -31,12 +31,13 @@ export function userTurnCount(session) {
|
|
|
31
31
|
return messages.filter((m) => m.role === 'user' && (typeof m.content === 'string'
|
|
32
32
|
|| (Array.isArray(m.content) && m.content.some((b) => b?.type !== 'tool_result')))).length;
|
|
33
33
|
}
|
|
34
|
-
/** Skips survive a pause
|
|
34
|
+
/** Skips survive a pause; "carry on" asks some again (forgetNotNow, forgetSkippedRows), the fixes put off stay put off. */
|
|
35
35
|
const skips = new WeakMap();
|
|
36
36
|
/** Start (or restart) the journey for this session. */
|
|
37
37
|
export function startJourneyFlow(session, packages = []) {
|
|
38
38
|
flows.set(session, { packages: [...packages] });
|
|
39
39
|
paused.delete(session);
|
|
40
|
+
ended.delete(session);
|
|
40
41
|
setJourneyRunActive(true);
|
|
41
42
|
}
|
|
42
43
|
export function getJourneyFlow(session) {
|
|
@@ -46,8 +47,45 @@ export function getJourneyFlow(session) {
|
|
|
46
47
|
...(f.queued ? { queued: Object.freeze({ ...f.queued }) } : {}),
|
|
47
48
|
...(f.page ? { page: [...f.page] } : {}),
|
|
48
49
|
...(f.pages ? { pages: f.pages } : {}),
|
|
50
|
+
...(f.developers ? { developers: [...f.developers] } : {}),
|
|
51
|
+
...(f.skippedAsked ? { skippedAsked: true } : {}),
|
|
49
52
|
}) : undefined;
|
|
50
53
|
}
|
|
54
|
+
/** G3: the rows skipped are asked about once a run. */
|
|
55
|
+
export function markSkippedAsked(session) {
|
|
56
|
+
const f = flows.get(session);
|
|
57
|
+
if (f)
|
|
58
|
+
f.skippedAsked = true;
|
|
59
|
+
}
|
|
60
|
+
/** B1: the people named on the people card, `me` last; none → Just me. */
|
|
61
|
+
export function setJourneyDevelopers(session, developers) {
|
|
62
|
+
const f = flows.get(session);
|
|
63
|
+
if (!f)
|
|
64
|
+
return;
|
|
65
|
+
if (developers?.length)
|
|
66
|
+
f.developers = [...developers];
|
|
67
|
+
else
|
|
68
|
+
delete f.developers;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* B8: sessions whose journey ended at DONE ("Next time … say carry on"), with
|
|
72
|
+
* the session's user-turn count then. Bare "carry on" resumes only right
|
|
73
|
+
* after: once another turn of the person's ran, the words keep their old
|
|
74
|
+
* meaning (as isJourneyPausedJustNow). clearJourneyFlow never touches the
|
|
75
|
+
* mark; a new start clears it.
|
|
76
|
+
*/
|
|
77
|
+
const ended = new WeakMap();
|
|
78
|
+
export const markJourneyEnded = (session) => { ended.set(session, userTurnCount(session)); };
|
|
79
|
+
export const isJourneyEndedJustNow = (session) => !!session && ended.has(session) && ended.get(session) === userTurnCount(session);
|
|
80
|
+
/**
|
|
81
|
+
* B8: what the last DONE summed up (the register's newest record time, its
|
|
82
|
+
* check runs and my object count). A DONE that finds the same says "Nothing
|
|
83
|
+
* left to carry on." A new start sentence forgets it (`forgetDoneFingerprint`).
|
|
84
|
+
*/
|
|
85
|
+
const doneFingerprints = new WeakMap();
|
|
86
|
+
export const doneFingerprint = (session) => doneFingerprints.get(session);
|
|
87
|
+
export const setDoneFingerprint = (session, print) => { doneFingerprints.set(session, print); };
|
|
88
|
+
export const forgetDoneFingerprint = (session) => { doneFingerprints.delete(session); };
|
|
51
89
|
/** The page of objects the next triage is shown; one more page handed out. */
|
|
52
90
|
export function setJourneyPage(session, keys) {
|
|
53
91
|
const f = flows.get(session);
|
|
@@ -158,6 +196,18 @@ export function noteJourneyStop(session, step) {
|
|
|
158
196
|
}
|
|
159
197
|
/** Paused, and no turn of the person's has run since the pause line. */
|
|
160
198
|
export const isJourneyPausedJustNow = (session) => !!session && paused.has(session) && paused.get(session) === userTurnCount(session);
|
|
199
|
+
/**
|
|
200
|
+
* R8: the folders this session's journey has read. The first read of a
|
|
201
|
+
* project folder in a session is the cold one (a synced folder fetches its
|
|
202
|
+
* files then); the chain says "Reading the folder…" before it.
|
|
203
|
+
*/
|
|
204
|
+
const foldersRead = new WeakMap();
|
|
205
|
+
export const folderReadThisSession = (session, cwd) => foldersRead.get(session)?.has(cwd) ?? false;
|
|
206
|
+
export function markFolderRead(session, cwd) {
|
|
207
|
+
const s = foldersRead.get(session) ?? new Set();
|
|
208
|
+
s.add(cwd);
|
|
209
|
+
foldersRead.set(session, s);
|
|
210
|
+
}
|
|
161
211
|
/** The session skips as the stage machine reads them. */
|
|
162
212
|
export function journeySkips(session) {
|
|
163
213
|
const s = skips.get(session);
|
|
@@ -170,6 +220,7 @@ export function journeySkips(session) {
|
|
|
170
220
|
...(s.prove ? { prove: true } : {}),
|
|
171
221
|
...(s.report ? { report: true } : {}),
|
|
172
222
|
...(s.rebuild?.size ? { rebuild: new Set(s.rebuild) } : {}),
|
|
223
|
+
...(s.retire?.size ? { retire: new Set(s.retire) } : {}),
|
|
173
224
|
};
|
|
174
225
|
}
|
|
175
226
|
export function addJourneySkips(session, add) {
|
|
@@ -189,6 +240,12 @@ export function addJourneySkips(session, add) {
|
|
|
189
240
|
for (const k of add.rebuild)
|
|
190
241
|
s.rebuild.add(k);
|
|
191
242
|
}
|
|
243
|
+
// Task 6: "Not yet" on the lead's retire card holds for the session, "carry on" included.
|
|
244
|
+
if (add.retire?.size) {
|
|
245
|
+
s.retire ??= new Set();
|
|
246
|
+
for (const k of add.retire)
|
|
247
|
+
s.retire.add(k);
|
|
248
|
+
}
|
|
192
249
|
skips.set(session, s);
|
|
193
250
|
}
|
|
194
251
|
/**
|
|
@@ -204,7 +261,11 @@ export function addJourneyLeftOut(session, keys) {
|
|
|
204
261
|
s.leftOut.add(k);
|
|
205
262
|
skips.set(session, s);
|
|
206
263
|
}
|
|
207
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* "Carry on" asks the check, the report, the rebuild hand-offs and the rows the
|
|
266
|
+
* triage left out again; fixes put off stay put off. The rows skipped at a row
|
|
267
|
+
* card are forgotten by forgetSkippedRows (G3), which "carry on" also calls.
|
|
268
|
+
*/
|
|
208
269
|
export function forgetNotNow(session) {
|
|
209
270
|
const s = skips.get(session);
|
|
210
271
|
if (!s)
|
|
@@ -214,3 +275,15 @@ export function forgetNotNow(session) {
|
|
|
214
275
|
delete s.rebuild;
|
|
215
276
|
delete s.leftOut;
|
|
216
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* G3: the rows skipped at a row card and the page rows left out, forgotten:
|
|
280
|
+
* the next DECIDE asks them again. Said Yes on "Look at the n rows you
|
|
281
|
+
* skipped?", and on "carry on". Nothing else the session put off is touched.
|
|
282
|
+
*/
|
|
283
|
+
export function forgetSkippedRows(session) {
|
|
284
|
+
const s = skips.get(session);
|
|
285
|
+
if (!s)
|
|
286
|
+
return;
|
|
287
|
+
s.decide.clear();
|
|
288
|
+
delete s.leftOut;
|
|
289
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 1 (B1) — the names typed at "Who else?" on the people card. Pure.
|
|
3
|
+
*
|
|
4
|
+
* Split on `,` `;` and ` and `; me, I and myself are dropped; the same name
|
|
5
|
+
* twice is kept once. A name is 2–60 characters of letters, spaces,
|
|
6
|
+
* apostrophes, dots and hyphens, at most four words — no digit, underscore or
|
|
7
|
+
* slash, so a package name can never pass for a person; a name equal to a
|
|
8
|
+
* package of the start scope is refused too. At most 9 names.
|
|
9
|
+
*/
|
|
10
|
+
import { shown } from '../register/atc-file.js';
|
|
11
|
+
export const MAX_PEOPLE = 9;
|
|
12
|
+
const NAME = /^[\p{L}][\p{L}'’.-]*(?: [\p{L}][\p{L}'’.-]*){0,3}$/u;
|
|
13
|
+
const SELF = new Set(['me', 'i', 'myself']);
|
|
14
|
+
/** `typed` is cleaned for the screen by the caller (shown). */
|
|
15
|
+
export const notANameLine = (typed) => `"${typed}" is not a person's name. Full names, for example James Miller.`;
|
|
16
|
+
export const TOO_MANY_PEOPLE_LINE = `At most ${MAX_PEOPLE} names, please.`;
|
|
17
|
+
/** `packages`: the start scope — a typed name equal to one of them (any case) is refused like any other non-name. */
|
|
18
|
+
export function parsePeople(said, packages = []) {
|
|
19
|
+
const scope = new Set(packages.map((p) => p.trim().toUpperCase()));
|
|
20
|
+
const names = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const raw of said.split(/\s*[,;]\s*|\s+and\s+/i)) {
|
|
23
|
+
const name = raw.trim().replace(/\s+/g, ' ');
|
|
24
|
+
if (!name || SELF.has(name.toLowerCase()))
|
|
25
|
+
continue;
|
|
26
|
+
if (name.length < 2 || name.length > 60 || !NAME.test(name) || scope.has(name.toUpperCase())) {
|
|
27
|
+
return { ok: false, line: notANameLine(shown(name)) };
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(name.toLowerCase()))
|
|
30
|
+
continue;
|
|
31
|
+
seen.add(name.toLowerCase());
|
|
32
|
+
names.push(name);
|
|
33
|
+
}
|
|
34
|
+
if (names.length > MAX_PEOPLE)
|
|
35
|
+
return { ok: false, line: TOO_MANY_PEOPLE_LINE };
|
|
36
|
+
return { ok: true, names };
|
|
37
|
+
}
|
package/dist/journey/rebuild.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import fs from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import { APP_LABEL, isAppLabel, objectKey, proposeRebuildKind, } from '@cspeach/register-core';
|
|
29
|
+
import { APP_LABEL, isAppLabel, objectKey, proposeRebuildKind, viewWords, } from '@cspeach/register-core';
|
|
30
30
|
import { activeProject } from '../register/store.js';
|
|
31
31
|
import { registerFor } from '../register/picture.js';
|
|
32
32
|
import { writeRecord } from '../register/records.js';
|
|
@@ -53,7 +53,7 @@ export const UNSURE_LINE = 'CSPeach cannot tell the kind from what it read. Chan
|
|
|
53
53
|
export const REBUILD_BRIEF_REL = (app) => `rebuilds/${app}/rebuild-brief.md`;
|
|
54
54
|
const SID = /^[A-Z][A-Z0-9]{2}$/;
|
|
55
55
|
const PACKAGE = /^(\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_$]{1,30}$/;
|
|
56
|
-
const
|
|
56
|
+
const NO_TARGET = { level: null, from: 'none', by: null };
|
|
57
57
|
const MODERNIZE = 'abap-modernize';
|
|
58
58
|
const nameOk = (type, name) => OBJECT_TYPE.test(type) && OBJECT_NAME.test(name);
|
|
59
59
|
/* ── names and commands ─────────────────────────────────────────────────── */
|
|
@@ -88,13 +88,13 @@ export function rebuildBrief(b) {
|
|
|
88
88
|
const sid = b.sid && SID.test(b.sid.toUpperCase()) ? b.sid.toUpperCase() : 'the SAP system';
|
|
89
89
|
const names = objs.map((o) => o.name).join(', ');
|
|
90
90
|
const why = screenedReason(b.reason);
|
|
91
|
-
const target = b.target && LEVEL.test(b.target) ? b.target : 'not set';
|
|
92
91
|
return [
|
|
93
92
|
`# Rebuild of ${objs.map((o) => `${o.name} (${o.type}${o.pkg ? `, package ${o.pkg}` : ''})`).join(', ')} on ${sid}`,
|
|
94
93
|
...(b.kind === 'app' ? [`Build an app that replaces the old program ${names}. Users create, change and view the same data the old program handles today.`] : []),
|
|
95
94
|
...(b.kind === 'api' ? [`Build an API service that replaces ${names}: no UI, OData service.`] : []),
|
|
96
95
|
...objs.map((o) => `Read the old program's behaviour from the SAP system (${o.type} ${o.name}) before designing: its selection fields, its output columns, its checks.`),
|
|
97
|
-
|
|
96
|
+
// Project view Task 5: the view's words — "target B (set by Sarah Davis)", "target A (project target)", "no target set".
|
|
97
|
+
`For the new build: ${viewWords.target(b.target ?? NO_TARGET)}.`,
|
|
98
98
|
...(why ? [`Why it was decided (context only, not a requirement): ${why.text}.`] : []),
|
|
99
99
|
...(why?.marked ? [MARKED_NOTE] : []),
|
|
100
100
|
// Ruling (fix round 1 follow-up): no quotable "report" phrase anywhere in the ask.
|
|
@@ -200,7 +200,7 @@ export async function rebuildCard(confirm, g) {
|
|
|
200
200
|
* all); a class or an API service is one card per object (review fix 3).
|
|
201
201
|
* `passed` names each rebuild CSPeach cannot hand off, with why.
|
|
202
202
|
*/
|
|
203
|
-
export function rebuildGroups(mine, skip, rulesOf = () => []) {
|
|
203
|
+
export function rebuildGroups(mine, skip, rulesOf = () => [], targetOf = () => undefined) {
|
|
204
204
|
const groups = new Map();
|
|
205
205
|
const passed = [];
|
|
206
206
|
for (const o of mine) {
|
|
@@ -224,7 +224,8 @@ export function rebuildGroups(mine, skip, rulesOf = () => []) {
|
|
|
224
224
|
const kind = own ?? p.kind;
|
|
225
225
|
const entry = {
|
|
226
226
|
type: o.type.toUpperCase(), name: o.name.toUpperCase(), key: o.key, package: o.package,
|
|
227
|
-
|
|
227
|
+
// Project view Task 5: the view's target, never a level read off the decision.
|
|
228
|
+
...(o.decision.reason ? { reason: o.decision.reason } : {}), target: targetOf(o.key) ?? null,
|
|
228
229
|
};
|
|
229
230
|
const id = kind === 'app' ? `app|${app}` : `one|${o.key}`;
|
|
230
231
|
const g = groups.get(id);
|