create-pmos 0.185.0 → 0.186.0
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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-report-setup-test.js +1 -0
- package/workspace/guide/reports-set-up.md +4 -2
- package/workspace/lib/routes-report-setups.js +37 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rs.js +3 -1
- package/workspace/public/pm/c3-report-setup.js +8 -3
- package/workspace/public/pm/c3-report-store.js +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.186.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T05:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T05:23:27Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -88,6 +88,7 @@ const TAB = '[data-rs-tab]';
|
|
|
88
88
|
await page.click('[data-rs-card] [data-rs-act="setup"]');
|
|
89
89
|
ok('Set up posts ONE setup with the ask and the proposal', !!(await until(() => sent('/api/report-setups/setup').length === 1)) && sent('/api/report-setups/setup')[0].body.ask && sent('/api/report-setups/setup')[0].body.proposal && /pricing/i.test(sent('/api/report-setups/setup')[0].body.proposal.name), JSON.stringify(sent('/api/report-setups/setup')).slice(0, 300));
|
|
90
90
|
ok('the set-up appears marked New with its first draft in Documents', !!(await until(async () => (await page.$$('[data-rs-reg]')).length === 4)) && /New/.test(await text('[data-rs-setups]')) && (await page.$$('[data-rs-doc]')).length === 7);
|
|
91
|
+
ok('the Set up toast says the first draft is ready and offers Open', /first draft is ready/.test(await text('[data-c3-toast]')) && !!(await page.$('[data-c3-toast-action]')), await text('[data-c3-toast]'));
|
|
91
92
|
ok('nothing else was posted: no copy, no issue', sent('/api/docs/create-copy').length === 0 && sent('/api/report/issue').length === 0);
|
|
92
93
|
|
|
93
94
|
/* ── found: Set up, Not a report with Undo ── */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Set up a report by asking, and the report finder"
|
|
4
4
|
description: "How to ask PMOS to draft a report you send, how the report finder finds reports you already send, how to set one up, and how a cycle becomes a document you issue."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Set up a report by asking, and the report finder
|
|
@@ -17,6 +17,7 @@ Open the **Reports** tab of the console. It has four parts: Ask PMOS, Found in y
|
|
|
17
17
|
2. Press **Set up**. PMOS reads your earlier copies and learns the sections. This takes a minute. You can leave the page. **Cancel** stops it.
|
|
18
18
|
3. PMOS shows its proposal: what it found, the proposed report, what it learned from, and the fixes it will apply.
|
|
19
19
|
4. Press **Set up** to accept the proposal. Press **Change** to say what to change, for example "Draft it on Tuesday instead". PMOS revises the proposal. Press **Cancel** to drop it.
|
|
20
|
+
5. PMOS writes the first draft at once. Press **Open** in the message to read it. You do not wait for the day of the schedule.
|
|
20
21
|
|
|
21
22
|
A proposal shows the name, the purpose, the audience, the output, the schedule, the sources and the sections in order. Change the output with the buttons: Google Slides, Google Doc, Google Sheets, Confluence or PDF. PMOS shows the report in the same format in the sheet.
|
|
22
23
|
|
|
@@ -39,6 +40,7 @@ The most likely finding also shows on the Briefing as **Report found**. Press **
|
|
|
39
40
|
**Set up** has one row for each report: its source, output, schedule, next draft and last issue. Each row has these buttons:
|
|
40
41
|
|
|
41
42
|
- **Open** opens the current draft.
|
|
43
|
+
- **Draft it now** shows in place of Open when the report has no draft. PMOS writes the draft of the next cycle and opens it.
|
|
42
44
|
- **All cycles** lists every cycle of the report.
|
|
43
45
|
- **Pause** stops the drafts. **Resume** starts them again.
|
|
44
46
|
|
|
@@ -46,7 +48,7 @@ The most likely finding also shows on the Briefing as **Report found**. Press **
|
|
|
46
48
|
|
|
47
49
|
## What PMOS does and does not do
|
|
48
50
|
|
|
49
|
-
- PMOS drafts on the day of the schedule. It sends nothing.
|
|
51
|
+
- PMOS drafts the first cycle when you press Set up, and each next cycle on the day of the schedule. It sends nothing.
|
|
50
52
|
- PMOS writes to Google or to Confluence only when you press **Create the copy** or **Update the copy**.
|
|
51
53
|
- A download happens only when you press a download item.
|
|
52
54
|
- You issue a report. An issued report is frozen.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* POST /api/report-setups/ask { ws, text } → { ok, id } starts the report-setup run (headless)
|
|
10
10
|
* POST /api/report-setups/change { ws, id, text } → { ok } a correction to a proposal; a second run revises it
|
|
11
11
|
* POST /api/report-setups/cancel { ws, id } → { ok }
|
|
12
|
-
* POST /api/report-setups/setup { ws, ask? | finding?, proposal? } → { ok, setup, first_draft } THE press that sets a report up
|
|
12
|
+
* POST /api/report-setups/setup { ws, ask? | finding?, proposal? } → { ok, setup, first_draft, drafted } THE press that sets a report up; drafts the first cycle
|
|
13
|
+
* POST /api/report-setups/draft { ws, id } → { ok, drafted: { slot, path }, id } Draft it now (the next cycle)
|
|
13
14
|
* POST /api/report-setups/dismiss { ws, finding } → { ok } "Not a report"
|
|
14
15
|
* POST /api/report-setups/pause | resume { ws, id } → { ok, setup }
|
|
15
16
|
* POST /api/report-setups/from-dashboard { ws, key, name, output, schedule, audience } → { ok, setup }
|
|
@@ -39,13 +40,14 @@ const RN = require('./report-needs.js');
|
|
|
39
40
|
const RC = require('./report-checks.js');
|
|
40
41
|
const RX = require('./report-export.js');
|
|
41
42
|
const RF = require('./report-finder.js');
|
|
43
|
+
const RD = require('./report-draft.js');
|
|
42
44
|
const logic = require('./schedule-logic.js');
|
|
43
45
|
const docSync = require('./doc-sync.js');
|
|
44
46
|
const headless = require('./headless.js');
|
|
45
47
|
|
|
46
48
|
const text = (v) => (typeof v === 'string' ? v : '');
|
|
47
49
|
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
48
|
-
const POSTS = ['ask', 'change', 'cancel', 'setup', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder'];
|
|
50
|
+
const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder'];
|
|
49
51
|
const MADE_BY = ['schedule', 'you', 'agent'];
|
|
50
52
|
|
|
51
53
|
const runs = new Map(); // ask id → session id, for the report-setup runs this server started
|
|
@@ -331,14 +333,30 @@ async function handle(req, res, url, ctx) {
|
|
|
331
333
|
if (!proposal) return send(bad(400, 'say which request or finding to set up, or give the proposal'));
|
|
332
334
|
const r = RS.setUp(root, { ...proposal, ...(from ? { from } : {}) }, opts);
|
|
333
335
|
if (!r.ok) return fail(r);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
// The first cycle is drafted now, at the slot of its first scheduled draft, so the PM has a document to read the moment it is set up
|
|
337
|
+
// (the approved mockup's "Sample draft made when you set up"). The schedule later redrafts the same slot. A failed draft leaves the set-up
|
|
338
|
+
// in place and says why; Draft it now on the row tries again.
|
|
339
|
+
const drafted = firstDraft(root, r, g.actor);
|
|
340
|
+
if (drafted.path) ctx.invalidateNeedsAgg && ctx.invalidateNeedsAgg();
|
|
341
|
+
return send({ status: 200, body: { ok: true, setup: setupOut(r.setup, new Date(), documentsOf(ctx, w), copyIndex(root)), first_draft: r.first_draft, drafted } });
|
|
336
342
|
}
|
|
337
343
|
if (sub === 'dismiss') {
|
|
338
344
|
const r = RS.dismissFinding(file, { id: text(body.finding), by: g.actor });
|
|
339
345
|
if (!r.ok) return fail(r);
|
|
340
346
|
return send({ status: 200, body: { ok: true } });
|
|
341
347
|
}
|
|
348
|
+
if (sub === 'draft') {
|
|
349
|
+
// Draft it now: the next cycle of a set-up report, written at the slot of its next scheduled draft. A page write, like Draft now on the report.
|
|
350
|
+
const s = RS.fold(RS.readLines(file)).setups.get(text(body.id));
|
|
351
|
+
if (!s || s.status === 'removed') return send(bad(404, 'no such report'));
|
|
352
|
+
if (s.source !== 'work') return send(bad(400, 'a dashboard report is drafted from its dashboard on its day'));
|
|
353
|
+
let next = null; try { next = RS.nextDraftAt(s.schedule, s.last_run, new Date()); } catch { next = null; }
|
|
354
|
+
const day = next ? `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}-${String(next.getDate()).padStart(2, '0')}` : null; // the local day, as setUp names a slot
|
|
355
|
+
const d = draftCycle(root, s.id, day, g.actor);
|
|
356
|
+
if (d.error) return send(bad(500, 'the draft was not written: ' + d.error));
|
|
357
|
+
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
|
|
358
|
+
return send({ status: 200, body: { ok: true, drafted: d, id: s.id + '/' + d.slot } });
|
|
359
|
+
}
|
|
342
360
|
if (sub === 'pause' || sub === 'resume') {
|
|
343
361
|
const r = RS[sub](root, text(body.id), opts);
|
|
344
362
|
if (!r.ok) return fail(r);
|
|
@@ -365,4 +383,19 @@ async function handle(req, res, url, ctx) {
|
|
|
365
383
|
return send(bad(404, 'no such report route'));
|
|
366
384
|
}
|
|
367
385
|
|
|
386
|
+
/** Draft a report's cycle now through the one writer (report-draft.js writeDraft). → { slot, path } | { error } */
|
|
387
|
+
function draftCycle(root, cadence, slot, actor) {
|
|
388
|
+
const d = RD.draftOf(root, { cadence, slot: slot || undefined });
|
|
389
|
+
if (!d || d.ok === false) return { error: (d && d.error) || 'the drafter answered nothing' };
|
|
390
|
+
// A draft already at that slot is the answer: open it, never overwrite its notes.
|
|
391
|
+
try { if (d.path && fs.statSync(path.join(path.resolve(String(root)), d.path)).isFile()) return { slot: d.slot, path: d.path, existing: true }; } catch { /* not there yet */ }
|
|
392
|
+
const w = RD.writeDraft(root, d, { actor, fresh: true });
|
|
393
|
+
if (!w || w.ok === false) return { error: (w && w.error) || 'the writer answered nothing' };
|
|
394
|
+
return { slot: d.slot, path: w.path };
|
|
395
|
+
}
|
|
396
|
+
function firstDraft(root, r, actor) {
|
|
397
|
+
if (!r.setup || r.setup.source !== 'work') return { error: 'a dashboard report is drafted from its dashboard on its day' };
|
|
398
|
+
return draftCycle(root, r.setup.id, r.first_draft && r.first_draft.slot, actor);
|
|
399
|
+
}
|
|
400
|
+
|
|
368
401
|
module.exports = { prefixes: ['/api/report-setups', '/api/report-documents', '/api/report-export'], handle, tick, ensureMigrated, documentsOf, listAnswer, copyOf };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.186.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.186.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -186,7 +186,8 @@ function setupFrom(b) {
|
|
|
186
186
|
startDocs(s, F);
|
|
187
187
|
if (F) findings = findings.filter((f) => f.id !== F.id);
|
|
188
188
|
if (askRec) asks = asks.filter((a) => a.id !== askRec.id);
|
|
189
|
-
|
|
189
|
+
const first = DOCS.filter((d) => d.setup === id && d.state === 'draft')[0];
|
|
190
|
+
return json({ ok: true, setup: setupView(s), first_draft: { slot: next || '', at: next ? next + 'T08:00:00.000Z' : null, day: next || '' }, drafted: first ? { slot: first.slot, path: first.path } : { error: 'nothing to draft' } });
|
|
190
191
|
}
|
|
191
192
|
function post(path, b) {
|
|
192
193
|
const k = path.replace('/api/report-setups/', '');
|
|
@@ -198,6 +199,7 @@ function post(path, b) {
|
|
|
198
199
|
}
|
|
199
200
|
if (k === 'cancel') { asks = asks.filter((a) => a.id !== b.id); return json({ ok: true }); }
|
|
200
201
|
if (k === 'setup') return setupFrom(b);
|
|
202
|
+
if (k === 'draft') { const s = SETUPS.find((x) => x.id === b.id); if (!s) return json({ ok: false, error: 'no such report' }, 404); const d = startDocs(s, null)[0]; return json({ ok: true, drafted: { slot: d.slot, path: d.path }, id: s.id + '/' + d.slot }); }
|
|
201
203
|
if (k === 'dismiss') { findings = findings.filter((f) => f.id !== b.finding); dismissed.add(b.finding); return json({ ok: true }); }
|
|
202
204
|
if (k === 'pause' || k === 'resume') { const s = SETUPS.find((x) => x.id === b.id); if (!s) return json({ ok: false, error: 'no such set-up' }, 404); s.status = k === 'pause' ? 'paused' : 'active'; return json({ ok: true, setup: setupView(s) }); }
|
|
203
205
|
if (k === 'from-dashboard') {
|
|
@@ -14,7 +14,7 @@ import { openSheet, closeSheet } from './c3-sheet.js';
|
|
|
14
14
|
import { openDocSheet } from './c3-doc-sheet.js';
|
|
15
15
|
import { toast } from './c3-toast.js';
|
|
16
16
|
import { FORMATS, OUTPUTS, formatOf, ensureReportCss } from './c3-doc-formats.js';
|
|
17
|
-
import { RS, onReports, loadReports, askPmos, changeIt, cancelAsk, setupFrom, dismissLater, pauseResume, fromDashboard, goTo, evidenceKind, normProposal, scheduleLabel, nextDraftDate, dayText, docsOf, outChip, copyChip, srcChip } from './c3-report-store.js';
|
|
17
|
+
import { RS, onReports, loadReports, askPmos, changeIt, cancelAsk, setupFrom, draftNow, dismissLater, pauseResume, fromDashboard, goTo, evidenceKind, normProposal, scheduleLabel, nextDraftDate, dayText, docsOf, outChip, copyChip, srcChip } from './c3-report-store.js';
|
|
18
18
|
|
|
19
19
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
20
20
|
/** 'Wed 21 Oct 2026' for a day, 'Oct 2026' for a month, any other slot as it is. */
|
|
@@ -122,6 +122,7 @@ function askCard(a, redo) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/* ── a report document in the document sheet ── */
|
|
125
|
+
RS.openDoc = (d) => openReportDoc(d); // the store's toasts open a document through this
|
|
125
126
|
export function openReportDoc(d, mode) {
|
|
126
127
|
ensureReportCss();
|
|
127
128
|
const setup = RS.setups.find((x) => x.id === d.setup) || null; const cyc = slotText(d.slot);
|
|
@@ -205,7 +206,10 @@ export function mountReportSetup(host, ctx) {
|
|
|
205
206
|
el('span', { class: 'chips2' }, stag(d), copyChip(d.output, d.copy), pbtn('ghost', 'Open', 'out', { onclick: () => openReportDoc(d) })))));
|
|
206
207
|
function openSetup(s) {
|
|
207
208
|
const L = docsOf(RS.docs, s.id); const d = L.find((x) => x.state === 'draft') || L[0];
|
|
208
|
-
if (d) openReportDoc(d);
|
|
209
|
+
if (d) { openReportDoc(d); return; }
|
|
210
|
+
if (s.source !== 'work' || s.status !== 'active') { toast('No draft yet. The first draft is ' + (s.next_draft ? dayText(s.next_draft) : 'when you confirm the day') + '.'); return; }
|
|
211
|
+
toast('Drafting ' + s.name + ' now. Nothing is sent.');
|
|
212
|
+
draftNow(s).then((nd) => { if (nd) openReportDoc(nd); });
|
|
209
213
|
}
|
|
210
214
|
function setupRow(s) {
|
|
211
215
|
const paused = s.status === 'paused'; const cy = docsOf(RS.docs, s.id); const n = s.cycles != null ? s.cycles : cy.length; const open = RS.cyc.has(s.id);
|
|
@@ -215,7 +219,8 @@ export function mountReportSetup(host, ctx) {
|
|
|
215
219
|
chip(icon('cal', 'sm'), s.label || scheduleLabel(s.schedule)), paused ? null : chip(icon('clock', 'sm'), 'Next draft ' + (s.next_draft ? dayText(s.next_draft) : 'not set yet')),
|
|
216
220
|
chip(icon('mail', 'sm'), 'Last issued ' + (s.last_issued ? dayText(s.last_issued) : 'not yet'))),
|
|
217
221
|
open ? cycRows(s) : null),
|
|
218
|
-
el('div', { class: 'acts' }, pbtn('outline', 'Open', 'out', { 'data-rs-act': 'open-reg', onclick: () => openSetup(s) })
|
|
222
|
+
el('div', { class: 'acts' }, cy.length || paused || s.source !== 'work' ? pbtn('outline', 'Open', 'out', { 'data-rs-act': 'open-reg', onclick: () => openSetup(s) })
|
|
223
|
+
: pbtn('outline', 'Draft it now', 'spark', { 'data-rs-act': 'draft-now', onclick: () => openSetup(s) }),
|
|
219
224
|
pbtn('ghost', 'All cycles (' + n + ')', 'fold', { 'data-rs-act': 'cyc', 'aria-expanded': String(open), onclick: () => { if (RS.cyc.has(s.id)) RS.cyc.delete(s.id); else RS.cyc.add(s.id); drawSetups(); } }),
|
|
220
225
|
pbtn('ghost', paused ? 'Resume' : 'Pause', paused ? 'play' : 'pause', { 'data-rs-act': 'pause', onclick: () => pauseResume(s, !paused) })));
|
|
221
226
|
}
|
|
@@ -114,9 +114,22 @@ export async function setupFrom(body) {
|
|
|
114
114
|
if (r.setup && r.setup.id) RS.newIds.add(r.setup.id);
|
|
115
115
|
await loadReports(RS.ws, true);
|
|
116
116
|
const fd = r.first_draft; const first = (r.setup && r.setup.next_draft) || (fd && typeof fd === 'object' ? fd.day || fd.slot : fd);
|
|
117
|
-
|
|
117
|
+
if (r.drafted && r.drafted.path) openDraftToast(r.setup.id, 'Set up. The first draft is ready to read. Nothing is sent.');
|
|
118
|
+
else toast('Set up. First draft ' + (first ? dayText(first) : 'once you confirm the day') + '. Nothing is sent.' + (r.drafted && r.drafted.error ? ' It could not be drafted yet: ' + r.drafted.error + '.' : ''));
|
|
118
119
|
return r;
|
|
119
120
|
}
|
|
121
|
+
/* the page that draws documents sets RS.openDoc, so a toast here can open one without this store importing the page */
|
|
122
|
+
function openDraftToast(id, text) {
|
|
123
|
+
const d = (RS.docs || []).filter((x) => x.setup === id && x.state === 'draft').sort((a, b) => (a.slot < b.slot ? 1 : -1))[0];
|
|
124
|
+
if (d && typeof RS.openDoc === 'function') toast({ text, actionLabel: 'Open', timeout: 8000, onAction: () => RS.openDoc(d) });
|
|
125
|
+
else toast(text);
|
|
126
|
+
}
|
|
127
|
+
/** Draft it now: the next cycle of a set-up report. → the new draft document, or null. */
|
|
128
|
+
export async function draftNow(setup) {
|
|
129
|
+
const r = await post('draft', { id: setup.id }); if (!r || !r.ok) { fail(r, 'It was not drafted'); return null; }
|
|
130
|
+
await loadReports(RS.ws, true);
|
|
131
|
+
return (RS.docs || []).find((x) => x.setup === setup.id && x.slot === r.drafted.slot) || null;
|
|
132
|
+
}
|
|
120
133
|
/** Not a report: the finding leaves the list at once, the POST waits five seconds, and Undo cancels it. */
|
|
121
134
|
export function dismissLater(id, onChange) {
|
|
122
135
|
RS.hidden.add(id); emit(); if (onChange) onChange();
|