@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.
- package/dist/agent/cold-prune.js +1 -1
- package/dist/agent/conversation-prompt.js +52 -0
- package/dist/agent/level.js +1 -1
- package/dist/agent/loop.js +90 -8
- package/dist/agent/oversized-results.js +204 -0
- 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/agent/turn-error-ux.js +14 -0
- 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/plan-continue-gate.js +11 -0
- package/dist/commands/team-plan.js +16 -6
- package/dist/commands/team.js +295 -68
- package/dist/cost/cost-log.js +4 -0
- package/dist/cost/system-block.js +25 -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/models/server-config.js +13 -0
- 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/extract-cca.js +8 -7
- package/dist/projects/index.js +0 -1
- 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/projects/workspace.js +6 -3
- 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 +37 -14
- package/dist/router/router-core.js +5 -3
- package/dist/session/fresh-start.js +15 -0
- package/dist/skill-catalog.js +4 -7
- package/dist/tools/_filesystem-shared.js +7 -0
- package/dist/tools/_team-work-path.js +53 -0
- package/dist/tools/approval.js +97 -19
- package/dist/tools/cut-markers.js +47 -0
- package/dist/tools/filesystem/extract-document.js +50 -2
- package/dist/tools/filesystem/file-edit.js +8 -1
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +8 -1
- package/dist/tools/filesystem/read-document.js +20 -4
- package/dist/tools/project/_merge-shared.js +2 -2
- package/dist/tools/project/_team-tool-shared.js +5 -0
- 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/text-elements/tools.js +7 -1
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/tool-output-read.js +5 -1
- package/dist/tools/update-method-intercept.js +18 -1
- package/dist/tools/write-pipeline.js +24 -5
- package/dist/ui/approval-modal.js +6 -3
- package/dist/ui/footer-line.js +19 -1
- package/package.json +3 -3
- package/dist/projects/merge-cca.js +0 -311
- package/dist/tools/project/cca_merge.js +0 -174
- 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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 8 (B11) — the project transport: the request a team or solo project
|
|
3
|
+
* records its fixes in. Asked once at the start (the journey's transport
|
|
4
|
+
* card, `team start --transport`) or set later by the lead (`team transport`),
|
|
5
|
+
* and stored in the project file as `transport: { request, by, at }`.
|
|
6
|
+
*
|
|
7
|
+
* `projectTransport(cwd)` is what the approval card and the modify/delete
|
|
8
|
+
* write tools read: the request of the project joined in THIS folder, or
|
|
9
|
+
* null. A folder without a project, a copy of one, a project file halfway
|
|
10
|
+
* through a sync, a value that is not a request number — all null, never a
|
|
11
|
+
* throw: there the write and the card behave exactly as before.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { RegisterError, TEAM_FILE, activeProject, readProjectFile } from './store.js';
|
|
16
|
+
import { writeWhole } from './write-whole.js';
|
|
17
|
+
/** A transport request number as SAP writes it: three letters or digits, K, six digits. */
|
|
18
|
+
export const TRANSPORT_REQUEST = /^[A-Z0-9]{3}K\d{6}$/;
|
|
19
|
+
/**
|
|
20
|
+
* The project transport in words, said in one place (Task 3): the journey's
|
|
21
|
+
* lines (indented, `journey/run.ts`) and `team status` (after the target
|
|
22
|
+
* lines) print these.
|
|
23
|
+
*/
|
|
24
|
+
export const TRANSPORT_NONE_WORDS = 'Transport: none yet (set it with: cspeach team transport <REQ>).';
|
|
25
|
+
/** The journey's line for the request: "Transport: S4HK900123." */
|
|
26
|
+
export const transportWords = (request) => `Transport: ${request}.`;
|
|
27
|
+
/** `team status`: the request, and that it is the project's: "Transport: S4HK900123 (project)." */
|
|
28
|
+
export const projectTransportWords = (request) => `Transport: ${request} (project).`;
|
|
29
|
+
/** The plain line for a number that is not a request number (shown as typed, cut short). */
|
|
30
|
+
export function notARequestLine(typed) {
|
|
31
|
+
const t = Array.from(typed.trim()).slice(0, 60).join('');
|
|
32
|
+
return `"${t}" is not a transport request number.`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The same folder, said two ways (as commands/team-discover.ts samePath): on
|
|
36
|
+
* Windows case does not matter, and a trailing separator never does. Kept
|
|
37
|
+
* here so the register layer does not reach into the commands.
|
|
38
|
+
*/
|
|
39
|
+
function samePath(a, b) {
|
|
40
|
+
const norm = (p) => {
|
|
41
|
+
const r = path.resolve(p).replace(/[\\/]+$/, '');
|
|
42
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
43
|
+
};
|
|
44
|
+
return norm(a) === norm(b);
|
|
45
|
+
}
|
|
46
|
+
/** The request of the project joined in this folder, or null. Never throws. */
|
|
47
|
+
export function projectTransport(cwd) {
|
|
48
|
+
try {
|
|
49
|
+
if (!cwd || !fs.existsSync(path.join(cwd, TEAM_FILE)))
|
|
50
|
+
return null;
|
|
51
|
+
const project = readProjectFile(cwd);
|
|
52
|
+
const joined = activeProject();
|
|
53
|
+
if (!joined || joined.projectId !== project.projectId || !samePath(joined.projectDir, cwd))
|
|
54
|
+
return null;
|
|
55
|
+
const t = project.transport;
|
|
56
|
+
const request = t && typeof t === 'object' ? t.request : undefined;
|
|
57
|
+
if (typeof request !== 'string')
|
|
58
|
+
return null;
|
|
59
|
+
const r = request.trim().toUpperCase();
|
|
60
|
+
return TRANSPORT_REQUEST.test(r) ? r : null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The project request a write falls back to (one order for every write tool:
|
|
68
|
+
* owning lock ?? explicit ?? this ?? session). A plan-grant id stays held to
|
|
69
|
+
* its own transport claim (args or session), so it never takes it.
|
|
70
|
+
*/
|
|
71
|
+
export function projectTransportForWrite(ctx, payload) {
|
|
72
|
+
return payload?.grant ? undefined : projectTransport(ctx.cwd) ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
/** The project request as a card and the footer say it: "S4HK903760 (project)". */
|
|
75
|
+
export const projectRequestLabel = (request) => `${request} (project)`;
|
|
76
|
+
/** The plan card's transport when its writes land in more than one request (each in the request its choice names). */
|
|
77
|
+
export const SEVERAL_REQUESTS_LABEL = 'several requests (each change goes to the request that holds it)';
|
|
78
|
+
/**
|
|
79
|
+
* Task 6 (project view, design §2 "Transport") — the ONE choice of the
|
|
80
|
+
* request a write goes to, for the approval card and for every write tool:
|
|
81
|
+
* - modify / delete: owning lock ?? explicit ?? project ?? session;
|
|
82
|
+
* - create: explicit ?? project ?? session (an owning lock is not a create's);
|
|
83
|
+
* - activate / release / run: none (those take no transport).
|
|
84
|
+
* Each caller hands in the facts it uses today: a 'resolve' write
|
|
85
|
+
* (set_source, update_method, delete) passes no session request; a
|
|
86
|
+
* 'fallback-resolve' write (class include, messages) and text-elements pass
|
|
87
|
+
* it. The card for a modify/delete passes none. So in a project folder the
|
|
88
|
+
* card and every write agree (the project request comes before the session);
|
|
89
|
+
* outside one, a fallback-resolve or text-elements modify can write under the
|
|
90
|
+
* session request while its card names none — today's behaviour, kept (byte
|
|
91
|
+
* rule). Outside a project folder `project` is null and the order is today's.
|
|
92
|
+
*/
|
|
93
|
+
export function transportForWrite(i) {
|
|
94
|
+
// As the card and the writes always read them: a request named (explicit) or
|
|
95
|
+
// held for the session counts when it is there at all (??); an owning lock
|
|
96
|
+
// and the project request count only when they name one.
|
|
97
|
+
const pick = (from, request) => {
|
|
98
|
+
const there = from === 'owning' || from === 'project' ? !!request : request !== undefined && request !== null;
|
|
99
|
+
if (!there)
|
|
100
|
+
return null;
|
|
101
|
+
const r = request;
|
|
102
|
+
return { request: r, from, label: from === 'owning' ? `${r} (object already locked here)` : from === 'project' ? projectRequestLabel(r) : r };
|
|
103
|
+
};
|
|
104
|
+
const none = { request: undefined, from: 'none', label: undefined };
|
|
105
|
+
switch (i.op) {
|
|
106
|
+
case 'create':
|
|
107
|
+
return pick('explicit', i.explicit) ?? pick('project', i.project) ?? pick('session', i.session) ?? none;
|
|
108
|
+
case 'modify':
|
|
109
|
+
case 'delete':
|
|
110
|
+
return pick('owning', i.owning) ?? pick('explicit', i.explicit) ?? pick('project', i.project) ?? pick('session', i.session) ?? none;
|
|
111
|
+
default:
|
|
112
|
+
return none;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Set (or, with null, clear) the project transport. The whole project file is
|
|
117
|
+
* written, then renamed — a sync tool must never pick up half a project file.
|
|
118
|
+
*/
|
|
119
|
+
export function writeProjectTransport(projectDir, entry) {
|
|
120
|
+
if (entry && !TRANSPORT_REQUEST.test(entry.request))
|
|
121
|
+
throw new RegisterError(notARequestLine(entry.request));
|
|
122
|
+
const { transport: _old, ...rest } = readProjectFile(projectDir);
|
|
123
|
+
const next = entry ? { ...rest, transport: entry } : rest;
|
|
124
|
+
writeWhole(path.join(projectDir, TEAM_FILE), `${JSON.stringify(next, null, 2)}\n`);
|
|
125
|
+
}
|
package/dist/register/records.js
CHANGED
|
@@ -113,7 +113,9 @@ export function decisionRowsFromCca(content, projectRoot) {
|
|
|
113
113
|
rows.set(key, {
|
|
114
114
|
key, classification: cls,
|
|
115
115
|
...(['high', 'medium', 'low'].includes(confidence) ? { confidence: confidence } : {}),
|
|
116
|
-
|
|
116
|
+
// The assessment's words, never a person's (project view §3.2): no one said Yes to them.
|
|
117
|
+
...(typeof o?.classification?.evidence === 'string' && o.classification.evidence.trim()
|
|
118
|
+
? { reason: o.classification.evidence.slice(0, 300), reasonFrom: 'proposal' } : {}),
|
|
117
119
|
});
|
|
118
120
|
}
|
|
119
121
|
}
|
|
@@ -162,20 +164,25 @@ export function fixRowsFromUpgradeProgress(content, baseline) {
|
|
|
162
164
|
}
|
|
163
165
|
return { rows, ambiguous };
|
|
164
166
|
}
|
|
167
|
+
/** A "Rule <id>: " a reason starts with — written by the model (or an older record), so code takes it off. */
|
|
168
|
+
const RULE_PREFIX = /^\s*rule\s+[A-Za-z0-9_-]{1,20}\s*:\s*/i;
|
|
169
|
+
/** A reason without a leading "Rule <id>:". Rule ids belong to the rules, never inside a recorded reason (project view Task 4). */
|
|
170
|
+
export const withoutRuleId = (reason) => reason.replace(RULE_PREFIX, '');
|
|
165
171
|
/**
|
|
166
172
|
* Decision rows from a saved clean-core triage — CONFIRMED rows only (one Yes
|
|
167
173
|
* per row, clean-core-confirm.ts). The five words map onto the four
|
|
168
|
-
* classifications (spec §4.2)
|
|
174
|
+
* classifications (spec §4.2). Project view §3.2: a row names a target only
|
|
175
|
+
* when a person picked it (no default A), and its reason says whose words it
|
|
176
|
+
* is — the person's when the confirm code marked them so (`reasonBy`), else
|
|
177
|
+
* the proposal's.
|
|
169
178
|
*/
|
|
170
179
|
export function decisionRowsFromCleanCore(content,
|
|
171
180
|
/**
|
|
172
|
-
* Who said Yes, and when. With it, a confirmed row
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* A fix with no target in the proposal still records the legacy A, and that
|
|
176
|
-
* is no target. Absent: rows exactly as before.
|
|
181
|
+
* Who said Yes, and when. With it, a confirmed row with a level carries
|
|
182
|
+
* `levelBy` (G8): the card showed that level and the person's Yes confirmed
|
|
183
|
+
* it. Absent: rows exactly as before.
|
|
177
184
|
*/
|
|
178
|
-
by) {
|
|
185
|
+
by, opts = {}) {
|
|
179
186
|
const rows = [];
|
|
180
187
|
for (const r of content.adjudications ?? []) {
|
|
181
188
|
if (r?.confirmed !== true)
|
|
@@ -188,14 +195,18 @@ by) {
|
|
|
188
195
|
: r.disposition === 'retire' ? { classification: 'retire' } : null;
|
|
189
196
|
if (!base)
|
|
190
197
|
continue;
|
|
191
|
-
const
|
|
198
|
+
const key = objectKey(r.objectType, r.objectName);
|
|
199
|
+
const reason = typeof r.reason === 'string' ? withoutRuleId(r.reason).trim().slice(0, 300) : '';
|
|
192
200
|
const named = moves ? !!r.targetLevel : base.classification === 'keep' && !!r.level;
|
|
201
|
+
const picked = by !== undefined && named && opts.personPicked?.has(key) === true;
|
|
193
202
|
rows.push({
|
|
194
|
-
key
|
|
203
|
+
key, ...base,
|
|
195
204
|
...(r.level ? { level: r.level } : {}),
|
|
196
|
-
...(
|
|
197
|
-
|
|
198
|
-
...(
|
|
205
|
+
...(by && r.level ? { levelBy: { name: by.name, at: by.at } } : {}),
|
|
206
|
+
// A target only beside the person who picked it: the proposal's is no target.
|
|
207
|
+
...(moves && picked && r.targetLevel ? { targetLevel: r.targetLevel } : {}),
|
|
208
|
+
...(reason ? { reason, reasonFrom: r.reasonBy === 'person' ? 'person' : 'proposal' } : {}),
|
|
209
|
+
...(picked ? { targetBy: { name: by.name, at: by.at } } : {}),
|
|
199
210
|
});
|
|
200
211
|
}
|
|
201
212
|
return rows;
|
|
@@ -225,6 +236,7 @@ opts = {}) {
|
|
|
225
236
|
}
|
|
226
237
|
if (env.artefactType === 'clean-core-decision') {
|
|
227
238
|
// Every row here had a person's Yes (confirmCleanCoreRows / the journey's row card).
|
|
239
|
+
// No `personPicked`: a target the model proposed is no target (decision 2).
|
|
228
240
|
const at = (opts.now ?? new Date()).toISOString();
|
|
229
241
|
const rows = decisionRowsFromCleanCore(env.content, { name: by.name, at });
|
|
230
242
|
return writeRecord(joined, 'decision', rows, by, { source, ...opts, now: opts.now ?? new Date(at) });
|
|
@@ -204,6 +204,11 @@ export const RULE_TEMPLATES = {
|
|
|
204
204
|
'classic-program': {
|
|
205
205
|
when: { types: ['PROG'], findingRules: ['SEARCH_PROBLEMATIC_STATEMENTS_FOR_ALV_CALLS'], targetAtMost: 'A' }, then: { disposition: 'rebuild' }, note: 'a classic list program cannot reach A in place',
|
|
206
206
|
},
|
|
207
|
+
// Default rule R6 (project view design §7.2): the same words, for a project whose own rules lack it.
|
|
208
|
+
'classic-program-fix': {
|
|
209
|
+
when: { types: ['PROG'], minFindings: 1 }, then: { disposition: 'fix' },
|
|
210
|
+
note: 'a classic program is fixed in place; a rebuild is decided with a target of A (cspeach team target) or by the lead',
|
|
211
|
+
},
|
|
207
212
|
};
|
|
208
213
|
/** The next free "R<n>" after the highest one in use. */
|
|
209
214
|
function nextId(rules) {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { esc } from './register-map.js';
|
|
24
24
|
import { cell, dayWords, n, ownerLabel } from './register-parts.js';
|
|
25
|
+
import { viewOf } from './register-model.js';
|
|
25
26
|
export const NO_RUN_YET = 'No check run yet, so there is no line to draw.';
|
|
26
27
|
/** Back to front, so the line that matters most — proven — is drawn over the others. */
|
|
27
28
|
const SERIES = [
|
|
@@ -95,16 +96,26 @@ export function progressChartSvg(points, { width = 640, height = 220, capped } =
|
|
|
95
96
|
const barRow = (label, pct, proven, active, extra = '') => `<li><span class="pct-label">${label}</span> `
|
|
96
97
|
+ `<span class="pct-bar"><span class="pct-fill${extra} ${pctClass(pct)}"></span></span>`
|
|
97
98
|
+ `<span class="pct-n">${n(num(pct))}% · ${n(num(proven))} of ${n(num(active))} proven</span></li>`;
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* How far each lane and each focus is now, as bars with the numbers beside them.
|
|
101
|
+
* A lane's bar is the view's lane done (its own objects, the proven and clean
|
|
102
|
+
* parts over its active ones), the same Done % the developers table prints.
|
|
103
|
+
*/
|
|
104
|
+
function doneBarsHtml(register, view) {
|
|
105
|
+
const done = new Map(view.lanes.map((s) => [s.scope.id, s.done]));
|
|
106
|
+
const lanes = (register.lanes ?? []).map((l) => {
|
|
107
|
+
const d = done.get(l.id) ?? { pct: 0, proven: 0, active: 0 };
|
|
108
|
+
return barRow(cell(ownerLabel(l)), d.pct, d.proven, d.active);
|
|
109
|
+
});
|
|
101
110
|
const focuses = (register.plan?.focuses ?? []).map((f) => barRow(`Focus: ${cell(f.label)}`, f.donePct, f.proven, f.active, ' f-focus'));
|
|
102
111
|
if (!lanes.length && !focuses.length)
|
|
103
112
|
return '';
|
|
104
113
|
return '<h3>Done now, per developer and focus</h3>'
|
|
105
114
|
+ `<ul class="pct-bars" data-role="done-bars">${[...lanes, ...focuses].join('')}</ul>`;
|
|
106
115
|
}
|
|
107
|
-
/** The Progress over time section's body, as the page and the status file both draw it. */
|
|
108
|
-
export function progressSectionHtml(register) {
|
|
109
|
-
|
|
116
|
+
/** The Progress over time section's body, as the page and the status file both draw it. `view`: the project view (built once by the page). */
|
|
117
|
+
export function progressSectionHtml(register, view) {
|
|
118
|
+
// No lanes: no lane bar, so no view is needed (an empty folder has no objects to read one from).
|
|
119
|
+
const lanes = register.lanes?.length ? viewOf(register, view) : { lanes: [] };
|
|
120
|
+
return progressChartSvg(register.progress, { capped: register.progressCapped }) + doneBarsHtml(register, lanes);
|
|
110
121
|
}
|
|
@@ -161,11 +161,14 @@ export function blockSvg(block, { cell = 10, gap = 2, role = 'group', colourBy =
|
|
|
161
161
|
// leave retired objects out, and a cell painted C that no count includes
|
|
162
162
|
// would show a manager two numbers for one folder.
|
|
163
163
|
//
|
|
164
|
-
// A clean (or verified) object nobody decided has
|
|
165
|
-
//
|
|
166
|
-
// "level not decided"
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
// A clean (or verified) object nobody decided has nothing to decide: it
|
|
165
|
+
// keeps its state colour too, and its title says so — never "level A" or
|
|
166
|
+
// "level not decided". Its level now may read A from the latest check, but
|
|
167
|
+
// the level counts put it in "clean, nothing to decide", and the map must
|
|
168
|
+
// agree with them. An out-of-scope object keeps its state colour as well:
|
|
169
|
+
// the level counts cover active objects only.
|
|
170
|
+
const nothing = byLevel && o.inScope !== false && !o.decision && (o.state === 'clean' || o.state === 'verified');
|
|
171
|
+
const asLevel = byLevel && o.inScope !== false && o.state !== 'retired' && !nothing;
|
|
169
172
|
return `<rect class="cell ${asLevel ? levelClass(o.levelNow) : stateClass(o.state)}"${keyed ? ` data-key="${esc(o.key)}"` : ''} x="${x}" y="${y}" width="${cell}" height="${cell}" rx="2">`
|
|
170
173
|
+ `<title>${esc(o.name)} · ${esc(o.type)}${asLevel ? ` · level ${esc(LEVEL_WORD[LEVEL_SET.has(o.levelNow) ? o.levelNow : 'none'])}` : nothing ? ' · clean, nothing to decide' : ''} · ${esc(stateWord(o))}</title></rect>`;
|
|
171
174
|
}).join('');
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
* Pure: a Register in, plain data out. No DOM and no clock — `now` is handed
|
|
9
9
|
* in — so the page, the status file and the tests all get the same answer.
|
|
10
10
|
*
|
|
11
|
-
* "Proven" means the system said so:
|
|
12
|
-
*
|
|
13
|
-
* "fixed" is a claim and is never counted here.
|
|
11
|
+
* "Proven" means the system said so: the project view's proven part (a fix the
|
|
12
|
+
* check proved, also for its own target) or clean (measured, never had any). A
|
|
13
|
+
* developer's "fixed" is a claim and is never counted here. Every count of
|
|
14
|
+
* objects reads the project view (register-core `projectView`), which the
|
|
15
|
+
* caller builds once and hands in, so the page, the status file and the
|
|
16
|
+
* terminal share one definition of each word.
|
|
14
17
|
*/
|
|
15
|
-
import { activeOf, laneActiveOf, laneProvenOf, provenOf, settledOf } from '@cspeach/register-core';
|
|
18
|
+
import { activeOf, laneActiveOf, laneProvenOf, projectView, provenOf, settledOf } from '@cspeach/register-core';
|
|
16
19
|
/**
|
|
17
20
|
* The three counts every surface divides by are defined once, in register-core,
|
|
18
21
|
* where the terminal reads them too. They are passed on from here so that
|
|
@@ -20,6 +23,13 @@ import { activeOf, laneActiveOf, laneProvenOf, provenOf, settledOf } from '@cspe
|
|
|
20
23
|
* status file — takes its numbers from the model, like everything else it draws.
|
|
21
24
|
*/
|
|
22
25
|
export { activeOf, laneActiveOf, laneProvenOf, provenOf, settledOf };
|
|
26
|
+
/**
|
|
27
|
+
* The project view these words read. The page and the status file build it once
|
|
28
|
+
* (`collectView`) and hand it in; a caller holding only a register (a test, an
|
|
29
|
+
* older caller) gets one built from the register alone: the same parts, with no
|
|
30
|
+
* project file to name the lead or a project target's setter.
|
|
31
|
+
*/
|
|
32
|
+
export const viewOf = (register, view) => view ?? projectView(register, { kind: 'team-project', formatVersion: 1, projectId: register.projectId, name: '', type: 'clean-core', lead: { name: 'the lead' } });
|
|
23
33
|
/** Worst first: the order cells are banded in a block and listed in a legend. */
|
|
24
34
|
export const STATE_ORDER = ['regressed', 'did-not-hold', 'untouched', 'decided', 'working', 'claimed', 'verified', 'clean', 'retired'];
|
|
25
35
|
export const STATE_LABEL = {
|
|
@@ -119,46 +129,45 @@ function upTo(parts, { tail, cap = LIST_CAP, sep = ', ', more } = {}) {
|
|
|
119
129
|
* normal pairing. Counting the ways would tell the manager four things need
|
|
120
130
|
* attention when three objects do, so this counts objects.
|
|
121
131
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
keys.add(o.key);
|
|
127
|
-
}
|
|
128
|
-
return keys;
|
|
132
|
+
/** Kinds of the view's needs that call for attention on the map: the system disagrees, or the writers do. */
|
|
133
|
+
const ATTENTION = new Set(['conflict', 'failed', 'regressed']);
|
|
134
|
+
function attentionKeys(view) {
|
|
135
|
+
return new Set(view.all.needs.filter((x) => ATTENTION.has(x.kind)).map((x) => x.key));
|
|
129
136
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
/** The view's proven part and clean part: what a check run says, over active objects. */
|
|
138
|
+
const provenCount = (view) => view.all.parts.proven + view.all.parts.clean;
|
|
139
|
+
export function summarize(register, { unreadable = 0, view } = {}) {
|
|
140
|
+
view = viewOf(register, view);
|
|
141
|
+
const all = view.all;
|
|
142
|
+
const proven = provenCount(view);
|
|
133
143
|
// Floor, never round: 7,997 of 8,000 is not 100% proven, and a manager who
|
|
134
144
|
// reads 100% stops looking. Only every last object earns the round number.
|
|
135
|
-
const provenPct =
|
|
136
|
-
const needAttention = attentionKeys(
|
|
145
|
+
const provenPct = all.active ? Math.floor((proven / all.active) * 100) : 0;
|
|
146
|
+
const needAttention = attentionKeys(view).size;
|
|
137
147
|
const lastMeasured = register.lastMeasuredAt ? `last measured ${formatWhen(register.lastMeasuredAt)}` : 'never measured';
|
|
138
148
|
// A project where every object was retired has nothing left to prove, and
|
|
139
149
|
// "0% proven" would read as a team that proved nothing. An empty project
|
|
140
150
|
// keeps the percentage: its map already says there is nothing in scope yet.
|
|
141
|
-
|
|
151
|
+
// "of active": the percentage divides by the active objects, never by all of them (Task 3).
|
|
152
|
+
const provenPhrase = all.total > 0 && all.active === 0 ? 'nothing left to prove' : `${provenPct}% of active proven`;
|
|
142
153
|
// A proof read out of a file is still a proof, and it is not the same thing
|
|
143
154
|
// as a run of the system. The one line a manager reads says how much of what
|
|
144
155
|
// it just called proven came that way, right beside the word.
|
|
145
|
-
const fromFile = (
|
|
156
|
+
const fromFile = all.objects.filter((v) => (v.part === 'proven' || v.part === 'clean') && v.proof.basis === 'file').length;
|
|
146
157
|
const fromFilePhrase = fromFile > 0 ? ` · ${count(fromFile)} of them ${FROM_FILE}` : '';
|
|
147
|
-
|
|
148
|
-
// way register-core does, so the line never prints a blank.
|
|
149
|
-
const decided = totals.decided ?? 0;
|
|
158
|
+
const decided = all.objects.filter((v) => v.decision.what !== 'none').length;
|
|
150
159
|
// Recording fix 4 (2026-10-07): a clean object nobody decided has nothing
|
|
151
|
-
// to decide.
|
|
152
|
-
const nothingToDecide =
|
|
153
|
-
const undecided =
|
|
160
|
+
// to decide. It is counted on its own; it is never "undecided".
|
|
161
|
+
const nothingToDecide = all.levels.nothingToDecide;
|
|
162
|
+
const undecided = Math.max(0, all.total - decided - nothingToDecide);
|
|
154
163
|
const decidedPhrase = nothingToDecide > 0
|
|
155
164
|
? `${count(decided)} decided${undecided > 0 ? `, ${count(undecided)} undecided` : ''} · ${count(nothingToDecide)} ${NOTHING_TO_DECIDE}`
|
|
156
165
|
: `${count(decided)} decided, ${count(undecided)} undecided`;
|
|
157
166
|
return {
|
|
158
|
-
objects:
|
|
167
|
+
objects: all.total, active: all.active, proven, provenPct, decided, undecided, needAttention,
|
|
159
168
|
lastMeasured, unreadable, fromFile,
|
|
160
169
|
...(nothingToDecide > 0 ? { nothingToDecide } : {}),
|
|
161
|
-
line: `${plural(
|
|
170
|
+
line: `${plural(all.total, 'object', 'objects')} · ${provenPhrase}${fromFilePhrase} · ${decidedPhrase} · ${plural(needAttention, 'needs attention', 'need attention')} · ${lastMeasured}`,
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
173
|
const DAY_MS = 86_400_000;
|
|
@@ -167,19 +176,26 @@ const DAY_MS = 86_400_000;
|
|
|
167
176
|
* one mark: where a cut list of OBJECTS sends the reader. On the page that is
|
|
168
177
|
* the map, where a cell opens. In the status file the map is a picture, so it
|
|
169
178
|
* is the object list the file carries instead.
|
|
179
|
+
*
|
|
180
|
+
* What waits for a person about one object — a regression, a failed fix, a
|
|
181
|
+
* conflict, an exception with no reason, a retire candidate — is the view's
|
|
182
|
+
* needs list, the same list the status page prints (one line per object, a fix
|
|
183
|
+
* proven for its own target is not a failed fix).
|
|
170
184
|
*/
|
|
171
|
-
export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface = 'page' } = {}) {
|
|
185
|
+
export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface = 'page', view } = {}) {
|
|
186
|
+
view = viewOf(register, view);
|
|
172
187
|
const items = [];
|
|
173
|
-
const
|
|
188
|
+
const byKey = new Map(register.objects.map((o) => [o.key, o]));
|
|
189
|
+
const needing = (kind) => view.all.needs.filter((x) => x.kind === kind).map((x) => byKey.get(x.key)).filter(Boolean);
|
|
174
190
|
// The four lists below name objects, and an object is a cell. Everything
|
|
175
191
|
// else here names something the map does not draw.
|
|
176
192
|
const onMap = surface === 'file' ? seeObjectList : seeMap;
|
|
177
|
-
const regressed =
|
|
193
|
+
const regressed = needing('regressed');
|
|
178
194
|
if (regressed.length) {
|
|
179
195
|
items.push({ kind: 'regressed', title: `${plural(regressed.length, 'object has', 'objects have')} more findings than before`,
|
|
180
196
|
detail: upTo(regressed.map((o) => o.name), { tail: onMap }), keys: regressed.map((o) => o.key) });
|
|
181
197
|
}
|
|
182
|
-
const didNotHold =
|
|
198
|
+
const didNotHold = needing('failed');
|
|
183
199
|
if (didNotHold.length) {
|
|
184
200
|
// Objects, not fixes. One object can carry three failed fixes, and "3 fixes
|
|
185
201
|
// did not hold" sends a lead looking for three things to put right.
|
|
@@ -194,7 +210,7 @@ export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface
|
|
|
194
210
|
}), { tail: onMap }),
|
|
195
211
|
keys: didNotHold.map((o) => o.key) });
|
|
196
212
|
}
|
|
197
|
-
const conflicts =
|
|
213
|
+
const conflicts = needing('conflict');
|
|
198
214
|
if (conflicts.length) {
|
|
199
215
|
items.push({ kind: 'conflict', title: `${plural(conflicts.length, 'decision needs', 'decisions need')} the lead to rule`,
|
|
200
216
|
detail: upTo(conflicts.map((o) => `${o.name} — ${Object.entries(o.decision.conflict).map(([w, c]) => `${w}: ${c}`).join(', ')}`), { tail: onMap, sep: '; ' }),
|
|
@@ -203,7 +219,7 @@ export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface
|
|
|
203
219
|
// An accepted exception is work nobody has to do — on a reason somebody gave.
|
|
204
220
|
// One with no reason (an older record, a hand-written file) is a decision the
|
|
205
221
|
// lead cannot defend yet.
|
|
206
|
-
const noReason =
|
|
222
|
+
const noReason = needing('reason');
|
|
207
223
|
if (noReason.length) {
|
|
208
224
|
items.push({ kind: 'exception-no-reason', title: `${plural(noReason.length, 'accepted exception has', 'accepted exceptions have')} no reason`,
|
|
209
225
|
detail: upTo(noReason.map((o) => o.name), { tail: onMap }), keys: noReason.map((o) => o.key) });
|
|
@@ -211,7 +227,9 @@ export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface
|
|
|
211
227
|
// A developer's "retire" is a candidate until the lead confirms it with a
|
|
212
228
|
// reason (spec 2026-10-06 D2). Until then the object stays active, and the
|
|
213
229
|
// lead is the one person who can move it.
|
|
214
|
-
|
|
230
|
+
// From the decision, as the tiles and team status count them: a candidate that waits for a conflict first is still one.
|
|
231
|
+
const candidates = view.all.objects.filter((v) => v.decision.what === 'retire-candidate' && v.part !== 'retired' && v.part !== 'out-of-scope')
|
|
232
|
+
.map((v) => byKey.get(v.key)).filter(Boolean);
|
|
215
233
|
if (candidates.length) {
|
|
216
234
|
items.push({ kind: 'retire-candidate', title: `${plural(candidates.length, 'retire candidate waits', 'retire candidates wait')} for your confirmation`,
|
|
217
235
|
detail: upTo(candidates.map((o) => o.name), { tail: onMap }), keys: candidates.map((o) => o.key) });
|
|
@@ -223,7 +241,7 @@ export function needsYou(register, { now, unreadable = 0, quietDays = 6, surface
|
|
|
223
241
|
items.push({ kind: 'out-of-scope-no-reason', title: `${plural(outNoReason.length, 'object was', 'objects were')} taken out of scope with no reason`,
|
|
224
242
|
detail: upTo(outNoReason.map((o) => o.name), { tail: onMap }), keys: outNoReason.map((o) => o.key) });
|
|
225
243
|
}
|
|
226
|
-
const unowned =
|
|
244
|
+
const unowned = view.unowned.objects;
|
|
227
245
|
if (unowned.length) {
|
|
228
246
|
const packages = [...new Set(unowned.map((o) => o.package))].sort();
|
|
229
247
|
items.push({ kind: 'unowned', title: `${plural(unowned.length, 'object has', 'objects have')} no owner`,
|
|
@@ -311,9 +329,10 @@ export function packageBlocks(register) {
|
|
|
311
329
|
/**
|
|
312
330
|
* Active objects nobody has measured. Their colour rests on what people said,
|
|
313
331
|
* and the page says so out loud: "not measured" is a state of knowledge, not a
|
|
314
|
-
* blank. A retired object is not counted — there is nothing
|
|
332
|
+
* blank. A retired or out-of-scope object is not counted — there is nothing
|
|
333
|
+
* left to prove. The view's coverage, the same number the status page prints.
|
|
315
334
|
*/
|
|
316
|
-
export const notMeasuredCount = (register
|
|
335
|
+
export const notMeasuredCount = (register, view) => viewOf(register, view).all.coverage.notChecked;
|
|
317
336
|
/**
|
|
318
337
|
* One object's measurements, newest first: when, by whom, how many open, and
|
|
319
338
|
* whether the numbers came from a system check or from a file somebody read in.
|