@bongos/core 1.21.107 → 1.21.108
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/.bongos-core.json +35 -35
- package/.claude/skills/goal-create/SKILL.md +8 -3
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +6 -4
- package/docs/copy-inventory.md +25 -19
- package/docs/copy-registry.json +92 -37
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +189 -188
- package/modules/copy-desk/docx.js +27 -7
- package/modules/copy-desk/page-status.js +3 -1
- package/modules/copy-desk/pages.js +44 -5
- package/modules/copy-desk/routes/copy-desk.js +4 -3
- package/modules/copy-desk/tests/copy_docx.mjs +38 -0
- package/modules/copy-desk/tests/copy_page_draft.mjs +79 -0
- package/modules/hall-ui/public/paintbrush.js +152 -12
- package/modules/hall-ui/public/studio.js +3 -1
- package/modules/hall-ui/public/style.css +16 -0
- package/modules/hall-ui/public/tweak-editor-lib.js +24 -12
- package/modules/hall-ui/records/craft-mode.md +5 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +12 -21
- package/scripts/gds/seed-verify-criteria.js +73 -18
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_submit.mjs +36 -0
- package/tests/hall_tweak_editor.mjs +19 -3
- package/tests/paintbrush.mjs +151 -2
- package/tests/planning_session_skill.mjs +40 -0
- package/tests/seed_verify_criteria.mjs +130 -0
|
@@ -82,6 +82,8 @@ const COMMENT_TEXT = Object.freeze({
|
|
|
82
82
|
shared: 'changes this on every page',
|
|
83
83
|
unplaced: 'goes to an engineer',
|
|
84
84
|
});
|
|
85
|
+
// A line the draft removes (task 1004696): a comment, but no style of its own.
|
|
86
|
+
const REMOVED_COMMENT = 'marked to be removed: an engineer takes it out. Rewrite it here to keep it';
|
|
85
87
|
const NOTE_TEXT = 'Rewrite any line where it stands, then upload this file back into the editor. '
|
|
86
88
|
+ 'Keep one paragraph per line: a paragraph added, removed or moved is refused. '
|
|
87
89
|
+ 'Keep each {…} as it is: the page fills in a value there.';
|
|
@@ -368,16 +370,25 @@ function textOf(el) {
|
|
|
368
370
|
// The lines as the file shows them: the draft's `after` where the draft is on
|
|
369
371
|
// this very reading, else the page's current text. A draft saved on an older
|
|
370
372
|
// reading is not shown: its keys may name other lines now.
|
|
373
|
+
//
|
|
374
|
+
// A line the draft removes (task 1004696) shows the page's words with a margin
|
|
375
|
+
// comment saying so: an empty paragraph is not a line, so blanking it would
|
|
376
|
+
// break the file's one-paragraph-per-line shape. Rewriting it in Word keeps it.
|
|
371
377
|
function shownLines(readingPage, draft) {
|
|
372
378
|
const after = new Map();
|
|
379
|
+
const removed = new Set();
|
|
373
380
|
if (draft && draft.reading_hash === readingPage.reading_hash) {
|
|
374
|
-
for (const l of draft.lines || [])
|
|
381
|
+
for (const l of draft.lines || []) {
|
|
382
|
+
if (l.remove) removed.add(l.key);
|
|
383
|
+
else after.set(l.key, l.after);
|
|
384
|
+
}
|
|
375
385
|
}
|
|
376
386
|
return (readingPage.lines || []).map((l) => ({
|
|
377
387
|
key: l.key,
|
|
378
388
|
section: l.section == null ? '' : String(l.section),
|
|
379
389
|
placement: l.placement,
|
|
380
390
|
text: after.has(l.key) ? after.get(l.key) : l.text,
|
|
391
|
+
...(removed.has(l.key) ? { removed: true } : {}),
|
|
381
392
|
}));
|
|
382
393
|
}
|
|
383
394
|
|
|
@@ -413,8 +424,8 @@ function documentXml({ title, lines }) {
|
|
|
413
424
|
current = l.section;
|
|
414
425
|
body.push(styledPara(STYLE.heading, run(l.section)));
|
|
415
426
|
}
|
|
416
|
-
const
|
|
417
|
-
const
|
|
427
|
+
const style = COMMENT_TEXT[l.placement] ? STYLE[l.placement] : null;
|
|
428
|
+
const note = l.removed ? REMOVED_COMMENT : COMMENT_TEXT[l.placement];
|
|
418
429
|
const sdt = `<w:sdt><w:sdtPr><w:tag w:val="${escapeXml(l.key)}"/><w:id w:val="${idx + 1}"/><w:text/></w:sdtPr>`
|
|
419
430
|
+ `<w:sdtContent>${run(l.text)}</w:sdtContent></w:sdt>`;
|
|
420
431
|
let inner = sdt;
|
|
@@ -853,10 +864,18 @@ function mapUpload({ readingPage, upload }) {
|
|
|
853
864
|
// file is the whole intended state. The lines go to pages.resolveDraft, the
|
|
854
865
|
// autosave's own resolver, which drops unchanged lines and takes every target
|
|
855
866
|
// from the reading.
|
|
867
|
+
//
|
|
868
|
+
// A line the draft removes (task 1004696) was shown with the page's words, so
|
|
869
|
+
// those are what the file is compared with: left alone, it stays a removal
|
|
870
|
+
// ({ key, remove: true }); rewritten in Word, the rewrite wins.
|
|
856
871
|
function mergeUpload({ readingPage, texts, baseline, draft }) {
|
|
857
872
|
const draftAfter = new Map();
|
|
873
|
+
const removed = new Set();
|
|
858
874
|
if (draft && draft.reading_hash === readingPage.reading_hash) {
|
|
859
|
-
for (const l of draft.lines || [])
|
|
875
|
+
for (const l of draft.lines || []) {
|
|
876
|
+
if (l.remove) removed.add(l.key);
|
|
877
|
+
else draftAfter.set(l.key, l.after);
|
|
878
|
+
}
|
|
860
879
|
}
|
|
861
880
|
const same = (a, b) => foldQuotes(tidy(a)) === foldQuotes(tidy(b));
|
|
862
881
|
const lines = [];
|
|
@@ -867,9 +886,10 @@ function mergeUpload({ readingPage, texts, baseline, draft }) {
|
|
|
867
886
|
const uploaded = texts.has(l.key) ? tidy(pageQuotes(l.text, texts.get(l.key))) : current;
|
|
868
887
|
const changedInFile = baseline && baseline.has(l.key) ? !same(uploaded, baseline.get(l.key)) : !same(uploaded, current);
|
|
869
888
|
const after = changedInFile ? uploaded : current;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
889
|
+
const tookFile = changedInFile && !same(after, current);
|
|
890
|
+
if (tookFile) fromFile.push(l.key);
|
|
891
|
+
else if (draftAfter.has(l.key) || removed.has(l.key)) kept.push(l.key);
|
|
892
|
+
lines.push(removed.has(l.key) && !tookFile ? { key: l.key, remove: true } : { key: l.key, after });
|
|
873
893
|
}
|
|
874
894
|
return { lines, from_file: fromFile, kept_from_draft: kept };
|
|
875
895
|
}
|
|
@@ -587,7 +587,9 @@ function composeEditor({ page, readingPage, round, builderId }) {
|
|
|
587
587
|
draft = {
|
|
588
588
|
reading_hash: d.draft.reading_hash,
|
|
589
589
|
saved_at: d.draft.saved_at || null,
|
|
590
|
-
|
|
590
|
+
// A removal (task 1004696) reads as an empty `after` plus `remove`,
|
|
591
|
+
// which is how every editor holds it.
|
|
592
|
+
lines: d.draft.lines.filter((l) => keys.has(l.key)).map((l) => (l.remove ? { key: l.key, after: '', remove: true } : { key: l.key, after: l.after })),
|
|
591
593
|
};
|
|
592
594
|
} else {
|
|
593
595
|
olderReading = true;
|
|
@@ -115,7 +115,9 @@ function composeDraftBlock(d) {
|
|
|
115
115
|
page_id: d.page_id,
|
|
116
116
|
reading_hash: d.reading_hash,
|
|
117
117
|
saved_at: d.saved_at,
|
|
118
|
-
|
|
118
|
+
// `remove` is written only on a removal (task 1004696), so every draft
|
|
119
|
+
// that removes nothing keeps its bytes.
|
|
120
|
+
lines: (d.lines || []).map((l) => ({ key: l.key, section: l.section, before: l.before, after: l.after, placement: l.placement, ...(l.remove ? { remove: true } : {}) })),
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -266,6 +268,12 @@ function replaceBlock(markdown, fence, block) {
|
|
|
266
268
|
// text is not a change and is dropped, so only changed lines are stored (D4).
|
|
267
269
|
// Placeholder arithmetic is NOT checked here: an autosave lands mid-sentence,
|
|
268
270
|
// and the check belongs to the submit (and again to the applier).
|
|
271
|
+
//
|
|
272
|
+
// A line sent as { key, remove: true } is the artist recommending the page stop
|
|
273
|
+
// showing it (task 1004696). It is stored with `after` empty and `remove: true`;
|
|
274
|
+
// an empty `after` WITHOUT the flag is still line_empty, so no client removes a
|
|
275
|
+
// line by accident. Removing words is a code change, so the submit hands every
|
|
276
|
+
// removal to an engineer (freezeBatch).
|
|
269
277
|
function resolveDraft({ readingPage, body, savedAt }) {
|
|
270
278
|
if (!readingPage || !Array.isArray(readingPage.lines)) return fail('reading_missing');
|
|
271
279
|
const b = body || {};
|
|
@@ -282,11 +290,19 @@ function resolveDraft({ readingPage, body, savedAt }) {
|
|
|
282
290
|
const out = [];
|
|
283
291
|
for (const raw of list) {
|
|
284
292
|
const key = raw && typeof raw.key === 'string' ? raw.key : '';
|
|
285
|
-
|
|
293
|
+
const remove = !!raw && raw.remove === true;
|
|
294
|
+
const malformed = !LINE_KEY_RE.test(key)
|
|
295
|
+
|| (raw.remove !== undefined && typeof raw.remove !== 'boolean')
|
|
296
|
+
|| (remove ? (raw.after !== undefined && tidy(raw.after) !== '') : typeof raw.after !== 'string');
|
|
297
|
+
if (malformed) return fail('line_malformed', { line: raw == null ? null : raw });
|
|
286
298
|
if (seen.has(key)) return fail('duplicate_line', { key });
|
|
287
299
|
seen.add(key);
|
|
288
300
|
const at = byKey.get(key);
|
|
289
301
|
if (!at) { unknown.push(key); continue; }
|
|
302
|
+
if (remove) {
|
|
303
|
+
out.push({ key, section: at.section == null ? null : at.section, before: at.text, after: '', placement: at.placement, remove: true });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
290
306
|
const after = tidy(raw.after);
|
|
291
307
|
if (after === tidy(at.text)) continue;
|
|
292
308
|
if (!after) { empty.push(key); continue; }
|
|
@@ -307,7 +323,7 @@ function resolveDraft({ readingPage, body, savedAt }) {
|
|
|
307
323
|
function sameDraft(saved, next) {
|
|
308
324
|
if (!saved || !next) return false;
|
|
309
325
|
const pick = (d) => JSON.stringify([d.page_id, d.reading_hash,
|
|
310
|
-
(d.lines || []).map((l) => [l.key, l.section == null ? null : l.section, l.before, l.after, l.placement])]);
|
|
326
|
+
(d.lines || []).map((l) => [l.key, l.section == null ? null : l.section, l.before, l.after, l.placement, !!l.remove])]);
|
|
311
327
|
return pick(saved) === pick(next);
|
|
312
328
|
}
|
|
313
329
|
|
|
@@ -356,7 +372,8 @@ function goesToEngineer(line) {
|
|
|
356
372
|
// frozen: the artist fixes those lines and submits again. `batch` is the
|
|
357
373
|
// `page-tweak` block's content without its `unplaced_tasks` (the lifecycle
|
|
358
374
|
// transaction fills those in once the engineer tasks exist). `unplaced` is the
|
|
359
|
-
// lines taken out of it
|
|
375
|
+
// lines taken out of it: the rewrites the applier cannot place, and every
|
|
376
|
+
// removal (`remove: true`).
|
|
360
377
|
function freezeBatch({ readingPage, draft, submittedAt }) {
|
|
361
378
|
if (!readingPage || !Array.isArray(readingPage.lines)) return fail('reading_missing');
|
|
362
379
|
if (!draft || !Array.isArray(draft.lines) || !draft.lines.length) return fail('nothing_to_submit');
|
|
@@ -371,6 +388,9 @@ function freezeBatch({ readingPage, draft, submittedAt }) {
|
|
|
371
388
|
if (keys.has(at.key)) { refused.push({ key: d.key, section: d.section == null ? null : d.section, before: d.before, code: 'ambiguous_target' }); continue; }
|
|
372
389
|
keys.add(at.key);
|
|
373
390
|
const line = { key: at.key, section: at.section == null ? null : at.section, before: at.text, after: d.after, placement: at.placement };
|
|
391
|
+
// A removal is a code change wherever the line lives, so it never joins
|
|
392
|
+
// the applier's batch (task 1004696).
|
|
393
|
+
if (d.remove) { unplaced.push({ ...line, after: '', remove: true }); continue; }
|
|
374
394
|
if (goesToEngineer(at)) { unplaced.push(line); continue; }
|
|
375
395
|
const want = placeholderCount(at.text);
|
|
376
396
|
const got = placeholderCount(d.after);
|
|
@@ -388,7 +408,8 @@ function freezeBatch({ readingPage, draft, submittedAt }) {
|
|
|
388
408
|
}
|
|
389
409
|
|
|
390
410
|
// The engineer task an unplaced rewrite files (ADR 0341 D4): one per line,
|
|
391
|
-
// keyed so a repeated submit finds the task it already filed.
|
|
411
|
+
// keyed so a repeated submit finds the task it already filed. A removal
|
|
412
|
+
// (task 1004696) files the same way, worded as a removal.
|
|
392
413
|
function unplacedSourceRef(roundTaskId, key) {
|
|
393
414
|
return `${UNPLACED_SOURCE}/${roundTaskId}/${key}`;
|
|
394
415
|
}
|
|
@@ -399,10 +420,28 @@ function clip(text, n) {
|
|
|
399
420
|
}
|
|
400
421
|
|
|
401
422
|
function unplacedTaskTitle(page, line) {
|
|
423
|
+
if (line.remove) return `Remove "${clip(line.before, 60)}" (${page.id})`;
|
|
402
424
|
return `Place and reword "${clip(line.before, 60)}" (${page.id})`;
|
|
403
425
|
}
|
|
404
426
|
|
|
427
|
+
function removalTaskBody(page, line, roundTaskId) {
|
|
428
|
+
const shared = line.placement === 'shared'
|
|
429
|
+
? ['It is shared shell text: it shows on every page of the surface, so removing it removes it everywhere.', '']
|
|
430
|
+
: [];
|
|
431
|
+
return [
|
|
432
|
+
`An artist recommends that **${String(page.title || page.id)}** (\`${page.id}\`${page.path ? `, ${page.path}` : ''}) stop showing a line, from the page's tweak round. Removing words from a page is a code change, so it comes to an engineer (task 1004696).`,
|
|
433
|
+
'',
|
|
434
|
+
...shared,
|
|
435
|
+
`- Section: ${line.section == null ? '(none)' : line.section}`,
|
|
436
|
+
`- Line: ${line.key} in the page reading`,
|
|
437
|
+
`- It says now: ${JSON.stringify(line.before)}`,
|
|
438
|
+
'',
|
|
439
|
+
`Find where this text is rendered and take it out, along with any element or spacing it would leave empty. If removing it would lose something a reader needs, say so on this task instead. The page's tweak round (task ${roundTaskId}) does not wait for this.`,
|
|
440
|
+
].join('\n');
|
|
441
|
+
}
|
|
442
|
+
|
|
405
443
|
function unplacedTaskBody(page, line, roundTaskId) {
|
|
444
|
+
if (line.remove) return removalTaskBody(page, line, roundTaskId);
|
|
406
445
|
const where = line.placement === 'shared'
|
|
407
446
|
? 'It is shared shell text (it shows on every page of the surface), and the copy registry has no row for it.'
|
|
408
447
|
: 'The copy registry cannot place it: the string is built somewhere the inventory does not read (data from an API, a string assembled in code, or markup the extractors miss).';
|
|
@@ -114,7 +114,8 @@ const ASK_BODY_SCHEMA = Object.freeze({
|
|
|
114
114
|
reason: { required: true, type: 'string', maxLength: flags.MAX_REASON_CHARS, minLength: 1 },
|
|
115
115
|
});
|
|
116
116
|
// The draft autosave (task 1004319 / TW08): the reading the editor was built
|
|
117
|
-
// on, and every changed line as { key, after }
|
|
117
|
+
// on, and every changed line as { key, after }, or { key, remove: true } for a
|
|
118
|
+
// line the artist recommends removing (task 1004696). Nothing else, and in
|
|
118
119
|
// particular no `before`, `section`, `placement` or file: every fact about a
|
|
119
120
|
// target is resolved from the page reading (pages.resolveDraft).
|
|
120
121
|
const DRAFT_BODY_SCHEMA = Object.freeze({
|
|
@@ -155,10 +156,10 @@ const PAGE_REFUSAL_STATUS = Object.freeze({
|
|
|
155
156
|
const PAGE_REFUSAL_MEANS = Object.freeze({
|
|
156
157
|
reading_moved: 'The page changed since the editor loaded it, so a line key may now mean a different line. Reload the page to get the current reading; nothing was saved.',
|
|
157
158
|
unknown_line: 'The page reading has no line with that key. Reload the editor.',
|
|
158
|
-
line_empty: 'A line cannot be rewritten to nothing.
|
|
159
|
+
line_empty: 'A line cannot be rewritten to nothing. To recommend the page stop showing it, send the line as { key, remove: true }: removing text is a code change, so the submit hands it to an engineer.',
|
|
159
160
|
line_too_long: `A line holds at most ${pages.MAX_LINE_CHARS} characters.`,
|
|
160
161
|
lines_refused: 'Some lines cannot be frozen as written: the page no longer shows them (target_gone), shows the same words in more than one place (ambiguous_target), or the rewrite dropped or added a {…} hole (placeholder_mismatch). Fix or remove each named line and submit again; nothing was submitted.',
|
|
161
|
-
nothing_to_submit: 'The draft has no rewritten lines yet.',
|
|
162
|
+
nothing_to_submit: 'The draft has no rewritten or removed lines yet.',
|
|
162
163
|
structure_changed: 'The file\'s lines no longer match the page: a paragraph was added, removed or moved, or a heading changed. Each one is listed. Put them back (one paragraph per line, in the page\'s order) or download the page again, and upload once more; nothing was saved.',
|
|
163
164
|
not_a_docx: 'Upload the .docx file this page downloaded, as the request body. Nothing was saved.',
|
|
164
165
|
upload_too_large: `A page file is at most ${Math.round(docx.MAX_UPLOAD_BYTES / (1024 * 1024))} MB. Nothing was saved.`,
|
|
@@ -246,6 +246,44 @@ test('a line changed in Word wins over the draft; without the file\'s baseline t
|
|
|
246
246
|
assert.deepEqual(merged.from_file, ['L0002']);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// A line the draft removes (task 1004696).
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
const removal = draftOf([
|
|
254
|
+
{ key: 'L0002', section: 'greeting', before: 'Welcome back', after: 'Hello again', placement: 'placed' },
|
|
255
|
+
{ key: 'L0006', section: 'footer', before: 'Resume', after: '', placement: 'placed', remove: true },
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
test('the download shows a removed line with the page\'s words and a margin comment, never as an empty paragraph', () => {
|
|
259
|
+
const files = unzipAll(download(removal));
|
|
260
|
+
assert.match(files.get('word/document.xml'), />Resume</);
|
|
261
|
+
assert.match(files.get('word/comments.xml'), /marked to be removed: an engineer takes it out\. Rewrite it here to keep it/);
|
|
262
|
+
assert.equal(upload(download(removal)).baseline.get('L0006'), 'Resume', 'the file records the words it showed');
|
|
263
|
+
assert.doesNotMatch(unzipAll(download()).get('word/comments.xml'), /marked to be removed/, 'no removal, no such comment');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('a removed line left alone in Word stays a removal through the upload', () => {
|
|
267
|
+
const edited = resave(download(removal), { document: (xml) => typeInto(xml, 'L0004', wordRuns(['Nothing waits'])) });
|
|
268
|
+
const { merged } = roundTrip(edited, removal);
|
|
269
|
+
assert.deepEqual(merged.lines.find((l) => l.key === 'L0006'), { key: 'L0006', remove: true });
|
|
270
|
+
assert.deepEqual(merged.kept_from_draft, ['L0002', 'L0006']);
|
|
271
|
+
const resolved = pages.resolveDraft({ readingPage: READING, body: { reading_hash: 'h-now', lines: merged.lines }, savedAt: 't' });
|
|
272
|
+
assert.equal(resolved.ok, true, JSON.stringify(resolved));
|
|
273
|
+
assert.deepEqual(resolved.draft.lines.map((l) => [l.key, l.after, !!l.remove]),
|
|
274
|
+
[['L0002', 'Hello again', false], ['L0004', 'Nothing waits', false], ['L0006', '', true]]);
|
|
275
|
+
// Without the file's record of what it showed, an unchanged line is still the page's words: the removal stays.
|
|
276
|
+
const noBaseline = roundTrip(resave(download(removal), { drop: ['customXml/item1.xml'] }), removal).merged;
|
|
277
|
+
assert.deepEqual(noBaseline.lines.find((l) => l.key === 'L0006'), { key: 'L0006', remove: true });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('a removed line rewritten in Word becomes that rewrite', () => {
|
|
281
|
+
const edited = resave(download(removal), { document: (xml) => typeInto(xml, 'L0006', wordRuns(['Pick up where you were'])) });
|
|
282
|
+
const { merged } = roundTrip(edited, removal);
|
|
283
|
+
assert.deepEqual(merged.lines.find((l) => l.key === 'L0006'), { key: 'L0006', after: 'Pick up where you were' });
|
|
284
|
+
assert.deepEqual(merged.from_file, ['L0006']);
|
|
285
|
+
});
|
|
286
|
+
|
|
249
287
|
// ---------------------------------------------------------------------------
|
|
250
288
|
// Structure changes are refused, by name.
|
|
251
289
|
// ---------------------------------------------------------------------------
|
|
@@ -198,5 +198,84 @@ test('the engineer task carries the page, the line and the wording, keyed per ro
|
|
|
198
198
|
}
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Removing a line (task 1004696): the artist recommends the page stop showing
|
|
203
|
+
// it. Saved as { key, remove: true }; at submit it goes to an engineer, never
|
|
204
|
+
// into the applier's batch.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
test('a removal is saved as remove: true with an empty after; an empty after WITHOUT the flag is still line_empty', () => {
|
|
208
|
+
const r = pages.resolveDraft({ readingPage: READING, savedAt: SAVED_AT, body: body([
|
|
209
|
+
{ key: 'L0006', remove: true },
|
|
210
|
+
{ key: 'L0003', after: 'Build it here', remove: false },
|
|
211
|
+
]) });
|
|
212
|
+
assert.equal(r.ok, true, JSON.stringify(r));
|
|
213
|
+
assert.deepEqual(r.draft.lines, [
|
|
214
|
+
{ key: 'L0003', section: 'hero', before: 'Build here', after: 'Build it here', placement: 'placed' },
|
|
215
|
+
{ key: 'L0006', section: 'footer', before: 'Resume', after: '', placement: 'placed', remove: true },
|
|
216
|
+
]);
|
|
217
|
+
const code = (lines) => pages.resolveDraft({ readingPage: READING, savedAt: SAVED_AT, body: body(lines) }).code;
|
|
218
|
+
assert.equal(code([{ key: 'L0006', after: '' }]), 'line_empty', 'no client removes a line by accident');
|
|
219
|
+
assert.equal(code([{ key: 'L0006', remove: true, after: 'Keep me' }]), 'line_malformed', 'a removal carries no words');
|
|
220
|
+
assert.equal(code([{ key: 'L0006', remove: 'yes' }]), 'line_malformed');
|
|
221
|
+
assert.equal(code([{ key: 'L0099', remove: true }]), 'unknown_line');
|
|
222
|
+
assert.equal(pages.resolveDraft({ readingPage: READING, savedAt: SAVED_AT, body: body([{ key: 'L0006', remove: true, after: ' ' }]) }).ok, true, 'an empty after beside the flag is the same removal');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('the stored block keeps a removal, and a draft with none keeps its bytes', () => {
|
|
226
|
+
const plain = draftOf([{ key: 'L0003', after: 'Build it here' }]);
|
|
227
|
+
assert.doesNotMatch(pages.composeDraftBlock(plain), /remove/, 'no removal, no new field');
|
|
228
|
+
const withRemoval = draftOf([{ key: 'L0003', after: 'Build it here' }, { key: 'L0006', remove: true }]);
|
|
229
|
+
const parsed = pages.parseDraftBlock(pages.composeDraftBlock(withRemoval));
|
|
230
|
+
assert.equal(parsed.ok, true);
|
|
231
|
+
assert.deepEqual(parsed.draft.lines[1], { key: 'L0006', section: 'footer', before: 'Resume', after: '', placement: 'placed', remove: true });
|
|
232
|
+
assert.equal(pages.sameDraft(parsed.draft, withRemoval), true);
|
|
233
|
+
assert.equal(pages.sameDraft(withRemoval, draftOf([{ key: 'L0003', after: 'Build it here' }, { key: 'L0006', after: 'Resume now' }])), false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('at submit a removal goes to an engineer, wherever the line lives, and never into the batch', () => {
|
|
237
|
+
const draft = draftOf([
|
|
238
|
+
{ key: 'L0001', remove: true }, // shared, with a registry row: the applier could place it, but removing is code
|
|
239
|
+
{ key: 'L0003', after: 'Build it here' },
|
|
240
|
+
{ key: 'L0004', remove: true }, // a line with holes: no placeholder arithmetic for a removal
|
|
241
|
+
]);
|
|
242
|
+
const f = pages.freezeBatch({ readingPage: READING, draft, submittedAt: 'S' });
|
|
243
|
+
assert.equal(f.ok, true, JSON.stringify(f));
|
|
244
|
+
assert.deepEqual(f.batch.lines.map((l) => l.key), ['L0003']);
|
|
245
|
+
assert.deepEqual(f.unplaced.map((l) => [l.key, l.before, l.after, l.remove]), [
|
|
246
|
+
['L0001', 'Skip to content', '', true],
|
|
247
|
+
['L0004', '{…} of {…} shipped', '', true],
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('a draft of only removals still submits: its batch is empty and every line goes to an engineer', () => {
|
|
252
|
+
const f = pages.freezeBatch({ readingPage: READING, draft: draftOf([{ key: 'L0006', remove: true }]), submittedAt: 'S' });
|
|
253
|
+
assert.equal(f.ok, true, JSON.stringify(f));
|
|
254
|
+
assert.deepEqual(f.batch.lines, []);
|
|
255
|
+
assert.deepEqual(f.unplaced.map((l) => l.key), ['L0006']);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('a removal the page no longer shows is refused target_gone, like any line', () => {
|
|
259
|
+
const draft = draftOf([{ key: 'L0006', remove: true }]);
|
|
260
|
+
const moved = { ...READING, reading_hash: 'h-next', lines: READING.lines.filter((l) => l.key !== 'L0006') };
|
|
261
|
+
const f = pages.freezeBatch({ readingPage: moved, draft, submittedAt: 'S' });
|
|
262
|
+
assert.equal(f.code, 'lines_refused');
|
|
263
|
+
assert.deepEqual(f.detail.refused.map((x) => [x.key, x.code]), [['L0006', 'target_gone']]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('the removal task is worded as a removal: what to take out, where, and that the round does not wait', () => {
|
|
267
|
+
const page = { id: 'landing:index', title: 'Home', path: '/' };
|
|
268
|
+
const line = { key: 'L0006', section: 'footer', before: 'Resume', after: '', placement: 'placed', remove: true };
|
|
269
|
+
assert.equal(pages.unplacedTaskTitle(page, line), 'Remove "Resume" (landing:index)');
|
|
270
|
+
const b = pages.unplacedTaskBody(page, line, '5001');
|
|
271
|
+
for (const want of ['landing:index', 'Home', 'L0006', 'footer', '"Resume"', 'take it out', 'task 5001', 'does not wait']) {
|
|
272
|
+
assert.ok(b.includes(want), `the body names ${want}`);
|
|
273
|
+
}
|
|
274
|
+
assert.doesNotMatch(b, /artist's wording/, 'there is no new wording to apply');
|
|
275
|
+
const shared = pages.unplacedTaskBody(page, { ...line, placement: 'shared' }, '5001');
|
|
276
|
+
assert.match(shared, /removes it everywhere/);
|
|
277
|
+
assert.equal(pages.unplacedSourceRef('5001', 'L0006'), 'page-tweak-unplaced/5001/L0006', 'keyed like any engineer line, so a repeated submit finds it');
|
|
278
|
+
});
|
|
279
|
+
|
|
201
280
|
console.log(`\ncopy_page_draft: ${passed} passed, ${failed} failed`);
|
|
202
281
|
if (failed) process.exit(1);
|