@cspeach/cli 1.1.48 → 1.1.50

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.
Files changed (93) hide show
  1. package/dist/agent/cold-prune.js +1 -1
  2. package/dist/agent/conversation-prompt.js +52 -0
  3. package/dist/agent/level.js +1 -1
  4. package/dist/agent/loop.js +90 -8
  5. package/dist/agent/oversized-results.js +204 -0
  6. package/dist/agent/sap-connection-adapter.js +2 -0
  7. package/dist/agent/system-prompt.js +2 -2
  8. package/dist/agent/tool-dispatch.js +13 -1
  9. package/dist/agent/turn-error-ux.js +14 -0
  10. package/dist/approvals/op-labels.js +49 -10
  11. package/dist/approvals/render.js +14 -23
  12. package/dist/approvals/risk-floor.js +196 -19
  13. package/dist/approvals/visible-changes.js +35 -0
  14. package/dist/commands/help.js +1 -1
  15. package/dist/commands/plan-continue-gate.js +11 -0
  16. package/dist/commands/team-plan.js +16 -6
  17. package/dist/commands/team.js +295 -68
  18. package/dist/cost/cost-log.js +4 -0
  19. package/dist/cost/system-block.js +25 -0
  20. package/dist/journey/chain.js +489 -61
  21. package/dist/journey/clean-core.js +61 -36
  22. package/dist/journey/flow.js +75 -2
  23. package/dist/journey/people.js +37 -0
  24. package/dist/journey/rebuild.js +7 -6
  25. package/dist/journey/run.js +129 -42
  26. package/dist/journey/trigger.js +16 -10
  27. package/dist/models/server-config.js +13 -0
  28. package/dist/one-shot.js +1 -1
  29. package/dist/project-context/render.js +7 -0
  30. package/dist/projects/clean-core-confirm.js +166 -44
  31. package/dist/projects/extract-cca.js +8 -7
  32. package/dist/projects/index.js +0 -1
  33. package/dist/projects/merge-upgrade.js +2 -2
  34. package/dist/projects/project-folder.js +2 -1
  35. package/dist/projects/save-command.js +51 -14
  36. package/dist/projects/workspace.js +6 -3
  37. package/dist/register/decision-rules.js +2 -1
  38. package/dist/register/export-files.js +18 -0
  39. package/dist/register/picture.js +3 -3
  40. package/dist/register/project-transport.js +125 -0
  41. package/dist/register/records.js +25 -13
  42. package/dist/register/rules-edit.js +5 -0
  43. package/dist/register/status-file/progress-chart.js +17 -6
  44. package/dist/register/status-file/register-map.js +8 -5
  45. package/dist/register/status-file/register-model.js +55 -36
  46. package/dist/register/status-file/register-parts.js +97 -82
  47. package/dist/register/status-file/register-status-file.js +16 -7
  48. package/dist/register/status-file/report-model.js +293 -205
  49. package/dist/register/status-file/team-css.js +1 -1
  50. package/dist/register/status-html.js +11 -4
  51. package/dist/register/store.js +2 -2
  52. package/dist/register/workbooks.js +24 -13
  53. package/dist/renderer/footer-line.js +10 -2
  54. package/dist/renderer/tool-labels.js +2 -2
  55. package/dist/repl/builtin-commands.js +1 -1
  56. package/dist/repl/continue-skill.js +5 -0
  57. package/dist/repl/level-command.js +6 -15
  58. package/dist/repl.js +37 -14
  59. package/dist/router/router-core.js +5 -3
  60. package/dist/session/fresh-start.js +15 -0
  61. package/dist/skill-catalog.js +4 -7
  62. package/dist/tools/_filesystem-shared.js +7 -0
  63. package/dist/tools/_team-work-path.js +53 -0
  64. package/dist/tools/approval.js +97 -19
  65. package/dist/tools/cut-markers.js +47 -0
  66. package/dist/tools/filesystem/extract-document.js +50 -2
  67. package/dist/tools/filesystem/file-edit.js +8 -1
  68. package/dist/tools/filesystem/file-read.js +2 -2
  69. package/dist/tools/filesystem/file-write.js +8 -1
  70. package/dist/tools/filesystem/read-document.js +20 -4
  71. package/dist/tools/project/_merge-shared.js +2 -2
  72. package/dist/tools/project/_team-tool-shared.js +5 -0
  73. package/dist/tools/project/rules_propose.js +2 -1
  74. package/dist/tools/project/team_findings.js +81 -28
  75. package/dist/tools/project/team_lane_move.js +127 -0
  76. package/dist/tools/project/upgrade_merge_progress.js +8 -4
  77. package/dist/tools/text-elements/tools.js +7 -1
  78. package/dist/tools/tool-loading.js +1 -1
  79. package/dist/tools/tool-output-read.js +5 -1
  80. package/dist/tools/update-method-intercept.js +18 -1
  81. package/dist/tools/write-pipeline.js +24 -5
  82. package/dist/ui/approval-modal.js +6 -3
  83. package/dist/ui/footer-line.js +19 -1
  84. package/package.json +3 -3
  85. package/dist/projects/merge-cca.js +0 -311
  86. package/dist/tools/project/cca_merge.js +0 -174
  87. package/test-harness/fe-draft/app.ts +0 -188
  88. package/test-harness/fe-draft/fe-draft.e2e.test.ts +0 -461
  89. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +0 -51
  90. package/test-harness/fe-draft/routes.ts +0 -224
  91. package/test-harness/fe-draft/routes.unit.test.ts +0 -45
  92. package/test-harness/fe-draft/server.ts +0 -225
  93. 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, laneActiveOf, laneProvenOf, measurementHistory, notMeasuredCount, provenOf, unknownStateCount, FROM_FILE, NOTHING_TO_DECIDE, } from './register-model.js';
