@cspeach/cli 1.1.31 → 1.1.33
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/loop.js +16 -6
- package/dist/cli.js +13 -4
- package/dist/commands/cost.js +6 -2
- package/dist/commands/help.js +18 -2
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-gate.js +10 -1
- package/dist/commands/plan-resume.js +4 -0
- package/dist/commands/team-rules.js +187 -0
- package/dist/commands/team.js +632 -90
- package/dist/commands/usage.js +106 -0
- package/dist/cost/month-usage.js +45 -0
- package/dist/journey/chain.js +265 -15
- package/dist/journey/clean-core.js +25 -3
- package/dist/journey/flow.js +51 -1
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +213 -20
- package/dist/journey/session-deps.js +5 -1
- package/dist/lane/app-chain.js +12 -2
- package/dist/one-shot.js +24 -17
- package/dist/projects/clean-core-confirm.js +38 -5
- package/dist/projects/save-command.js +20 -3
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +49 -19
- package/dist/register/export-files.js +58 -0
- package/dist/register/measure.js +191 -9
- package/dist/register/plan-import.js +6 -3
- package/dist/register/records.js +15 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -0
- package/dist/register/status-file/register-status-file.js +106 -108
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +1 -0
- package/dist/register/status-html.js +25 -6
- package/dist/register/store.js +4 -3
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +22 -56
- package/dist/register/write-whole.js +30 -2
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/repl/builtin-commands.js +7 -0
- package/dist/repl/journey-entry.js +4 -2
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +2 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +157 -86
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/queued-step.js +45 -0
- package/dist/skills/bundled-skills.js +53 -53
- package/dist/tools/dispatch-skill.js +8 -4
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_findings.js +13 -2
- package/dist/upgrade-check.js +49 -4
- package/package.json +120 -120
- package/.cspeach/project.json +0 -4
package/dist/journey/run.js
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* inside a slash command the journey queues or names.
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { estateSummaryOf, usageLineWithWindow } from '@cspeach/register-core';
|
|
16
|
-
import { runTeamCommand, whoAmI, writeReports } from '../commands/team.js';
|
|
15
|
+
import { estateSummaryOf, provenForTargetWords, usageLineWithWindow } from '@cspeach/register-core';
|
|
16
|
+
import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, ownerMatch, runTeamCommand, severalLine, whoAmI, writeExports, writeReports } from '../commands/team.js';
|
|
17
|
+
import { EXPORTS_DIR, exportsOn, setExportsAlways } from '../register/export-files.js';
|
|
17
18
|
import { shown, shownUpTo } from '../register/atc-file.js';
|
|
18
|
-
import { activeProject, readEstate, readLanes, readProjectFile } from '../register/store.js';
|
|
19
|
-
import {
|
|
19
|
+
import { activeProject, readEstate, readLanes, readProjectFile, VARIANT_NAME } from '../register/store.js';
|
|
20
|
+
import { registerFor } from '../register/picture.js';
|
|
21
|
+
import { claimedSinceCheck, fixFailedCheck, myObjects, provenForTarget, readJourneyFacts, stepOf, toDecide, toFix, toProve, toRedesign, unreadableReason, STEPS, } from './clean-core.js';
|
|
20
22
|
/* ── words ──────────────────────────────────────────────────────────────── */
|
|
21
23
|
export const UNREADABLE_LINES = {
|
|
22
24
|
'object-list': 'The project\'s object list in this folder can\'t be read right now — it may still be syncing. Try again in a minute.',
|
|
@@ -77,10 +79,12 @@ export function notRecordedLine(error) {
|
|
|
77
79
|
export const objectShown = (name) => (OBJECT_NAME.test(name.toUpperCase()) ? name.toUpperCase() : shown(name));
|
|
78
80
|
/** An object marked fix whose name CSPeach never puts in a command: one plain line, and it is passed over. */
|
|
79
81
|
export const passedOverLine = (name) => `${objectShown(name)}: the name is not an ABAP name, so CSPeach passed it over.`;
|
|
82
|
+
/** F16: the check a project measures with, said as a line. */
|
|
83
|
+
export const checkedWithLine = (variant) => ` Checked with: ${variant}.`;
|
|
80
84
|
/* ── commands the journey queues ────────────────────────────────────────── */
|
|
81
|
-
const OBJECT_TYPE = /^[A-Z]{4}$/;
|
|
85
|
+
export const OBJECT_TYPE = /^[A-Z]{4}$/;
|
|
82
86
|
/** An ABAP object name: letters, digits, underscores, an optional /NAMESPACE/ in front. */
|
|
83
|
-
const OBJECT_NAME = /^(\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_]{1,40}$/;
|
|
87
|
+
export const OBJECT_NAME = /^(\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_]{1,40}$/;
|
|
84
88
|
const LANE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
|
|
85
89
|
export const MAX_REASON = 300;
|
|
86
90
|
const chars = (...codes) => String.fromCharCode(...codes);
|
|
@@ -155,6 +159,7 @@ export async function runJourneyStage(stage, ctx) {
|
|
|
155
159
|
case 'INVENTORY': return await inventory(ctx);
|
|
156
160
|
case 'START': return await start(ctx);
|
|
157
161
|
case 'JOIN': return await joinStage(ctx);
|
|
162
|
+
case 'UNOWNED': return unowned(ctx);
|
|
158
163
|
case 'DECIDE': return decideStage(ctx);
|
|
159
164
|
case 'FIX': return fixStage(ctx);
|
|
160
165
|
case 'PROVE': return await prove(ctx);
|
|
@@ -186,11 +191,21 @@ function nextStepOnly(stage, ctx) {
|
|
|
186
191
|
case 'INVENTORY': return stop(`Next: ${step(stage, 'look at what custom code is on the system.')}`);
|
|
187
192
|
case 'START': {
|
|
188
193
|
const named = (ctx.packages ?? []).map((p) => p.trim().toUpperCase()).filter((p) => PACKAGE.test(p));
|
|
189
|
-
|
|
194
|
+
// F16: headless shows no card; the check it would start with is said as a line.
|
|
195
|
+
const objects = inventoryObjects(ctx.cwd, named);
|
|
196
|
+
return result(stage, [
|
|
197
|
+
`Next: ${step(stage, `start your project${named.length ? ` on ${named.join(', ')}` : ''} in this folder.`)}`,
|
|
198
|
+
checkedWithLine(DEFAULT_ATC_VARIANT),
|
|
199
|
+
// I15: above 500 objects the first check is per package, said as a line.
|
|
200
|
+
...(objects !== null && objects > FIRST_CHECK_WHOLE_MAX ? [FIRST_CHECK_PER_PACKAGE_LINE] : []),
|
|
201
|
+
], { kind: 'stop' });
|
|
190
202
|
}
|
|
191
203
|
case 'JOIN': return stop(`Next: ${step(stage, 'pick up the project in this folder.')}`);
|
|
204
|
+
case 'UNOWNED': return unowned(ctx);
|
|
192
205
|
case 'DECIDE':
|
|
193
206
|
case 'FIX': {
|
|
207
|
+
if (needsFirstCheck(facts(ctx)))
|
|
208
|
+
return nothingMeasuredResult(stage);
|
|
194
209
|
const r = stage === 'DECIDE' ? decideStage(ctx) : fixStage(ctx);
|
|
195
210
|
// Names passed over are still said; the step line gives way to the command.
|
|
196
211
|
return r.next.kind === 'queue'
|
|
@@ -198,7 +213,7 @@ function nextStepOnly(stage, ctx) {
|
|
|
198
213
|
: r;
|
|
199
214
|
}
|
|
200
215
|
case 'PROVE': return stop(`Next: ${step(stage, 'let SAP check your fixes.')}`);
|
|
201
|
-
case 'REPORT': return stop(`Next: ${step(stage, 'make the status page.')}`);
|
|
216
|
+
case 'REPORT': return nothingMeasured(facts(ctx)) ? nothingMeasuredResult(stage) : stop(`Next: ${step(stage, 'make the status page.')}`);
|
|
202
217
|
case 'DONE': return done(ctx);
|
|
203
218
|
}
|
|
204
219
|
}
|
|
@@ -234,21 +249,65 @@ async function start(ctx) {
|
|
|
234
249
|
const unknown = known ? packages.filter((p) => !known.includes(p)) : [];
|
|
235
250
|
if (unknown.length)
|
|
236
251
|
return stop(`CSPeach did not find ${unknown.length === 1 ? 'the package' : 'the packages'} ${unknown.join(', ')} on the system.`);
|
|
237
|
-
const
|
|
252
|
+
const asWith = ctx.variant && VARIANT_NAME.test(ctx.variant) ? ['--atc-variant', ctx.variant] : [];
|
|
253
|
+
// I15: above 500 objects the start measures nothing; the chain offers the per-package first check.
|
|
254
|
+
const big = isBigStart(ctx.cwd, packages);
|
|
255
|
+
const { code, said } = await quiet(ctx, ['start', ...packages, '--folder', ctx.cwd, ...asWith, ...(big ? ['--no-measure'] : [])]);
|
|
238
256
|
if (code !== 0)
|
|
239
257
|
return stop(failure(said, 'The project could not be started.'));
|
|
240
258
|
let name = packages.join(', ');
|
|
259
|
+
let variant = asWith[1] ?? DEFAULT_ATC_VARIANT;
|
|
241
260
|
try {
|
|
242
|
-
|
|
261
|
+
const project = readProjectFile(ctx.cwd);
|
|
262
|
+
name = shown(project.name);
|
|
263
|
+
if (project.atcVariant && VARIANT_NAME.test(project.atcVariant))
|
|
264
|
+
variant = project.atcVariant;
|
|
243
265
|
}
|
|
244
|
-
catch { /* the name it was started with */ }
|
|
266
|
+
catch { /* the name and the check it was started with */ }
|
|
245
267
|
const objects = said.map((l) => /^\s*([\d,]+) objects?, matched against/.exec(l)?.[1]).find(Boolean);
|
|
246
268
|
const check = said.map((l) => /First check \([^)]*\): ([\d,]+) of ([\d,]+) objects? with findings/.exec(l)).find(Boolean);
|
|
247
|
-
const lines = [`✓ Your project on ${name} started here
|
|
269
|
+
const lines = [`✓ Your project on ${name} started here.`, checkedWithLine(variant)];
|
|
248
270
|
if (objects)
|
|
249
271
|
lines.push(` ${objects} ${objects === '1' ? 'object' : 'objects'}, matched against the system's count.`);
|
|
250
272
|
lines.push(check ? ` First check: ${check[1]} of ${check[2]} objects with findings.` : ' No check yet.');
|
|
251
|
-
|
|
273
|
+
// Review T7 finding 2: init decides "big" from the live pull, which may differ from the inventory.
|
|
274
|
+
const perPackage = !check && said.includes(FIRST_CHECK_PER_PACKAGE_LINE);
|
|
275
|
+
return { ...result('START', lines, { kind: 'continue' }), ...(perPackage ? { firstCheckPerPackage: true } : {}) };
|
|
276
|
+
}
|
|
277
|
+
/** Code objects the folder's inventory holds for these packages; null when it does not know them all. */
|
|
278
|
+
function inventoryObjects(cwd, packages) {
|
|
279
|
+
const found = readEstate(cwd);
|
|
280
|
+
const s = found ? estateSummaryOf(found.estate) : null;
|
|
281
|
+
if (!s || !packages.length)
|
|
282
|
+
return null;
|
|
283
|
+
const by = new Map(s.packages.map((p) => [p.name.toUpperCase(), p.codeObjects]));
|
|
284
|
+
if (packages.some((p) => !by.has(p.toUpperCase())))
|
|
285
|
+
return null;
|
|
286
|
+
return packages.reduce((n, p) => n + (by.get(p.toUpperCase()) ?? 0), 0);
|
|
287
|
+
}
|
|
288
|
+
/** A start the 30-minute whole-baseline check would cut (I15): more than 500 objects, known from the inventory. */
|
|
289
|
+
export function isBigStart(cwd, packages) {
|
|
290
|
+
const n = inventoryObjects(cwd, packages.map((p) => p.trim().toUpperCase()).filter(Boolean));
|
|
291
|
+
return n !== null && n > FIRST_CHECK_WHOLE_MAX;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The first check, one package at a time (I15): `team refresh --all
|
|
295
|
+
* --per-package`, which resumes where it stopped. Never throws.
|
|
296
|
+
*/
|
|
297
|
+
export async function firstCheckPerPackage(ctx) {
|
|
298
|
+
try {
|
|
299
|
+
const { code, said } = await quiet(ctx, ['refresh', '--all', '--per-package']);
|
|
300
|
+
const done = said.find((l) => /^✓ [\d,]+ packages? measured, one record each\./.test(l));
|
|
301
|
+
if (code === 0 && done)
|
|
302
|
+
return result('START', [` First check: ${done.replace(/^✓ /, '')}`], { kind: 'continue' });
|
|
303
|
+
const stopped = said.find((l) => /^✗ (Stopped|Lost the connection)/.test(l));
|
|
304
|
+
return result('START', [stopped ? ` ${stopped.replace(/^✗ /, '')}` : failure(said, 'The first check could not finish.'), FIRST_CHECK_PER_PACKAGE_LINE], { kind: 'stop' });
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
if (process.env['CSPEACH_DEBUG'] === '1')
|
|
308
|
+
throw e;
|
|
309
|
+
return result('START', ['The first check could not finish.', FIRST_CHECK_PER_PACKAGE_LINE], { kind: 'stop' });
|
|
310
|
+
}
|
|
252
311
|
}
|
|
253
312
|
/* JOIN — a project in this folder this laptop does not record into. */
|
|
254
313
|
async function joinStage(ctx) {
|
|
@@ -273,15 +332,77 @@ async function joinStage(ctx) {
|
|
|
273
332
|
if (res.cancelled || res.value !== 'yes')
|
|
274
333
|
return result('JOIN', ['Nothing changed in this folder.'], { kind: 'stop' });
|
|
275
334
|
}
|
|
335
|
+
// Step 2 (spec §3.6, F13, cut 5): a lane owner who shares my first name is
|
|
336
|
+
// asked about once; two of them are never guessed between.
|
|
337
|
+
let asName;
|
|
338
|
+
if (folder.kind === 'someone-else') {
|
|
339
|
+
const match = ownerByFirstName(ctx.cwd, me(ctx));
|
|
340
|
+
if (match.kind === 'several') {
|
|
341
|
+
return result('JOIN', [severalLine(match)], { kind: 'stop' });
|
|
342
|
+
}
|
|
343
|
+
if (match.kind === 'one') {
|
|
344
|
+
const res = await ctx.team.confirm({
|
|
345
|
+
id: 'journey-join-name',
|
|
346
|
+
tag: 'QUESTION',
|
|
347
|
+
title: `Are you ${match.owner}?`,
|
|
348
|
+
facts: [match.lane],
|
|
349
|
+
choices: [{ value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' }],
|
|
350
|
+
initial: 0,
|
|
351
|
+
aliases: { y: 'yes', n: 'no' },
|
|
352
|
+
});
|
|
353
|
+
if (res.cancelled || res.value === null)
|
|
354
|
+
return result('JOIN', ['Nothing changed in this folder.'], { kind: 'stop' });
|
|
355
|
+
if (res.value === 'yes')
|
|
356
|
+
asName = match.owner;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
276
359
|
// Results go straight into the folder.
|
|
277
|
-
const { code, said } = await quiet(ctx, ['join', ctx.cwd, '--outbox', ctx.cwd]);
|
|
360
|
+
const { code, said } = await quiet(ctx, ['join', ctx.cwd, '--outbox', ctx.cwd, ...(asName ? ['--as', asName] : [])]);
|
|
278
361
|
if (code !== 0)
|
|
279
362
|
return result('JOIN', [failure(said, 'The project in this folder could not be opened.')], { kind: 'stop' });
|
|
280
363
|
const line = folder.kind === 'mine-not-joined'
|
|
281
364
|
? `Picking up your project on ${name}.`
|
|
282
|
-
: `✓ Working in ${lead}'s project ${name}.`;
|
|
365
|
+
: `✓ Working in ${lead}'s project ${name}${asName ? ` as ${asName}` : ''}.`;
|
|
283
366
|
return result('JOIN', [line], { kind: 'continue' });
|
|
284
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The lane owner I may be, decided by team.ts `ownerMatch` (the one shared
|
|
370
|
+
* rule), with that person's lanes' size for the card.
|
|
371
|
+
*/
|
|
372
|
+
function ownerByFirstName(cwd, myName) {
|
|
373
|
+
let lanes = [];
|
|
374
|
+
try {
|
|
375
|
+
lanes = readLanes(cwd);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return { kind: 'none' };
|
|
379
|
+
}
|
|
380
|
+
const m = ownerMatch(lanes, myName);
|
|
381
|
+
if (m.kind === 'several')
|
|
382
|
+
return m;
|
|
383
|
+
if (m.kind !== 'one')
|
|
384
|
+
return { kind: 'none' };
|
|
385
|
+
const ids = new Set(m.laneIds);
|
|
386
|
+
const packages = new Set(lanes.filter((l) => ids.has(l.id)).flatMap((l) => l.packages)).size;
|
|
387
|
+
let objects = null;
|
|
388
|
+
try {
|
|
389
|
+
objects = registerFor(cwd).register.lanes.filter((l) => ids.has(l.id)).reduce((n, l) => n + l.objects, 0);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
objects = null;
|
|
393
|
+
}
|
|
394
|
+
const size = `${plural(packages, 'package', 'packages')}${objects === null ? '' : `, ${plural(objects, 'object', 'objects')}`}`;
|
|
395
|
+
return { kind: 'one', owner: m.owner, lane: `Your lane: ${size}.` };
|
|
396
|
+
}
|
|
397
|
+
/* UNOWNED — joined, and nothing here is mine yet (spec §3.6). Never DONE. */
|
|
398
|
+
function unowned(ctx) {
|
|
399
|
+
let lead = '';
|
|
400
|
+
try {
|
|
401
|
+
lead = readProjectFile(ctx.cwd).lead.name;
|
|
402
|
+
}
|
|
403
|
+
catch { /* the line without a name */ }
|
|
404
|
+
return result('UNOWNED', [`You own nothing here yet. Ask ${lead ? shown(lead) : 'the lead'} to move packages to you.`], { kind: 'stop' });
|
|
405
|
+
}
|
|
285
406
|
/* DECIDE — the triage of my objects, a model step. */
|
|
286
407
|
function decideStage(ctx) {
|
|
287
408
|
const f = facts(ctx);
|
|
@@ -325,6 +446,24 @@ function proveScope(f, waiting) {
|
|
|
325
446
|
objects: r.objects.filter((o) => o.inScope && packages.includes(o.package)).length,
|
|
326
447
|
};
|
|
327
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* What a check that ran says about one object's fix. A fix the check says did
|
|
451
|
+
* not hold, with nothing open for its own target, is proven FOR THAT TARGET
|
|
452
|
+
* (spec §3.2, proof first) — "would only matter for A" only when every finding
|
|
453
|
+
* left is one that only level A cares about.
|
|
454
|
+
*/
|
|
455
|
+
export function proveLine(name, now) {
|
|
456
|
+
if (now.state === 'verified')
|
|
457
|
+
return `${name}: fix proven.`;
|
|
458
|
+
// F12: an object clean at its check never had a fix to prove.
|
|
459
|
+
if (now.state === 'clean')
|
|
460
|
+
return `${name}: no findings.`;
|
|
461
|
+
if (provenForTarget(now))
|
|
462
|
+
return `${name}: ${provenForTargetWords(now)}.`;
|
|
463
|
+
if (fixFailedCheck(now) && !claimedSinceCheck(now))
|
|
464
|
+
return `${name}: fix failed the check.`;
|
|
465
|
+
return `${name}: not checked yet.`;
|
|
466
|
+
}
|
|
328
467
|
/* PROVE — SAP checks the claimed fixes. */
|
|
329
468
|
async function prove(ctx) {
|
|
330
469
|
const f = facts(ctx);
|
|
@@ -357,12 +496,8 @@ async function prove(ctx) {
|
|
|
357
496
|
const name = objectShown(was.name);
|
|
358
497
|
if (!now || !now.lastMeasuredAt || now.lastMeasuredAt === was.lastMeasuredAt)
|
|
359
498
|
lines.push(`${name}: not checked yet.`);
|
|
360
|
-
else if (now.state === 'verified' || now.state === 'clean')
|
|
361
|
-
lines.push(`${name}: fix proven.`);
|
|
362
|
-
else if (fixFailedCheck(now) && !claimedSinceCheck(now))
|
|
363
|
-
lines.push(`${name}: fix failed the check.`);
|
|
364
499
|
else
|
|
365
|
-
lines.push(
|
|
500
|
+
lines.push(proveLine(name, now));
|
|
366
501
|
}
|
|
367
502
|
if (!measured && lines.every((l) => l.endsWith('not checked yet.'))) {
|
|
368
503
|
return result('PROVE', [failure(said, 'SAP could not check them right now.')], { kind: 'stop' });
|
|
@@ -371,6 +506,9 @@ async function prove(ctx) {
|
|
|
371
506
|
}
|
|
372
507
|
/* REPORT — the status page and the workbook, in reports/. */
|
|
373
508
|
async function report(ctx) {
|
|
509
|
+
// A status page with nothing measured on it is no report (review T7 finding 1).
|
|
510
|
+
if (nothingMeasured(facts(ctx)))
|
|
511
|
+
return nothingMeasuredResult('REPORT');
|
|
374
512
|
const joined = activeProject();
|
|
375
513
|
if (!joined)
|
|
376
514
|
return result('REPORT', [UNREADABLE_LINES.other], { kind: 'stop' });
|
|
@@ -406,13 +544,68 @@ async function report(ctx) {
|
|
|
406
544
|
}
|
|
407
545
|
}
|
|
408
546
|
}
|
|
547
|
+
// The report is saved whatever happens to the CSV files: a failure here is one line, never the stage.
|
|
548
|
+
try {
|
|
549
|
+
lines.push(...await csvExport(ctx, joined.projectDir, project));
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
const said = e instanceof Error ? e.message : String(e);
|
|
553
|
+
lines.push(` The CSV files could not be written: ${failureLine([`✗ ${said.split('\n')[0]}`], 'the folder could not be written to').replace(/[\s.…]+$/, '')}.`);
|
|
554
|
+
}
|
|
409
555
|
return result('REPORT', lines, { kind: 'continue' }, { report: true });
|
|
410
556
|
}
|
|
557
|
+
const CSV_SAVED = '✓ Saved the CSV files for your own tools in the exports folder.';
|
|
558
|
+
/**
|
|
559
|
+
* The CSV export at REPORT (spec §3.4, D2): written when the project file's
|
|
560
|
+
* switch is on; otherwise one card. "Every time" turns the switch on (visible
|
|
561
|
+
* in the project file), "Just this once" writes them, "No" or a card put away
|
|
562
|
+
* writes nothing. A headless chain never reaches REPORT (it stops at the
|
|
563
|
+
* next-step line before it), so there is no card and no file there.
|
|
564
|
+
*/
|
|
565
|
+
async function csvExport(ctx, projectDir, project) {
|
|
566
|
+
const write = () => {
|
|
567
|
+
const { saved, held } = writeExports(projectDir, project, path.join(projectDir, 'reports', EXPORTS_DIR), ctx.team.deps);
|
|
568
|
+
return [
|
|
569
|
+
...(saved.length ? [CSV_SAVED] : []),
|
|
570
|
+
...held.map((name) => ` "${name}" is open in another program, so it was not updated. Close it and say carry on.`),
|
|
571
|
+
];
|
|
572
|
+
};
|
|
573
|
+
if (exportsOn(project))
|
|
574
|
+
return write();
|
|
575
|
+
const res = await ctx.team.confirm({
|
|
576
|
+
id: 'journey-export',
|
|
577
|
+
tag: 'QUESTION',
|
|
578
|
+
title: 'Also write CSV files for your own tools?',
|
|
579
|
+
choices: [{ value: 'every', label: 'Every time' }, { value: 'once', label: 'Just this once' }, { value: 'no', label: 'No' }],
|
|
580
|
+
initial: 2,
|
|
581
|
+
aliases: { e: 'every', j: 'once', n: 'no' },
|
|
582
|
+
});
|
|
583
|
+
if (res.cancelled || (res.value !== 'every' && res.value !== 'once'))
|
|
584
|
+
return [];
|
|
585
|
+
if (res.value === 'once')
|
|
586
|
+
return write();
|
|
587
|
+
setExportsAlways(projectDir);
|
|
588
|
+
return [...write(), ' They are written with every report from now on.'];
|
|
589
|
+
}
|
|
411
590
|
/** The closing count of objects kept: "1 kept as is", "2 kept as they are". */
|
|
412
591
|
export const keptPart = (n) => `${n} ${n === 1 ? 'kept as is' : 'kept as they are'}`;
|
|
413
592
|
/* DONE — a short summary. */
|
|
593
|
+
/** Said instead of "Done for now." when the project has no counted check at all. */
|
|
594
|
+
export const NOTHING_MEASURED_LINE = 'Nothing is measured yet.';
|
|
595
|
+
/** The folder's project has no counted check run (I15, review T7 finding 1). */
|
|
596
|
+
export function nothingMeasured(f) {
|
|
597
|
+
return !!f.register && f.register.runs.length === 0;
|
|
598
|
+
}
|
|
599
|
+
/** Nothing measured, and too many objects for one check: the per-package first check comes first (I15). */
|
|
600
|
+
export function needsFirstCheck(f) {
|
|
601
|
+
return nothingMeasured(f) && f.register.objects.length > FIRST_CHECK_WHOLE_MAX;
|
|
602
|
+
}
|
|
603
|
+
const nothingMeasuredResult = (stage) => result(stage, [NOTHING_MEASURED_LINE, FIRST_CHECK_PER_PACKAGE_LINE], { kind: 'stop' });
|
|
414
604
|
function done(ctx) {
|
|
415
605
|
const f = facts(ctx);
|
|
606
|
+
// Never "Done for now." with nothing measured.
|
|
607
|
+
if (nothingMeasured(f))
|
|
608
|
+
return nothingMeasuredResult('DONE');
|
|
416
609
|
const lines = ['Done for now.'];
|
|
417
610
|
if (f.register) {
|
|
418
611
|
const mine = myObjects(f.register, f.me);
|
|
@@ -65,7 +65,11 @@ export async function sessionTeamDeps(o) {
|
|
|
65
65
|
connect: noSap ? undefined : async () => ({
|
|
66
66
|
sql: (q, max) => adt.sqlQuery(q, max),
|
|
67
67
|
...(o.sapAlias && o.sapAlias !== 'none' ? { system: { sid: o.sapAlias } } : {}),
|
|
68
|
-
atc: {
|
|
68
|
+
atc: {
|
|
69
|
+
runSet: (refs, variant, opts) => adt.atcRunSet(refs, variant, opts),
|
|
70
|
+
// The finding's own document names the API it uses, when its worklist entry does not.
|
|
71
|
+
...(adt.atcFindingDocument ? { findingDocument: (href) => adt.atcFindingDocument(href) } : {}),
|
|
72
|
+
},
|
|
69
73
|
}),
|
|
70
74
|
ask: askWith(confirm),
|
|
71
75
|
...(o.signal ? { onInterrupt: interruptOn(o.signal) } : {}),
|
package/dist/lane/app-chain.js
CHANGED
|
@@ -29,10 +29,16 @@ import { chainFromToken } from './chain-token.js';
|
|
|
29
29
|
import { containSpecPath, specDisplayPath, SpecProtectedError } from './roots.js';
|
|
30
30
|
import { resolveAtTokenAsync } from '../projects/workspace.js';
|
|
31
31
|
import { parseFromFlag } from '../skills/promotion-dispatch.js';
|
|
32
|
+
import { rebuildStartedAfterPlanSave } from '../journey/rebuild.js';
|
|
32
33
|
export const STEP_1 = 'Step 1 of 3: finding the gaps in the spec.';
|
|
33
34
|
export const STEP_2 = 'Step 2 of 3: choosing the build lane.';
|
|
34
35
|
export const STEP_3 = 'Step 3 of 3: planning the build.';
|
|
35
36
|
export const START_QUESTION = 'Build this now?';
|
|
37
|
+
/**
|
|
38
|
+
* Audit M-2 — who queued the chain's commands: CSPeach. No line of its own: the
|
|
39
|
+
* step lines (Step 2/3) and the plan board already stand for the step.
|
|
40
|
+
*/
|
|
41
|
+
const APP_ORIGIN = { by: 'app' };
|
|
36
42
|
/** Trailing characters a sentence puts after a token: `see @spec.md.` / `(@spec.md)`. */
|
|
37
43
|
const TRAILING_PUNCT = /[.,;:!?)\]}>]+$/;
|
|
38
44
|
/**
|
|
@@ -300,7 +306,7 @@ export async function afterSpecGapSave(h, savedPath) {
|
|
|
300
306
|
h.emit(`Next: ${cmd}`);
|
|
301
307
|
return;
|
|
302
308
|
}
|
|
303
|
-
h.queueNext(cmd);
|
|
309
|
+
h.queueNext(cmd, APP_ORIGIN);
|
|
304
310
|
h.emit(STEP_2);
|
|
305
311
|
}
|
|
306
312
|
/** The result was written but could not be read as a spec-gap: the flow waits at step 1. */
|
|
@@ -348,6 +354,10 @@ export async function afterPlanSave(h, savedPath) {
|
|
|
348
354
|
|| canonicalLanePath(flow.lane.path) !== canonicalLanePath(lane.path) || flow.lane.sha256 !== lane.sha256)) {
|
|
349
355
|
return;
|
|
350
356
|
}
|
|
357
|
+
// Step 2 (I6): a plan saved for a rebuild brief under <project>/rebuilds/<APP>/
|
|
358
|
+
// moves that rebuild to `started`, with this plan's file name as its link.
|
|
359
|
+
if (flow)
|
|
360
|
+
await rebuildStartedAfterPlanSave({ cwd: h.cwd, specToken: cleanSpecToken(flow.specToken), savedPath, emit: h.emit });
|
|
351
361
|
const end = () => { if (flow)
|
|
352
362
|
clearAppFlow(h.session); };
|
|
353
363
|
const cmd = startCommand(savedPath);
|
|
@@ -397,7 +407,7 @@ export async function afterPlanSave(h, savedPath) {
|
|
|
397
407
|
h.emit(OBJECT_LIST_FAILED);
|
|
398
408
|
}
|
|
399
409
|
}
|
|
400
|
-
h.queueNext(cmd);
|
|
410
|
+
h.queueNext(cmd, APP_ORIGIN);
|
|
401
411
|
}
|
|
402
412
|
else {
|
|
403
413
|
h.emit(`Start it later with: ${cmd}`);
|
package/dist/one-shot.js
CHANGED
|
@@ -77,6 +77,8 @@ import './tools/subagent/schedule_draft_create.js';
|
|
|
77
77
|
import './tools/subagent/agent_run.js';
|
|
78
78
|
import './tools/subagent/read_agent.js';
|
|
79
79
|
import { folderTag } from './session/project-match.js';
|
|
80
|
+
import { detectRunStart } from './runs/front-door.js';
|
|
81
|
+
import { guardRunLabel } from './runs/shadow.js';
|
|
80
82
|
export async function runOneShot(prompt, opts) {
|
|
81
83
|
applyColorMode(); // NO_COLOR (piece 2 §1.2)
|
|
82
84
|
const cfg = await loadConfig();
|
|
@@ -196,24 +198,26 @@ export async function runOneShot(prompt, opts) {
|
|
|
196
198
|
}
|
|
197
199
|
const session = newSession(makeSessionId(), alias, null, resolveModelRole('session_default', cfg), folderTag(process.cwd()));
|
|
198
200
|
await saveSession(session);
|
|
199
|
-
// Guided
|
|
200
|
-
// sentence prints its next step and queues nothing; no
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
201
|
+
// Guided runs — the same front door as both REPLs (audit G-2). One-shot is
|
|
202
|
+
// headless: a start sentence prints its next step and queues nothing; no
|
|
203
|
+
// model call. A bare "continue" / "carry on" keeps its old meaning here
|
|
204
|
+
// (resume: false — final fix wave).
|
|
205
|
+
const door = await detectRunStart(prompt, { cwd: process.cwd(), session, resume: false });
|
|
206
|
+
if (door.kind === 'line') {
|
|
207
|
+
console.error(door.line);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const input = door.input;
|
|
211
|
+
if (door.kind === 'run') {
|
|
212
|
+
const { runJourneyEntry } = await import('./repl/journey-entry.js');
|
|
213
|
+
await runJourneyEntry(door.trigger, {
|
|
214
|
+
session, cwd: process.cwd(), adt, sapAlias: alias, standalone, headless: true,
|
|
215
|
+
emit: (line) => writeStdout(line + '\n'),
|
|
216
|
+
signal: new AbortController().signal,
|
|
217
|
+
});
|
|
218
|
+
return 0;
|
|
215
219
|
}
|
|
216
|
-
const parsed = parseShortcut(
|
|
220
|
+
const parsed = parseShortcut(input);
|
|
217
221
|
const body = parsed.prompt;
|
|
218
222
|
let skill;
|
|
219
223
|
if (parsed.skill) {
|
|
@@ -260,6 +264,9 @@ export async function runOneShot(prompt, opts) {
|
|
|
260
264
|
finally {
|
|
261
265
|
routingBeat.stop();
|
|
262
266
|
}
|
|
267
|
+
// Defensive (spec §4 step 2): a run label never dispatches in this release.
|
|
268
|
+
// No shadow here: one-shot exits when the turn ends (ruling 5).
|
|
269
|
+
c = guardRunLabel(c);
|
|
263
270
|
if (c.offline) {
|
|
264
271
|
// Remote classify failed twice — local-rules fallback fired. Print
|
|
265
272
|
// to stderr so it doesn't pollute stdout for scripted callers, but
|
|
@@ -7,11 +7,42 @@ export function rowWords(r) {
|
|
|
7
7
|
const parts = [WORD[r.disposition]];
|
|
8
8
|
if (r.level)
|
|
9
9
|
parts.push(`level ${r.level}`);
|
|
10
|
+
// Step 2 (F3): a target is said only when the proposal named one; the record keeps the legacy A, which is no target.
|
|
10
11
|
if (r.disposition === 'fix' || r.disposition === 'rebuild')
|
|
11
|
-
parts.push(`target ${r.targetLevel
|
|
12
|
+
parts.push(r.targetLevel ? `target ${r.targetLevel}` : 'no target set');
|
|
12
13
|
return `${parts.join(', ')}${r.reason ? `, because: ${r.reason}` : ''}`;
|
|
13
14
|
}
|
|
14
15
|
const DECIDE_HINT = 'cspeach team decide <TYPE> <NAME> <decision>';
|
|
16
|
+
/** What a recorded reason keeps (records.ts). */
|
|
17
|
+
const MAX_ROW_REASON = 300;
|
|
18
|
+
const RULE_ROW = {
|
|
19
|
+
keep: 'keep', fix: 'fix', rebuild: 'rebuild', 'retire-candidate': 'retire',
|
|
20
|
+
};
|
|
21
|
+
/** A "Rule <id>: " the reason starts with — written by the model, so code checks it. */
|
|
22
|
+
const RULE_PREFIX = /^\s*rule\s+[A-Za-z0-9_-]{1,20}\s*:\s*/i;
|
|
23
|
+
/**
|
|
24
|
+
* The row read against the rule that decides its object. Agrees (same
|
|
25
|
+
* disposition): the reason starts "Rule <id>: " (the rule's note when the row
|
|
26
|
+
* gave none). Differs: the row stays the model's, and `differs` is the fact
|
|
27
|
+
* line "Differs from rule <id>: <why>". No rule: a "Rule <id>:" the model
|
|
28
|
+
* wrote is taken off — the row is CSPeach's judgement. The person still
|
|
29
|
+
* answers the card either way.
|
|
30
|
+
*/
|
|
31
|
+
export function ruleRow(r, p) {
|
|
32
|
+
const own = (r.reason ?? '').replace(RULE_PREFIX, '').trim();
|
|
33
|
+
if (!p)
|
|
34
|
+
return { row: own === r.reason ? r : { ...r, reason: own } };
|
|
35
|
+
const id = shown(p.ruleId);
|
|
36
|
+
const why = shownUpTo(p.why, 200);
|
|
37
|
+
// Agrees = same disposition, and the same target when the rule itself names one (owner ruling, review 9 #7).
|
|
38
|
+
const targetDiffers = p.targetFrom === 'rule' && p.target !== undefined && r.targetLevel !== p.target;
|
|
39
|
+
if (RULE_ROW[p.disposition] !== r.disposition || targetDiffers) {
|
|
40
|
+
return { row: own === r.reason ? r : { ...r, reason: own }, differs: `Differs from rule ${id}: ${why}` };
|
|
41
|
+
}
|
|
42
|
+
// Prefix + reason stay within the 300 the record keeps (records.ts would cut the end); cut by code point.
|
|
43
|
+
const head = `Rule ${id}: `;
|
|
44
|
+
return { row: { ...r, reason: head + Array.from(own || why).slice(0, Math.max(0, MAX_ROW_REASON - head.length)).join('') } };
|
|
45
|
+
}
|
|
15
46
|
export async function confirmCleanCoreRows(ex, a) {
|
|
16
47
|
if (!a.joined) {
|
|
17
48
|
// Review Focus 4: the file is still saved; say why nothing is recorded.
|
|
@@ -23,8 +54,9 @@ export async function confirmCleanCoreRows(ex, a) {
|
|
|
23
54
|
return ex;
|
|
24
55
|
}
|
|
25
56
|
const rows = [];
|
|
26
|
-
for (const
|
|
27
|
-
const
|
|
57
|
+
for (const given of ex.content.adjudications) {
|
|
58
|
+
const { row: r, differs } = a.rules ? ruleRow(given, a.rules(keyOf(given))) : { row: given, differs: undefined };
|
|
59
|
+
const answer = await a.confirm(`Record ${r.objectType} ${r.objectName}: ${rowWords(r)}?${differs ? ` (${differs})` : ''}`, { default: 'n' });
|
|
28
60
|
rows.push({ ...r, confirmed: answer === 'y' });
|
|
29
61
|
}
|
|
30
62
|
const yes = rows.filter((r) => r.confirmed).length;
|
|
@@ -54,7 +86,8 @@ function proposalWords(r) {
|
|
|
54
86
|
}
|
|
55
87
|
export async function decideCleanCoreRows(ex, j) {
|
|
56
88
|
const all = ex.content.adjudications;
|
|
57
|
-
const
|
|
89
|
+
const ruled = all.map((r) => (j.rules ? ruleRow(r, j.rules(keyOf(r))) : { row: r, differs: undefined }));
|
|
90
|
+
const rows = ruled.map(({ row }) => ({ ...row, confirmed: false }));
|
|
58
91
|
const tally = { total: all.length, yes: [], kept: [], skipped: [], cancelled: false };
|
|
59
92
|
const footnote = `Step ${stepOf('DECIDE')} of ${STEPS}`;
|
|
60
93
|
let answered = 0;
|
|
@@ -72,7 +105,7 @@ export async function decideCleanCoreRows(ex, j) {
|
|
|
72
105
|
id: 'journey-row',
|
|
73
106
|
tag: 'QUESTION',
|
|
74
107
|
title: `Record ${type} ${name}: ${proposalWords({ ...r, objectType: type, objectName: name })}?`,
|
|
75
|
-
facts: [...(why ? [`Why: ${why}`] : []), `Row ${i + 1} of ${rows.length}.`],
|
|
108
|
+
facts: [...(why ? [`Why: ${why}`] : []), ...(ruled[i].differs ? [ruled[i].differs] : []), `Row ${i + 1} of ${rows.length}.`],
|
|
76
109
|
choices: [
|
|
77
110
|
{ value: 'yes', label: 'Yes' },
|
|
78
111
|
...(canKeep ? [{ value: 'keep', label: 'Keep as is' }] : []),
|
|
@@ -398,11 +398,28 @@ export async function runSaveCommand(args) {
|
|
|
398
398
|
if (spec.artefactType === 'clean-core-decision') {
|
|
399
399
|
const { confirmCleanCoreRows, decideCleanCoreRows } = await import('./clean-core-confirm.js');
|
|
400
400
|
const { activeProject } = await import('../register/store.js');
|
|
401
|
-
const
|
|
401
|
+
const project = activeProject();
|
|
402
|
+
const joined = project !== null;
|
|
403
|
+
// Step 2 (D6): the rule that decides each row, said on its card. Never blocks a save: unreadable → no rule line.
|
|
404
|
+
let rules;
|
|
405
|
+
if (project) {
|
|
406
|
+
try {
|
|
407
|
+
const { ruleProposalsFor } = await import('../register/decision-rules.js');
|
|
408
|
+
const r = ruleProposalsFor(project.projectDir, project.outboxDir);
|
|
409
|
+
rules = r.lookup;
|
|
410
|
+
// Inside a journey no file name is said (Task 7); the triage tool carries the warning to the model.
|
|
411
|
+
if (r.warning && !journey)
|
|
412
|
+
args.log(r.warning);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// No rule can be read: every row is CSPeach's judgement, so a "Rule <id>:" the model wrote is taken off (review 9 #2).
|
|
416
|
+
rules = () => null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
402
419
|
if (journey && joined) {
|
|
403
420
|
// Guided journey: [Yes] [Keep as is] [Skip] per row.
|
|
404
421
|
const r = await decideCleanCoreRows(extract, {
|
|
405
|
-
card: journey.card, ask: journey.ask, skip: journey.skip, log: args.log,
|
|
422
|
+
card: journey.card, ask: journey.ask, skip: journey.skip, log: args.log, ...(rules ? { rules } : {}),
|
|
406
423
|
});
|
|
407
424
|
extract = r.extract;
|
|
408
425
|
tally = r.tally;
|
|
@@ -415,7 +432,7 @@ export async function runSaveCommand(args) {
|
|
|
415
432
|
}
|
|
416
433
|
else {
|
|
417
434
|
extract = await confirmCleanCoreRows(extract, {
|
|
418
|
-
...(args.confirm ? { confirm: args.confirm } : {}), log: args.log, joined,
|
|
435
|
+
...(args.confirm ? { confirm: args.confirm } : {}), log: args.log, joined, ...(rules ? { rules } : {}),
|
|
419
436
|
});
|
|
420
437
|
}
|
|
421
438
|
}
|
|
@@ -50,6 +50,43 @@ export async function listCustomPackages(sql, prefixes = ['Z', 'Y']) {
|
|
|
50
50
|
}
|
|
51
51
|
return [...out.values()].sort((a, b) => a.package.localeCompare(b.package));
|
|
52
52
|
}
|
|
53
|
+
/** The data preview never returns more than this many rows in one answer. */
|
|
54
|
+
export const SQL_ROW_CAP = 5000;
|
|
55
|
+
/** A quote in a paging key is doubled, so it cannot end the literal. */
|
|
56
|
+
const lit = (v) => v.replace(/\x27/g, '\x27\x27');
|
|
57
|
+
/**
|
|
58
|
+
* One package (or one $TMP prefix). Below the cap: today's single query for
|
|
59
|
+
* `expected + 50` rows. Above it (`expected + 50 > 5000`, review I14): keyset
|
|
60
|
+
* pages of 5,000 by OBJ_NAME until a page is short. The key is `>=` with the
|
|
61
|
+
* rows already seen dropped, so a name held by two object types that a page
|
|
62
|
+
* boundary splits is never lost. The control total of the caller still decides.
|
|
63
|
+
*/
|
|
64
|
+
async function pullOne(sql, pkg, select, expected) {
|
|
65
|
+
if (expected + 50 <= SQL_ROW_CAP)
|
|
66
|
+
return (await sql(select, expected + 50)).rows;
|
|
67
|
+
const rows = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
let last;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const page = (await sql(`${select}${last === undefined ? '' : ` AND OBJ_NAME >= '${lit(last)}'`} ORDER BY OBJ_NAME, OBJECT`, SQL_ROW_CAP)).rows;
|
|
72
|
+
let fresh = 0;
|
|
73
|
+
for (const r of page) {
|
|
74
|
+
const k = `${clean(r.OBJECT)}|${clean(r.OBJ_NAME)}`;
|
|
75
|
+
if (seen.has(k))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(k);
|
|
78
|
+
rows.push(r);
|
|
79
|
+
fresh += 1;
|
|
80
|
+
}
|
|
81
|
+
if (page.length < SQL_ROW_CAP)
|
|
82
|
+
return rows;
|
|
83
|
+
const next = clean(page[page.length - 1].OBJ_NAME);
|
|
84
|
+
if (fresh === 0 || next === last) {
|
|
85
|
+
throw new BaselineError(`Package ${pkg}: the pull could not page past ${next}. The baseline was NOT written.`);
|
|
86
|
+
}
|
|
87
|
+
last = next;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
53
90
|
export async function pullBaseline(sql, opts) {
|
|
54
91
|
const types = new Set((opts.objectTypes ?? CODE_OBJECT_TYPES).map((t) => t.toUpperCase()));
|
|
55
92
|
const objects = [];
|
|
@@ -65,11 +102,11 @@ export async function pullBaseline(sql, opts) {
|
|
|
65
102
|
for (const [prefix, n] of Object.entries(byPrefix)) {
|
|
66
103
|
if (!PREFIX_RE.test(prefix))
|
|
67
104
|
throw new BaselineError(`Not a valid name prefix: ${prefix}`);
|
|
68
|
-
res.rows.push(...(await sql
|
|
105
|
+
res.rows.push(...(await pullOne(sql, pkg, `${select} AND OBJ_NAME LIKE '${prefix}%'`, n)));
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
else {
|
|
72
|
-
res.rows.push(...(await sql
|
|
109
|
+
res.rows.push(...(await pullOne(sql, pkg, select, expected)));
|
|
73
110
|
}
|
|
74
111
|
if (res.rows.length !== expected) {
|
|
75
112
|
throw new BaselineError(`Package ${pkg}: the system counted ${expected} objects but the pull returned ${res.rows.length}. `
|