@cspeach/cli 1.1.52 → 1.1.54
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/conversation-prompt.js +1 -0
- package/dist/agent/loop.js +150 -17
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/repair-partial.js +9 -0
- package/dist/agent/tool-dispatch.js +17 -2
- package/dist/approvals/op-labels.js +6 -3
- package/dist/approvals/render.js +3 -2
- package/dist/auth/auth-file.js +3 -0
- package/dist/auth/key-resolver.js +8 -3
- package/dist/auth/me.js +6 -2
- package/dist/cli.js +22 -0
- package/dist/commands/login.js +22 -4
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +5 -0
- package/dist/commands/plan-resume.js +2 -2
- package/dist/commands/plan-startup.js +3 -3
- package/dist/commands/sap-link.js +14 -1
- package/dist/commands/team-build-help.js +19 -0
- package/dist/commands/team-build.js +97 -0
- package/dist/commands/team-plan.js +2 -2
- package/dist/commands/team-spec.js +107 -0
- package/dist/commands/team.js +74 -4
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/journey/chain.js +8 -0
- package/dist/journey/clean-core.js +2 -2
- package/dist/journey/rebuild.js +8 -0
- package/dist/journey/run.js +9 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/licence/paths.js +5 -0
- package/dist/licence/seat-kind.js +110 -0
- package/dist/licence/seat-state.js +4 -0
- package/dist/licence/terminal-seat.js +43 -0
- package/dist/licence/turn-gate.js +4 -0
- package/dist/one-shot.js +33 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +133 -18
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +83 -20
- package/dist/projects/saved-lines.js +19 -17
- package/dist/projects/spec-share.js +239 -0
- package/dist/register/picture.js +2 -2
- package/dist/register/store.js +19 -0
- package/dist/renderer/change-card.js +14 -0
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/tool-labels.js +2 -0
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/builtin-commands.js +4 -3
- package/dist/repl/chat-lane.js +10 -1
- package/dist/repl/file-picker.js +52 -5
- package/dist/repl/mode-cycle.js +12 -0
- package/dist/repl/open-questions-gate.js +159 -0
- package/dist/repl/ready-phrase.js +108 -0
- package/dist/repl/startup-account.js +64 -1
- package/dist/repl/team-build-screen.js +116 -0
- package/dist/repl/team-build-start.js +178 -0
- package/dist/repl.js +216 -11
- package/dist/sap-link/policy.js +4 -0
- package/dist/sap-link/registry.js +9 -3
- package/dist/sap-link/server.js +4 -1
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/user-prompt.js +2 -0
- package/dist/tools/_team-work-path.js +1 -1
- package/dist/tools/approval.js +12 -1
- package/dist/tools/cap/sap-link-tool.js +2 -0
- package/dist/tools/project/spec_answers_record.js +243 -0
- package/dist/tools/project/team_findings.js +2 -2
- package/dist/tools/seat-gate.js +41 -0
- package/dist/tools/tool-loading.js +2 -0
- package/dist/ui/approval-modal.js +2 -1
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/files-card.js +19 -1
- package/package.json +2 -2
package/dist/commands/login.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* 4) Poll /v1/auth/device/poll every interval until verified or maxAttempts
|
|
9
9
|
* 5) Save api_key to the OS credential store (this CSPeach home's account);
|
|
10
10
|
* auth.json keeps customer_id + savedAt (the key only without a store)
|
|
11
|
-
* 6) Call GET /v1/me —
|
|
12
|
-
*
|
|
11
|
+
* 6) Call GET /v1/me — record the seat kind (seat.json, Team build); if
|
|
12
|
+
* trial_mode === 'byok', pull the org's Anthropic key, or prompt for the
|
|
13
|
+
* user's own
|
|
13
14
|
* 7) Store that key in the OS keychain (cspeach.anthropic/api-key) — never
|
|
14
15
|
* in auth.json (2026-10-02)
|
|
15
16
|
* 8) GET /v1/me/connection — Azure tenant mode: the company's Azure
|
|
@@ -30,6 +31,8 @@ import { getGlyphs } from '../renderer/glyphs.js';
|
|
|
30
31
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
31
32
|
import { runConnectionStep } from '../connection/login-step.js';
|
|
32
33
|
import { clientIdConfigured } from '../connection/azure-facts.js';
|
|
34
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
35
|
+
import { resolveSeatKind, makeSeatRecord, writeSeatRecord } from '../licence/seat-kind.js';
|
|
33
36
|
const ANTHROPIC_KEYCHAIN_SERVICE = 'cspeach.anthropic';
|
|
34
37
|
const ANTHROPIC_KEYCHAIN_ACCOUNT = 'api-key';
|
|
35
38
|
/** Server-supplied text (user code, URL, email, plan) is one line with no control characters. */
|
|
@@ -106,11 +109,24 @@ export async function runLoginCommand(deps) {
|
|
|
106
109
|
deps.print(done(savedTo === 'store'
|
|
107
110
|
? 'Saved your CSPeach key to your OS credential store'
|
|
108
111
|
: 'Saved your CSPeach key to auth.json in your CSPeach home (no OS credential store is available)'));
|
|
109
|
-
// 6) /v1/me —
|
|
112
|
+
// 6) /v1/me — the seat kind, and whether BYOK is required. Team build (R5):
|
|
113
|
+
// the user-agent names this CLI, so the proxy never takes a current CLI for
|
|
114
|
+
// one too old for a functional seat.
|
|
115
|
+
// Team build: the seat kind is recorded once, from this answer only. A
|
|
116
|
+
// failed call, or a word we do not know, records nothing (never a guess).
|
|
117
|
+
let seatRecorded = false;
|
|
110
118
|
try {
|
|
111
|
-
const r = await fetch(`${proxy}/v1/me`, { headers: { Authorization: `Bearer ${polled.api_key}` } });
|
|
119
|
+
const r = await fetch(`${proxy}/v1/me`, { headers: { 'User-Agent': `cspeach/${CLI_VERSION}`, Authorization: `Bearer ${polled.api_key}` } });
|
|
112
120
|
if (r.ok) {
|
|
113
121
|
const me = await r.json();
|
|
122
|
+
const seat = resolveSeatKind({ fresh: { ok: true, profile: me, serverDate: null }, stored: null, apiKey: polled.api_key, now: new Date(), legacySignIn: false });
|
|
123
|
+
if (seat.ok) {
|
|
124
|
+
try {
|
|
125
|
+
await writeSeatRecord(makeSeatRecord(polled.api_key, seat.kind, new Date()));
|
|
126
|
+
seatRecorded = true;
|
|
127
|
+
}
|
|
128
|
+
catch { /* reported by the one line below */ }
|
|
129
|
+
}
|
|
114
130
|
deps.print(done(`Signed in as ${serverText(me.email)} · ${serverText(me.plan)}`));
|
|
115
131
|
if (me.trial_mode === 'byok' || me.plan === 'byo-key') {
|
|
116
132
|
// Try to fetch the org's Anthropic key first — non-fatal if unavailable.
|
|
@@ -146,6 +162,8 @@ export async function runLoginCommand(deps) {
|
|
|
146
162
|
catch (err) {
|
|
147
163
|
deps.print(formatInfo(`Could not read your account details (${err instanceof Error ? err.message : String(err)}) · sign-in still worked`).trimEnd());
|
|
148
164
|
}
|
|
165
|
+
if (!seatRecorded)
|
|
166
|
+
deps.print(formatInfo('Could not record your seat kind · run cspeach login again when you are online').trimEnd());
|
|
149
167
|
// 8) Azure tenant mode — the company's connection settings (no secrets).
|
|
150
168
|
// { kind: 'none' } changes nothing; an Azure answer is confirmed on screen.
|
|
151
169
|
const step = await (deps.connectionStep ?? runConnectionStep)({
|
|
@@ -34,7 +34,7 @@ import { collectPlannedObjects, plannedObjectFacts } from '../projects/plan-obje
|
|
|
34
34
|
import { isDeployPhase } from '../projects/plan-lane.js';
|
|
35
35
|
import { askDeployCard } from './deploy-card.js';
|
|
36
36
|
import { decideNextPhaseGate, getAndResetPlanPhaseSubagentDispatches, getAndResetPlanDeviation, } from './plan-gate.js';
|
|
37
|
-
import { applyAuditResult, applyPhaseResolution, buildResumeCommand, offerNextPhaseAutoRun, } from './plan-resume.js';
|
|
37
|
+
import { applyAuditResult, applyPhaseResolution, buildResumeCommand, offerNextPhaseAutoRun, planBuildTransport, } from './plan-resume.js';
|
|
38
38
|
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
39
39
|
import { clean } from '../renderer/tool-labels.js';
|
|
40
40
|
import { runBatchAudit, pendingAuditPhaseIds, selectPlanCardAction, } from './plan-batch-audit.js';
|
|
@@ -251,7 +251,7 @@ async function postPhaseChain(args) {
|
|
|
251
251
|
const built = after.content.phases.every((q) => isPhaseSatisfiedForRun(st[q.id], q.audit));
|
|
252
252
|
const nextAfter = computeNextPhaseForRun(after.content.phases, st);
|
|
253
253
|
if (built && !nextAfter) {
|
|
254
|
-
args.log('', ...planCompletionLines(after.content.phases));
|
|
254
|
+
args.log('', ...planCompletionLines(after.content.phases, undefined, planBuildTransport(after.content.phases)));
|
|
255
255
|
// Step 2 (C5): a rebuild whose plan this is now reads "built, not yet checked".
|
|
256
256
|
await afterPlanComplete(cur);
|
|
257
257
|
args.log(`Plan file: ${savedLocation(cur, args.cwd)}`);
|
|
@@ -5,6 +5,7 @@ import { SAP_PAUSE_LINE } from './plan-sap-pause.js';
|
|
|
5
5
|
import { unreachableCode } from '../sap/unreachable.js';
|
|
6
6
|
import { unreachableCodeInText } from '../sap/reachability-state.js';
|
|
7
7
|
import { recordFullNotice } from '../renderer/notice-log.js';
|
|
8
|
+
import { isFunctionalSeat } from '../licence/seat-state.js';
|
|
8
9
|
const LOCK_WAIT_MS = 10_000;
|
|
9
10
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
11
|
/** Network code when SAP did not answer at all (an HTTP error means it answered). */
|
|
@@ -20,6 +21,10 @@ function errText(err) {
|
|
|
20
21
|
* network failure) — never blocks the step. Unreachable SAP is rethrown.
|
|
21
22
|
*/
|
|
22
23
|
async function probeLock(deps, type, name) {
|
|
24
|
+
// Team build Task 6 (ruling R8 I1) — a functional seat never takes a SAP
|
|
25
|
+
// lock, not even a probe: 'unknown' never blocks, and SAP is not touched.
|
|
26
|
+
if (isFunctionalSeat())
|
|
27
|
+
return 'unknown';
|
|
23
28
|
let l;
|
|
24
29
|
try {
|
|
25
30
|
l = await deps.lock(type, name);
|
|
@@ -521,7 +521,7 @@ export async function preparePlanResume(args) {
|
|
|
521
521
|
// C2 (D24): a finished UI-less backend stack chains to /abap-fiori-build
|
|
522
522
|
// with the exact binding name — the marketed idea→app story must not
|
|
523
523
|
// silently end at the service binding.
|
|
524
|
-
args.log(...planCompletionLines(content.phases));
|
|
524
|
+
args.log(...planCompletionLines(content.phases, undefined, planBuildTransport(content.phases)));
|
|
525
525
|
}
|
|
526
526
|
else {
|
|
527
527
|
const blocked = content.phases.filter((p) => statuses[p.id] === 'blocked').map((p) => titleOfId(p.id));
|
|
@@ -1102,7 +1102,7 @@ export async function finishPlanResume(args) {
|
|
|
1102
1102
|
args.log(renderPlanProgressLine({ content: { ...extract.content, phases: provisionalPhases }, statuses, currentId: null }));
|
|
1103
1103
|
if (!next && provisionalPhases.every((p) => isPhaseSatisfied(statuses[p.id], p.audit))) {
|
|
1104
1104
|
// C2 (D24): same chain as preparePlanResume — single source in plan-run.ts.
|
|
1105
|
-
args.log(...planCompletionLines(provisionalPhases));
|
|
1105
|
+
args.log(...planCompletionLines(provisionalPhases, undefined, planBuildTransport(provisionalPhases)));
|
|
1106
1106
|
// Step 2 (C5): the live run finished — a rebuild whose plan this is reads built.
|
|
1107
1107
|
// Not at the re-opened complete plan above (preparePlanResume).
|
|
1108
1108
|
await afterPlanComplete(args.prepared.path);
|
|
@@ -17,7 +17,7 @@ import { basename, dirname } from 'node:path';
|
|
|
17
17
|
import { readProjectFile } from '../projects/status.js';
|
|
18
18
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
19
19
|
import { computeNextPhaseForRun, isPhaseSatisfiedForRun, phaseDisplayTitle, statusesFromItems, } from '../projects/plan-run.js';
|
|
20
|
-
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
20
|
+
import { collectPlannedObjects, plannedObjectFacts, planGrantCoversWrites, planGrantFactLines, planTransportFact, } from '../projects/plan-objects.js';
|
|
21
21
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from './plan-resume.js';
|
|
22
22
|
import { newestPlanFamilyFiles } from '../session/recap.js';
|
|
23
23
|
import { recordCliCard } from '../session/cli-record.js';
|
|
@@ -100,8 +100,8 @@ function objectListFacts(objects, transport, grantable) {
|
|
|
100
100
|
return [
|
|
101
101
|
`It will create or change these ${objects.length} SAP object${objects.length === 1 ? '' : 's'}:`,
|
|
102
102
|
...plannedObjectFacts(objects).map((r) => ` ${r}`),
|
|
103
|
-
|
|
104
|
-
...(grantable ? planGrantFactLines() : ['CSPeach asks before each write.']),
|
|
103
|
+
planTransportFact(transport, objects),
|
|
104
|
+
...(grantable && planGrantCoversWrites(transport, objects) ? planGrantFactLines() : ['CSPeach asks before each write.']),
|
|
105
105
|
];
|
|
106
106
|
}
|
|
107
107
|
const OBJECT_LIST_FAILED = 'The object list could not be approved. Each SAP write asks first.';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// only plain registry messages, never raw exception text.
|
|
7
7
|
import { ensureSapLink, stopSapLink, SapLinkError } from '../sap-link/registry.js';
|
|
8
8
|
import { LinkConfigError } from '../sap-link/project.js';
|
|
9
|
+
import { seatSignInMessage } from '../licence/seat-kind.js';
|
|
9
10
|
function waitForSignal() {
|
|
10
11
|
return new Promise((resolve) => {
|
|
11
12
|
const done = () => {
|
|
@@ -29,10 +30,22 @@ export async function runSapLinkCommand(argv, deps) {
|
|
|
29
30
|
deps.print('Name the SAP system: cspeach sap link <alias>');
|
|
30
31
|
return 1;
|
|
31
32
|
}
|
|
33
|
+
// Asked once, before the link starts. Not confirmed asks to sign in and stops: a developer is
|
|
34
|
+
// never wrongly locked read-only. Only a functional seat starts the link read-only.
|
|
35
|
+
const seat = deps.seat ? await deps.seat().catch(() => 'unconfirmed') : undefined;
|
|
36
|
+
if (seat === 'unconfirmed') {
|
|
37
|
+
deps.print(seatSignInMessage());
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const linkDeps = seat === undefined
|
|
41
|
+
? deps.linkDeps
|
|
42
|
+
: { ...deps.linkDeps, readOnlySeat: () => seat === 'functional' };
|
|
32
43
|
try {
|
|
33
|
-
const r = await ensureSapLink(projectDir, alias,
|
|
44
|
+
const r = await ensureSapLink(projectDir, alias, linkDeps);
|
|
34
45
|
deps.print(`SAP link on: ${r.handle.url} → ${alias}`);
|
|
35
46
|
deps.print(`Services: ${r.services.join(', ')}`);
|
|
47
|
+
if (r.seatNote)
|
|
48
|
+
deps.print(r.seatNote);
|
|
36
49
|
if (r.roleNote)
|
|
37
50
|
deps.print(r.roleNote);
|
|
38
51
|
if (r.tlsNote)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team build (spec 2026-10-25, plan Task 12) — `cspeach help team-build`.
|
|
3
|
+
* The how-to for two people sharing one folder: the functional writes the
|
|
4
|
+
* spec, the developer builds it. Copy is fixed by the plan; change it only
|
|
5
|
+
* together with docs/guide/team-build.md.
|
|
6
|
+
*/
|
|
7
|
+
export const TEAM_BUILD_HOW_TO = Object.freeze([
|
|
8
|
+
'Team build — two people, one folder.',
|
|
9
|
+
'1. Make a folder inside your shared OneDrive, SharePoint or network folder.',
|
|
10
|
+
'2. Right-click it and choose "Open in Terminal".',
|
|
11
|
+
'3. Type cspeach. That is the only command. CSPeach sets the folder up the first time.',
|
|
12
|
+
'4. Talk through the requirement. Say when it is ready for the developer.',
|
|
13
|
+
'5. Tell the developer the folder name. They do steps 2 and 3 on their laptop.',
|
|
14
|
+
'Your SAP user decides what you may see in SAP. A functional seat reads SAP and never changes it.',
|
|
15
|
+
]);
|
|
16
|
+
export function printTeamBuildHelp(emit = console.log) {
|
|
17
|
+
for (const line of TEAM_BUILD_HOW_TO)
|
|
18
|
+
emit(line);
|
|
19
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team build (spec 2026-10-25 §7) — the `build` project type: a spec made with
|
|
3
|
+
* the live system, then built from it. No baseline is pulled and nothing is
|
|
4
|
+
* measured, so the folder is made without a SAP connection.
|
|
5
|
+
*
|
|
6
|
+
* A team build folder is `formatVersion: 2`. Clean-core and upgrade folders
|
|
7
|
+
* stay `formatVersion: 1`, byte for byte; every branch here keys on
|
|
8
|
+
* `type === 'build' && formatVersion === 2`. A folder made with the old
|
|
9
|
+
* `build` type (formatVersion 1, a baseline) is a legacy build folder.
|
|
10
|
+
*
|
|
11
|
+
* Also here: the guard a functional seat meets on the terminal verbs that
|
|
12
|
+
* record work (spec §7.2: a functional seat joins any team project read-only).
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { RegisterError, TEAM_FILE, defaultRand, projectIdFor, } from '../register/store.js';
|
|
17
|
+
import { writeWhole } from '../register/write-whole.js';
|
|
18
|
+
export const LEGACY_BUILD_LINE = 'This folder was set up with the old build type. The build type changed: start a fresh folder for the team build.';
|
|
19
|
+
/**
|
|
20
|
+
* The verbs that measure, decide or report on a baseline: clean-core and
|
|
21
|
+
* upgrade projects only. Not `discover` (ruling R9): it writes to its own
|
|
22
|
+
* folder (`--out` or the folder you are in), never to the joined project.
|
|
23
|
+
*/
|
|
24
|
+
export const CLEAN_CORE_ONLY_VERBS = [
|
|
25
|
+
'lane', 'refresh', 'fixed', 'target', 'rebuild', 'decide', 'rule', 'sheet', 'report', 'export', 'focus', 'priority', 'plan', 'transport',
|
|
26
|
+
];
|
|
27
|
+
/** The terminal verbs that record work into a team project. A functional seat runs none of them. */
|
|
28
|
+
export const FUNCTIONAL_RECORDING_VERBS = [
|
|
29
|
+
'lane', 'refresh', 'fixed', 'target', 'rebuild', 'decide', 'rule', 'transport', 'plan', 'focus', 'priority',
|
|
30
|
+
];
|
|
31
|
+
export const FUNCTIONAL_VERB_LINE = 'Your seat reads the team project and never records work into it. Ask a developer on the project to run this.';
|
|
32
|
+
export function buildVerbRefusal(verb) {
|
|
33
|
+
return `This is a team build folder: a spec made with the live system, then built from it. "cspeach team ${verb}" belongs to clean-core and upgrade projects.`;
|
|
34
|
+
}
|
|
35
|
+
export function buildJoinLine(project, writerId) {
|
|
36
|
+
return `✓ Joined the team build "${project.name}" (lead ${project.lead.name}) as ${writerId}.`;
|
|
37
|
+
}
|
|
38
|
+
export function isBuildProject(p) {
|
|
39
|
+
return p.type === 'build' && p.formatVersion === 2;
|
|
40
|
+
}
|
|
41
|
+
export function isLegacyBuildFolder(p) {
|
|
42
|
+
return p.type === 'build' && p.formatVersion !== 2;
|
|
43
|
+
}
|
|
44
|
+
/** The `--type` a verb was given (`--type build` or `--type=build`); undefined when none. */
|
|
45
|
+
function typeOption(rest) {
|
|
46
|
+
for (let i = 0; i < rest.length; i++) {
|
|
47
|
+
const a = rest[i];
|
|
48
|
+
if (a.startsWith('--type='))
|
|
49
|
+
return a.slice('--type='.length);
|
|
50
|
+
if (a === '--type')
|
|
51
|
+
return rest[i + 1];
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const hasOption = (rest, name) => rest.some((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
56
|
+
/**
|
|
57
|
+
* True when a terminal verb records work: every verb in the list, and `init`
|
|
58
|
+
* and `start` (they pull, measure and write a clean-core or upgrade project)
|
|
59
|
+
* unless they set up a team build (`--type build`). The read-only forms are
|
|
60
|
+
* left out: `lane` alone or `lane list`, `lane propose` without `--apply`,
|
|
61
|
+
* `plan history`, `priority` alone or `priority show`, and `focus list`.
|
|
62
|
+
*/
|
|
63
|
+
export function isRecordingVerb(sub, rest) {
|
|
64
|
+
if (sub === 'init' || sub === 'start')
|
|
65
|
+
return typeOption(rest) !== 'build';
|
|
66
|
+
if (!sub || !FUNCTIONAL_RECORDING_VERBS.includes(sub))
|
|
67
|
+
return false;
|
|
68
|
+
const [verb] = rest;
|
|
69
|
+
switch (sub) {
|
|
70
|
+
case 'lane': return !(verb === undefined || verb === 'list' || (verb === 'propose' && !hasOption(rest, 'apply')));
|
|
71
|
+
case 'plan': return verb !== 'history';
|
|
72
|
+
case 'priority': return !(verb === undefined || verb === 'show');
|
|
73
|
+
case 'focus': return !(verb === 'list' && rest.length === 1);
|
|
74
|
+
default: return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The team build folder: the project file (written whole, then renamed),
|
|
79
|
+
* `specs/` for the developer and `writers/` for the records.
|
|
80
|
+
*/
|
|
81
|
+
export function createBuildProject(dir, a) {
|
|
82
|
+
if (fs.existsSync(path.join(dir, TEAM_FILE)))
|
|
83
|
+
throw new RegisterError('This folder already holds a team project.');
|
|
84
|
+
const project = {
|
|
85
|
+
kind: 'team-project',
|
|
86
|
+
formatVersion: 2,
|
|
87
|
+
projectId: projectIdFor(a.name, a.rand ?? defaultRand),
|
|
88
|
+
name: a.name,
|
|
89
|
+
type: 'build',
|
|
90
|
+
lead: a.lead.account ? { name: a.lead.name, account: a.lead.account } : { name: a.lead.name },
|
|
91
|
+
createdAt: a.now.toISOString(),
|
|
92
|
+
};
|
|
93
|
+
writeWhole(path.join(dir, TEAM_FILE), `${JSON.stringify(project, null, 2)}\n`);
|
|
94
|
+
fs.mkdirSync(path.join(dir, 'specs'), { recursive: true });
|
|
95
|
+
fs.mkdirSync(path.join(dir, 'writers'), { recursive: true });
|
|
96
|
+
return project;
|
|
97
|
+
}
|
|
@@ -19,7 +19,7 @@ import fs from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { createHash } from 'node:crypto';
|
|
21
21
|
import { buildRegister, DEFAULT_WEIGHTS, objectKey, proposeLanes, utcWords, } from '@cspeach/register-core';
|
|
22
|
-
import { RegisterError, collectRecords, readBaseline, readLanes, readProjectFile, writeLanes, } from '../register/store.js';
|
|
22
|
+
import { RegisterError, collectRecords, coreProject, readBaseline, readLanes, readProjectFile, writeLanes, } from '../register/store.js';
|
|
23
23
|
import { diffPlan, readPlanSheet } from '../register/plan-import.js';
|
|
24
24
|
import { writeLeadRecord } from '../register/records.js';
|
|
25
25
|
import { registerFor } from '../register/picture.js';
|
|
@@ -531,7 +531,7 @@ async function planImport(args, deps) {
|
|
|
531
531
|
// not handed in yet: a newer word of the lead's is still newer.
|
|
532
532
|
const folder = collectRecords(lead.projectDir).records;
|
|
533
533
|
const own = path.resolve(lead.outboxDir) !== path.resolve(lead.projectDir) ? collectRecords(lead.outboxDir).records : [];
|
|
534
|
-
const input = { project: lead.project, baseline: readBaseline(lead.projectDir), lanes: readLanes(lead.projectDir), progress: false };
|
|
534
|
+
const input = { project: coreProject(lead.project), baseline: readBaseline(lead.projectDir), lanes: readLanes(lead.projectDir), progress: false };
|
|
535
535
|
const register = buildRegister({ ...input, records: folder });
|
|
536
536
|
const atOrBefore = (r) => {
|
|
537
537
|
const at = r?.at;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team build (spec 2026-10-25) — `cspeach team spec list|ready [<slug>]`.
|
|
3
|
+
*
|
|
4
|
+
* list every spec in this team build: status, title, author, date, open questions
|
|
5
|
+
* ready [<slug>] mark a draft "ready for build" for the developer
|
|
6
|
+
*
|
|
7
|
+
* The same marking runs from the plain phrase "this is ready for the
|
|
8
|
+
* developer" in the REPL (repl/ready-phrase.ts).
|
|
9
|
+
*
|
|
10
|
+
* Ruling R10: the build project is the folder CSPeach runs in, never
|
|
11
|
+
* `activeProject()` — a developer on a clean-core wave keeps that one active.
|
|
12
|
+
* Ruling R12: status belongs to the spec folder (slug), an added-only record
|
|
13
|
+
* in `specs/<slug>/status/`, not to one envelope version.
|
|
14
|
+
*
|
|
15
|
+
* Both seats run it: the functional authors the spec, the developer reads it.
|
|
16
|
+
* `spec` is neither a recording verb nor a clean-core-only verb.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { TEAM_FILE, readProjectFile } from '../register/store.js';
|
|
21
|
+
import { isBuildProject } from './team-build.js';
|
|
22
|
+
import { listSharedSpecs, specAuthorFor, writeStatusRecord } from '../projects/spec-share.js';
|
|
23
|
+
export const NOT_BUILD_SPEC_LINE = 'This folder is not a team build. "cspeach team spec" works in a team build folder.';
|
|
24
|
+
export const NO_DRAFT_LINE = 'No draft spec here yet. Work the requirement through spec gap first, then say when it is ready.';
|
|
25
|
+
export const markedReadyLine = (title) => `Marked ready for the developer: "${title}". They will see it when they open CSPeach in this folder.`;
|
|
26
|
+
const USAGE = 'Usage: cspeach team spec list|ready [<slug>]';
|
|
27
|
+
/**
|
|
28
|
+
* The team build in exactly this folder, or null. Cheap outside a team
|
|
29
|
+
* folder: one file-exists check. An unreadable project file is null too.
|
|
30
|
+
*/
|
|
31
|
+
export function buildFolderAt(cwd) {
|
|
32
|
+
const dir = path.resolve(cwd);
|
|
33
|
+
if (!fs.existsSync(path.join(dir, TEAM_FILE)))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const project = readProjectFile(dir);
|
|
37
|
+
return isBuildProject(project) ? { dir, project } : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const draftSpecs = (teamDir) => listSharedSpecs(teamDir).filter((s) => s.status === 'draft');
|
|
44
|
+
/** Add the "ready for build" record to the spec's folder. */
|
|
45
|
+
export function markSpecReady(spec, projectId, by, now, rand) {
|
|
46
|
+
return writeStatusRecord(spec.specDir, {
|
|
47
|
+
projectId, slug: spec.slug, file: path.basename(spec.envelopePath), status: 'ready for build', by, at: now.toISOString(),
|
|
48
|
+
}, rand);
|
|
49
|
+
}
|
|
50
|
+
export async function specCommand(args, deps, cwd = process.cwd()) {
|
|
51
|
+
const [verb, ...rest] = args;
|
|
52
|
+
const here = buildFolderAt(cwd);
|
|
53
|
+
if (!here) {
|
|
54
|
+
deps.print(NOT_BUILD_SPEC_LINE);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
switch (verb) {
|
|
58
|
+
case 'list': {
|
|
59
|
+
const specs = listSharedSpecs(here.dir);
|
|
60
|
+
if (!specs.length) {
|
|
61
|
+
deps.print('No specs here yet.');
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
for (const s of specs) {
|
|
65
|
+
deps.print(`${s.status.padEnd(16)} ${s.title} (${s.author}, ${s.createdAt.slice(0, 10)}) — ${s.openQuestions.length} open`);
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
case 'ready': {
|
|
70
|
+
const slug = rest[0];
|
|
71
|
+
const all = listSharedSpecs(here.dir);
|
|
72
|
+
let spec;
|
|
73
|
+
if (slug) {
|
|
74
|
+
spec = all.find((s) => s.slug === slug);
|
|
75
|
+
if (!spec) {
|
|
76
|
+
deps.print(`No spec "${slug}" here. Specs: ${all.length ? all.map((s) => s.slug).join(', ') : 'none yet'}`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
if (spec.status !== 'draft') {
|
|
80
|
+
deps.print(`"${spec.title}" is already ${spec.status}.`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const drafts = all.filter((s) => s.status === 'draft');
|
|
86
|
+
if (!drafts.length) {
|
|
87
|
+
deps.print(NO_DRAFT_LINE);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (drafts.length > 1) {
|
|
91
|
+
deps.print('Say which: cspeach team spec ready <slug>');
|
|
92
|
+
for (const s of drafts)
|
|
93
|
+
deps.print(` ${s.slug} ${s.title}`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
spec = drafts[0];
|
|
97
|
+
}
|
|
98
|
+
const by = specAuthorFor(here.project.projectId, deps.identity().name);
|
|
99
|
+
markSpecReady(spec, here.project.projectId, by, (deps.now ?? (() => new Date()))(), deps.rand);
|
|
100
|
+
deps.print(markedReadyLine(spec.title));
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
deps.print(USAGE);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/commands/team.js
CHANGED
|
@@ -23,7 +23,7 @@ import os from 'node:os';
|
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { decidedCount, decisionWhatOf, isAppLabel, isPatternWord, laneIdFor, levelOf, normalizeRuleId, objectKey, partCounts, proposeLanes, scopeFromWords, targetsOf, viewWords, } from '@cspeach/register-core';
|
|
25
25
|
import { LOCAL_PACKAGE, listCustomPackages, pullBaseline, BaselineError } from '../register/baseline.js';
|
|
26
|
-
import { BASELINE_FILE, TEAM_FILE, RegisterError, activeProject, defaultOutboxDir, loadState, projectIdFor, readBaseline, readEstate, readLanes, readProjectFile, saveState, sha256, VARIANT_NAME, writeLanes, writeProjectVariant, writerIdFor, } from '../register/store.js';
|
|
26
|
+
import { BASELINE_FILE, TEAM_FILE, RegisterError, activeProject, coreProject, defaultOutboxDir, loadState, projectIdFor, readBaseline, readEstate, readLanes, readProjectFile, saveState, sha256, VARIANT_NAME, writeLanes, writeProjectVariant, writerIdFor, } from '../register/store.js';
|
|
27
27
|
import { MAX_API_DOCUMENTS, MAX_SECOND_PASS, measure, noVariantYetMessage, scopeLabel, scopeObjects, variantMismatchMessage, } from '../register/measure.js';
|
|
28
28
|
import { measureFromFile, shown, shownUpTo, upToFive } from '../register/atc-file.js';
|
|
29
29
|
import { writeLeadRecord, writeRecord } from '../register/records.js';
|
|
@@ -39,7 +39,11 @@ import { lookupVariant, variantLookupLine } from '../register/variant.js';
|
|
|
39
39
|
import { TRANSPORT_NONE_WORDS, TRANSPORT_REQUEST, notARequestLine, projectTransportWords, writeProjectTransport } from '../register/project-transport.js';
|
|
40
40
|
import { teamDiscover, evidenceRows, EVIDENCE_LINE, OUTSIDE_LINE } from './team-discover.js';
|
|
41
41
|
import { rulesCommand } from './team-rules.js';
|
|
42
|
+
import { specCommand } from './team-spec.js';
|
|
42
43
|
import { focusCommand, laneChangeRecord, laneProposal, nowLine, planCommand, priorityCommand, reasonFrom } from './team-plan.js';
|
|
44
|
+
import { CLEAN_CORE_ONLY_VERBS, FUNCTIONAL_VERB_LINE, buildJoinLine, buildVerbRefusal, createBuildProject, isBuildProject, isRecordingVerb, } from './team-build.js';
|
|
45
|
+
import { seatSignInMessage } from '../licence/seat-kind.js';
|
|
46
|
+
import { getSeatKind } from '../licence/seat-state.js';
|
|
43
47
|
const CLASSIFICATIONS = ['keep', 'fix', 'retire', 'redesign'];
|
|
44
48
|
const TYPES = ['clean-core', 'upgrade', 'build'];
|
|
45
49
|
/**
|
|
@@ -128,6 +132,27 @@ function matches(pattern, value) {
|
|
|
128
132
|
export async function runTeamCommand(args, deps) {
|
|
129
133
|
const [sub, ...rest] = args;
|
|
130
134
|
try {
|
|
135
|
+
// Team build (spec §7.2): a functional seat reads a team project and never
|
|
136
|
+
// records work into it. Read-only verbs never ask for the seat. On by
|
|
137
|
+
// default (final review F1): without deps.seat the process seat decides
|
|
138
|
+
// (seat-state.ts — developer unless start-up resolved functional), so a
|
|
139
|
+
// caller that builds its own TeamDeps (the clean-core journey) is held too.
|
|
140
|
+
if (isRecordingVerb(sub, rest)) {
|
|
141
|
+
const seat = await (deps.seat ?? (async () => getSeatKind()))();
|
|
142
|
+
if (seat === 'functional') {
|
|
143
|
+
deps.print(FUNCTIONAL_VERB_LINE);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (seat === 'unconfirmed') {
|
|
147
|
+
deps.print(seatSignInMessage());
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// A team build folder has no baseline: the clean-core verbs are refused in one line.
|
|
152
|
+
if (sub && CLEAN_CORE_ONLY_VERBS.includes(sub) && activeIsBuild()) {
|
|
153
|
+
deps.print(buildVerbRefusal(sub));
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
131
156
|
switch (sub) {
|
|
132
157
|
case 'init': return await init(rest, deps);
|
|
133
158
|
case 'start': return await start(rest, deps);
|
|
@@ -151,8 +176,9 @@ export async function runTeamCommand(args, deps) {
|
|
|
151
176
|
case 'priority': return priorityCommand(rest, deps);
|
|
152
177
|
case 'plan': return await planCommand(rest, deps);
|
|
153
178
|
case 'rebuild': return await rebuildVerb(rest, deps);
|
|
179
|
+
case 'spec': return await specCommand(rest, deps);
|
|
154
180
|
default:
|
|
155
|
-
deps.print('Usage: cspeach team init|join|lane|refresh|status|outbox|rule|leave|decide|target|transport|fixed|sheet|report|export|discover|focus|priority|plan|start|rebuild|rules (cspeach --help for details)');
|
|
181
|
+
deps.print('Usage: cspeach team init|join|lane|refresh|status|outbox|rule|leave|decide|target|transport|fixed|sheet|report|export|discover|focus|priority|plan|start|rebuild|rules|spec (cspeach --help for details)');
|
|
156
182
|
return sub ? 1 : 0;
|
|
157
183
|
}
|
|
158
184
|
}
|
|
@@ -170,6 +196,28 @@ export async function runTeamCommand(args, deps) {
|
|
|
170
196
|
return 1;
|
|
171
197
|
}
|
|
172
198
|
}
|
|
199
|
+
/** The joined project is a team build. Unreadable or none: no guard (the verb says why itself). */
|
|
200
|
+
function activeIsBuild() {
|
|
201
|
+
try {
|
|
202
|
+
const joined = activeProject();
|
|
203
|
+
return !!joined && isBuildProject(readProjectFile(joined.projectDir));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The team build folder (Task 7): no SAP connection, nothing pulled or
|
|
211
|
+
* measured. The lead joins with the outbox in the folder itself.
|
|
212
|
+
*/
|
|
213
|
+
function initBuild(dir, name, lead, deps, asName) {
|
|
214
|
+
createBuildProject(dir, { name, lead: { name: lead }, now: (deps.now ?? (() => new Date()))(), ...(deps.rand ? { rand: deps.rand } : {}) });
|
|
215
|
+
const quiet = { ...deps, print: () => { } };
|
|
216
|
+
join([dir, '--outbox', dir], quiet, asName);
|
|
217
|
+
deps.print(`✓ Team build "${name}" set up in this folder. Lead: ${lead}.`);
|
|
218
|
+
deps.print(' Specs save to specs/ for the developer. Start CSPeach here: cspeach');
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
173
221
|
/** True when this seat is the project's lead. The one test: requireLead and team_lane_move both use it. */
|
|
174
222
|
export function isLead(project, deps) {
|
|
175
223
|
return whoAmI(deps).trim().toLowerCase() === project.lead.name.trim().toLowerCase();
|
|
@@ -191,6 +239,8 @@ const INIT_USAGE = 'Usage: cspeach team init <folder> --name "<name>" [--lead "<
|
|
|
191
239
|
+ '[--packages ZFI_*,ZSD_*] [--include-local] [--atc-variant <NAME>] [--no-measure] [--no-from-discovery]';
|
|
192
240
|
/** Everything `init` understands. Anything else is a typo, and a typo is refused, never ignored. */
|
|
193
241
|
const INIT_OPTIONS = ['name', 'lead', 'type', 'packages', 'include-local', 'sap-alias', 'atc-variant', 'no-measure', 'from-discovery', 'no-from-discovery'];
|
|
242
|
+
/** What `init --type build` takes: a team build has no packages, no check and no discovery to read. */
|
|
243
|
+
const INIT_BUILD_OPTIONS = ['name', 'lead', 'type'];
|
|
194
244
|
/** The two that take no value, so neither can eat the folder name. */
|
|
195
245
|
const INIT_FLAGS = ['include-local', 'no-measure', 'from-discovery', 'no-from-discovery'];
|
|
196
246
|
/** Typed on `init` out of muscle memory: they choose what to MEASURE, which is another verb. */
|
|
@@ -220,7 +270,9 @@ function unknownInitOption(name) {
|
|
|
220
270
|
}
|
|
221
271
|
const START_USAGE = 'Usage: cspeach team start <packages> | --for "<area words, e.g. FI and SD>" [--developers "<names>, me"] '
|
|
222
272
|
+ '[--name "<name>"] [--atc-variant <NAME>] [--transport <REQUEST>] [--folder <folder>] [--no-measure] [--sap-alias <name>]';
|
|
223
|
-
const START_OPTIONS = ['name', 'atc-variant', 'folder', 'no-measure', 'sap-alias', 'for', 'developers', 'transport'];
|
|
273
|
+
const START_OPTIONS = ['name', 'atc-variant', 'folder', 'no-measure', 'sap-alias', 'for', 'developers', 'transport', 'type'];
|
|
274
|
+
/** What `start --type build` takes besides the type: a team build has no packages, no check and no transport yet. */
|
|
275
|
+
const START_BUILD_OPTIONS = ['type', 'name', 'folder'];
|
|
224
276
|
/** The check a project measures with when nobody names one. */
|
|
225
277
|
export const DEFAULT_ATC_VARIANT = 'ABAP_CLOUD_READINESS';
|
|
226
278
|
/** I16: `--for` reads the packages from the folder's discovery; without one there is nothing to read them from. */
|
|
@@ -288,6 +340,13 @@ async function start(args, deps) {
|
|
|
288
340
|
if (!START_OPTIONS.includes(name))
|
|
289
341
|
throw new RegisterError(`--${name} is not an option of team start.\n${START_USAGE}`);
|
|
290
342
|
}
|
|
343
|
+
// Team build (Task 7): the folder you are in, its name, you as the lead. Nothing to pull.
|
|
344
|
+
if (opts.type !== undefined) {
|
|
345
|
+
if (opts.type !== 'build' || positional.length || Object.keys(opts).some((o) => !START_BUILD_OPTIONS.includes(o)))
|
|
346
|
+
throw new RegisterError(START_USAGE);
|
|
347
|
+
const dir = path.resolve(opts.folder && opts.folder !== 'true' ? opts.folder : process.cwd());
|
|
348
|
+
return initBuild(dir, opts.name && opts.name !== 'true' ? opts.name : path.basename(dir), deps.identity().name, deps);
|
|
349
|
+
}
|
|
291
350
|
const typed = positional.join(',').split(',').map((p) => p.trim().toUpperCase()).filter(Boolean);
|
|
292
351
|
if (typed.some(isPatternWord))
|
|
293
352
|
throw new RegisterError(NOT_A_PATTERN);
|
|
@@ -432,6 +491,12 @@ async function init(args, deps) {
|
|
|
432
491
|
const type = (opts.type ?? 'clean-core');
|
|
433
492
|
if (!TYPES.includes(type))
|
|
434
493
|
throw new RegisterError(`--type must be one of: ${TYPES.join(', ')}`);
|
|
494
|
+
if (type === 'build') {
|
|
495
|
+
// A team build pulls, measures and discovers nothing: those options are refused, as on `start --type build`.
|
|
496
|
+
if (Object.keys(opts).some((o) => !INIT_BUILD_OPTIONS.includes(o)))
|
|
497
|
+
throw new RegisterError(INIT_USAGE);
|
|
498
|
+
return initBuild(dir, name, lead, deps, given || undefined);
|
|
499
|
+
}
|
|
435
500
|
// One variant per team project (D6), named here or never. A name SAP could
|
|
436
501
|
// not accept is refused before a folder exists, not after it is written in.
|
|
437
502
|
const atcVariant = opts['atc-variant'] && opts['atc-variant'] !== 'true' ? opts['atc-variant'].trim().toUpperCase() : '';
|
|
@@ -700,6 +765,11 @@ function join(args, deps, asName) {
|
|
|
700
765
|
state.projects[project.projectId] = { projectDir, outboxDir, writerId, ...(saved ? { name: saved } : {}) };
|
|
701
766
|
state.active = project.projectId;
|
|
702
767
|
saveState(state);
|
|
768
|
+
// A team build has no objects to own: one line.
|
|
769
|
+
if (isBuildProject(project)) {
|
|
770
|
+
deps.print(buildJoinLine(project, writerId));
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
703
773
|
deps.print(`✓ Joined "${project.name}" (${objects} objects, lead ${project.lead.name}) as ${writerId}.`);
|
|
704
774
|
// Outbox is the team project folder itself: records land where everyone reads them.
|
|
705
775
|
if (path.relative(projectDir, outboxDir) === '') {
|
|
@@ -2494,7 +2564,7 @@ const heldExportLine = (name) => ` ! "${name}" is open in another program, so i
|
|
|
2494
2564
|
/** The CSV export of the team project folder, as the team page reads it, into `dir`. */
|
|
2495
2565
|
export function writeExports(projectDir, project, dir, deps) {
|
|
2496
2566
|
const { register } = registerFor(projectDir, outboxFor(projectDir));
|
|
2497
|
-
return writeExportFiles(register, project, dir, (deps.now?.() ?? new Date()).toISOString());
|
|
2567
|
+
return writeExportFiles(register, coreProject(project), dir, (deps.now?.() ?? new Date()).toISOString());
|
|
2498
2568
|
}
|
|
2499
2569
|
/**
|
|
2500
2570
|
* `team export [--out <folder>] [--always]` (spec §3.4, D2): the CSV files for
|
package/dist/docx/doc-meta.js
CHANGED
|
@@ -11,6 +11,7 @@ const KIND_TITLE = {
|
|
|
11
11
|
'spec-gap': 'Spec gaps', design: 'Design', estimate: 'Estimate', plan: 'Plan',
|
|
12
12
|
'upgrade-scan': 'Upgrade scan', 'upgrade-report': 'Upgrade report',
|
|
13
13
|
answer: 'CSPeach answer', conversation: 'CSPeach conversation',
|
|
14
|
+
'change-options': 'Change options',
|
|
14
15
|
};
|
|
15
16
|
/** Z/Y objects, SAP-style names: ZCL_X, ZFI_DUP_INVOICE_CHECK, Z_REPORT, /NS/ZCL_X. */
|
|
16
17
|
const OBJECT_RE = /(?<![\w/])((?:\/[A-Z0-9]+\/)?[ZY][A-Z0-9_]{2,39})\b/;
|
|
@@ -67,6 +67,8 @@ function titlePage(meta) {
|
|
|
67
67
|
facts.push(`Session: ${meta.session}`);
|
|
68
68
|
if (meta.person)
|
|
69
69
|
facts.push(`Signed in as: ${meta.person}`);
|
|
70
|
+
if (meta.preparedFor)
|
|
71
|
+
facts.push(`Prepared for: ${meta.preparedFor}`);
|
|
70
72
|
return [
|
|
71
73
|
new Paragraph({ text: meta.title, heading: HeadingLevel.TITLE, spacing: { before: 2400 } }),
|
|
72
74
|
...facts.map((f) => new Paragraph({ text: f })),
|
package/dist/journey/chain.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { estateSummaryOf, viewWords } from '@cspeach/register-core';
|
|
17
17
|
import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, isLead, ruleObject, runTeamCommand, whoAmI } from '../commands/team.js';
|
|
18
|
+
import { FUNCTIONAL_VERB_LINE } from '../commands/team-build.js';
|
|
19
|
+
import { isFunctionalSeat } from '../licence/seat-state.js';
|
|
18
20
|
import { activeProject, readBaseline, readEstate, readProjectFile, VARIANT_NAME } from '../register/store.js';
|
|
19
21
|
import { findingsByObject } from '../register/findings.js';
|
|
20
22
|
import { shown, shownUpTo } from '../register/atc-file.js';
|
|
@@ -245,6 +247,12 @@ export async function enterJourney(trigger, h) {
|
|
|
245
247
|
h.emit(trigger.line);
|
|
246
248
|
return;
|
|
247
249
|
}
|
|
250
|
+
// Final review F1: the clean-core and upgrade steps record work and run
|
|
251
|
+
// SAP's checks. A functional seat is told so once, before any card or read.
|
|
252
|
+
if (isFunctionalSeat()) {
|
|
253
|
+
h.emit(FUNCTIONAL_VERB_LINE);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
248
256
|
startJourneyFlow(h.session, trigger.kind === 'start' ? trigger.packages : []);
|
|
249
257
|
// B8: a start sentence is a new run: its DONE never reads as "nothing left" against an earlier one.
|
|
250
258
|
if (trigger.kind === 'start')
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { projectView, provenForTarget as provenForTargetCore } from '@cspeach/register-core';
|
|
29
|
-
import { DISCOVERY_DIR, TEAM_FILE, activeProject, readBaseline, readEstate, readProjectFile } from '../register/store.js';
|
|
29
|
+
import { DISCOVERY_DIR, TEAM_FILE, activeProject, coreProject, readBaseline, readEstate, readProjectFile } from '../register/store.js';
|
|
30
30
|
import { registerFor } from '../register/picture.js';
|
|
31
31
|
import { reportFileNames } from '../register/workbooks.js';
|
|
32
32
|
import { samePath } from '../commands/team-discover.js';
|
|
@@ -250,7 +250,7 @@ export function readJourneyFacts(cwd, me, skip) {
|
|
|
250
250
|
const joined = activeProject();
|
|
251
251
|
// The project file folderRead already read (joined means its folder is this one).
|
|
252
252
|
facts.register = registerFor(joined.projectDir, joined.outboxDir, project).register;
|
|
253
|
-
facts.view = projectView(facts.register, project, skip?.decide ? { skipped: skip.decide } : {});
|
|
253
|
+
facts.view = projectView(facts.register, coreProject(project), skip?.decide ? { skipped: skip.decide } : {});
|
|
254
254
|
facts.reportsAt = reportsAt(cwd, folder.name);
|
|
255
255
|
}
|
|
256
256
|
catch {
|