@tokenoftrust/storefront-runner 2.4.6 → 2.4.7

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.
@@ -0,0 +1,12 @@
1
+ -- The candidate preparation ledger has no writer or reader: candidate progress lives in the KV
2
+ -- ReviewEnvironment index, and the release state in the tenant pipeline. Its tables are renamed
3
+ -- aside, not dropped, so the rows survive until a backup is taken; rename back to restore. The
4
+ -- indexes are dropped so their names are free (they are re-creatable from the rows).
5
+ DROP INDEX IF EXISTS `idx_candidate_attempts_tenant`;
6
+ DROP INDEX IF EXISTS `idx_candidate_attempts_candidate`;
7
+ DROP INDEX IF EXISTS `uq_candidate_attempts_candidate_number`;
8
+ ALTER TABLE `candidate_attempts` RENAME TO `retired_candidate_attempts_20261010`;
9
+ DROP INDEX IF EXISTS `idx_candidates_tenant_status`;
10
+ DROP INDEX IF EXISTS `uq_candidates_tenant_change`;
11
+ DROP INDEX IF EXISTS `uq_candidates_tenant_repo_pr`;
12
+ ALTER TABLE `candidates` RENAME TO `retired_candidates_20261010`;
@@ -564,94 +564,6 @@ export const newsletterSends = sqliteTable(
564
564
  (t) => ({ pk: primaryKey({ columns: [t.tenantId, t.collection, t.digestId] }) }),
565
565
  );
566
566
 
567
- // --- Canonical publish-candidate identity -----------------------------------
568
- // ONE canonical candidate per `(tenant_id, repository, pr_number)` — the real
569
- // forge PR. Retries/rebuilds are NOT new candidates; they are child `attempts`
570
- // (below). This is the durable identity spine other units resolve against; the
571
- // KV `ReviewEnvironment` (`cust:*`) and aggregate/release models (`rel:*`) are
572
- // read projections that reference this row's `change_id`/`pr_number`.
573
- //
574
- // `id` is a SYNTHETIC surrogate (`cand_<uuid>`), deliberately DISTINCT from the
575
- // canonical key so foreign keys (attempts) never depend on the mutable natural
576
- // key. `change_id` is the REAL forge change id — the stable branch/build
577
- // identity (`candidate/<change_id>`) — preserved, never derived from `id`.
578
- // `tenant_id` is the isolation boundary, written from the resolved session,
579
- // NEVER a client body. The UNIQUE `(tenant_id, repository, pr_number)` is what
580
- // makes "one candidate per PR" a schema invariant (upsert-by-key, not
581
- // insert-then-dedupe); the UNIQUE `(tenant_id, change_id)` keeps the branch/
582
- // build identity unambiguous for resolve-by-changeId callers.
583
- export const candidates = sqliteTable(
584
- "candidates",
585
- {
586
- id: text("id").primaryKey(),
587
- tenantId: text("tenant_id").notNull(),
588
- // The `<tenant>-<tag>` forge repository the PR lives in.
589
- repository: text("repository").notNull(),
590
- // The forge PR number — the third leg of the canonical key.
591
- prNumber: integer("pr_number").notNull(),
592
- // The real forge change id — stable branch/build identity (`candidate/<change_id>`).
593
- changeId: text("change_id").notNull(),
594
- // Source branch, conventionally `candidate/<change_id>` (nullable convenience mirror).
595
- branch: text("branch"),
596
- title: text("title"),
597
- author: text("author"),
598
- // Canonical candidate lifecycle: open | preparing | ready | needs-attention | accepted | rejected | closed.
599
- status: text("status").notNull().default("open"),
600
- // The PR head/base shas as last reconciled (nullable until first read).
601
- headSha: text("head_sha"),
602
- baseSha: text("base_sha"),
603
- createdAt: text("created_at").notNull(),
604
- updatedAt: text("updated_at").notNull(),
605
- },
606
- (t) => ({
607
- uqCandidatesCanonical: uniqueIndex("uq_candidates_tenant_repo_pr").on(
608
- t.tenantId,
609
- t.repository,
610
- t.prNumber,
611
- ),
612
- uqCandidatesChange: uniqueIndex("uq_candidates_tenant_change").on(t.tenantId, t.changeId),
613
- idxCandidatesTenantStatus: index("idx_candidates_tenant_status").on(t.tenantId, t.status),
614
- }),
615
- );
616
-
617
- // One `attempt` = one build/check/rebase EXECUTION of a candidate. Attempts are
618
- // the child rows retries produce — they must NEVER queue as separate top-level
619
- // candidate rows. `candidate_id` references `candidates.id` in code only (no SQL
620
- // FK — repo convention, matches `webhook_deliveries.event_id` / `orders`).
621
- // `tenant_id` is denormalized for tenant-scoped reads/isolation without a join.
622
- // `attempt_number` is monotonic per candidate (1,2,3…) giving stable ordering;
623
- // UNIQUE `(candidate_id, attempt_number)` makes a double-assigned number a loud
624
- // failure rather than a silent duplicate. `kind`: build | check | rebase;
625
- // `status`: pending | running | passed | failed | cancelled (mirrors
626
- // `IntegrationRun`'s state vocabulary). `evidence` is a JSON EvidenceReport
627
- // snapshot; `detail` a human-legible terminal-state message.
628
- export const candidateAttempts = sqliteTable(
629
- "candidate_attempts",
630
- {
631
- id: text("id").primaryKey(),
632
- candidateId: text("candidate_id").notNull(),
633
- tenantId: text("tenant_id").notNull(),
634
- attemptNumber: integer("attempt_number").notNull(),
635
- kind: text("kind").notNull(),
636
- status: text("status").notNull().default("pending"),
637
- // The candidate head sha this attempt executed against, or null if unknown.
638
- headSha: text("head_sha"),
639
- detail: text("detail"),
640
- evidence: text("evidence"),
641
- startedAt: text("started_at").notNull(),
642
- // Null while pending/running; set on a terminal transition.
643
- finishedAt: text("finished_at"),
644
- },
645
- (t) => ({
646
- uqAttemptNumber: uniqueIndex("uq_candidate_attempts_candidate_number").on(
647
- t.candidateId,
648
- t.attemptNumber,
649
- ),
650
- idxAttemptsCandidate: index("idx_candidate_attempts_candidate").on(t.candidateId),
651
- idxAttemptsTenant: index("idx_candidate_attempts_tenant").on(t.tenantId),
652
- }),
653
- );
654
-
655
567
  // One active submit per tenant/change. This serializes identity authorization,
656
568
  // the Gitea mutation, and audit finalization so a verified callback cannot
657
569
  // become stale between its forge read and its KV writes. `expires_at` is
@@ -27,7 +27,6 @@ import type {
27
27
  MembershipEntry,
28
28
  MergeCandidate,
29
29
  PreviewJobServices,
30
- ReadModelTip,
31
30
  } from "../../src/lib/pipeline/preview-jobs";
32
31
 
33
32
  /** Files a fake build materializes. */
@@ -176,11 +175,6 @@ export function fakePreviewServices(state: DurableObjectState): PreviewJobServic
176
175
  return row?.sha ?? null;
177
176
  },
178
177
 
179
- async readModelTip(): Promise<ReadModelTip | null> {
180
- const rm = sql.exec<{ sha: string; membership: string; green: number }>(`SELECT * FROM fake_read_model`).toArray()[0];
181
- return rm ? { sha: rm.sha, membership: JSON.parse(rm.membership), green: rm.green === 1 } : null;
182
- },
183
-
184
178
  async materialize({ sha, resume }) {
185
179
  if (await fires("evict-before-build")) evict("before build");
186
180
  const moveTo = sql
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -147,8 +147,7 @@ export interface ReviewEnvironment {
147
147
  * seeds this projection BEFORE the async reconcile webhook ever fires, so a
148
148
  * dead/unregistered webhook shows as "queued since <submittedAt>, never
149
149
  * picked up" instead of no record at all). Additive + optional: every OTHER
150
- * writer of this projection (`reconcileCandidate`, `acceptCandidateAtomic`,
151
- * terminal cleanup) simply carries it forward unset/
150
+ * writer of this projection (`reconcileCandidate`, terminal cleanup) simply carries it forward unset/
152
151
  * untouched — only the submit-ack path ever sets it. Deliberately distinct
153
152
  * from `updatedAt`, which every writer bumps on any recompute for any reason.
154
153
  */