@worca/app 1.3.0-rc.2 → 1.4.0-rc.1
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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -15,6 +15,16 @@ function h(doc, tag, cls, text) {
|
|
|
15
15
|
const dayLabel = (iso) => { const d = new Date(iso); return Number.isFinite(d.getTime()) ? `${MO[d.getUTCMonth()]} ${d.getUTCDate()}` : ''; };
|
|
16
16
|
const dot = (doc, tone) => { const d = h(doc, 'span', `dot tm-dot ${tone}`); d.setAttribute('aria-hidden', 'true'); return d; };
|
|
17
17
|
const code = (doc, text) => h(doc, 'code', 'mono', text);
|
|
18
|
+
/** A project, workspace or home NAME inside prose: bold and mono, so it reads as a name, not a word. */
|
|
19
|
+
const ref = (doc, name) => h(doc, 'b', 'ref mono', name);
|
|
20
|
+
/** `text` with every occurrence of `name` rendered as a ref; plain text when the name is absent. */
|
|
21
|
+
function withRef(doc, text, name) {
|
|
22
|
+
const frag = doc.createDocumentFragment();
|
|
23
|
+
if (!name || !String(text).includes(name)) { frag.append(String(text)); return frag; }
|
|
24
|
+
const parts = String(text).split(name);
|
|
25
|
+
parts.forEach((part, i) => { if (i) frag.append(ref(doc, name)); if (part) frag.append(part); });
|
|
26
|
+
return frag;
|
|
27
|
+
}
|
|
18
28
|
function btn(doc, cls, text) { const b = h(doc, 'button', `btn-ghost btn-mini ${cls}`, text); b.type = 'button'; return b; }
|
|
19
29
|
|
|
20
30
|
/** Classify a project status (from /api/team-metrics/scopes → projects[]) into a cell variant. */
|
|
@@ -44,19 +54,61 @@ function recordSwitch(doc, s) {
|
|
|
44
54
|
return label;
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
const cap = (t) => (t ? t[0].toUpperCase() + t.slice(1) : t);
|
|
58
|
+
/**
|
|
59
|
+
* One project's team-metrics state in a few words: `short` is what a row chip and the project
|
|
60
|
+
* page's stat card show ("on · 3 runs", "via acme/gateway", "push failed"), `detail` the sentence
|
|
61
|
+
* behind it, `tone` the dot colour. The full cell (renderProjectTmCell) says the same at length.
|
|
62
|
+
*/
|
|
63
|
+
export function projectTmSummary(s) {
|
|
64
|
+
const { kind } = projectTmState(s);
|
|
65
|
+
const excluded = s.record === false;
|
|
66
|
+
const runs = s.runs == null ? '' : `${s.runs} run${s.runs === 1 ? '' : 's'}`;
|
|
67
|
+
const since = s.enabledAt ? `since ${dayLabel(s.enabledAt)}` : '';
|
|
68
|
+
switch (kind) {
|
|
69
|
+
case 'no-origin': return { kind, tone: 'muted', short: 'not available', detail: s.noGit ? 'not a git repository' : 'no origin remote' };
|
|
70
|
+
case 'off': return { kind, tone: 'grey', short: 'off', detail: 'runs stay on this machine' };
|
|
71
|
+
case 'delegate-invalid': return { kind, tone: 'red', short: 'delegate invalid', detail: `points at ${s.delegateTo} · runs are not being recorded` };
|
|
72
|
+
case 'blocked': return { kind, tone: 'amber', short: 'on · not recording', detail: s.delegateCode === 'CONFIG_UNKNOWN' ? 'branch not read yet' : (s.delegateDetail || 'the metrics branch could not be resolved') };
|
|
73
|
+
case 'rejected': return { kind, tone: 'red', short: 'push failed', detail: s.lastErrorCode === 'PUSH_REJECTED' && s.lastErrorHint ? 'branch protection' : String(s.lastError || '').trim().split('\n')[0] };
|
|
74
|
+
case 'pending': return { kind, tone: 'amber', short: `on · ${s.pending} pending push`, detail: since };
|
|
75
|
+
case 'delegated': return { kind, tone: excluded ? 'grey' : 'green', short: excluded ? `via ${s.delegateTo} · yours excluded` : `via ${s.delegateTo}`, ref: s.delegateTo, detail: [runs ? `${runs} recorded` : '', 'recorded in that project\'s branch'].filter(Boolean).join(' · ') };
|
|
76
|
+
default: return { kind, tone: excluded ? 'grey' : 'green', short: excluded ? 'on · yours excluded' : (runs ? `on · ${runs}` : 'on'), detail: since };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The Projects-row chip: a dot, the word and the short state — no controls, no hints. Everything
|
|
82
|
+
* it leaves out is on the project page's Team tab (renderProjectTmCell). Expert-level like the
|
|
83
|
+
* page tab; the row itself is the only control.
|
|
84
|
+
*/
|
|
85
|
+
export function renderProjectTmChip(s, { doc = globalThis.document } = {}) {
|
|
86
|
+
const chip = h(doc, 'span', 'pl-team-item pl-tm');
|
|
87
|
+
chip.dataset.key = s.key;
|
|
88
|
+
const sum = projectTmSummary(s);
|
|
89
|
+
chip.dataset.kind = sum.kind;
|
|
90
|
+
if (sum.tone !== 'muted') chip.append(dot(doc, sum.tone));
|
|
91
|
+
const state = h(doc, 'span', `pl-team-state${sum.tone === 'muted' ? ' muted' : ''}`);
|
|
92
|
+
state.append(withRef(doc, sum.short, sum.ref));
|
|
93
|
+
chip.append(h(doc, 'span', 'pl-team-name', 'Metrics'), ' ', state);
|
|
94
|
+
chip.title = `Team metrics: ${cap(sum.short)}${sum.detail ? ` · ${sum.detail}` : ''}`;
|
|
95
|
+
return chip;
|
|
96
|
+
}
|
|
97
|
+
|
|
47
98
|
/**
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
* rows (see .tm-cell in style.css). The control never wraps
|
|
51
|
-
*
|
|
99
|
+
* The project page's team-metrics block (and, before this branch, the Projects-list cell): a
|
|
100
|
+
* fixed two-row grid — the "Team metrics" title over a status line on the left, and ONE control
|
|
101
|
+
* column on the right that spans both rows (see .tm-cell in style.css). The control never wraps
|
|
102
|
+
* under the status. `heading: false` drops the title when a panel head already names it.
|
|
52
103
|
*/
|
|
53
|
-
export function renderProjectTmCell(s, { doc = globalThis.document } = {}) {
|
|
104
|
+
export function renderProjectTmCell(s, { doc = globalThis.document, heading = true } = {}) {
|
|
54
105
|
const cell = h(doc, 'div', 'tm-cell');
|
|
55
106
|
cell.dataset.key = s.key;
|
|
56
|
-
cell.append(h(doc, 'span', 'tm-label', 'Team metrics'));
|
|
107
|
+
if (heading) cell.append(h(doc, 'span', 'tm-label', 'Team metrics'));
|
|
57
108
|
const line = h(doc, 'div', 'tm-line');
|
|
58
109
|
const status = h(doc, 'span', 'tm-status');
|
|
59
110
|
const { kind } = projectTmState(s);
|
|
111
|
+
cell.dataset.kind = kind; // the Getting started guide reads it (no-origin / off / on)
|
|
60
112
|
const actions = h(doc, 'div', 'tm-actions');
|
|
61
113
|
const runs = s.runs == null ? '' : ` · ${s.runs} run${s.runs === 1 ? '' : 's'}`;
|
|
62
114
|
const excluded = s.record === false; // personal opt-out (the "Include my runs" switch)
|
|
@@ -74,7 +126,7 @@ export function renderProjectTmCell(s, { doc = globalThis.document } = {}) {
|
|
|
74
126
|
case 'delegate-invalid':
|
|
75
127
|
line.append(dot(doc, 'red'));
|
|
76
128
|
// The resolver's code distinguishes chain / unknown from dangling; dangling keeps the mockup copy.
|
|
77
|
-
status.append(`Delegate invalid · points at `,
|
|
129
|
+
status.append(`Delegate invalid · points at `, ref(doc, s.delegateTo),
|
|
78
130
|
s.delegateCode === 'DELEGATE_CHAIN' ? ', which itself delegates (no chains)'
|
|
79
131
|
: s.delegateCode === 'DELEGATE_UNKNOWN' ? ', which is not a project in Worca on this machine'
|
|
80
132
|
: ', which no longer records');
|
|
@@ -102,8 +154,8 @@ export function renderProjectTmCell(s, { doc = globalThis.document } = {}) {
|
|
|
102
154
|
line.append(dot(doc, excluded ? 'grey' : 'green'));
|
|
103
155
|
// Kept short: the status shares ~220px with the switch (see .tm-status), so the
|
|
104
156
|
// opted-out copy leads with what matters and drops the run count.
|
|
105
|
-
if (excluded) status.append('On for the team · in ',
|
|
106
|
-
else status.append('On · recorded in ',
|
|
157
|
+
if (excluded) status.append('On for the team · in ', ref(doc, s.delegateTo), ' · yours excluded');
|
|
158
|
+
else status.append('On · recorded in ', ref(doc, s.delegateTo), runs);
|
|
107
159
|
actions.append(recordSwitch(doc, s));
|
|
108
160
|
break;
|
|
109
161
|
default:
|
|
@@ -135,7 +187,9 @@ export function renderProjectTmCell(s, { doc = globalThis.document } = {}) {
|
|
|
135
187
|
if (s.enabled && Array.isArray(s.homeFor) && s.homeFor.length) {
|
|
136
188
|
const names = s.homeFor.join(', ');
|
|
137
189
|
const hint = h(doc, 'small', 'hint tm-hint tm-home-for');
|
|
138
|
-
hint.append(
|
|
190
|
+
hint.append('Metrics home for ');
|
|
191
|
+
s.homeFor.forEach((x, i) => { if (i) hint.append(', '); hint.append(ref(doc, x)); });
|
|
192
|
+
hint.append(' · "Include my runs" covers its workspace runs too');
|
|
139
193
|
hint.title = `Workspace runs of ${names} are written to this project's branch. Turning "Include my runs" off here also stops your workspace runs from being recorded.`;
|
|
140
194
|
cell.append(hint);
|
|
141
195
|
}
|
|
@@ -230,7 +284,7 @@ export function renderMetricsHomePicker(members, { selectedPath, doc = globalThi
|
|
|
230
284
|
status.append(dot(doc, 'green'), lines);
|
|
231
285
|
row.append(label, status);
|
|
232
286
|
} else if (m.enabled && m.delegateTo) {
|
|
233
|
-
status.append(dot(doc, 'grey'),
|
|
287
|
+
status.append(dot(doc, 'grey'), 'Delegates to ', ref(doc, m.delegateTo), ' · cannot host metrics');
|
|
234
288
|
row.classList.add('off');
|
|
235
289
|
row.append(name, status);
|
|
236
290
|
} else {
|
|
@@ -276,22 +330,29 @@ function homeMark(doc) {
|
|
|
276
330
|
* Answers what people scan for — how big, where runs go, is anything wrong — on one line.
|
|
277
331
|
*/
|
|
278
332
|
export function renderWsSummary(ws, { doc = globalThis.document, pending = false } = {}) {
|
|
279
|
-
|
|
333
|
+
let frag = doc.createDocumentFragment();
|
|
280
334
|
const n = (ws.projectPaths || ws.members || []).length;
|
|
281
335
|
frag.append(`${n} project${n === 1 ? '' : 's'}`);
|
|
336
|
+
// Everything after the count is team-metrics state: expert detail (docs/ui-levels.md), so it rides
|
|
337
|
+
// one tagged span the stylesheet can drop. `frag` is re-pointed so the appends below land in it.
|
|
338
|
+
const out = frag;
|
|
339
|
+
const tm = h(doc, 'span', 'ws-sum-tm');
|
|
340
|
+
tm.dataset.minLevel = 'expert';
|
|
341
|
+
out.append(tm);
|
|
342
|
+
frag = tm;
|
|
282
343
|
// The scopes call is still out: say so instead of "no metrics home", which reads as final.
|
|
283
|
-
if (pending) { frag.append(' · ', h(doc, 'span', 'ws-sum-pending', 'checking metrics…')); return
|
|
344
|
+
if (pending) { frag.append(' · ', h(doc, 'span', 'ws-sum-pending', 'checking metrics…')); return out; }
|
|
284
345
|
const home = ws.home || { state: 'unset' };
|
|
285
346
|
if (home.state === 'unset') {
|
|
286
347
|
frag.append(' · ', h(doc, 'span', 'muted', 'no metrics home'));
|
|
287
348
|
} else {
|
|
288
|
-
frag.append(' · ', homeMark(doc),
|
|
349
|
+
frag.append(' · ', homeMark(doc), ref(doc, home.slug));
|
|
289
350
|
if (home.state !== 'ok') frag.append(' · ', h(doc, 'span', 'ws-sum-bad', home.detail || 'stale'));
|
|
290
351
|
else if (home.runs != null) frag.append(` · ${home.runs} workspace run${home.runs === 1 ? '' : 's'}`); // project-level runs live elsewhere
|
|
291
352
|
}
|
|
292
353
|
const silent = (ws.members || []).filter((x) => x.state !== 'home' && !x.recordsOn).length;
|
|
293
354
|
if (silent) frag.append(' · ', h(doc, 'span', 'ws-sum-warn', `${silent} not recording`));
|
|
294
|
-
return
|
|
355
|
+
return out;
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
/** One table cell: what this member's metrics branch is, or that there is none. */
|
|
@@ -432,7 +493,7 @@ export function renderWsMetricsRow(ws, { doc = globalThis.document } = {}) {
|
|
|
432
493
|
root.append(you);
|
|
433
494
|
} else if (home.record === false) {
|
|
434
495
|
const you = h(doc, 'small', 'hint ws-home-hint warn');
|
|
435
|
-
you.append(dot(doc, 'amber'),
|
|
496
|
+
you.append(dot(doc, 'amber'), ' Your workspace runs are not recorded: "Include my runs" is off on ', ref(doc, home.slug), '.');
|
|
436
497
|
root.append(you);
|
|
437
498
|
}
|
|
438
499
|
root.append(h(doc, 'div', 'ws-route-results'));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Team metrics page renderers (team-metrics-design.md §4.10; mockup boards 1–3). Pure DOM:
|
|
3
3
|
// every function takes an aggregate() result (src/shared/team-metrics/aggregate.mjs) and
|
|
4
4
|
// returns detached elements. app.js owns fetch, mounting and delegated events.
|
|
5
|
-
import { niceScale, roundedTopBar } from './stats-view.mjs';
|
|
5
|
+
import { niceScale, roundedTopBar, savedMultChip } from './stats-view.mjs';
|
|
6
6
|
import { safeHttpUrl } from '../../src/shared/team-metrics/aggregate.mjs';
|
|
7
7
|
|
|
8
8
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
@@ -25,6 +25,7 @@ export const TM_FMT = {
|
|
|
25
25
|
if (s < 3600) return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
|
|
26
26
|
return `${Math.floor(s / 3600)}h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`;
|
|
27
27
|
},
|
|
28
|
+
hours: (h) => `${(Math.round(((Number(h) || 0) + Number.EPSILON) * 10) / 10).toLocaleString('en-US', { maximumFractionDigits: 1 })} h`,
|
|
28
29
|
pct: (v) => (v == null ? '—' : `${Math.round(v * 100)}%`),
|
|
29
30
|
day: (iso) => { const d = new Date(iso); return `${MO[d.getUTCMonth()]} ${d.getUTCDate()}`; },
|
|
30
31
|
};
|
|
@@ -136,7 +137,8 @@ function deltaChip(doc, text, title) {
|
|
|
136
137
|
}
|
|
137
138
|
const signedPct = (v) => (v == null ? null : `${v >= 0 ? '+' : '−'}${Math.abs(Math.round(v * 100))}%`);
|
|
138
139
|
|
|
139
|
-
/** The six mockup tile glyphs, path data verbatim from boards 1–2 (`svg.stat-ico`)
|
|
140
|
+
/** The six mockup tile glyphs, path data verbatim from boards 1–2 (`svg.stat-ico`); `saved` (a money bag
|
|
141
|
+
* resting on an open hand) is not a board glyph — it is shared with the Stats view. */
|
|
140
142
|
const TILE_ICONS = Object.freeze({
|
|
141
143
|
spend: ['M12 3v18M17 7.5c0-1.9-2.2-3-5-3s-5 1.1-5 3 2.2 2.6 5 3 5 1.1 5 3-2.2 3-5 3-5-1.1-5-3'],
|
|
142
144
|
runs: ['M5 20v-7M12 20V4M19 20v-11'],
|
|
@@ -144,6 +146,7 @@ const TILE_ICONS = Object.freeze({
|
|
|
144
146
|
duration: ['circle:12,12,8', 'M12 8v4l3 2'],
|
|
145
147
|
autonomy: ['M4 12h4l2-5 4 10 2-5h4'],
|
|
146
148
|
cycles: ['M3 12a9 9 0 1 0 3-6.7', 'M3 4v4h4'],
|
|
149
|
+
saved: ['M9.6 4.6h3.8', 'M9.9 4.6c.3-1.5 2.9-1.5 3.2 0', 'M9.6 4.6C7.8 6.2 6.8 7.8 6.8 9.4c0 1.6.6 2.9 1.4 3.8h6.6c.8-.9 1.4-2.2 1.4-3.8 0-1.6-1-3.2-2.8-4.8', 'M8.2 13.2c-.5 0-1 .2-1.3.6L3 17.6', 'M7 21l1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91L14.8 13.2', 'M2 16.6l6 6'],
|
|
147
150
|
});
|
|
148
151
|
function tileIcon(doc, key) {
|
|
149
152
|
const svg = s(doc, 'svg', { class: 'stat-ico', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.9, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
|
|
@@ -190,6 +193,23 @@ export function renderTmKpiRow(agg, { doc = globalThis.document, now = Date.now(
|
|
|
190
193
|
? ['attributed to the workspace, not to single projects']
|
|
191
194
|
: nowInRange ? [{ b: TM_FMT.usd(k.spendThisMonthUsd) }, ` so far in ${month}`] : [],
|
|
192
195
|
}));
|
|
196
|
+
|
|
197
|
+
// Saved (money-saved design §10): Σ human hours × the effective rate − spend. Gated like the
|
|
198
|
+
// Off-policy tile: records pushed before this feature carry no `human`, and a range made only
|
|
199
|
+
// of those must not print "−$spend".
|
|
200
|
+
if (k.humanRuns > 0) {
|
|
201
|
+
const saved = Number(k.savedUsd || 0);
|
|
202
|
+
const savedTile = tile(doc, {
|
|
203
|
+
icon: 'saved', label: 'Saved', chip: deltaChip(doc, d && withTitle(signedPct(d.savedPct))),
|
|
204
|
+
value: `${saved < 0 ? '−' : ''}${TM_FMT.usd(Math.abs(saved))}`,
|
|
205
|
+
sub: ['≈ ', { b: TM_FMT.hours(k.humanHours) }, ' of human work'],
|
|
206
|
+
});
|
|
207
|
+
savedTile.querySelector('.stat-value').classList.toggle('is-neg', saved < 0);
|
|
208
|
+
savedTile.querySelector('.stat-value').classList.toggle('is-pos', saved > 0);
|
|
209
|
+
const mult = savedMultChip(doc, saved, k.spendUsd); // "32× spend", right of the delta pill
|
|
210
|
+
if (mult) savedTile.querySelector('.stat-label').append(mult);
|
|
211
|
+
row.append(savedTile);
|
|
212
|
+
}
|
|
193
213
|
row.append(tile(doc, {
|
|
194
214
|
icon: 'runs', label: 'Runs', chip: deltaChip(doc, d && signedPct(d.runsPct)), value: String(k.runs),
|
|
195
215
|
sub: [{ b: String(k.done) }, ` done · ${k.failed} failed · ${k.stopped} stopped · `, { b: TM_FMT.pct(k.successRate) }, ' success'],
|
|
@@ -204,16 +224,46 @@ export function renderTmKpiRow(agg, { doc = globalThis.document, now = Date.now(
|
|
|
204
224
|
row.append(tile(doc, {
|
|
205
225
|
icon: 'autonomy', label: 'Autonomy', chip: deltaChip(doc, d && d.autonomyPts != null ? `${d.autonomyPts >= 0 ? '+' : '−'}${Math.abs(d.autonomyPts)} pts` : null),
|
|
206
226
|
value: TM_FMT.pct(k.autonomy), meterPct: k.autonomy == null ? 0 : k.autonomy * 100,
|
|
207
|
-
sub: ['active ÷ wall-clock · ', { b: k.interventionsPerRun == null ? '—' : String(k.interventionsPerRun) }, ' interventions per run'],
|
|
227
|
+
sub: ['active ÷ (wall-clock − paused) · ', { b: k.interventionsPerRun == null ? '—' : String(k.interventionsPerRun) }, ' interventions per run'],
|
|
208
228
|
}));
|
|
209
229
|
row.append(tile(doc, {
|
|
210
230
|
icon: 'cycles', label: 'Review cycles', chip: deltaChip(doc, d && d.reviewCycles != null ? `${d.reviewCycles >= 0 ? '+' : '−'}${Math.abs(d.reviewCycles)}` : null),
|
|
211
231
|
value: k.reviewCyclesMean == null ? '—' : String(k.reviewCyclesMean),
|
|
212
232
|
sub: ['mean implement→review loops · ', { b: TM_FMT.pct(k.convergeInOneRate) }, ' converge in one'],
|
|
213
233
|
}));
|
|
234
|
+
// Team policy (team-policy design §10, board 10): only once a record in range carries a policy.
|
|
235
|
+
if (k.policyRuns > 0) {
|
|
236
|
+
row.append(tile(doc, {
|
|
237
|
+
icon: 'cycles', label: 'Off-policy', value: String(k.offPolicyRuns),
|
|
238
|
+
sub: [{ b: String(k.capOverrides) }, ` cap override${k.capOverrides === 1 ? '' : 's'} · `, { b: String(k.deviations) }, ` deviation${k.deviations === 1 ? '' : 's'} · of ${k.policyRuns} run${k.policyRuns === 1 ? '' : 's'} under a policy`],
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
214
241
|
return row;
|
|
215
242
|
}
|
|
216
243
|
|
|
244
|
+
/**
|
|
245
|
+
* The pooled-budget tile (team-policy design §7, board 10): advisory, never a pause. `spentUsd`
|
|
246
|
+
* is the sum of the records in the budget's current window; the pace line projects it to the
|
|
247
|
+
* window's end. Warn from 80 %, over from 100 %.
|
|
248
|
+
*/
|
|
249
|
+
export function renderPooledBudgetTile({ budgetUsd, window: win = 'monthly', spentUsd = 0, windowStartMs, windowEndMs }, { doc = globalThis.document, now = Date.now() } = {}) {
|
|
250
|
+
const ratio = budgetUsd > 0 ? spentUsd / budgetUsd : 0;
|
|
251
|
+
const elapsed = windowEndMs > windowStartMs ? Math.max(0, Math.min(1, (now - windowStartMs) / (windowEndMs - windowStartMs))) : 1;
|
|
252
|
+
const pace = elapsed > 0.05 ? spentUsd / elapsed : null;
|
|
253
|
+
const end = new Date(windowEndMs);
|
|
254
|
+
const endLabel = `${MO[end.getMonth()]} ${end.getDate()}`;
|
|
255
|
+
const card = tile(doc, {
|
|
256
|
+
icon: 'spend', label: 'Pooled budget', value: TM_FMT.pct(ratio), meterPct: ratio * 100,
|
|
257
|
+
sub: [{ b: TM_FMT.usd(spentUsd) }, ' of ', { b: TM_FMT.usd(budgetUsd) }, ` this ${win === 'weekly' ? 'week' : 'month'}`,
|
|
258
|
+
...(pace != null && ratio < 1 ? [' · on pace for ', { b: TM_FMT.usd(pace) }, ` by ${endLabel}`] : ratio >= 1 ? [' · over budget'] : [])],
|
|
259
|
+
});
|
|
260
|
+
card.classList.add('tm-pooled');
|
|
261
|
+
const fill = card.querySelector('.stat-meter-fill');
|
|
262
|
+
if (fill) fill.classList.toggle('warn', ratio >= 0.8 && ratio < 1);
|
|
263
|
+
if (fill) fill.classList.toggle('over', ratio >= 1);
|
|
264
|
+
return card;
|
|
265
|
+
}
|
|
266
|
+
|
|
217
267
|
const CW = 560, CH = 240, L = 44, R = 12, T = 18, B = 26;
|
|
218
268
|
const PW = CW - L - R, PH = CH - T - B;
|
|
219
269
|
|
|
@@ -293,7 +343,8 @@ export function renderStackedWeekChart({ title, weeks, stacks, valueOf, yFmt, ti
|
|
|
293
343
|
const BREAKDOWN_SPECS = {
|
|
294
344
|
workflow: { title: 'By workflow', sub: 'share of spend', cols: [['label', 'Workflow'], ['runs', 'Runs'], ['usd', 'Spend'], ['perRunUsd', 'Per run'], ['successRate', 'Success'], ['share', '']] },
|
|
295
345
|
source: { title: 'By ticket', sub: 'task source · top 5', limit: 5, cols: [['label', 'Source'], ['runs', 'Runs'], ['usd', 'Spend'], ['perRunUsd', 'Per run'], ['cyclesMean', 'Cycles']] },
|
|
296
|
-
|
|
346
|
+
// `overrides`: team caps continued past or exceeded (team policy §10, board 10); "–" without a policy.
|
|
347
|
+
actor: { title: 'By actor', sub: 'who ran it', cols: [['label', 'Actor'], ['runs', 'Runs'], ['usd', 'Spend'], ['perRunUsd', 'Per run'], ['successRate', 'Success'], ['overrides', 'Overrides']] },
|
|
297
348
|
// No spend column: a run's cost is not a per-project fact. Runs touched and files changed are;
|
|
298
349
|
// the bar is the share of runs in range that touched the project. Cost questions go through
|
|
299
350
|
// the row filter, which narrows every panel to the runs that touched it.
|
|
@@ -306,6 +357,7 @@ function cellText(key, row) {
|
|
|
306
357
|
case 'usd': case 'perRunUsd': return row[key] == null ? '—' : TM_FMT.usd(row[key]);
|
|
307
358
|
case 'successRate': return TM_FMT.pct(row[key]);
|
|
308
359
|
case 'cyclesMean': case 'filesChanged': return row[key] == null ? '—' : String(row[key]);
|
|
360
|
+
case 'overrides': return row[key] ? String(row[key]) : '–';
|
|
309
361
|
default: return String(row[key] ?? '—');
|
|
310
362
|
}
|
|
311
363
|
}
|
|
@@ -379,7 +431,7 @@ export function renderRunsTable(runs, { doc = globalThis.document, total = runs.
|
|
|
379
431
|
if (!runs.length) { card.append(h(doc, 'div', 'chart-empty hint', 'No runs recorded in this range')); return card; }
|
|
380
432
|
const table = h(doc, 'table', 'tm-tbl tm-run-tbl');
|
|
381
433
|
const thead = h(doc, 'thead'); const thr = h(doc, 'tr');
|
|
382
|
-
for (const c of ['Title', 'Workflow', 'Result', 'Cost', 'Duration', 'Cycles', 'PR', 'Actor', 'Started']) thr.append(h(doc, 'th', null, c));
|
|
434
|
+
for (const c of ['Title', 'Workflow', 'Result', 'Cost', 'Saved', 'Duration', 'Cycles', 'PR', 'Actor', 'Started']) thr.append(h(doc, 'th', null, c));
|
|
383
435
|
thead.append(thr); table.append(thead);
|
|
384
436
|
const tbody = h(doc, 'tbody');
|
|
385
437
|
for (const r of runs.slice(0, limit)) {
|
|
@@ -388,6 +440,17 @@ export function renderRunsTable(runs, { doc = globalThis.document, total = runs.
|
|
|
388
440
|
tr.append(h(doc, 'td', null, r.workflow ?? '—'));
|
|
389
441
|
const res = h(doc, 'td'); res.append(h(doc, 'span', `badge ${r.result === 'done' ? 'green' : r.result === 'failed' ? 'red' : 'grey'}`, r.result)); tr.append(res);
|
|
390
442
|
tr.append(h(doc, 'td', 'mono num', TM_FMT.usd(r.usd)));
|
|
443
|
+
// Saved per run (money-saved design §10): hours × rate − cost, coloured like the KPI tile;
|
|
444
|
+
// "—" for a record pushed without `human`.
|
|
445
|
+
const saved = h(doc, 'td', 'mono num tm-saved');
|
|
446
|
+
if (r.savedUsd == null) saved.textContent = '—';
|
|
447
|
+
else {
|
|
448
|
+
saved.textContent = `${r.savedUsd < 0 ? '−' : ''}${TM_FMT.usd(Math.abs(r.savedUsd))}`;
|
|
449
|
+
saved.classList.toggle('neg', r.savedUsd < 0);
|
|
450
|
+
saved.classList.toggle('pos', r.savedUsd > 0);
|
|
451
|
+
saved.title = `≈ ${TM_FMT.hours(r.humanHours)} of human work`;
|
|
452
|
+
}
|
|
453
|
+
tr.append(saved);
|
|
391
454
|
tr.append(h(doc, 'td', 'mono num', TM_FMT.duration(r.wallMs)));
|
|
392
455
|
tr.append(h(doc, 'td', 'mono num', r.reviewCycles == null ? '—' : String(r.reviewCycles)));
|
|
393
456
|
const pr = h(doc, 'td', 'mono');
|