@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
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
* style attribute.
|
|
22
22
|
*/
|
|
23
23
|
import { esc, blockSvg, legendHtml, levelLegendHtml, stateWord, LEVEL_BUCKETS as LEVELS, LEVEL_WORD as LEVEL_NAME } from './register-map.js';
|
|
24
|
-
import { formatWhen,
|
|
25
|
-
import {
|
|
24
|
+
import { formatWhen, measurementHistory, notMeasuredCount, unknownStateCount, viewOf, FROM_FILE, NOTHING_TO_DECIDE, } from './register-model.js';
|
|
25
|
+
import { DECISION_WORDS, decisionWhatOf, developerTable, overlapNote, } from '@cspeach/register-core';
|
|
26
26
|
// The map owns it, because the map's cells need it too; it is offered here as
|
|
27
27
|
// well, where every other shared mark lives.
|
|
28
28
|
export { stateWord };
|
|
29
|
+
/** The classifications the view has words for; any other is printed as the folder wrote it. */
|
|
30
|
+
const VIEW_CLASSIFICATIONS = new Set(['keep', 'fix', 'redesign', 'retire']);
|
|
31
|
+
/** The chips, in order: the view's decision, and the class that colours it. */
|
|
32
|
+
const CHIPS = [['keep', 'keep'], ['exception', 'exception'], ['fix', 'fix'], ['rebuild', 'redesign'], ['retire-candidate', 'retire'], ['retired', 'retired']];
|
|
29
33
|
/** Digits grouped the way a reader expects: 1,500, not 1500. */
|
|
30
34
|
export const n = (value) => Number(value ?? 0).toLocaleString('en-US');
|
|
31
35
|
/**
|
|
@@ -100,43 +104,37 @@ export const ownerLabel = (lane) => (lane ? `${lane.owner} (${lane.id})` : 'nobo
|
|
|
100
104
|
* never against all of them: a retired object is not an unproven one. Without
|
|
101
105
|
* that column "Proven 2, Objects 4" reads as half done when it is two of three,
|
|
102
106
|
* and the lead and the manager argue about a number neither of them chose. The
|
|
103
|
-
*
|
|
107
|
+
* counts are the project view's lane parts: an object counts once, in the lane
|
|
108
|
+
* that owns it, and Proven is the lane's done (the proven and clean parts over
|
|
109
|
+
* its active objects) — the same rule as every Done % (Task 3 fix round 1).
|
|
110
|
+
* "(n from a file)" counts the same objects, measured from a file.
|
|
104
111
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* optional: a caller that has none gets exactly the table it got before.
|
|
108
|
-
*
|
|
109
|
-
* @param {object[]} lanes `register.lanes`
|
|
110
|
-
* @param {object[]} [objects] `register.objects`
|
|
112
|
+
* @param {object[]} lanes `register.lanes` (their order and owners)
|
|
113
|
+
* @param {object} view the project view of the register (required)
|
|
111
114
|
* @returns {string}
|
|
112
115
|
*/
|
|
113
|
-
export function lanesTableHtml(lanes,
|
|
116
|
+
export function lanesTableHtml(lanes, view) {
|
|
114
117
|
if (!lanes?.length)
|
|
115
118
|
return '<p>The lead has not set up lanes yet.</p>';
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (o.measuredBasis !== 'file' || (o.state !== 'verified' && o.state !== 'clean'))
|
|
121
|
-
continue;
|
|
122
|
-
if (o.inScope === false)
|
|
123
|
-
continue; // the same objects the lane's Proven counts
|
|
124
|
-
// Every lane that claims the object, exactly as the lane's own counts are
|
|
125
|
-
// taken: an object two lanes both claim is proven for both of them, so
|
|
126
|
-
// "(n from a file)" can never be larger than the Proven it sits beside.
|
|
127
|
-
for (const id of o.laneOverlap?.length ? o.laneOverlap : [o.lane])
|
|
128
|
-
fromFile.set(id, (fromFile.get(id) ?? 0) + 1);
|
|
129
|
-
}
|
|
119
|
+
if (!view?.lanes)
|
|
120
|
+
throw new Error('lanesTableHtml needs the view of the team project.');
|
|
121
|
+
const scope = new Map(view.lanes.map((s) => [s.scope.id, s]));
|
|
122
|
+
const fileProven = (s) => s.objects.filter((v) => (v.part === 'proven' || v.part === 'clean') && v.proof.basis === 'file').length;
|
|
130
123
|
const heads = ['Owner', 'Objects', 'Active', 'Proven', 'Open problems', 'Last record']
|
|
131
124
|
.map((h) => `<th scope="col">${h}</th>`).join('');
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
125
|
+
const counts = (l) => {
|
|
126
|
+
const s = scope.get(l.id);
|
|
127
|
+
if (!s)
|
|
128
|
+
return { objects: 0, active: 0, proven: 0, file: 0, open: 0 };
|
|
129
|
+
return { objects: s.total, active: s.active, proven: s.done.proven, file: fileProven(s), open: s.parts['failed-check'] };
|
|
130
|
+
};
|
|
131
|
+
const row = (l) => {
|
|
132
|
+
const c = counts(l);
|
|
133
|
+
return `<tr><td>${esc(ownerLabel(l))}</td><td>${n(c.objects)}</td>`
|
|
134
|
+
+ `<td>${n(c.active)}</td><td>${n(c.proven)}${c.file ? ` (${n(c.file)} ${FROM_FILE})` : ''}</td>`
|
|
135
|
+
+ `<td>${n(c.open)}</td>`
|
|
136
|
+
+ `<td>${esc(formatWhen(l.lastRecordAt))}</td></tr>`;
|
|
135
137
|
};
|
|
136
|
-
const row = (l) => `<tr><td>${esc(ownerLabel(l))}</td><td>${n(l.objects)}</td>`
|
|
137
|
-
+ `<td>${n(laneActiveOf(l))}</td><td>${provenCell(l)}</td>`
|
|
138
|
-
+ `<td>${n((l.byState?.regressed ?? 0) + (l.byState?.['did-not-hold'] ?? 0))}</td>`
|
|
139
|
-
+ `<td>${esc(formatWhen(l.lastRecordAt))}</td></tr>`;
|
|
140
138
|
return `<div class="scroller"><table><thead><tr>${heads}</tr></thead>`
|
|
141
139
|
+ `<tbody>${lanes.map(row).join('')}</tbody></table></div>`;
|
|
142
140
|
}
|
|
@@ -275,12 +273,14 @@ export function measuredWord(o, runs) {
|
|
|
275
273
|
return 'not measured';
|
|
276
274
|
return `${n(o.openFindings)} open${o.measuredBasis === 'file' ? ', from a file' : ''}`;
|
|
277
275
|
}
|
|
278
|
-
/** The line under the summary. Empty when every active object has been measured. */
|
|
279
|
-
export function notMeasuredLine(register) {
|
|
280
|
-
|
|
276
|
+
/** The line under the summary, from the project view. Empty when every active object has been measured. */
|
|
277
|
+
export function notMeasuredLine(register, view) {
|
|
278
|
+
view = viewOf(register, view);
|
|
279
|
+
const count = notMeasuredCount(register, view);
|
|
281
280
|
if (!count)
|
|
282
281
|
return '';
|
|
283
|
-
|
|
282
|
+
const active = view.all.active;
|
|
283
|
+
return `${n(count)} of ${n(active)} active object${active === 1 ? '' : 's'} `
|
|
284
284
|
+ `${count === 1 ? 'has' : 'have'} never been measured. ${count === 1 ? 'Its' : 'Their'} colour rests on what people said.`;
|
|
285
285
|
}
|
|
286
286
|
/** The evidence panel's measurement block: the two numbers, then every run that covered this object. */
|
|
@@ -407,36 +407,42 @@ export function decisionWord(decision, projectType, state) {
|
|
|
407
407
|
// Recording fix 4: a clean (or verified) object nobody decided has nothing to decide.
|
|
408
408
|
if (!decision)
|
|
409
409
|
return state === 'clean' || state === 'verified' ? 'clean — nothing to decide' : 'not decided';
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return DISPOSITION_WORD[d];
|
|
410
|
+
// Task 9 fix round 2: the view's words. A writer's retire is a candidate; only the lead's ruling is retired.
|
|
411
|
+
if (projectType === 'clean-core' && VIEW_CLASSIFICATIONS.has(decision.classification)) {
|
|
412
|
+
return DECISION_WORDS[decisionWhatOf({ classification: decision.classification, exception: decision.exception, ruling: decision.from === 'ruling' || decision.ruling === true })];
|
|
414
413
|
}
|
|
415
414
|
return CLASSIFICATION_WORD[decision.classification] ?? short(decision.classification);
|
|
416
415
|
}
|
|
417
416
|
const tile = (label, value, note, cls = '') => `<li class="tile${cls ? ` ${cls}` : ''}"><span class="tile-n">${n(value)}</span> <span class="tile-label">${esc(label)}</span> <span class="tile-note">${esc(note)}</span></li>`;
|
|
418
417
|
/**
|
|
419
418
|
* Five tiles, a number and two lines each, then the two level bars. Every
|
|
420
|
-
* number
|
|
421
|
-
* the workbook print — so nothing here is counted twice.
|
|
419
|
+
* number is the project view's — the same parts the status page, the terminal
|
|
420
|
+
* and the workbook print — so nothing here is counted twice. Proven is the
|
|
421
|
+
* view's proven part plus clean, over active objects; "Fix failed" is the
|
|
422
|
+
* fix-failed part, labelled as the lane table labels it (a fix proven for its own target is not one); retire
|
|
423
|
+
* candidates are the view's count, from the decision (also one that waits for a conflict first).
|
|
422
424
|
*/
|
|
423
|
-
export function whereItStandsHtml(register) {
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const
|
|
425
|
+
export function whereItStandsHtml(register, view) {
|
|
426
|
+
view = viewOf(register, view);
|
|
427
|
+
const all = view.all;
|
|
428
|
+
const p = all.parts;
|
|
429
|
+
const proven = all.objects.filter((v) => v.part === 'proven' || v.part === 'clean');
|
|
430
|
+
const fromFile = proven.filter((v) => v.proof.basis === 'file').length;
|
|
431
|
+
const decided = all.objects.filter((v) => v.decision.what !== 'none').length;
|
|
432
|
+
const didNotHold = p['failed-check'];
|
|
433
|
+
const candidates = all.retireCandidates;
|
|
428
434
|
return '<ul class="tiles" data-role="tiles">'
|
|
429
|
-
+ tile('Decided',
|
|
430
|
-
+ tile('Claimed',
|
|
431
|
-
+ tile('Proven',
|
|
432
|
-
+ tile('
|
|
433
|
-
+ tile('Retired',
|
|
435
|
+
+ tile('Decided', decided, `of ${n(all.total)} objects`)
|
|
436
|
+
+ tile('Claimed', p['awaiting-check'], 'a developer says fixed')
|
|
437
|
+
+ tile('Proven', proven.length, fromFile ? `${n(fromFile)} of them ${FROM_FILE}` : 'a check run says so')
|
|
438
|
+
+ tile('Fix failed', didNotHold, 'the check still finds it', didNotHold ? 'bad' : '')
|
|
439
|
+
+ tile('Retired', p.retired, 'nothing left to do')
|
|
434
440
|
// Only when there are some: a developer said retire and the lead has not
|
|
435
441
|
// confirmed it yet, so these objects still count as active work.
|
|
436
|
-
+ (
|
|
442
|
+
+ (candidates > 0 ? tile('Retire candidates', candidates, 'wait for the lead to confirm', 'retire-candidates') : '')
|
|
437
443
|
+ '</ul>'
|
|
438
|
-
+ (!(
|
|
439
|
-
+ levelBarsHtml(register);
|
|
444
|
+
+ (!(decided > 0) ? '<p class="empty-note" data-role="nothing-decided">Nothing decided yet. Developers record decisions with /abap-clean-core or cspeach team decide.</p>' : '')
|
|
445
|
+
+ levelBarsHtml(register, view);
|
|
440
446
|
}
|
|
441
447
|
/**
|
|
442
448
|
* One bar: the five buckets side by side, then the same thing in words — colour
|
|
@@ -464,22 +470,34 @@ function levelBar(title, counts, nothingToDecide = 0) {
|
|
|
464
470
|
+ `<svg viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="${esc(`${title}: ${words}`)}">${rects}</svg>`
|
|
465
471
|
+ `<p class="levelwords">${esc(words)}</p></figure>`;
|
|
466
472
|
}
|
|
467
|
-
/**
|
|
468
|
-
export
|
|
469
|
-
|
|
473
|
+
/** What "level now" means, under the level bars. One place, so the words change in one place. */
|
|
474
|
+
export const LEVEL_LEGEND_LINE = 'Level now is what the latest check shows; a judged D stays D.';
|
|
475
|
+
/**
|
|
476
|
+
* Levels at decision and now, in words as well as colour, counted by the view
|
|
477
|
+
* (pass the caller's view, so a page builds one). A folder with no levels says
|
|
478
|
+
* so; a register from a build older than level counts reads as none, as before.
|
|
479
|
+
*/
|
|
480
|
+
export function levelBarsHtml(register, view) {
|
|
481
|
+
const b = register.totals?.byLevel ? viewOf(register, view).all.levels : undefined;
|
|
470
482
|
const none = !b || ['A', 'B', 'C', 'D'].every((l) => !(b.decided?.[l]) && !(b.now?.[l]));
|
|
471
483
|
return `<div class="levels" data-role="levels">${levelBar('Levels at decision', b?.decided, b?.nothingToDecide ?? 0)}${levelBar('Levels now', b?.now, b?.nothingToDecide ?? 0)}`
|
|
472
|
-
+ `<p class="legend-line">${none ? 'No levels recorded yet. ' : ''}
|
|
484
|
+
+ `<p class="legend-line">${none ? 'No levels recorded yet. ' : ''}${esc(LEVEL_LEGEND_LINE)}</p></div>`;
|
|
473
485
|
}
|
|
474
486
|
/**
|
|
475
487
|
* One line of chips; the accepted exceptions open a list of object and reason.
|
|
476
488
|
* An exception is work nobody has to do, so the reason is the whole of it — and
|
|
477
489
|
* where there is none, the list says so rather than leaving a blank.
|
|
478
490
|
*/
|
|
479
|
-
export function dispositionsHtml(register) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
491
|
+
export function dispositionsHtml(register, view) {
|
|
492
|
+
// Task 9 fix round 2: counted from the project view, so the chips agree with the tiles. Each active
|
|
493
|
+
// object's decision in the view's words; retired is the view's retired part (an object the lead ruled).
|
|
494
|
+
const pv = viewOf(register, view);
|
|
495
|
+
const active = pv.all.objects.filter((v) => v.inScope && v.part !== 'retired');
|
|
496
|
+
const count = (what) => active.filter((v) => v.decision.what === what).length;
|
|
497
|
+
const chips = CHIPS.map(([what, cls]) => `<li class="chip d-${cls}">${esc(DECISION_WORDS[what])} <b>${n(what === 'retired' ? pv.all.parts.retired : count(what))}</b></li>`).join('');
|
|
498
|
+
// Task 9 fix round 3: the list is the chip's set — active objects only.
|
|
499
|
+
const exceptionKeys = new Set(active.filter((v) => v.decision.what === 'exception').map((v) => v.key));
|
|
500
|
+
const ex = register.objects.filter((o) => exceptionKeys.has(o.key));
|
|
483
501
|
const list = !ex.length ? '' : `<details class="exceptions" data-role="exceptions"><summary>${ex.length === 1 ? 'The accepted exception and why' : `The ${n(ex.length)} accepted exceptions and why`}</summary><ul>`
|
|
484
502
|
+ ex.map((o) => `<li><span class="ident">${cell(o.name)}</span> — ${o.decision.reason ? cell(o.decision.reason) : '<span class="quiet-note">no reason recorded</span>'}</li>`).join('')
|
|
485
503
|
+ '</ul></details>';
|
|
@@ -489,36 +507,33 @@ export function dispositionsHtml(register) {
|
|
|
489
507
|
* Who owns what, per developer — register-core's sheet model for the numbers
|
|
490
508
|
* (the workbook prints the same row), the page's own words for the date.
|
|
491
509
|
*/
|
|
492
|
-
export function developersTableHtml(register) {
|
|
510
|
+
export function developersTableHtml(register, view) {
|
|
493
511
|
if (!register.lanes?.length)
|
|
494
512
|
return '<p>The lead has not set up lanes yet.</p>';
|
|
495
|
-
|
|
513
|
+
view = viewOf(register, view);
|
|
514
|
+
const t = developerTable(register, view);
|
|
496
515
|
// How much of a lane's Proven came out of a file, said beside the number —
|
|
497
|
-
// as the lane table always has.
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
516
|
+
// as the lane table always has. Counted over the lane's own objects in the
|
|
517
|
+
// view (one owner per object), the same proven and clean parts
|
|
518
|
+
// register-core's developerTable counts as Proven, so "(n from a file)" is
|
|
519
|
+
// never larger than the Proven beside it.
|
|
520
|
+
const fromFile = new Map(view.lanes.map((s) => [s.scope.id,
|
|
521
|
+
s.objects.filter((v) => (v.part === 'proven' || v.part === 'clean') && v.proof.basis === 'file').length]));
|
|
522
|
+
const nextOf = new Map(view.lanes.map((s) => [s.scope.id, s.next]));
|
|
523
|
+
// Done %: the view's lane done — the same owned objects and parts as every other cell of the row.
|
|
524
|
+
const doneOf = new Map(view.lanes.map((s) => [s.scope.id, s.done.pct]));
|
|
506
525
|
const provenAt = t.header.indexOf('Proven');
|
|
507
526
|
const num = (v, col, lane) => {
|
|
508
527
|
const said = col === provenAt ? fromFile.get(lane.id) ?? 0 : 0;
|
|
509
528
|
return `<td>${n(v)}${said ? ` (${n(said)} ${FROM_FILE})` : ''}</td>`;
|
|
510
529
|
};
|
|
511
530
|
// Spec 2026-10-06 §9 item 7: how far each lane is, what it does next (the
|
|
512
|
-
// first three
|
|
513
|
-
// the focus — a plain count, recorded and never blocked.
|
|
514
|
-
const nameOf = new Map(register.objects.map((o) => [o.key, o.name]));
|
|
531
|
+
// first three of its OWN open work, the view's next) and how much recorded
|
|
532
|
+
// work sits outside the focus — a plain count, recorded and never blocked.
|
|
515
533
|
const plan = (lane) => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const next = keys.map((k) => nameOf.get(k)).filter((name) => name);
|
|
520
|
-
return `<td>${n(lane.donePct ?? 0)}%</td>`
|
|
521
|
-
+ `<td class="next">${keys.length ? next.map((name) => `<span class="ident">${cell(name)}</span>`).join(', ') : '<span class="quiet-note">nothing open</span>'}</td>`
|
|
534
|
+
const next = (nextOf.get(lane.id) ?? []).slice(0, 3).map((v) => v.name);
|
|
535
|
+
return `<td>${n(doneOf.get(lane.id) ?? 0)}%</td>`
|
|
536
|
+
+ `<td class="next">${next.length ? next.map((name) => `<span class="ident">${cell(name)}</span>`).join(', ') : '<span class="quiet-note">nothing open</span>'}</td>`
|
|
522
537
|
+ `<td>${n(lane.outsideFocus ?? 0)}</td>`;
|
|
523
538
|
};
|
|
524
539
|
const head = [...t.header, 'Done %', 'Next', 'Outside the focus'].map((h) => `<th scope="col">${esc(h)}</th>`).join('');
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
* listeners and reads the keyboard. Two pictures of one folder that disagree
|
|
33
33
|
* would be worse than no picture at all.
|
|
34
34
|
*/
|
|
35
|
+
import { projectView as projectViewOf } from '@cspeach/register-core';
|
|
35
36
|
import { esc } from './register-map.js';
|
|
36
37
|
import { formatWhen } from './register-model.js';
|
|
37
38
|
import { n, provenanceLine, readNote } from './register-parts.js';
|
|
38
|
-
import { FIX_RULE, LEVEL_KEY, NO_TARGET_LINE, READING_LINE, awaitingWord, dayWords, lastCheckWord, reportView, trendLine, } from './report-model.js';
|
|
39
|
+
import { FIX_RULE, LANE_COLUMNS, LEVEL_KEY, NO_TARGET_LINE, READING_LINE, awaitingWord, dayWords, lastCheckWord, reportView, trendLine, } from './report-model.js';
|
|
39
40
|
const FILE_CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src data:";
|
|
40
41
|
const files = (count) => `${n(count)} file${count === 1 ? '' : 's'}`;
|
|
41
42
|
/**
|
|
@@ -85,16 +86,18 @@ function changedHtml(view) {
|
|
|
85
86
|
+ (c.moreObjects ? `<p class="rp-tail">The rest of the objects a check proved or failed are on the Objects sheet.</p>` : '')
|
|
86
87
|
+ '</section>';
|
|
87
88
|
}
|
|
88
|
-
const LANE_HEAD =
|
|
89
|
+
const LANE_HEAD = LANE_COLUMNS.slice(1);
|
|
89
90
|
function lanesHtml(view) {
|
|
90
91
|
const rows = view.lanes.slice(0, LANE_CAP).map((r) => `<tr><th scope="row">${esc(r.developer)}</th>`
|
|
91
|
-
+ `<td data-l="Lane packages" class="rp-pk">${esc(r.packagesWord)}</td><td data-l="Objects">${n(r.objects)}</td
|
|
92
|
+
+ `<td data-l="Lane packages" class="rp-pk">${esc(r.packagesWord)}</td><td data-l="Objects">${n(r.objects)}</td>`
|
|
93
|
+
+ `<td data-l="Active">${n(r.active)}</td><td data-l="Retired">${n(r.retired)}</td><td data-l="Decided">${n(r.decided)}</td>`
|
|
92
94
|
+ `<td data-l="Fixes proven">${n(r.fixesProven)}</td><td data-l="Awaiting check">${esc(awaitingWord(r))}</td><td data-l="Kept">${n(r.kept)}</td>`
|
|
93
95
|
+ `<td data-l="Rebuilds">${n(r.rebuilds)}</td><td data-l="Fix failed">${n(r.fixFailed)}</td><td data-l="Last check">${esc(lastCheckWord(r))}</td></tr>`).join('');
|
|
94
96
|
const more = view.lanes.length > LANE_CAP ? `<p class="rp-tail">And ${n(view.lanes.length - LANE_CAP)} more on the workbook's Summary sheet.</p>` : '';
|
|
95
97
|
return '<section class="rp-detail" data-role="lanes"><h2>By lane / developer</h2>'
|
|
96
98
|
+ `<div class="scroller"><table class="rp-devs" aria-label="By lane / developer"><thead><tr><th scope="col">Developer</th>${LANE_HEAD.map((h) => `<th scope="col">${h}</th>`).join('')}</tr></thead>`
|
|
97
99
|
+ `<tbody>${rows}</tbody></table></div>${more}`
|
|
100
|
+
+ (view.lanesNote ? `<p class="rp-tail" data-role="lanes-note">${esc(view.lanesNote)}</p>` : '')
|
|
98
101
|
+ "<p class=\"rp-tail\">Lanes are listed in the order they were set up, never ranked. On the workbook's Objects sheet, filter Lane to see one developer's list.</p></section>";
|
|
99
102
|
}
|
|
100
103
|
function keptHtml(view) {
|
|
@@ -102,7 +105,10 @@ function keptHtml(view) {
|
|
|
102
105
|
const body = k.length
|
|
103
106
|
? `<ul class="rp-kept-list">${k.slice(0, KEPT_CAP).map((x) => `<li><p>${esc(x.line)}</p>${x.why ? `<p class="rp-why">${esc(x.why)}</p>` : ''}<p class="rp-why">${esc(x.by)}</p></li>`).join('')}</ul>`
|
|
104
107
|
+ (k.length > KEPT_CAP ? `<p class="rp-tail">And ${n(k.length - KEPT_CAP)} more on the workbook's For the business sheet.</p>` : '')
|
|
105
|
-
:
|
|
108
|
+
: view.keptElsewhere
|
|
109
|
+
// True even when every keep decision sits in another part: the check counts those by what it shows.
|
|
110
|
+
? `<p>No object is kept as is now. ${n(view.keptElsewhere)} ${view.keptElsewhere === 1 ? 'object decided keep is' : 'objects decided keep are'} counted by what the check shows.</p>`
|
|
111
|
+
: '<p>No object is kept by decision.</p>';
|
|
106
112
|
return `<section class="rp-kept" data-role="kept"><h2>Kept as is, by decision (${n(k.length)})</h2>${body}</section>`;
|
|
107
113
|
}
|
|
108
114
|
function rebuildsHtml(view) {
|
|
@@ -133,12 +139,15 @@ function rebuildsHtml(view) {
|
|
|
133
139
|
* `css` is the only value written in unescaped, and the only one that never came
|
|
134
140
|
* out of the folder. Every name, package, decision and person goes through `esc()`.
|
|
135
141
|
*
|
|
142
|
+
* `projectView` is the project view of this register and project, when the
|
|
143
|
+
* caller already built it (`collectView` does); else it is built here, once.
|
|
144
|
+
*
|
|
136
145
|
* @param {{ project: object, register: object, css: string, asOf: string, folderName: string,
|
|
137
146
|
* files?: number, checksum?: string, unreadable?: number, needs?: object[], previousAsOf?: string|null,
|
|
138
|
-
* estate?: unknown }} view
|
|
147
|
+
* estate?: unknown, projectView?: object }} view
|
|
139
148
|
* @returns {string}
|
|
140
149
|
*/
|
|
141
|
-
export function buildStatusHtml({ project, register, needs = [], css, asOf, folderName, files: filesRead = 0, checksum, unreadable = 0, previousAsOf = null, estate = undefined, }) {
|
|
150
|
+
export function buildStatusHtml({ project, register, needs = [], css, asOf, folderName, files: filesRead = 0, checksum, unreadable = 0, previousAsOf = null, estate = undefined, projectView = undefined, }) {
|
|
142
151
|
// A file with no time on it is worse than no file: somebody will act on it.
|
|
143
152
|
if (!asOf)
|
|
144
153
|
throw new Error('buildStatusHtml needs asOf — the time the folder was read. The file states it as the truth of the picture.');
|
|
@@ -149,7 +158,7 @@ export function buildStatusHtml({ project, register, needs = [], css, asOf, fold
|
|
|
149
158
|
// A closing style tag would end the stylesheet early. Refuse; never rewrite it.
|
|
150
159
|
if (/<\/style/i.test(css))
|
|
151
160
|
throw new Error('buildStatusHtml refuses css that closes the style element.');
|
|
152
|
-
const view = reportView({ project, register, asOf, previousAsOf });
|
|
161
|
+
const view = reportView({ view: projectView ?? projectViewOf(register, project), register, asOf, previousAsOf });
|
|
153
162
|
const trend = trendLine(view.trend);
|
|
154
163
|
const levelKey = `Levels: ${['A', 'B', 'C', 'D'].map((l) => `${l} ${LEVEL_KEY[l]}`).join('; ')}. ${FIX_RULE}`;
|
|
155
164
|
return `<!doctype html>
|