25
- import { DISPOSITION_ORDER, DISPOSITION_WORD, developerTable, dispositionOf, overlapNote, } from '@cspeach/register-core';
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
- * three counts come from register-core, which is also what the terminal uses.
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
- * A lane summary counts states and nothing else, so how much of a lane's proven
106
- * work came out of a file has to be counted here, off the objects. `objects` is
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, objects) {
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
- // One pass over the objects, not one per lane: a real estate has 20,000 of
117
- // them and ten lanes, and this table is drawn on every render.
118
- const fromFile = new Map();
119
- for (const o of objects ?? []) {
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 provenCell = (l) => {
133
- const said = fromFile.get(l.id) ?? 0;
134
- return `${n(laneProvenOf(l))}${said ? ` (${n(said)} ${FROM_FILE})` : ''}`;
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
- const count = notMeasuredCount(register);
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
- return `${n(count)} of ${n(register.totals.active)} active object${register.totals.active === 1 ? '' : 's'} `
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
- if (projectType === 'clean-core') {
411
- const d = dispositionOf(decision);
412
- if (d)
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 comes from register-core's totals — the same ones the terminal and
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
- const t = register.totals ?? {};
425
- const s = t.byState ?? {};
426
- const fromFile = (t.fromFile?.verified ?? 0) + (t.fromFile?.clean ?? 0);
427
- const didNotHold = s['did-not-hold'] ?? 0;
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', t.decided ?? 0, `of ${n(t.objects)} objects`)
430
- + tile('Claimed', s.claimed ?? 0, 'a developer says fixed')
431
- + tile('Proven', provenOf(s), fromFile ? `${n(fromFile)} of them ${FROM_FILE}` : 'a check run says so')
432
- + tile('Did not hold', didNotHold, 'claimed, and the check still finds it', didNotHold ? 'bad' : '')
433
- + tile('Retired', s.retired ?? 0, 'nothing left to do')
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
- + ((t.retireCandidates ?? 0) > 0 ? tile('Retire candidates', t.retireCandidates, 'wait for the lead to confirm', 'retire-candidates') : '')
442
+ + (candidates > 0 ? tile('Retire candidates', candidates, 'wait for the lead to confirm', 'retire-candidates') : '')
437
443
  + '</ul>'
438
- + (!(t.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>' : '')
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
- /** Levels at decision and now, in words as well as colour. A folder with no levels says so. */
468
- export function levelBarsHtml(register) {
469
- const b = register.totals?.byLevel;
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. ' : ''}A level moves only when a check proves the fix.</p></div>`;
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
- const d = register.totals?.byDisposition ?? {};
481
- const chips = DISPOSITION_ORDER.map((k) => `<li class="chip d-${k}">${esc(DISPOSITION_WORD[k])} <b>${n(d[k] ?? 0)}</b></li>`).join('');
482
- const ex = register.objects.filter((o) => dispositionOf(o.decision) === 'exception');
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
- const t = developerTable(register);
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. One pass over the objects, counted for every
498
- // lane that claims one, exactly as register-core counts the lane's Proven.
499
- const fromFile = new Map();
500
- for (const o of register.objects) {
501
- if (o.measuredBasis !== 'file' || (o.state !== 'verified' && o.state !== 'clean'))
502
- continue;
503
- for (const id of o.laneOverlap?.length ? o.laneOverlap : [o.lane])
504
- fromFile.set(id, (fromFile.get(id) ?? 0) + 1);
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 by effective priority) and how much recorded work sits outside
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
- // A key with no object behind it names nothing a reader can find, so it is
517
- // left out rather than printed as the raw key.
518
- const keys = (lane.next ?? []).slice(0, 3);
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 = ['Lane packages', 'Objects', 'Decided', 'Fixes proven', 'Awaiting check', 'Kept', 'Rebuilds', 'Fix failed', 'Last check'];
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><td data-l="Decided">${n(r.decided)}</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
- : '<p>No object is kept by decision.</p>';
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>