@letterstory/cli 0.71.0 → 0.72.2
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/lib/cli.mjs +5 -2
- package/lib/commands/admin.mjs +242 -0
- package/lib/commands/phantom.mjs +6 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
} from "./commands.mjs";
|
|
57
57
|
|
|
58
58
|
// Keep in sync with cli/package.json.
|
|
59
|
-
export const VERSION = "0.
|
|
59
|
+
export const VERSION = "0.72.2";
|
|
60
60
|
|
|
61
61
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
62
62
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -645,7 +645,10 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
645
645
|
trial-run, trial-save, the Write input bank
|
|
646
646
|
(inputs, input, inputs-harvest, input-save),
|
|
647
647
|
screens (screen-new, screens, screen,
|
|
648
|
-
screen-advance),
|
|
648
|
+
screen-advance), the bake-off (bakeoff,
|
|
649
|
+
bakeoff-pick, bakeoff-merge), programmes
|
|
650
|
+
(programmes, programme, programme-new,
|
|
651
|
+
programme-stop, programme-advance),
|
|
649
652
|
and the arena, shredder
|
|
650
653
|
and enhancer twins (every flag is listed in
|
|
651
654
|
cli/lib/commands/admin.mjs).
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -505,6 +505,24 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
505
505
|
* admin tuning screens | screen <id> list / one screen: per candidate, each measure against the Base, with its noise
|
|
506
506
|
* admin tuning screen-advance <id> queue what the caps allow now (the cron does it every minute)
|
|
507
507
|
*
|
|
508
|
+
* The bake-off: a screen's versions graded item by item against the checklist (the eval list's
|
|
509
|
+
* confirmed and unvetted categories, and the craft axes commitment / economy / movement), the winner
|
|
510
|
+
* per item tallied across the inputs, and a merge that composes the best of each into one candidate.
|
|
511
|
+
* Deterministic; a person overrides a grade and confirms the merge, which is a hypothesis until the
|
|
512
|
+
* eye test confirms it.
|
|
513
|
+
* admin tuning bakeoff <screen-id> [--regrade] the winners table: item, trusted, leader, wins per version, graded; then the conflicts a merge would meet
|
|
514
|
+
* admin tuning bakeoff-pick <screen-id> --input <key> --item <category:id|axis:id> --winner <version|none> a person's grade on one input and item (replaces the machine's)
|
|
515
|
+
* admin tuning bakeoff-merge <screen-id> [--resolve item=version,…] [--note "…"] [--compare 10] save the merged file as a candidate of the base kernel (never live); --compare starts a comparison against live on that many pairs
|
|
516
|
+
*
|
|
517
|
+
* Programmes: the bake-off run by the machine, generation after generation, on the cron, with a run
|
|
518
|
+
* budget and stop rules (the budget; N flat generations; a finalist that won its picks). Each
|
|
519
|
+
* generation screens the base against its challengers, merges the winners, compares the merge
|
|
520
|
+
* against live; a merge that cleared the gates and lost no craft becomes the base. People pick.
|
|
521
|
+
* admin tuning programmes | programme <id> list / one programme: every generation, its table, merge and comparison, and what happens next
|
|
522
|
+
* admin tuning programme-new --title "…" --kernel <kernel-id|service-kernel-id> --seeds 21,22,23 [--inputs id,…|--n 8] [--filter "…"] [--pairs 10] --budget 200 [--flat 2]
|
|
523
|
+
* admin tuning programme-stop <id> [--reason "…"] stop it by hand
|
|
524
|
+
* admin tuning programme-advance <id> one manual tick (the cron takes the same step every minute)
|
|
525
|
+
*
|
|
508
526
|
* Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
|
|
509
527
|
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
510
528
|
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
@@ -564,6 +582,49 @@ function tagFilterParams(raw) {
|
|
|
564
582
|
* A session as an agent reads it: each trial's changes and runs with their headline numbers, and
|
|
565
583
|
* (with `texts`) the latest finished text per trial. The full session (traces, records) is `--json`.
|
|
566
584
|
*/
|
|
585
|
+
/** A programme as a person reads it: the row, then one block per generation, then what happens next. */
|
|
586
|
+
function printProgramme(io, r) {
|
|
587
|
+
const p = r.programme;
|
|
588
|
+
io.log(
|
|
589
|
+
`programme ${p.title} (${p.id}): ${p.status}${p.stop_reason ? ` — ${p.stop_reason}` : ""} · generation ${p.generation} · base ${p.base_version === null ? "live" : `v${p.base_version}`} · ${p.runs_used}/${p.budget_runs} runs · seeds v${p.seed_versions.join(", v")} · ${p.pairs} pairs/generation · stop after ${p.max_flat_generations} flat`
|
|
590
|
+
);
|
|
591
|
+
for (const g of r.generations) {
|
|
592
|
+
const sm = g.summary ?? {};
|
|
593
|
+
io.log("");
|
|
594
|
+
io.log(
|
|
595
|
+
`generation ${g.generation}: ${g.status} · screen ${g.screen_id} · v${g.versions.join(", v")}${g.merged_version !== null ? ` · merged v${g.merged_version}` : ""}${g.comparison_id ? ` · comparison ${g.comparison_id}` : ""}`
|
|
596
|
+
);
|
|
597
|
+
if (sm.from)
|
|
598
|
+
io.log(
|
|
599
|
+
` from: base ${sm.from.base === null ? "live" : `v${sm.from.base}`}${sm.from.seeds ? ` · seeds v${sm.from.seeds.join(", v")}` : ""}${sm.from.strongest?.length ? ` · strongest v${sm.from.strongest.join(", v")}` : ""}${sm.from.merged != null ? ` · merged v${sm.from.merged}` : ""}${sm.from.mutations?.length ? ` · mutations ${sm.from.mutations.map((m) => `${m.category} → v${m.version}`).join(", ")}` : ""}`
|
|
600
|
+
);
|
|
601
|
+
if (sm.leader !== undefined)
|
|
602
|
+
io.log(
|
|
603
|
+
` leader ${sm.leader === null ? "none" : `v${sm.leader}`}${sm.flat ? " · flat (nothing moved off the base)" : ""}`
|
|
604
|
+
);
|
|
605
|
+
for (const row of sm.table ?? [])
|
|
606
|
+
if (row.leader !== null && row.leader !== sm.base)
|
|
607
|
+
io.log(
|
|
608
|
+
` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : "unvetted "} v${row.leader} ${Object.entries(
|
|
609
|
+
row.wins
|
|
610
|
+
)
|
|
611
|
+
.map(([v, n]) => `v${v}:${n}`)
|
|
612
|
+
.join(" ")}`
|
|
613
|
+
);
|
|
614
|
+
if (sm.merge)
|
|
615
|
+
io.log(
|
|
616
|
+
` merge: from v${sm.merge.base} → v${sm.merge.version}; applied ${sm.merge.applied.map((a) => `${a.item} from v${a.from}`).join(", ") || "nothing"}${sm.merge.conflicts.length ? `; conflicts kept the base: ${sm.merge.conflicts.map((c) => c.path).join(", ")}` : ""}`
|
|
617
|
+
);
|
|
618
|
+
if (sm.comparison)
|
|
619
|
+
io.log(
|
|
620
|
+
` comparison: quality ${sm.comparison.quality ?? "unread"}${sm.comparison.guards_worse.length ? ` · guards worse: ${sm.comparison.guards_worse.join(", ")}` : " · gates clean"} · ${sm.comparison.finalist ? "finalist" : "not a finalist"}${sm.comparison.picks ? ` · picks ${sm.comparison.picks}` : ""}${sm.comparison.delta?.ceiling != null ? ` · ceiling ${sm.comparison.delta.ceiling > 0 ? "+" : ""}${sm.comparison.delta.ceiling}` : ""}${sm.comparison.next_arm.length ? ` · next arm: ${sm.comparison.next_arm.map((a) => `${a.category}${a.patch ? " (patch)" : ""}`).join(", ")}` : ""}`
|
|
621
|
+
);
|
|
622
|
+
if (sm.error) io.log(` error: ${sm.error}`);
|
|
623
|
+
}
|
|
624
|
+
io.log("");
|
|
625
|
+
io.log(`next: ${r.next}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
567
628
|
function sessionSummary(session, texts) {
|
|
568
629
|
const runsOf = (trialId) => session.runs.filter((r) => r.trialId === trialId);
|
|
569
630
|
return {
|
|
@@ -1534,6 +1595,187 @@ async function cmdAdminTuning(ctx) {
|
|
|
1534
1595
|
? printResult(io, flags, r)
|
|
1535
1596
|
: io.log(`queued ${r.queued} run(s)${r.waiting ? `; waiting: ${r.waiting}` : ""}`);
|
|
1536
1597
|
}
|
|
1598
|
+
// --- The bake-off: a screen's versions graded item by item, the winners table, the merge ---
|
|
1599
|
+
case "bakeoff": {
|
|
1600
|
+
if (!id) throw new CliError("Usage: admin tuning bakeoff <screen-id> [--regrade]");
|
|
1601
|
+
const r = await staffRequest(
|
|
1602
|
+
ctx,
|
|
1603
|
+
"GET",
|
|
1604
|
+
`${T}/screens/${id}/bakeoff${flagBool(flags.regrade) ? "?regrade=1" : ""}`
|
|
1605
|
+
);
|
|
1606
|
+
if (json) return printResult(io, flags, r);
|
|
1607
|
+
io.log(
|
|
1608
|
+
`bake-off ${r.screen.title}: v${r.versions.join(", v")} on ${r.inputs.length} input(s); leader ${r.leader === null ? "none" : `v${r.leader}`}`
|
|
1609
|
+
);
|
|
1610
|
+
if (r.unversioned.length) io.log(`left out (not a saved version): ${r.unversioned.join(", ")}`);
|
|
1611
|
+
io.log("");
|
|
1612
|
+
io.log(
|
|
1613
|
+
`${"item".padEnd(32)} ${"trusted".padEnd(8)} ${"leader".padEnd(7)} ${r.versions.map((v) => `v${v}`.padStart(6)).join("")} graded`
|
|
1614
|
+
);
|
|
1615
|
+
for (const row of r.table)
|
|
1616
|
+
io.log(
|
|
1617
|
+
`${row.item.id.padEnd(32)} ${(row.trusted ? "yes" : "no").padEnd(8)} ${(row.leader === null ? "—" : `v${row.leader}`).padEnd(7)} ${r.versions.map((v) => String(row.wins[v] ?? 0).padStart(6)).join("")} ${row.graded}`
|
|
1618
|
+
);
|
|
1619
|
+
io.log("");
|
|
1620
|
+
io.log(
|
|
1621
|
+
r.conflicts_if_merged.length
|
|
1622
|
+
? `conflicts if merged: ${r.conflicts_if_merged.map((c) => `${c.path} ← ${c.candidates.map((x) => `${x.item} v${x.version}`).join(" | ")}`).join("; ")}`
|
|
1623
|
+
: "conflicts if merged: none"
|
|
1624
|
+
);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
case "bakeoff-pick": {
|
|
1628
|
+
const input = flagStr(flags.input);
|
|
1629
|
+
const item = flagStr(flags.item);
|
|
1630
|
+
const winner = flagStr(flags.winner);
|
|
1631
|
+
if (!id || !input || !item || !winner)
|
|
1632
|
+
throw new CliError(
|
|
1633
|
+
"Usage: admin tuning bakeoff-pick <screen-id> --input <key> --item <category:id|axis:id> --winner <version|none>"
|
|
1634
|
+
);
|
|
1635
|
+
const winnerVersion = winner === "none" ? null : Number(winner);
|
|
1636
|
+
if (winnerVersion !== null && !(Number.isInteger(winnerVersion) && winnerVersion > 0))
|
|
1637
|
+
throw new CliError("--winner is a version number, or none");
|
|
1638
|
+
const r = await staffRequest(ctx, "POST", `${T}/screens/${id}/bakeoff/pick`, {
|
|
1639
|
+
input_key: input,
|
|
1640
|
+
item_id: item,
|
|
1641
|
+
winner_version: winnerVersion,
|
|
1642
|
+
});
|
|
1643
|
+
if (json) return printResult(io, flags, r);
|
|
1644
|
+
const row = r.read.table.find((x) => x.item.id === item);
|
|
1645
|
+
return io.log(
|
|
1646
|
+
`graded ${item} on ${input}: ${winnerVersion === null ? "none" : `v${winnerVersion}`}${row ? ` · leader now ${row.leader === null ? "none" : `v${row.leader}`}` : ""}`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
case "bakeoff-merge": {
|
|
1650
|
+
if (!id)
|
|
1651
|
+
throw new CliError(
|
|
1652
|
+
'Usage: admin tuning bakeoff-merge <screen-id> [--resolve item=version,…] [--note "…"] [--compare 10]'
|
|
1653
|
+
);
|
|
1654
|
+
const resolve = {};
|
|
1655
|
+
for (const pair of flagList(flags.resolve).flatMap((x) =>
|
|
1656
|
+
x
|
|
1657
|
+
.split(",")
|
|
1658
|
+
.map((y) => y.trim())
|
|
1659
|
+
.filter(Boolean)
|
|
1660
|
+
)) {
|
|
1661
|
+
const [item, version] = pair.split("=").map((x) => x.trim());
|
|
1662
|
+
const v = Number(version);
|
|
1663
|
+
if (!item || !(Number.isInteger(v) && v > 0))
|
|
1664
|
+
throw new CliError(`--resolve: "${pair}" is not item=version`);
|
|
1665
|
+
resolve[item] = v;
|
|
1666
|
+
}
|
|
1667
|
+
const compare = flags.compare === undefined ? undefined : { pairs: flagNum(flags.compare) ?? 10 };
|
|
1668
|
+
const r = await staffRequest(
|
|
1669
|
+
ctx,
|
|
1670
|
+
"POST",
|
|
1671
|
+
`${T}/screens/${id}/bakeoff/merge`,
|
|
1672
|
+
compact({
|
|
1673
|
+
resolve: Object.keys(resolve).length ? resolve : undefined,
|
|
1674
|
+
note: flagStr(flags.note),
|
|
1675
|
+
compare,
|
|
1676
|
+
})
|
|
1677
|
+
);
|
|
1678
|
+
if (json) return printResult(io, flags, r);
|
|
1679
|
+
io.log(
|
|
1680
|
+
`merged from v${r.base} → candidate v${r.version}${r.comparison_id ? ` · comparison ${r.comparison_id}` : ""}`
|
|
1681
|
+
);
|
|
1682
|
+
for (const p of r.provenance)
|
|
1683
|
+
io.log(` ${p.applied ? "applied" : `skipped (${p.skipped})`} ${p.item} ${p.path} from v${p.from}`);
|
|
1684
|
+
if (r.conflicts.length)
|
|
1685
|
+
io.log(
|
|
1686
|
+
`conflicts left (the base's value kept): ${r.conflicts.map((c) => `${c.path} ← ${c.candidates.map((x) => `${x.item} v${x.version}`).join(" | ")}`).join("; ")}`
|
|
1687
|
+
);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
// --- Programmes: the bake-off run by the machine over generations ---
|
|
1691
|
+
case "programmes": {
|
|
1692
|
+
const r = await staffRequest(ctx, "GET", `${T}/programmes?limit=${flagNum(flags.limit) ?? 30}`);
|
|
1693
|
+
if (json) return printResult(io, flags, r);
|
|
1694
|
+
return io.log(
|
|
1695
|
+
r.programmes
|
|
1696
|
+
.map(
|
|
1697
|
+
(x) =>
|
|
1698
|
+
`${x.id} ${x.status.padEnd(7)} ${x.title} · generation ${x.generation} · base ${x.base_version === null ? "live" : `v${x.base_version}`} · ${x.runs_used}/${x.budget_runs} runs${x.stop_reason ? ` · ${x.stop_reason}` : ""}`
|
|
1699
|
+
)
|
|
1700
|
+
.join("\n") || "(no programmes)"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
case "programme": {
|
|
1704
|
+
if (!id) throw new CliError("Usage: admin tuning programme <programme-id>");
|
|
1705
|
+
const r = await staffRequest(ctx, "GET", `${T}/programmes/${id}`);
|
|
1706
|
+
if (json) return printResult(io, flags, r);
|
|
1707
|
+
printProgramme(io, r);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
case "programme-new": {
|
|
1711
|
+
const title = flagStr(flags.title);
|
|
1712
|
+
const kernel = flagStr(flags.kernel);
|
|
1713
|
+
const seeds = flagList(flags.seeds)
|
|
1714
|
+
.flatMap((x) => x.split(","))
|
|
1715
|
+
.map((x) => Number(x.trim()))
|
|
1716
|
+
.filter((x) => Number.isFinite(x));
|
|
1717
|
+
const budget = flagNum(flags.budget);
|
|
1718
|
+
if (!title || !kernel || !seeds.length || budget === undefined)
|
|
1719
|
+
throw new CliError(
|
|
1720
|
+
'Usage: admin tuning programme-new --title "…" --kernel <kernel-id|service-kernel-id> --seeds 21,22,23 [--inputs id,…|--n 8] [--filter "…"] [--pairs 10] --budget 200 [--flat 2]'
|
|
1721
|
+
);
|
|
1722
|
+
const filter = Object.fromEntries(
|
|
1723
|
+
Object.entries(tagFilterParams(flagStr(flags.filter))).map(([k, v]) => [
|
|
1724
|
+
k,
|
|
1725
|
+
v
|
|
1726
|
+
.split(",")
|
|
1727
|
+
.map((x) => x.trim())
|
|
1728
|
+
.filter(Boolean),
|
|
1729
|
+
])
|
|
1730
|
+
);
|
|
1731
|
+
const inputIds = flagList(flags.inputs).flatMap((x) =>
|
|
1732
|
+
x
|
|
1733
|
+
.split(",")
|
|
1734
|
+
.map((y) => y.trim())
|
|
1735
|
+
.filter(Boolean)
|
|
1736
|
+
);
|
|
1737
|
+
const r = await staffRequest(
|
|
1738
|
+
ctx,
|
|
1739
|
+
"POST",
|
|
1740
|
+
`${T}/programmes`,
|
|
1741
|
+
compact({
|
|
1742
|
+
title,
|
|
1743
|
+
base_kernel_id: kernel,
|
|
1744
|
+
seed_versions: seeds,
|
|
1745
|
+
input_ids: inputIds.length ? inputIds : undefined,
|
|
1746
|
+
n: flagNum(flags.n),
|
|
1747
|
+
filter: Object.keys(filter).length ? filter : undefined,
|
|
1748
|
+
pairs: flagNum(flags.pairs),
|
|
1749
|
+
budget_runs: budget,
|
|
1750
|
+
max_flat_generations: flagNum(flags.flat),
|
|
1751
|
+
})
|
|
1752
|
+
);
|
|
1753
|
+
if (json) return printResult(io, flags, r);
|
|
1754
|
+
return io.log(
|
|
1755
|
+
`programme ${r.programme.id}: generation 1 screening v${r.programme.seed_versions.join(", v")} against live; budget ${r.programme.budget_runs} runs · ${r.next}`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
case "programme-stop": {
|
|
1759
|
+
if (!id) throw new CliError('Usage: admin tuning programme-stop <programme-id> [--reason "…"]');
|
|
1760
|
+
const r = await staffRequest(
|
|
1761
|
+
ctx,
|
|
1762
|
+
"POST",
|
|
1763
|
+
`${T}/programmes/${id}/stop`,
|
|
1764
|
+
compact({ reason: flagStr(flags.reason) })
|
|
1765
|
+
);
|
|
1766
|
+
if (json) return printResult(io, flags, r);
|
|
1767
|
+
return io.log(`programme ${r.programme.id} ${r.programme.status}: ${r.programme.stop_reason}`);
|
|
1768
|
+
}
|
|
1769
|
+
case "programme-advance": {
|
|
1770
|
+
if (!id) throw new CliError("Usage: admin tuning programme-advance <programme-id>");
|
|
1771
|
+
const r = await staffRequest(ctx, "POST", `${T}/programmes/${id}/advance`, {});
|
|
1772
|
+
if (json) return printResult(io, flags, r);
|
|
1773
|
+
if (!r.transitions.length) io.log("nothing to do yet");
|
|
1774
|
+
for (const t of r.transitions)
|
|
1775
|
+
io.log(`generation ${t.generation}: ${t.from} → ${t.to}${t.note ? ` (${t.note})` : ""}`);
|
|
1776
|
+
if (r.read) io.log(`next: ${r.read.next}`);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1537
1779
|
// --- Research: our arena's versions, research comparisons, the deploy split ---
|
|
1538
1780
|
case "arenas": {
|
|
1539
1781
|
const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
|
package/lib/commands/phantom.mjs
CHANGED
|
@@ -73,6 +73,12 @@ export const ARGS = {
|
|
|
73
73
|
theme: "theme",
|
|
74
74
|
customer_ref: { flag: "customer-ref" },
|
|
75
75
|
collection_id: { flag: "collection" },
|
|
76
|
+
redesign: { flag: "redesign", type: "bool", negate: "no-redesign" },
|
|
77
|
+
// `--layout none` clears the choice (the site's theme then suggests one).
|
|
78
|
+
layout: {
|
|
79
|
+
value: (flags) =>
|
|
80
|
+
flags.layout === undefined ? undefined : flags.layout === "none" ? null : String(flags.layout),
|
|
81
|
+
},
|
|
76
82
|
},
|
|
77
83
|
},
|
|
78
84
|
collection: { value: (flags) => readJsonObjectFlag(flags, "collection-config") },
|