@bongos/core 1.19.1062 → 1.19.1064
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 +123 -48
- package/.claude/skills/grade-recover/SKILL.md +1 -0
- package/.claude/skills/tweak/SKILL.md +70 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +12 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +301 -3
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +44 -7
- package/docs/copy-registry.json +393 -26
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +36 -4
- package/docs/page-readings.json +34 -1
- package/modules/copy-desk/page-status.js +63 -0
- package/modules/copy-desk/pages.js +1 -0
- package/modules/copy-desk/routes/copy-desk.js +30 -0
- package/modules/hall-ui/public/tweak-editor-lib.js +137 -0
- package/modules/hall-ui/public/tweak-editor.css +437 -0
- package/modules/hall-ui/public/tweak-editor.html +111 -0
- package/modules/hall-ui/public/tweak-editor.js +447 -0
- package/modules/hall-ui/public/tweak-editor.states.json +99 -0
- package/modules/hall-ui/records/tweak-editor.md +32 -0
- package/modules/lifecycle/db-grade.js +16 -0
- package/modules/lifecycle/migrations/lifecycle_013_task_visual_slots.sql +52 -0
- package/modules/lifecycle/page-tweak-hold.js +74 -0
- package/modules/lifecycle/page-tweak-reads.js +33 -1
- package/modules/lifecycle/routes/tasks.js +4 -2
- package/modules/lifecycle/routes/visuals.js +82 -0
- package/modules/lifecycle/task-visual-db.js +40 -1
- package/modules/lifecycle/task-visuals.js +24 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/copy-apply.js +277 -1
- package/scripts/gds/page-reader.js +2 -0
- package/scripts/gds/ship-finish.js +27 -2
- package/scripts/gds/ship-flow.js +17 -1
- package/scripts/gds/ship-land.js +30 -7
- package/scripts/gds/ship-merge.js +43 -15
- package/scripts/gds/strand-watch.js +41 -1
- package/scripts/gds/task.js +23 -5
- package/scripts/gds/tweak-renders.js +200 -0
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_editor.mjs +181 -0
- package/tests/hall_audit.mjs +5 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_tweak_editor.mjs +432 -0
- package/tests/page_tweak_hold.mjs +265 -0
- package/tests/publish_status_branch.mjs +23 -0
- package/tests/task_visual_slots.mjs +161 -0
- package/tests/tweak_batch_apply.mjs +265 -0
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
// arithmetic does not hold, or the file is outside a registry surface. None of
|
|
31
31
|
// them means the wording was wrong — they mean a person should look.
|
|
32
32
|
//
|
|
33
|
-
// Run: node scripts/gds/copy-apply.js
|
|
33
|
+
// Run: node scripts/gds/copy-apply.js --batch (a PAGE TWEAK round you hold: every line, task 1004322)
|
|
34
|
+
// node scripts/gds/copy-apply.js --batch --batch-file round.md --dry-run (a local round; writes nothing)
|
|
35
|
+
// node scripts/gds/copy-apply.js (the task you are HOLDING — the normal call)
|
|
34
36
|
// node scripts/gds/copy-apply.js --dry-run (say what would change, write nothing)
|
|
35
37
|
// node scripts/gds/copy-apply.js --task 1234 (a claim you hold, when you hold more than one)
|
|
36
38
|
// node scripts/gds/copy-apply.js --patch-file p.json (a local patch — used by the tests)
|
|
@@ -53,9 +55,12 @@
|
|
|
53
55
|
|
|
54
56
|
const fs = require('node:fs');
|
|
55
57
|
const path = require('node:path');
|
|
58
|
+
const { spawnSync } = require('node:child_process');
|
|
56
59
|
|
|
57
60
|
const ci = require('./copy-inventory.js');
|
|
58
61
|
const proposals = require('../../modules/copy-desk/proposals.js');
|
|
62
|
+
// The page tweak format's one reader and writer (ADR 0341 D4), for --batch.
|
|
63
|
+
const pages = require('../../modules/copy-desk/pages.js');
|
|
59
64
|
const { apiCall, arg, hasFlag, cliExit } = require('./cli-lib.js');
|
|
60
65
|
|
|
61
66
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -305,11 +310,275 @@ async function loadPatch({ taskId, patchFile }) {
|
|
|
305
310
|
return { ok: true, patch: parsed.patch, task };
|
|
306
311
|
}
|
|
307
312
|
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// --batch: a whole PAGE at once (task 1004322 / BV2.TW11; ADR 0341 D4).
|
|
316
|
+
//
|
|
317
|
+
// A page tweak's round carries a ```page-tweak``` block: every line the artist
|
|
318
|
+
// rewrote, frozen at submit, each with its reading key, its section, the text the
|
|
319
|
+
// page showed (`before`), the artist's wording (`after`) and where the registry
|
|
320
|
+
// placed it (`file`, `line`, `string_id`). --batch lands every line through the
|
|
321
|
+
// SAME rules one proposal lands by (ADR 0233): the span is re-derived from the
|
|
322
|
+
// tree as it stands, the `${expr}` holes go back in, and every doubt is a named
|
|
323
|
+
// refusal. What is new is only that there are many lines, and so:
|
|
324
|
+
//
|
|
325
|
+
// * A REFUSED LINE IS NAMED, NEVER SKIPPED. It goes in the applied block's
|
|
326
|
+
// `refused` list with its code, and the artist sees it at approval. The rest
|
|
327
|
+
// of the batch still lands (D4: "the batch lands with the lines that applied").
|
|
328
|
+
// * ORDER IS BY SOURCE POSITION, BOTTOM UP, per file. A rewrite can change how
|
|
329
|
+
// many lines a span covers, which would move every later line number; the
|
|
330
|
+
// line number is only ever a tie-breaker between duplicates, but applying from
|
|
331
|
+
// the bottom keeps even the tie-breaker true for every line still to come.
|
|
332
|
+
// * A READING LINE MAY BE PART OF A REGISTRY STRING. The page reader places a
|
|
333
|
+
// visible fragment on the registry row that contains it (a word inside a
|
|
334
|
+
// sentence built around a hole), so `before` is either the row's whole text
|
|
335
|
+
// or appears in it exactly once; the rewrite replaces that one occurrence.
|
|
336
|
+
// Anything else is `no_exact_span` (absent) or `ambiguous_target` (twice).
|
|
337
|
+
// * TWO LINES ON ONE STRING compose: the second is resolved against the text the
|
|
338
|
+
// first one left, never against the stale registry row.
|
|
339
|
+
//
|
|
340
|
+
// Then the command regenerates the registry, re-reads the changed pages
|
|
341
|
+
// (page-reader.js --stale, which opens a browser) and writes the
|
|
342
|
+
// ```page-tweak-applied``` block from the NEW reading: reading_hash_after and
|
|
343
|
+
// lines_after (the page's non-shared lines, the baseline drift is measured from,
|
|
344
|
+
// D8) plus the refused list.
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
const REFUSAL_CODES = Object.freeze(['target_gone', 'ambiguous_target', 'no_exact_span', 'placeholder_mismatch', 'path_outside_surface', 'unknown_surface', 'file_unreadable']);
|
|
348
|
+
|
|
349
|
+
function countOccurrences(haystack, needle) {
|
|
350
|
+
if (!needle) return 0;
|
|
351
|
+
let n = 0;
|
|
352
|
+
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) n += 1;
|
|
353
|
+
return n;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Which registry row a batch line means. A string id is a hash of (surface,
|
|
357
|
+
// text), so the same sentence in two files shares one id: the row in the line's
|
|
358
|
+
// own file wins, the nearest to its line if that file has it twice. Pure.
|
|
359
|
+
function registryRowFor(line, registryById) {
|
|
360
|
+
const rows = (line && line.string_id && registryById.get(line.string_id)) || [];
|
|
361
|
+
if (rows.length <= 1) return rows[0] || null;
|
|
362
|
+
const inFile = rows.filter((e) => e.file === line.file);
|
|
363
|
+
const pool = inFile.length ? inFile : rows;
|
|
364
|
+
if (pool.length === 1) return pool[0];
|
|
365
|
+
const near = (e) => Math.abs((e.line || 0) - (line.line || 0));
|
|
366
|
+
return pool.slice().sort((a, b) => near(a) - near(b))[0];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const rowKey = (e) => `${e.id}|${e.file}|${e.line}`;
|
|
370
|
+
|
|
371
|
+
// One batch line -> the single-proposal patch shape (proposals.js), resolved
|
|
372
|
+
// against the registry row its string_id names. `texts` carries a row's text as
|
|
373
|
+
// earlier lines of this batch left it. Pure.
|
|
374
|
+
function patchForBatchLine(line, registryById, texts = new Map()) {
|
|
375
|
+
const entry = registryRowFor(line, registryById);
|
|
376
|
+
if (!entry) {
|
|
377
|
+
return { ok: false, code: 'target_gone', detail: { string_id: line && line.string_id, what_this_means: 'The copy registry no longer has the string this line was placed on: the page changed after the artist submitted.' } };
|
|
378
|
+
}
|
|
379
|
+
const want = proposals.placeholderCount(line.before);
|
|
380
|
+
const got = proposals.placeholderCount(line.after);
|
|
381
|
+
if (want !== got) {
|
|
382
|
+
return { ok: false, code: 'placeholder_mismatch', detail: { expected: want, got, what_this_means: 'The rewrite does not keep the values the page drops into this line.' } };
|
|
383
|
+
}
|
|
384
|
+
const current = texts.has(rowKey(entry)) ? texts.get(rowKey(entry)) : entry.text;
|
|
385
|
+
let proposed;
|
|
386
|
+
if (current === line.before) proposed = line.after;
|
|
387
|
+
else {
|
|
388
|
+
const n = countOccurrences(current, line.before);
|
|
389
|
+
if (n === 0) return { ok: false, code: 'no_exact_span', detail: { file: entry.file, looked_for: line.before, in: current, what_this_means: 'The line the page shows cannot be found inside the string the registry placed it on, so no exact span exists.' } };
|
|
390
|
+
if (n > 1) return { ok: false, code: 'ambiguous_target', detail: { file: entry.file, occurrences: n, what_this_means: 'The line appears more than once inside its string, and the batch cannot say which one it meant.' } };
|
|
391
|
+
proposed = current.replace(line.before, () => line.after);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
patch: { row: rowKey(entry), surface: entry.surface, file: entry.file, line: entry.line, origin: entry.origin, current, proposed },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// applyBatchToSources({ batch, registry, readSource }) -> { applied, refused, sources }
|
|
400
|
+
//
|
|
401
|
+
// The whole batch, as a pure function of the registry and the files' current
|
|
402
|
+
// contents (readSource(rel) returns a file's text or throws). Writes nothing.
|
|
403
|
+
// `applied` and `refused` come back in PAGE order (the batch's own order);
|
|
404
|
+
// `sources` maps each changed file to its new contents.
|
|
405
|
+
function applyBatchToSources({ batch, registry, readSource }) {
|
|
406
|
+
const registryById = new Map();
|
|
407
|
+
for (const e of (registry && registry.entries) || []) {
|
|
408
|
+
if (!registryById.has(e.id)) registryById.set(e.id, []);
|
|
409
|
+
registryById.get(e.id).push(e);
|
|
410
|
+
}
|
|
411
|
+
const lines = ((batch && batch.lines) || []).map((l, i) => ({ ...l, order: i }));
|
|
412
|
+
const byId = (l) => registryRowFor(l, registryById);
|
|
413
|
+
// Bottom up per file, by the registry's line (the tree's truth, not the batch's).
|
|
414
|
+
const work = lines.slice().sort((a, b) => {
|
|
415
|
+
const ea = byId(a), eb = byId(b);
|
|
416
|
+
const fa = ea ? ea.file : '', fb = eb ? eb.file : '';
|
|
417
|
+
if (fa !== fb) return fa < fb ? -1 : 1;
|
|
418
|
+
return ((eb && eb.line) || 0) - ((ea && ea.line) || 0) || a.order - b.order;
|
|
419
|
+
});
|
|
420
|
+
const sources = new Map();
|
|
421
|
+
const texts = new Map();
|
|
422
|
+
const applied = [];
|
|
423
|
+
const refused = [];
|
|
424
|
+
const refuse = (l, r) => refused.push({ key: l.key, section: l.section == null ? null : l.section, before: l.before, code: r.code, detail: r.detail || {} });
|
|
425
|
+
for (const l of work) {
|
|
426
|
+
const p = patchForBatchLine(l, registryById, texts);
|
|
427
|
+
if (!p.ok) { refuse(l, p); continue; }
|
|
428
|
+
const guard = checkPatchPath(p.patch);
|
|
429
|
+
if (!guard.ok) { refuse(l, guard); continue; }
|
|
430
|
+
let source = sources.get(guard.rel);
|
|
431
|
+
if (source === undefined) {
|
|
432
|
+
try { source = readSource(guard.rel); } catch (err) { refuse(l, { code: 'file_unreadable', detail: { file: guard.rel, reason: err.message } }); continue; }
|
|
433
|
+
}
|
|
434
|
+
const out = applyPatchToSource(source, guard.rel, p.patch);
|
|
435
|
+
if (!out.ok) { refuse(l, out); continue; }
|
|
436
|
+
sources.set(guard.rel, out.source);
|
|
437
|
+
texts.set(p.patch.row, p.patch.proposed);
|
|
438
|
+
applied.push({ key: l.key, section: l.section == null ? null : l.section, file: guard.rel, line: out.line, before: out.before, after: out.after, proposed: p.patch.proposed, order: l.order });
|
|
439
|
+
}
|
|
440
|
+
const byOrder = (x, y) => x.order - y.order;
|
|
441
|
+
const orderOf = new Map(lines.map((l) => [l.key, l.order]));
|
|
442
|
+
refused.sort((x, y) => orderOf.get(x.key) - orderOf.get(y.key));
|
|
443
|
+
return { applied: applied.sort(byOrder).map(({ order, ...rest }) => rest), refused, sources };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// The ```page-tweak-applied``` block's content, from the page's NEW reading.
|
|
447
|
+
// lines_after is the non-shared lines' texts: shared shell lines are left out of
|
|
448
|
+
// drift on both sides (D8, builder pick 7), so they must be left out here too or
|
|
449
|
+
// every one of them would read as "removed" on the next drift check.
|
|
450
|
+
function appliedBlockFor({ pageId, readingPage, refused, appliedAt }) {
|
|
451
|
+
return {
|
|
452
|
+
page_id: pageId,
|
|
453
|
+
applied_at: appliedAt,
|
|
454
|
+
reading_hash_after: readingPage ? readingPage.reading_hash : null,
|
|
455
|
+
lines_after: readingPage ? (readingPage.lines || []).filter((l) => l && l.placement !== 'shared').map((l) => String(l.text)) : [],
|
|
456
|
+
refused: (refused || []).map((x) => ({ key: x.key, code: x.code })),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Load the round's batch: the task you hold, or a local file (the tests' and the
|
|
461
|
+
// dry run's entry point). A file may hold a whole round description or a bare
|
|
462
|
+
// batch object; either way it goes through pages.parseBatchBlock, the one reader.
|
|
463
|
+
async function loadBatch({ taskId, batchFile, readFile = (p) => fs.readFileSync(p, 'utf8'), api = apiCall }) {
|
|
464
|
+
let description;
|
|
465
|
+
if (batchFile) {
|
|
466
|
+
let raw;
|
|
467
|
+
try { raw = readFile(path.resolve(process.cwd(), batchFile)); } catch (err) { return { ok: false, code: 'batch_file_unreadable', detail: { reason: err.message } }; }
|
|
468
|
+
description = raw.trim().startsWith('{') ? ['```' + pages.FENCES.batch, raw.trim(), '```'].join('\n') : raw;
|
|
469
|
+
} else {
|
|
470
|
+
const res = await api('GET', `/api/bongos/tasks/${taskId}`);
|
|
471
|
+
if (!res.ok) return { ok: false, code: 'task_unreadable', detail: { task: taskId, status: res.status } };
|
|
472
|
+
const task = (res.data && res.data.task) || {};
|
|
473
|
+
if (task.source !== pages.SOURCE) return { ok: false, code: 'not_a_page_tweak', detail: { task: taskId, source: task.source || null, what_this_means: '--batch applies a page tweak round. For a single copy proposal, run this without --batch.' } };
|
|
474
|
+
description = task.description || '';
|
|
475
|
+
}
|
|
476
|
+
const parsed = pages.parseBatchBlock(description);
|
|
477
|
+
if (!parsed.ok) return parsed;
|
|
478
|
+
return { ok: true, batch: parsed.batch, description };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Re-read the pages whose files the batch changed (page-reader.js --stale: this
|
|
482
|
+
// page, and any other page sharing a changed file). Opens a browser.
|
|
483
|
+
function rereadStalePages({ spawn = spawnSync } = {}) {
|
|
484
|
+
const r = spawn(process.execPath, [path.join(__dirname, 'page-reader.js'), '--stale'], { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
485
|
+
return r.status === 0 ? { ok: true } : { ok: false, code: r.status === 3 ? 'no_browser' : 'reread_failed', detail: { exit: r.status } };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function readingFor(pageId, { readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
|
|
489
|
+
try {
|
|
490
|
+
const doc = JSON.parse(readFile(path.join(REPO_ROOT, 'docs', 'page-readings.json')));
|
|
491
|
+
return (doc.pages || []).find((p) => p.id === pageId) || null;
|
|
492
|
+
} catch { return null; }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Write the applied block into the round's description (replacing an earlier one:
|
|
496
|
+
// a re-apply after a send-back supersedes it), through PATCH /tasks/:id.
|
|
497
|
+
async function writeAppliedBlock({ taskId, description, block, api = apiCall }) {
|
|
498
|
+
const next = pages.replaceBlock(description, pages.FENCES.applied, pages.composeAppliedBlock(block));
|
|
499
|
+
if (!next.ok) return next;
|
|
500
|
+
const res = await api('PATCH', `/api/bongos/tasks/${taskId}`, { description: next.description });
|
|
501
|
+
if (!res.ok) return { ok: false, code: 'task_write_failed', detail: { task: taskId, status: res.status, what_this_means: 'The files are applied, but the round does not yet record it. PATCH /tasks/:id needs the task.edit permission (Metic+).' } };
|
|
502
|
+
return { ok: true };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// The --batch command. `deps` is the test seam: every read, write, re-read and API
|
|
506
|
+
// call can be replaced, so the whole path runs against a fixture round.
|
|
507
|
+
async function batchMain({ taskId, batchFile, dryRun, now = () => new Date().toISOString() }, deps = {}) {
|
|
508
|
+
const readFile = deps.readFile || ((p) => fs.readFileSync(p, 'utf8'));
|
|
509
|
+
const writeFile = deps.writeFile || ((p, s) => fs.writeFileSync(p, s));
|
|
510
|
+
const api = deps.api || apiCall;
|
|
511
|
+
const log = deps.log || console.log;
|
|
512
|
+
const loaded = await loadBatch({ taskId, batchFile, readFile, api });
|
|
513
|
+
if (!loaded.ok) return refuse(loaded);
|
|
514
|
+
const { batch } = loaded;
|
|
515
|
+
const registry = deps.registry || JSON.parse(readFile(path.join(REPO_ROOT, 'docs', 'copy-registry.json')));
|
|
516
|
+
const result = applyBatchToSources({ batch, registry, readSource: (rel) => readFile(path.join(REPO_ROOT, rel)) });
|
|
517
|
+
|
|
518
|
+
log(`copy-apply --batch: ${batch.page_id}, ${batch.lines.length} line(s): ${result.applied.length} applied, ${result.refused.length} refused.`);
|
|
519
|
+
for (const a of result.applied) {
|
|
520
|
+
log(` ${a.key} ${a.file}:${a.line}`);
|
|
521
|
+
log(` - ${a.before}`);
|
|
522
|
+
log(` + ${a.after}`);
|
|
523
|
+
}
|
|
524
|
+
for (const x of result.refused) {
|
|
525
|
+
log(` ${x.key} REFUSED (${x.code})${x.detail && x.detail.what_this_means ? `: ${x.detail.what_this_means}` : ''}`);
|
|
526
|
+
log(` line: ${JSON.stringify(x.before)}`);
|
|
527
|
+
}
|
|
528
|
+
if (dryRun) { log('copy-apply --batch: --dry-run, nothing written.'); return 0; }
|
|
529
|
+
|
|
530
|
+
for (const [rel, source] of result.sources) writeFile(path.join(REPO_ROOT, rel), source);
|
|
531
|
+
const { reg } = (deps.regenerateRegistry || (() => ci.write()))();
|
|
532
|
+
const missing = result.applied.filter((a) => !reg.entries.some((e) => e.file === a.file && e.text === a.proposed));
|
|
533
|
+
if (missing.length) {
|
|
534
|
+
console.error(`copy-apply --batch: WROTE THE FILES, but the regenerated registry does not contain the new wording for ${missing.map((m) => m.key).join(', ')}.`);
|
|
535
|
+
console.error(' An edit landed somewhere the inventory does not read as copy. Review `git diff` before doing anything else.');
|
|
536
|
+
return 3;
|
|
537
|
+
}
|
|
538
|
+
log(`copy-apply --batch: files written, registry regenerated (${reg.counts ? reg.counts.strings : reg.entries.length} strings).`);
|
|
539
|
+
|
|
540
|
+
const reread = (deps.reread || rereadStalePages)();
|
|
541
|
+
if (!reread.ok) {
|
|
542
|
+
console.error(`copy-apply --batch: the page could not be re-read (${reread.code}). The files are applied; run node scripts/gds/page-reader.js --stale, then this command again to write the applied block.`);
|
|
543
|
+
return 4;
|
|
544
|
+
}
|
|
545
|
+
const readingPage = (deps.readingFor || ((id) => readingFor(id, { readFile })))(batch.page_id);
|
|
546
|
+
const block = appliedBlockFor({ pageId: batch.page_id, readingPage, refused: result.refused, appliedAt: now() });
|
|
547
|
+
if (batchFile) {
|
|
548
|
+
log('copy-apply --batch: the page-tweak-applied block (a --batch-file run writes no task):');
|
|
549
|
+
log(pages.composeAppliedBlock(block));
|
|
550
|
+
} else {
|
|
551
|
+
const wrote = await writeAppliedBlock({ taskId, description: loaded.description, block, api });
|
|
552
|
+
if (!wrote.ok) return refuse(wrote);
|
|
553
|
+
log(`copy-apply --batch: recorded the page-tweak-applied block on task ${taskId} (reading ${block.reading_hash_after}, ${block.lines_after.length} lines, ${block.refused.length} refused).`);
|
|
554
|
+
}
|
|
555
|
+
if (!result.applied.length) {
|
|
556
|
+
console.error('copy-apply --batch: NO line applied. Every refusal is recorded; there is nothing to ship until they are resolved.');
|
|
557
|
+
return 2;
|
|
558
|
+
}
|
|
559
|
+
log(' Review with `git diff`, render the page, then ship this task the normal way (it waits for the artist).');
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
308
563
|
async function main() {
|
|
309
564
|
let taskId = arg('--task');
|
|
310
565
|
const patchFile = arg('--patch-file');
|
|
311
566
|
const dryRun = hasFlag('--dry-run');
|
|
312
567
|
|
|
568
|
+
// --batch (task 1004322): a page tweak round. The claim gate is the same one the
|
|
569
|
+
// single proposal uses; --batch-file is the local, claim-free entry point, and
|
|
570
|
+
// like --patch-file it lands nothing a task authorised.
|
|
571
|
+
if (hasFlag('--batch')) {
|
|
572
|
+
const batchFile = arg('--batch-file');
|
|
573
|
+
if (!batchFile) {
|
|
574
|
+
const held = taskId ? await claimOnTask(taskId) : await claimedTaskId();
|
|
575
|
+
if (!held.ok) return cliExit(refuse(held));
|
|
576
|
+
taskId = held.taskId;
|
|
577
|
+
console.log(`copy-apply --batch: the round you are holding — task ${taskId}`);
|
|
578
|
+
}
|
|
579
|
+
return cliExit(await batchMain({ taskId, batchFile, dryRun }));
|
|
580
|
+
}
|
|
581
|
+
|
|
313
582
|
if (!taskId && !patchFile) {
|
|
314
583
|
const held = await claimedTaskId();
|
|
315
584
|
if (!held.ok) return cliExit(refuse(held));
|
|
@@ -379,6 +648,13 @@ if (require.main === module) {
|
|
|
379
648
|
}
|
|
380
649
|
|
|
381
650
|
module.exports = {
|
|
651
|
+
// task 1004322: --batch, a page tweak round.
|
|
652
|
+
REFUSAL_CODES,
|
|
653
|
+
registryRowFor,
|
|
654
|
+
patchForBatchLine,
|
|
655
|
+
applyBatchToSources,
|
|
656
|
+
appliedBlockFor,
|
|
657
|
+
batchMain,
|
|
382
658
|
surfaceRootFor,
|
|
383
659
|
checkPatchPath,
|
|
384
660
|
locateOccurrence,
|
|
@@ -671,4 +671,6 @@ module.exports = {
|
|
|
671
671
|
filesHash, readingHash, buildMatcher, placeText, collectStrings, holeData,
|
|
672
672
|
mergeStates, assembleReading, serialize, buildDoc, checkReadings, planRenders,
|
|
673
673
|
captureInPage, mergeReadings,
|
|
674
|
+
// task 1004322: /tweak renders the page the reader reads, through the same harness.
|
|
675
|
+
startHarness, SURFACE_MODULE, PINNED_CLOCK,
|
|
674
676
|
};
|
|
@@ -17,7 +17,8 @@ const { uploadSessionOnShip } = require('./ship-session-upload.js');
|
|
|
17
17
|
const { gitOk, shellOk } = require('./ship-git.js');
|
|
18
18
|
const { fetchDeployedVersion } = require('./ship-deploy-target.js');
|
|
19
19
|
const { landBailed, reconciledCardStatus } = require('./ship-land.js');
|
|
20
|
-
const { autoMerge } = require('./ship-merge.js');
|
|
20
|
+
const { autoMerge, publishHeld } = require('./ship-merge.js');
|
|
21
|
+
const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
21
22
|
const { normalizeApiError, playShipBell, postLandShipOutcome } = require('./ship-io.js');
|
|
22
23
|
const { getServerReward, noteServerReward } = require('./ship-state.js');
|
|
23
24
|
|
|
@@ -176,7 +177,10 @@ function postShipCloseout(taskId) {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine, card, dbOnly = false }) {
|
|
180
|
+
async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine, card, dbOnly = false, held = false }) {
|
|
181
|
+
// task 1004322 (ADR 0341 D7): a page tweak whose grade passed is HELD for its
|
|
182
|
+
// artist. It publishes its branch and PR and stops; nothing merges.
|
|
183
|
+
if (held) return finishHeld({ taskId, summary, postGradeStatus, card });
|
|
180
184
|
// task 1002541: the land outcome carries WHY a land didn't happen. Without it
|
|
181
185
|
// the card had only the status to go on and printed 'landing automatically'
|
|
182
186
|
// over a merge that had already bailed.
|
|
@@ -226,6 +230,26 @@ async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGrad
|
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
232
|
|
|
233
|
+
// finishHeld — the tail of a held pass (task 1004322). The session is finished
|
|
234
|
+
// work, so it syncs exactly as a confirmed ship does (the applier's session
|
|
235
|
+
// reward is computed from the upload, and pays on land). No bell and no tree
|
|
236
|
+
// close-out: nothing landed, and a send-back brings the round back to be
|
|
237
|
+
// re-applied. `deps` is a test seam only.
|
|
238
|
+
async function finishHeld({ taskId, summary, postGradeStatus, card }, deps = {}) {
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log('Publishing the applied branch for the artist (ADR 0341 D7):');
|
|
241
|
+
const land = await (deps.publishHeld || publishHeld)(taskId, summary);
|
|
242
|
+
if (land && land.held) {
|
|
243
|
+
for (const line of tweakHold.heldLines(taskId, { prUrl: land.prUrl })) console.log(line);
|
|
244
|
+
} else {
|
|
245
|
+
console.error(` The branch did not publish${land && land.reason ? `: ${land.reason}` : ''}. The task stays at completed with its passed grade.`);
|
|
246
|
+
console.error(` Re-run to publish it again (the grade is not re-run): node scripts/gds/ship.js ${taskId}`);
|
|
247
|
+
}
|
|
248
|
+
const usage = await (deps.postShipSync || postShipSync)(taskId, 'confirmed');
|
|
249
|
+
await (deps.printCompletionCard || printCompletionCard)({ taskId, postGradeStatus, card, usage, land });
|
|
250
|
+
return land;
|
|
251
|
+
}
|
|
252
|
+
|
|
229
253
|
// task 1275: sum the committed diffstat (added/removed lines) over baseline...HEAD.
|
|
230
254
|
// Shared by the completion card and the in-review card. Best-effort: returns
|
|
231
255
|
// nulls when there's no baseline or git is unavailable.
|
|
@@ -377,6 +401,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
377
401
|
module.exports = {
|
|
378
402
|
diffstatFor,
|
|
379
403
|
finishShip,
|
|
404
|
+
finishHeld,
|
|
380
405
|
mergeAndDeploy,
|
|
381
406
|
onboardingNudgeBestEffort,
|
|
382
407
|
postShipSync,
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -27,6 +27,7 @@ const { apiErrorLine, normalizeApiError, isUnknownFieldRejection } = require('./
|
|
|
27
27
|
const { pushVia } = require('./ship-deploy-target.js');
|
|
28
28
|
|
|
29
29
|
const { diffstatFor, finishShip } = require('./ship-finish.js');
|
|
30
|
+
const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
30
31
|
const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge, shouldConfirmWithoutGrade } = require('./ship-resume.js');
|
|
31
32
|
const { fileNoArtifactEscalation, noArtifactRefusalLines } = require('./ship-escalation.js');
|
|
32
33
|
const { noteServerReward } = require('./ship-state.js');
|
|
@@ -583,6 +584,7 @@ async function shipMain() {
|
|
|
583
584
|
let postGradeJustUnlocked = r.data.just_unlocked || [];
|
|
584
585
|
let gradePassed = false;
|
|
585
586
|
let advanceToMerge = false;
|
|
587
|
+
let held = false; // task 1004322: a page tweak's pass, held for its artist (ADR 0341 D7)
|
|
586
588
|
let gradeObj = null; // task 1270: the grade result, hoisted for the completion card
|
|
587
589
|
// V3.R27 (#247): credit breakdown surfaced from the grade or confirm
|
|
588
590
|
// response. Kept separate from `credits` so we only print the multiplier
|
|
@@ -696,6 +698,7 @@ async function shipMain() {
|
|
|
696
698
|
gradeObj = g;
|
|
697
699
|
gradePassed = g.passed;
|
|
698
700
|
advanceToMerge = gradePost.data.advanceToMerge;
|
|
701
|
+
held = gradePost.data.held === true;
|
|
699
702
|
postGradeStatus = gradePost.data.taskStatus;
|
|
700
703
|
credits = gradePost.data.creditsAwarded;
|
|
701
704
|
postGradeJustUnlocked = gradePost.data.just_unlocked || [];
|
|
@@ -726,7 +729,7 @@ async function shipMain() {
|
|
|
726
729
|
markShipProgress(postGradeStatus === 'shipped' ? 'landed' : 'graded', taskId);
|
|
727
730
|
}
|
|
728
731
|
await finishShip({
|
|
729
|
-
taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly,
|
|
732
|
+
taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly, held,
|
|
730
733
|
// task 1270: data for the deterministic completion card.
|
|
731
734
|
card: {
|
|
732
735
|
title: ac.title,
|
|
@@ -895,6 +898,18 @@ async function regradeMain() {
|
|
|
895
898
|
console.error(` node scripts/gds/ship.js ${taskId} --verified`);
|
|
896
899
|
process.exit(1);
|
|
897
900
|
}
|
|
901
|
+
// task 1004322 (ADR 0341 D7): a page tweak already HELD for its artist has a
|
|
902
|
+
// passed grade. A plain re-run lands here (completed -> resume-grade), and
|
|
903
|
+
// re-grading would spend the panel again for the verdict it already has, so it
|
|
904
|
+
// only re-publishes the branch (the recovery for a publish that failed).
|
|
905
|
+
if (tweakHold.isHeldForArtist(task, tRes.data.grade ? tRes.data.grade.passed : null)) {
|
|
906
|
+
console.log(`Task #${taskId} already passed its grade and is waiting for the artist: re-publishing its branch, not re-grading.`);
|
|
907
|
+
await finishShip({
|
|
908
|
+
taskId, summary: summaryArg || task.value_summary || '', held: true, postGradeStatus: 'completed', doneLine: 'Done.',
|
|
909
|
+
card: { title: task.title, valueSummary: summaryArg || task.value_summary || '', notes: '' },
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
898
913
|
console.log(`Re-grading task #${taskId}: ${task.title}`);
|
|
899
914
|
console.log(` eligibility: ${elig.reason} (${elig.via})`);
|
|
900
915
|
|
|
@@ -1101,6 +1116,7 @@ async function regradeMain() {
|
|
|
1101
1116
|
// normal ship (finishShip, #1093/C9) so a re-grade lands identically.
|
|
1102
1117
|
await finishShip({
|
|
1103
1118
|
taskId, summary, advanceToMerge: gradePost.data.advanceToMerge, skipMerge, dbOnly,
|
|
1119
|
+
held: gradePost.data.held === true, // task 1004322: a page tweak's pass waits for its artist
|
|
1104
1120
|
postGradeStatus,
|
|
1105
1121
|
doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
|
|
1106
1122
|
// task 1270: completion card for the re-grade path too.
|
package/scripts/gds/ship-land.js
CHANGED
|
@@ -46,6 +46,12 @@ const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('../../modules/lifecycl
|
|
|
46
46
|
function landed(o = {}) { return { ok: true, pending: false, reason: null, nextStep: null, noArtifact: o.noArtifact === true, noArtifactReason: o.noArtifactReason || null }; }
|
|
47
47
|
function landPending() { return { ok: false, pending: true, reason: null, nextStep: null }; }
|
|
48
48
|
function landBailed(reason, nextStep = null, bailId = null) { return { ok: false, pending: false, reason, nextStep, bailId }; }
|
|
49
|
+
// task 1004322 (ADR 0341 D7): the FOURTH outcome, and only a page tweak reaches it.
|
|
50
|
+
// Its grade passed and its branch and PR are published, but nothing is landing and
|
|
51
|
+
// nothing should: the artist approves the applied page first. Not a bail (nothing
|
|
52
|
+
// failed, and the card must not say "Land it") and not pending (no auto-merge is
|
|
53
|
+
// armed, so it does not land on its own).
|
|
54
|
+
function landHeld({ prUrl = null } = {}) { return { ok: false, pending: false, held: true, reason: null, nextStep: null, prUrl }; }
|
|
49
55
|
|
|
50
56
|
// ---------- which bails a PR can still land (task 1002572, part 2) ----------
|
|
51
57
|
//
|
|
@@ -79,12 +85,17 @@ function strandCommand(taskId, via) {
|
|
|
79
85
|
|
|
80
86
|
// ciLand — dispatch the 'ci' confirmed->shipped land to the local or the
|
|
81
87
|
// server-mediated path. Async because the server path awaits GDS API calls.
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// opts.hold (task 1004322): PUBLISH ONLY — push the branch and open the PR, never
|
|
89
|
+
// arm auto-merge and never poll for a land. A page tweak held for its artist takes
|
|
90
|
+
// it; every other caller passes nothing and gets the unchanged path.
|
|
91
|
+
async function ciLand(taskId, branch, valueSummary, opts = {}) {
|
|
92
|
+
console.log(opts.hold
|
|
93
|
+
? ' held for the artist — publishing the branch and its PR only (no auto-merge, no land)'
|
|
94
|
+
: ' deploy mode: ci — landing via PR auto-merge (no droplet key needed)');
|
|
84
95
|
if (pushVia() === 'server') {
|
|
85
|
-
return ciLandServer(taskId, branch, valueSummary);
|
|
96
|
+
return ciLandServer(taskId, branch, valueSummary, opts);
|
|
86
97
|
}
|
|
87
|
-
return await ciLandLocal(taskId, branch, valueSummary);
|
|
98
|
+
return await ciLandLocal(taskId, branch, valueSummary, opts);
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
// task 1001916: render a FAILED POST /tasks/:id/publish-branch for the builder.
|
|
@@ -165,7 +176,7 @@ function formatPublishFailure(data, status, branch, opts = {}) {
|
|
|
165
176
|
// hand the branch to the GDS, which pushes it + opens the PR + auto-merges with
|
|
166
177
|
// the server-side credential. Requires a RE-AUTHED (non-box) cli session — a
|
|
167
178
|
// box-scoped session is 403'd by the endpoint (it must `/builder-reauth` first).
|
|
168
|
-
async function ciLandServer(taskId, branch, valueSummary) {
|
|
179
|
+
async function ciLandServer(taskId, branch, valueSummary, { hold = false } = {}) {
|
|
169
180
|
console.log(' ci-land: server-mediated publish (this machine has no GitHub push credential)');
|
|
170
181
|
const headSha = gitOk('git rev-parse HEAD');
|
|
171
182
|
// Make sure origin/main is current so the THIN bundle's `origin/main..HEAD`
|
|
@@ -195,9 +206,14 @@ async function ciLandServer(taskId, branch, valueSummary) {
|
|
|
195
206
|
console.log(` ci-land: server publish failed — ${e}. Task stays at confirmed. ${strandNextStep(taskId, 'server')}`);
|
|
196
207
|
return landBailed(`server publish failed — ${e}`, strandCommand(taskId, 'server'));
|
|
197
208
|
}
|
|
198
|
-
|
|
209
|
+
// task 1004322: the server does not arm a held page tweak (it reads the hold
|
|
210
|
+
// itself), so do not tell the builder to merge it on GitHub.
|
|
211
|
+
console.log(hold
|
|
212
|
+
? ` ci-land: server pushed ${branch} + PR ${r.data.pr_url} (auto-merge NOT armed: waiting for the artist).`
|
|
213
|
+
: ` ci-land: server pushed ${branch} + PR ${r.data.pr_url} (auto-merge ${r.data.auto_merge ? 'enabled' : 'NOT enabled — merge it on GitHub'}).`);
|
|
199
214
|
const assigneeLine = prAssigneeLine(r.data.pr_assignee);
|
|
200
215
|
if (assigneeLine) console.log(assigneeLine);
|
|
216
|
+
if (hold) return landHeld({ prUrl: r.data.pr_url || null });
|
|
201
217
|
return pollServerPublish(taskId, branch);
|
|
202
218
|
} finally {
|
|
203
219
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
@@ -858,7 +874,7 @@ function pushCapturing(branch, deps = {}) {
|
|
|
858
874
|
// ciLandLocal — today's local path: this machine pushes the branch + opens the PR
|
|
859
875
|
// + enables auto-merge via git + gh. async since task 1169 — it asks the server to
|
|
860
876
|
// post the gate-author-trust attestation between the push and the PR open.
|
|
861
|
-
async function ciLandLocal(taskId, branch, valueSummary) {
|
|
877
|
+
async function ciLandLocal(taskId, branch, valueSummary, { hold = false } = {}) {
|
|
862
878
|
const headSha = gitOk('git rev-parse HEAD');
|
|
863
879
|
|
|
864
880
|
console.log(` pushing ${branch} to origin...`);
|
|
@@ -924,6 +940,12 @@ async function ciLandLocal(taskId, branch, valueSummary) {
|
|
|
924
940
|
console.log(` ci-land: reusing open PR ${existing.url}`);
|
|
925
941
|
}
|
|
926
942
|
|
|
943
|
+
// task 1004322: a held page tweak stops here — published, never armed.
|
|
944
|
+
if (hold) {
|
|
945
|
+
const pr = ghJson(['pr', 'view', branch, '--json', 'number,url,state']);
|
|
946
|
+
console.log(` ci-land: PR ${(pr && pr.url) || 'opened'} — auto-merge NOT armed: waiting for the artist.`);
|
|
947
|
+
return landHeld({ prUrl: (pr && pr.url) || null });
|
|
948
|
+
}
|
|
927
949
|
console.log(' ci-land: enabling auto-merge (lands when required checks pass)...');
|
|
928
950
|
// task 1718: an enable-failure is NOT a dead-end — the PR is open and the server
|
|
929
951
|
// still lands green PRs. Don't return false (which stranded the builder at
|
|
@@ -1075,6 +1097,7 @@ module.exports = {
|
|
|
1075
1097
|
LAND_BAIL_NO_TASK_BRANCH,
|
|
1076
1098
|
landBailRecoverableByPr,
|
|
1077
1099
|
landBailed,
|
|
1100
|
+
landHeld,
|
|
1078
1101
|
landPending,
|
|
1079
1102
|
landPendingLines,
|
|
1080
1103
|
landPollAfterAutoMerge,
|
|
@@ -86,6 +86,46 @@ async function autoMerge(taskId, valueSummary, opts = {}, deps = {}) {
|
|
|
86
86
|
return await land(taskId, branch, valueSummary);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// regenerateShipArtifacts — every generated artifact a branch must carry fresh,
|
|
90
|
+
// in the order the land has always refreshed them. Moved out of mergeLocally
|
|
91
|
+
// unchanged (task 1004322) so the held publish below carries the same set.
|
|
92
|
+
function regenerateShipArtifacts() {
|
|
93
|
+
regenerateDiagrams();
|
|
94
|
+
// Refresh the generated code symbol skeleton (Path B / task 1224) on the same beat.
|
|
95
|
+
regenerateRepoMap();
|
|
96
|
+
// Refresh the generated session-log index + the bounded CLAUDE.md §13 snippet (ADR 0062 §8 / task 1226).
|
|
97
|
+
regenerateSessionIndex();
|
|
98
|
+
// Refresh the generated file-map sections (skills + scheduled-tasks) on the same beat (ADR 0066 / task 1276).
|
|
99
|
+
regenerateFileMap();
|
|
100
|
+
// Refresh the copy registry + inventory on the same beat (task 1003556). Its rows
|
|
101
|
+
// carry line numbers, so ANY edit to a scanned UI file staled it — and nothing in
|
|
102
|
+
// the ship path regenerated it, so the PR was born stale and CI said so.
|
|
103
|
+
regenerateCopyInventory();
|
|
104
|
+
// Refresh the generated OpenAPI spec + API reference from the shipped route tree (task 1918).
|
|
105
|
+
regenerateApiDocs();
|
|
106
|
+
// ...then the typed client FROM that spec (+ the devbox vendored copy) on the same beat (task 2051).
|
|
107
|
+
regenerateApiClient();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// publishHeld — the held pass's whole "land" (task 1004322, ADR 0341 D7). A page
|
|
111
|
+
// tweak whose grade passed waits at completed for its artist, and the Approval
|
|
112
|
+
// queue shows the APPLIED BRANCH, so the branch and its PR must exist now: a
|
|
113
|
+
// grade-parked ship otherwise never publishes, and the later approve would strand
|
|
114
|
+
// at no_branch_no_tip. This publishes, in every deploy mode, through the PR path
|
|
115
|
+
// (never a local merge to main) and stops: no auto-merge, no poll, no land.
|
|
116
|
+
// `deps` is a test seam only.
|
|
117
|
+
async function publishHeld(taskId, valueSummary, deps = {}) {
|
|
118
|
+
const branch = (deps.currentBranch || currentBranch)();
|
|
119
|
+
if (!branch || branch === 'main' || branch === 'HEAD') {
|
|
120
|
+
return landBailed(
|
|
121
|
+
`nothing to publish — this checkout is on ${branch}, not the task branch`,
|
|
122
|
+
onMainNextStep(taskId, (deps.localTaskBranchExists || localTaskBranchExists)(taskId)),
|
|
123
|
+
); // untagged on purpose: only autoMerge routes by bail id
|
|
124
|
+
}
|
|
125
|
+
(deps.regenerate || regenerateShipArtifacts)();
|
|
126
|
+
return await (deps.ciLand || ciLand)(taskId, branch, valueSummary, { hold: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
90
130
|
const branch = currentBranch();
|
|
91
131
|
if (!branch || branch === 'main' || branch === 'HEAD') {
|
|
@@ -116,21 +156,7 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
116
156
|
|
|
117
157
|
// Refresh the data-driven diagrams before computing commits-ahead, so a
|
|
118
158
|
// diagram-only refresh still counts as something to deploy.
|
|
119
|
-
|
|
120
|
-
// Refresh the generated code symbol skeleton (Path B / task 1224) on the same beat.
|
|
121
|
-
regenerateRepoMap();
|
|
122
|
-
// Refresh the generated session-log index + the bounded CLAUDE.md §13 snippet (ADR 0062 §8 / task 1226).
|
|
123
|
-
regenerateSessionIndex();
|
|
124
|
-
// Refresh the generated file-map sections (skills + scheduled-tasks) on the same beat (ADR 0066 / task 1276).
|
|
125
|
-
regenerateFileMap();
|
|
126
|
-
// Refresh the copy registry + inventory on the same beat (task 1003556). Its rows
|
|
127
|
-
// carry line numbers, so ANY edit to a scanned UI file staled it — and nothing in
|
|
128
|
-
// the ship path regenerated it, so the PR was born stale and CI said so.
|
|
129
|
-
regenerateCopyInventory();
|
|
130
|
-
// Refresh the generated OpenAPI spec + API reference from the shipped route tree (task 1918).
|
|
131
|
-
regenerateApiDocs();
|
|
132
|
-
// ...then the typed client FROM that spec (+ the devbox vendored copy) on the same beat (task 2051).
|
|
133
|
-
regenerateApiClient();
|
|
159
|
+
regenerateShipArtifacts();
|
|
134
160
|
|
|
135
161
|
// Resolve the deploy mode ONCE (task 1093 / C9) — it reads config/deploy.json;
|
|
136
162
|
// reused at the fetch-bail check + the ci-land branch below.
|
|
@@ -422,6 +448,8 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
422
448
|
|
|
423
449
|
module.exports = {
|
|
424
450
|
autoMerge,
|
|
451
|
+
// task 1004322: the held page tweak's publish-only path.
|
|
452
|
+
publishHeld,
|
|
425
453
|
// task 1002572 (PART 2): the pure recovery command for a ship run from main —
|
|
426
454
|
// exported so the "don't point back at the dead end" wording is pinned.
|
|
427
455
|
onMainNextStep,
|