@tokenoftrust/cli 1.4.0-rc.14 → 1.4.0-rc.15

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/src/plan.mjs CHANGED
@@ -2,9 +2,10 @@
2
2
  * The shared "operation plan" affordance (unit U10) — the load-bearing
3
3
  * cross-cutting requirement from decision `operator-verb-and-hosting-model`:
4
4
  * every MUTATING operator verb (build / accept / ship / retire) must STATE
5
- * EXACTLY what it will do — which PR moves to main, which deploy targets
6
- * (preview / live) are touched, and their URLs — and get an explicit confirm
7
- * before acting. No silent multi-step mutations, in either surface (the CLI
5
+ * EXACTLY what it will do — which PR is queued/integrated/deployed, which
6
+ * deploy targets (preview / live) are touched, and their URLs — and get an
7
+ * explicit confirm before acting. (`accept` now queue-integrates a PR into the
8
+ * `preview` aggregate — unit b08 — rather than merging it to main.) No silent multi-step mutations, in either surface (the CLI
8
9
  * here, and `AdminPublishTab.astro`'s confirm dialog, which renders the same
9
10
  * shape of plan text server-side/inline).
10
11
  *
@@ -14,13 +15,14 @@
14
15
  * half — print the plan, then gate on an explicit yes (reusing `prompt.mjs`'s
15
16
  * TTY-safe `promptYesNo`; a non-TTY without `--yes` never silently proceeds).
16
17
  *
17
- * SHIP is context-dependent (decision `ship-context-dependent-semantics`):
18
- * run by the developer in their own checkout, the candidate is still open, so
19
- * shipping means accept-then-deploy (merge PR main, THEN deploy main to
20
- * preview + live). Run by an operator already targeting a PR that's merged,
21
- * shipping means just deploy (main preview + live) — there's nothing left
22
- * to merge. `planForAction` takes an explicit `context` so the caller (which
23
- * knows which situation it's in) picks the right narration; it never guesses.
18
+ * SHIP has ONE meaning (unit b10 — supersedes the retired context-dependent
19
+ * ship, decision `ship-context-dependent-semantics`): it publishes the
20
+ * tenant's CURRENT GREEN AGGREGATE the batch of PRs that integrated
21
+ * cleanly to live. No merge, no PR/candidate targeting, no developer-vs-
22
+ * operator branching. The plan states the pinned aggregate sha, its
23
+ * content-addressed artifact digest, every included PR, the rollback target,
24
+ * and the go-live paywall verdict, so the human reviews EXACTLY what "green"
25
+ * means before confirming.
24
26
  *
25
27
  * Dependency-free (no imports besides the sibling `prompt.mjs`).
26
28
  */
@@ -52,6 +54,11 @@ function targetLabel({ pr, changeId }) {
52
54
  * endpoint?: string|null,
53
55
  * targets?: { preview?: string|null, live?: string|null },
54
56
  * context?: "developer"|"operator",
57
+ * pinnedSha?: string|null,
58
+ * artifactDigest?: string|null,
59
+ * includedPrs?: Array<{ prNumber?: number|null, changeId?: string|null, headSha?: string|null }>,
60
+ * rollbackTarget?: { receiptId: string, aggregateSha: string } | null,
61
+ * paywall?: { allowed: boolean, message?: string|null } | null,
55
62
  * }} params
56
63
  * @returns {string[]} plan lines (no leading/trailing blank line)
57
64
  */
@@ -64,7 +71,14 @@ export function planForAction({
64
71
  endpoint = null,
65
72
  targets = {},
66
73
  context = "operator",
74
+ pinnedSha = null,
75
+ artifactDigest = null,
76
+ includedPrs = null,
77
+ rollbackTarget = null,
78
+ paywall = null,
67
79
  }) {
80
+ void context; // retained param — no action currently branches on it (ship, the
81
+ // last one that did, is now ONE meaning; kept so a future action can opt in).
68
82
  const label = targetLabel({ pr, changeId });
69
83
  const lines = [`${titleFor(action)} plan:`];
70
84
  if (tenant) lines.push(` tenant: ${tenant}`);
@@ -82,21 +96,43 @@ export function planForAction({
82
96
  break;
83
97
  }
84
98
  case "accept": {
85
- lines.push(` effect: merge ${label} into main.`);
99
+ // Accept now means QUEUE-INTEGRATE-INTO-PREVIEW (unit b08), NOT merge-to-main:
100
+ // the candidate lands in the protected `preview` aggregate (serialized merge →
101
+ // rebuild → combined-evidence gate), and the aggregate goes live only later via
102
+ // `tot ship`. So the plan states the integration, never a merge or a go-live.
103
+ const into = tenant ? `${tenant}'s preview aggregate` : "the preview aggregate";
104
+ lines.push(
105
+ ` effect: queue ${label} for integration into ${into} — NO merge to main, NO go-live.`,
106
+ );
86
107
  break;
87
108
  }
88
109
  case "ship": {
89
- const deployTargets = deployTargetsLine(targets);
90
- if (context === "developer") {
91
- // The candidate is still open in the developer's own checkout — ship
92
- // is accept-then-deploy in one gated step.
93
- lines.push(
94
- ` effect: merge ${label} into main, then deploy main → ${deployTargets}.`,
95
- );
96
- } else {
97
- // Operator targeting a PR that's already merged — nothing left to
98
- // merge, so ship is just the deploy half.
99
- lines.push(` effect: deploy main → ${deployTargets}.`);
110
+ // ONE meaning (b10): publish the tenant's CURRENT GREEN AGGREGATE to live.
111
+ // No merge b09's orchestrator ships the already-materialized, already-
112
+ // reviewed digest. State exactly WHAT that is: the pinned sha, the
113
+ // content-addressed artifact digest, every included PR, the rollback
114
+ // target, and the go-live paywall verdict — so the human reviews the
115
+ // real content of "green" before confirming.
116
+ lines.push(
117
+ ` effect: publish the current green aggregate to live${targets.live ? ` (${targets.live})` : ""} — no merge.`,
118
+ );
119
+ if (pinnedSha) lines.push(` pinned sha: ${pinnedSha}`);
120
+ if (artifactDigest) lines.push(` artifact digest: ${artifactDigest}`);
121
+ if (Array.isArray(includedPrs)) {
122
+ lines.push(` included PRs (${includedPrs.length}):`);
123
+ for (const p of includedPrs) {
124
+ const prLabel = p && p.prNumber != null ? `#${p.prNumber}` : p?.changeId || "(no PR)";
125
+ const shortSha = p && p.headSha ? ` ${String(p.headSha).slice(0, 8)}` : "";
126
+ lines.push(` - ${prLabel}${shortSha}`);
127
+ }
128
+ }
129
+ lines.push(
130
+ rollbackTarget
131
+ ? ` rollback to: ${rollbackTarget.aggregateSha} (receipt ${rollbackTarget.receiptId})`
132
+ : " rollback to: (none — first-ever ship)",
133
+ );
134
+ if (paywall && paywall.allowed === false) {
135
+ lines.push(` ⚠ paywall: ${paywall.message || "go-live is blocked by the storefront subscription gate"}`);
100
136
  }
101
137
  break;
102
138
  }
@@ -121,13 +157,6 @@ function titleFor(action) {
121
157
  return action ? action[0].toUpperCase() + action.slice(1) : "Operation";
122
158
  }
123
159
 
124
- /** Render "preview (<url>) + live (<url>)", degrading gracefully when a URL is unknown. Pure. */
125
- function deployTargetsLine(targets = {}) {
126
- const preview = targets.preview ? `preview (${targets.preview})` : "preview";
127
- const live = targets.live ? `live (${targets.live})` : "live";
128
- return `${preview} + ${live}`;
129
- }
130
-
131
160
  /**
132
161
  * Print a plan and gate on an explicit confirm — the CLI half of the shared
133
162
  * affordance. Prints every line, a trailing blank line, then: