@rightkit/social-ui 0.0.0-stage → 0.1.0

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +40 -2
  5. package/dist/appName.d.ts +2 -0
  6. package/dist/appName.js +8 -0
  7. package/dist/components/ScopeSelect.d.ts +7 -0
  8. package/dist/components/ScopeSelect.js +6 -0
  9. package/dist/components/Shared.d.ts +34 -0
  10. package/dist/components/Shared.js +55 -0
  11. package/dist/data/derive.d.ts +21 -0
  12. package/dist/data/derive.js +47 -0
  13. package/dist/data/draftRules.d.ts +10 -0
  14. package/dist/data/draftRules.js +20 -0
  15. package/dist/data/engine/engineDataSource.d.ts +194 -0
  16. package/dist/data/engine/engineDataSource.js +836 -0
  17. package/dist/data/engine/errors.d.ts +13 -0
  18. package/dist/data/engine/errors.js +21 -0
  19. package/dist/data/engine/mapping.d.ts +172 -0
  20. package/dist/data/engine/mapping.js +1487 -0
  21. package/dist/data/engine/protocol.d.ts +910 -0
  22. package/dist/data/engine/protocol.js +67 -0
  23. package/dist/data/index.d.ts +18 -0
  24. package/dist/data/index.js +23 -0
  25. package/dist/data/mock/devSource.d.ts +7 -0
  26. package/dist/data/mock/devSource.js +10 -0
  27. package/dist/data/mock/fixtures-analytics.d.ts +7 -0
  28. package/dist/data/mock/fixtures-analytics.js +123 -0
  29. package/dist/data/mock/fixtures-briefs.d.ts +4 -0
  30. package/dist/data/mock/fixtures-briefs.js +230 -0
  31. package/dist/data/mock/fixtures-content.d.ts +4 -0
  32. package/dist/data/mock/fixtures-content.js +89 -0
  33. package/dist/data/mock/fixtures-digests.d.ts +3 -0
  34. package/dist/data/mock/fixtures-digests.js +40 -0
  35. package/dist/data/mock/fixtures-drafts.d.ts +2 -0
  36. package/dist/data/mock/fixtures-drafts.js +74 -0
  37. package/dist/data/mock/fixtures-gate.d.ts +3 -0
  38. package/dist/data/mock/fixtures-gate.js +49 -0
  39. package/dist/data/mock/fixtures-hooks.d.ts +2 -0
  40. package/dist/data/mock/fixtures-hooks.js +103 -0
  41. package/dist/data/mock/fixtures-inbox.d.ts +11 -0
  42. package/dist/data/mock/fixtures-inbox.js +213 -0
  43. package/dist/data/mock/fixtures-links.d.ts +5 -0
  44. package/dist/data/mock/fixtures-links.js +89 -0
  45. package/dist/data/mock/fixtures-memory.d.ts +3 -0
  46. package/dist/data/mock/fixtures-memory.js +148 -0
  47. package/dist/data/mock/fixtures-ops.d.ts +15 -0
  48. package/dist/data/mock/fixtures-ops.js +403 -0
  49. package/dist/data/mock/fixtures-optimise.d.ts +18 -0
  50. package/dist/data/mock/fixtures-optimise.js +221 -0
  51. package/dist/data/mock/fixtures-pacing.d.ts +5 -0
  52. package/dist/data/mock/fixtures-pacing.js +285 -0
  53. package/dist/data/mock/fixtures-publish.d.ts +2 -0
  54. package/dist/data/mock/fixtures-publish.js +129 -0
  55. package/dist/data/mock/fixtures-store.d.ts +10 -0
  56. package/dist/data/mock/fixtures-store.js +13 -0
  57. package/dist/data/mock/fixtures-variants.d.ts +5 -0
  58. package/dist/data/mock/fixtures-variants.js +138 -0
  59. package/dist/data/mock/fixtures.d.ts +6 -0
  60. package/dist/data/mock/fixtures.js +420 -0
  61. package/dist/data/mock/mockDataSource.d.ts +98 -0
  62. package/dist/data/mock/mockDataSource.js +903 -0
  63. package/dist/data/money.d.ts +14 -0
  64. package/dist/data/money.js +36 -0
  65. package/dist/data/platforms.d.ts +12 -0
  66. package/dist/data/platforms.js +26 -0
  67. package/dist/data/source.d.ts +196 -0
  68. package/dist/data/source.js +1 -0
  69. package/dist/data/time.d.ts +23 -0
  70. package/dist/data/time.js +86 -0
  71. package/dist/data/types.d.ts +1092 -0
  72. package/dist/data/types.js +3 -0
  73. package/dist/env.d.ts +59 -0
  74. package/dist/hooks/useAsync.d.ts +19 -0
  75. package/dist/hooks/useAsync.js +25 -0
  76. package/dist/index.d.ts +56 -0
  77. package/dist/index.js +58 -0
  78. package/dist/lib/host.d.ts +21 -0
  79. package/dist/lib/host.js +32 -0
  80. package/dist/lib/shell.d.ts +156 -0
  81. package/dist/lib/shell.js +16 -0
  82. package/dist/screens/AccountsScreen.d.ts +8 -0
  83. package/dist/screens/AccountsScreen.js +100 -0
  84. package/dist/screens/AnalyticsScreen.d.ts +7 -0
  85. package/dist/screens/AnalyticsScreen.js +64 -0
  86. package/dist/screens/ApprovalsScreen.d.ts +10 -0
  87. package/dist/screens/ApprovalsScreen.js +144 -0
  88. package/dist/screens/BriefsScreen.d.ts +7 -0
  89. package/dist/screens/BriefsScreen.js +83 -0
  90. package/dist/screens/CalendarScreen.d.ts +10 -0
  91. package/dist/screens/CalendarScreen.js +143 -0
  92. package/dist/screens/CampaignsScreen.d.ts +7 -0
  93. package/dist/screens/CampaignsScreen.js +52 -0
  94. package/dist/screens/ChannelMemoryScreen.d.ts +7 -0
  95. package/dist/screens/ChannelMemoryScreen.js +109 -0
  96. package/dist/screens/ContentScreen.d.ts +7 -0
  97. package/dist/screens/ContentScreen.js +64 -0
  98. package/dist/screens/DigestsScreen.d.ts +7 -0
  99. package/dist/screens/DigestsScreen.js +53 -0
  100. package/dist/screens/DraftsScreen.d.ts +10 -0
  101. package/dist/screens/DraftsScreen.js +108 -0
  102. package/dist/screens/FailuresScreen.d.ts +8 -0
  103. package/dist/screens/FailuresScreen.js +111 -0
  104. package/dist/screens/HookTestsScreen.d.ts +7 -0
  105. package/dist/screens/HookTestsScreen.js +54 -0
  106. package/dist/screens/InboxScreen.d.ts +12 -0
  107. package/dist/screens/InboxScreen.js +76 -0
  108. package/dist/screens/LinksScreen.d.ts +7 -0
  109. package/dist/screens/LinksScreen.js +52 -0
  110. package/dist/screens/PacingScreen.d.ts +7 -0
  111. package/dist/screens/PacingScreen.js +88 -0
  112. package/dist/screens/PublishJobsScreen.d.ts +7 -0
  113. package/dist/screens/PublishJobsScreen.js +66 -0
  114. package/dist/screens/SuggestionsScreen.d.ts +8 -0
  115. package/dist/screens/SuggestionsScreen.js +94 -0
  116. package/dist/screens/TextGateScreen.d.ts +7 -0
  117. package/dist/screens/TextGateScreen.js +111 -0
  118. package/dist/screens/ThreadsScreen.d.ts +8 -0
  119. package/dist/screens/ThreadsScreen.js +113 -0
  120. package/dist/screens/TypedBriefsScreen.d.ts +7 -0
  121. package/dist/screens/TypedBriefsScreen.js +75 -0
  122. package/dist/screens/labels.d.ts +121 -0
  123. package/dist/screens/labels.js +272 -0
  124. package/package.json +56 -3
@@ -0,0 +1,221 @@
1
+ export const SCOPE_ALPHA = "fixture-alpha/alpha-shorts/alpha-main";
2
+ export const SCOPE_BETA = "fixture-beta/beta-studio/beta-main";
3
+ export const SCOPE_GAMMA = "fixture-gamma/gamma-news/gamma-main";
4
+ export const MOCK_MONTH = "2026-10";
5
+ export const FIXTURE_SCOPES = [
6
+ { id: SCOPE_ALPHA, label: "fixture-alpha / alpha-shorts / alpha-main" },
7
+ // Empty-suggestions scope: an allowance is set, but no published post has enough evidence yet.
8
+ { id: SCOPE_BETA, label: "fixture-beta / beta-studio / beta-main" },
9
+ // No allowance for the month, so the spend report is missing.
10
+ { id: SCOPE_GAMMA, label: "fixture-gamma / gamma-news / gamma-main" },
11
+ ];
12
+ const ALPHA_LINEAGE_JOBS = ["pub-0118", "pub-0121", "pub-0127", "pub-0133", "pub-0141"];
13
+ export const FIXTURE_PROPOSALS = [
14
+ {
15
+ // Waiting for a decision. Due 14 days after the evaluation instant.
16
+ id: "opt_3f9a0c1d2e4b5a60",
17
+ scopeId: SCOPE_ALPHA,
18
+ kind: "remake",
19
+ state: "proposed",
20
+ revision: 1,
21
+ sourceJobId: "pub-0141",
22
+ platform: "instagram",
23
+ metric: "views",
24
+ window: "lifetime",
25
+ observedAtUtc: "2026-10-11T06:00:00Z",
26
+ value: "18450",
27
+ threshold: "16200",
28
+ sampleSize: 30,
29
+ decileSize: 3,
30
+ dueAtUtc: "2026-10-25T06:00:00Z",
31
+ lineageJobIds: ALPHA_LINEAGE_JOBS,
32
+ lineagePullIds: ["pull-0412", "pull-0418"],
33
+ decisionNote: null,
34
+ decidedAtUtc: null,
35
+ calendarEntryId: null,
36
+ createdAtUtc: "2026-10-11T06:05:00Z",
37
+ updatedAtUtc: "2026-10-11T06:05:00Z",
38
+ },
39
+ {
40
+ // Accepted: its draft calendar entry exists and still needs a caption and approval.
41
+ id: "opt_77b1e0a9c2d43f15",
42
+ scopeId: SCOPE_ALPHA,
43
+ kind: "remake",
44
+ state: "accepted",
45
+ revision: 2,
46
+ sourceJobId: "pub-0127",
47
+ platform: "youtube",
48
+ metric: "views",
49
+ window: "lifetime",
50
+ observedAtUtc: "2026-10-08T06:00:00Z",
51
+ value: "9120",
52
+ threshold: "8800",
53
+ sampleSize: 40,
54
+ decileSize: 4,
55
+ dueAtUtc: "2026-10-22T06:00:00Z",
56
+ lineageJobIds: ["pub-0110", "pub-0115", "pub-0127"],
57
+ lineagePullIds: ["pull-0401"],
58
+ decisionNote: null,
59
+ decidedAtUtc: "2026-10-10T09:30:00Z",
60
+ calendarEntryId: "remake_entry_opt_77b1e0a9c2d43f15",
61
+ createdAtUtc: "2026-10-08T06:10:00Z",
62
+ updatedAtUtc: "2026-10-10T09:30:00Z",
63
+ },
64
+ {
65
+ // Dismissed with a note. Creates no calendar entry.
66
+ id: "opt_c04d82e6f1a7b390",
67
+ scopeId: SCOPE_ALPHA,
68
+ kind: "remake",
69
+ state: "dismissed",
70
+ revision: 2,
71
+ sourceJobId: "pub-0102",
72
+ platform: "instagram",
73
+ metric: "saves",
74
+ window: "lifetime",
75
+ observedAtUtc: "2026-10-05T06:00:00Z",
76
+ value: "310",
77
+ threshold: "280",
78
+ sampleSize: 20,
79
+ decileSize: 2,
80
+ dueAtUtc: "2026-10-19T06:00:00Z",
81
+ lineageJobIds: ["pub-0098", "pub-0102"],
82
+ lineagePullIds: ["pull-0377"],
83
+ decisionNote: "Same idea as a post last week.",
84
+ decidedAtUtc: "2026-10-09T16:10:00Z",
85
+ calendarEntryId: null,
86
+ createdAtUtc: "2026-10-05T06:12:00Z",
87
+ updatedAtUtc: "2026-10-09T16:10:00Z",
88
+ },
89
+ ];
90
+ // Limit, reserved, committed and remaining are consistent: reserved equals the one open estimate, and
91
+ // committed equals the report's measured actuals.
92
+ export const FIXTURE_ALLOWANCES = [
93
+ {
94
+ scopeId: SCOPE_ALPHA,
95
+ allowance: {
96
+ month: MOCK_MONTH,
97
+ currency: "EUR",
98
+ limitMicro: 50_000_000,
99
+ reservedMicro: 3_000_000,
100
+ committedMicro: 12_400_000,
101
+ remainingMicro: 34_600_000,
102
+ revision: 4,
103
+ updatedAtUtc: "2026-10-12T14:00:00Z",
104
+ },
105
+ },
106
+ {
107
+ scopeId: SCOPE_BETA,
108
+ allowance: {
109
+ month: MOCK_MONTH,
110
+ currency: "EUR",
111
+ limitMicro: 5_000_000,
112
+ reservedMicro: 0,
113
+ committedMicro: 0,
114
+ remainingMicro: 5_000_000,
115
+ revision: 1,
116
+ updatedAtUtc: "2026-10-01T08:00:00Z",
117
+ },
118
+ },
119
+ ];
120
+ export const FIXTURE_REPORTS = [
121
+ {
122
+ scopeId: SCOPE_ALPHA,
123
+ report: {
124
+ month: MOCK_MONTH,
125
+ currency: "EUR",
126
+ limitMicro: 50_000_000,
127
+ reservedMicro: 3_000_000,
128
+ committedMicro: 12_400_000,
129
+ remainingMicro: 34_600_000,
130
+ reconciles: true,
131
+ routes: [
132
+ {
133
+ routeId: "example.text.model-a.input",
134
+ countReserved: 1,
135
+ countCommitted: 2,
136
+ countReleased: 0,
137
+ countExpired: 0,
138
+ openMicro: 3_000_000,
139
+ forecastMicro: 12_000_000,
140
+ measuredMicro: 12_400_000,
141
+ varianceMicro: 400_000,
142
+ overReservations: 0,
143
+ },
144
+ {
145
+ routeId: "example.image.render",
146
+ countReserved: 0,
147
+ countCommitted: 0,
148
+ countReleased: 1,
149
+ countExpired: 1,
150
+ openMicro: 0,
151
+ forecastMicro: 0,
152
+ measuredMicro: 0,
153
+ varianceMicro: 0,
154
+ overReservations: 0,
155
+ },
156
+ ],
157
+ },
158
+ },
159
+ {
160
+ scopeId: SCOPE_BETA,
161
+ report: {
162
+ month: MOCK_MONTH,
163
+ currency: "EUR",
164
+ limitMicro: 5_000_000,
165
+ reservedMicro: 0,
166
+ committedMicro: 0,
167
+ remainingMicro: 5_000_000,
168
+ reconciles: true,
169
+ routes: [],
170
+ },
171
+ },
172
+ ];
173
+ // Latest version of each route. The first is qualified. The second has no verifier, so it is unqualified
174
+ // ("unverified"). The third was fetched more than 60 days before the mock clock, so it is unqualified.
175
+ export const FIXTURE_PRICES = [
176
+ {
177
+ routeId: "example.text.model-a.input",
178
+ version: 2,
179
+ provider: "Example Model Co",
180
+ modelSlug: "model-a",
181
+ unit: "per_1M_input",
182
+ amountMicro: 2_500_000,
183
+ currency: "EUR",
184
+ sourceUrl: "https://pricing.example/mock/model-a",
185
+ sourcePathLine: null,
186
+ fetchedAtUtc: "2026-10-10T09:00:00Z",
187
+ verifiedBy: "fixture reviewer",
188
+ qualified: true,
189
+ reason: null,
190
+ },
191
+ {
192
+ routeId: "example.image.render",
193
+ version: 1,
194
+ provider: "Example Render Co",
195
+ modelSlug: "render-b",
196
+ unit: "per_asset",
197
+ amountMicro: 40_000,
198
+ currency: "EUR",
199
+ sourceUrl: null,
200
+ sourcePathLine: "fixtures/prices.toml:12",
201
+ fetchedAtUtc: "2026-10-09T09:00:00Z",
202
+ verifiedBy: null,
203
+ qualified: false,
204
+ reason: "unverified: no verified_by",
205
+ },
206
+ {
207
+ routeId: "example.video.audio",
208
+ version: 1,
209
+ provider: "Example Audio Co",
210
+ modelSlug: "audio-c",
211
+ unit: "per_minute_audio",
212
+ amountMicro: 600_000,
213
+ currency: "EUR",
214
+ sourceUrl: "https://pricing.example/mock/audio-c",
215
+ sourcePathLine: null,
216
+ fetchedAtUtc: "2026-07-01T09:00:00Z",
217
+ verifiedBy: "fixture reviewer",
218
+ qualified: false,
219
+ reason: "fetched_at is more than 60 days before now",
220
+ },
221
+ ];
@@ -0,0 +1,5 @@
1
+ import type { PublishNextView, PublishQuota } from "../types.js";
2
+ /** The pacing view of one channel. A channel with nothing queued has an empty view. */
3
+ export declare function pacingViewFor(scopeId: string): PublishNextView;
4
+ /** The pacing of one account. An account with no stored row reads as the defaults, not opted in. */
5
+ export declare function quotaFor(scopeId: string, platform: string, remoteAccount: string): PublishQuota;
@@ -0,0 +1,285 @@
1
+ import { MOCK_NOW_UTC } from "./fixtures.js";
2
+ import { SCOPE_ALPHA, SCOPE_BETA, SCOPE_GAMMA } from "./fixtures-optimise.js";
3
+ const DAY_RESETS_UTC = "2026-10-13T00:00:00Z";
4
+ const DEFAULT_MIN_GAP_SECS = 900;
5
+ const DEFAULT_MAX_PER_DAY = 10;
6
+ function job(input) {
7
+ return { readable: true, ...input };
8
+ }
9
+ function blocked(input) {
10
+ return { readable: true, ...input };
11
+ }
12
+ const ALPHA_READY = [
13
+ job({
14
+ jobId: "pn-fixture-0101",
15
+ scopeId: SCOPE_ALPHA,
16
+ platform: "instagram",
17
+ remoteAccount: "fixture-ig-1",
18
+ entryId: "fixture-entry-0401",
19
+ state: "queued",
20
+ phase: "create",
21
+ scheduledAtUtc: "2026-10-12T18:00:00Z",
22
+ }),
23
+ ];
24
+ const ALPHA_BLOCKED = [
25
+ blocked({
26
+ jobId: "pn-fixture-0102",
27
+ scopeId: SCOPE_ALPHA,
28
+ platform: "instagram",
29
+ remoteAccount: "fixture-ig-2",
30
+ entryId: "fixture-entry-0402",
31
+ state: "queued",
32
+ phase: "create",
33
+ scheduledAtUtc: "2026-10-12T19:00:00Z",
34
+ reason: "not_due",
35
+ reasons: ["not_due"],
36
+ untilUtc: "2026-10-12T19:00:00Z",
37
+ detail: "The scheduled time is 19:00:00 UTC.",
38
+ behindJobId: null,
39
+ }),
40
+ blocked({
41
+ jobId: "pn-fixture-0103",
42
+ scopeId: SCOPE_ALPHA,
43
+ platform: "instagram",
44
+ remoteAccount: "fixture-ig-1",
45
+ entryId: "fixture-entry-0403",
46
+ state: "queued",
47
+ phase: "create",
48
+ scheduledAtUtc: "2026-10-12T18:10:00Z",
49
+ reason: "account_gap",
50
+ reasons: ["account_gap"],
51
+ untilUtc: "2026-10-12T18:50:00Z",
52
+ detail: "The last create on this account was at 17:00:00 UTC. The 1200-second gap ends at 18:50:00 UTC.",
53
+ behindJobId: "pn-fixture-0101",
54
+ }),
55
+ blocked({
56
+ jobId: "pn-fixture-0104",
57
+ scopeId: SCOPE_ALPHA,
58
+ platform: "facebook",
59
+ remoteAccount: "fixture-fb-1",
60
+ entryId: "fixture-entry-0404",
61
+ state: "waiting",
62
+ phase: "create",
63
+ scheduledAtUtc: "2026-10-12T19:30:00Z",
64
+ reason: "rate_limited_until",
65
+ reasons: ["rate_limited_until", "not_due"],
66
+ untilUtc: "2026-10-12T19:30:00Z",
67
+ detail: "The platform asked us to wait until 19:30:00 UTC.",
68
+ behindJobId: null,
69
+ }),
70
+ blocked({
71
+ jobId: "pn-fixture-0105",
72
+ scopeId: SCOPE_ALPHA,
73
+ platform: "youtube",
74
+ remoteAccount: "fixture-yt-1",
75
+ entryId: "fixture-entry-0405",
76
+ state: "queued",
77
+ phase: "create",
78
+ scheduledAtUtc: "2026-10-12T18:05:00Z",
79
+ reason: "account_paused",
80
+ reasons: ["account_paused"],
81
+ untilUtc: "2026-10-14T00:00:00Z",
82
+ detail: "A person paused this account until 2026-10-14 00:00:00 UTC.",
83
+ behindJobId: null,
84
+ }),
85
+ blocked({
86
+ jobId: "pn-fixture-0106",
87
+ scopeId: SCOPE_ALPHA,
88
+ platform: "instagram",
89
+ remoteAccount: "fixture-ig-3",
90
+ entryId: "fixture-entry-0406",
91
+ state: "queued",
92
+ phase: "create",
93
+ scheduledAtUtc: "2026-10-12T17:00:00Z",
94
+ reason: "outcome_unknown_on_account",
95
+ reasons: ["outcome_unknown_on_account"],
96
+ untilUtc: null,
97
+ detail: "Another job on this account has an unknown outcome. A person must resolve it on the server.",
98
+ behindJobId: null,
99
+ }),
100
+ blocked({
101
+ jobId: "pn-fixture-0107",
102
+ scopeId: SCOPE_ALPHA,
103
+ platform: "linkedin",
104
+ remoteAccount: "fixture-li-1",
105
+ entryId: "fixture-entry-0407",
106
+ state: "queued",
107
+ phase: "create",
108
+ scheduledAtUtc: "2026-10-12T17:30:00Z",
109
+ reason: "account_not_ready",
110
+ reasons: ["account_not_ready"],
111
+ untilUtc: null,
112
+ detail: "The accounts store says this account cannot publish: its token has expired.",
113
+ behindJobId: null,
114
+ }),
115
+ blocked({
116
+ jobId: "pn-fixture-0108",
117
+ scopeId: SCOPE_ALPHA,
118
+ platform: "threads",
119
+ remoteAccount: "fixture-th-1",
120
+ entryId: "fixture-entry-0408",
121
+ state: "queued",
122
+ phase: "create",
123
+ scheduledAtUtc: "2026-10-12T16:00:00Z",
124
+ reason: "daily_cap",
125
+ reasons: ["daily_cap"],
126
+ untilUtc: DAY_RESETS_UTC,
127
+ detail: "The daily cap for this account is reached. It resets at the next UTC midnight.",
128
+ behindJobId: null,
129
+ }),
130
+ // A state the desktop does not show: kept, unread, with its raw JSON.
131
+ {
132
+ readable: false,
133
+ reason: 'publish state "paused_by_test" is not shown by the desktop yet',
134
+ rawJson: JSON.stringify({ job_id: "pn-fixture-0109", state: "paused_by_test" }, null, 2),
135
+ },
136
+ ];
137
+ const ALPHA_IN_PROGRESS = [
138
+ job({
139
+ jobId: "pn-fixture-0110",
140
+ scopeId: SCOPE_ALPHA,
141
+ platform: "instagram",
142
+ remoteAccount: "fixture-ig-1",
143
+ entryId: "fixture-entry-0410",
144
+ state: "running",
145
+ phase: "poll",
146
+ scheduledAtUtc: "2026-10-12T18:05:00Z",
147
+ }),
148
+ ];
149
+ const VIEWS = new Map([
150
+ [
151
+ SCOPE_ALPHA,
152
+ { nowUtc: MOCK_NOW_UTC, ready: ALPHA_READY, readyTotal: ALPHA_READY.length, blocked: ALPHA_BLOCKED, inProgress: ALPHA_IN_PROGRESS },
153
+ ],
154
+ [SCOPE_BETA, { nowUtc: MOCK_NOW_UTC, ready: [], readyTotal: 0, blocked: [], inProgress: [] }],
155
+ [
156
+ SCOPE_GAMMA,
157
+ {
158
+ nowUtc: MOCK_NOW_UTC,
159
+ ready: [
160
+ job({
161
+ jobId: "pn-fixture-0201",
162
+ scopeId: SCOPE_GAMMA,
163
+ platform: "bluesky",
164
+ remoteAccount: "fixture-bsky-1",
165
+ entryId: "fixture-entry-0501",
166
+ state: "queued",
167
+ phase: "create",
168
+ scheduledAtUtc: "2026-10-12T18:00:00Z",
169
+ }),
170
+ ],
171
+ readyTotal: 1,
172
+ blocked: [],
173
+ inProgress: [],
174
+ },
175
+ ],
176
+ ]);
177
+ /** The pacing view of one channel. A channel with nothing queued has an empty view. */
178
+ export function pacingViewFor(scopeId) {
179
+ return VIEWS.get(scopeId) ?? { nowUtc: MOCK_NOW_UTC, ready: [], readyTotal: 0, blocked: [], inProgress: [] };
180
+ }
181
+ /** Pacing of one account as stored. An account with no stored limits has the host app defaults. */
182
+ const QUOTAS = [
183
+ {
184
+ scopeId: SCOPE_ALPHA,
185
+ platform: "instagram",
186
+ remoteAccount: "fixture-ig-1",
187
+ optedIn: true,
188
+ minGapSecs: 1200,
189
+ maxPerDay: 6,
190
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
191
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
192
+ pausedUntilUtc: null,
193
+ lastCreateAtUtc: "2026-10-12T17:00:00Z",
194
+ createsToday: 4,
195
+ dayResetsAtUtc: DAY_RESETS_UTC,
196
+ rateLimitedAtUtc: null,
197
+ rateLimitedUntilUtc: null,
198
+ },
199
+ {
200
+ scopeId: SCOPE_ALPHA,
201
+ platform: "instagram",
202
+ remoteAccount: "fixture-ig-2",
203
+ optedIn: false,
204
+ minGapSecs: DEFAULT_MIN_GAP_SECS,
205
+ maxPerDay: DEFAULT_MAX_PER_DAY,
206
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
207
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
208
+ pausedUntilUtc: null,
209
+ lastCreateAtUtc: "2026-10-11T20:00:00Z",
210
+ createsToday: 0,
211
+ dayResetsAtUtc: DAY_RESETS_UTC,
212
+ rateLimitedAtUtc: null,
213
+ rateLimitedUntilUtc: null,
214
+ },
215
+ {
216
+ scopeId: SCOPE_ALPHA,
217
+ platform: "youtube",
218
+ remoteAccount: "fixture-yt-1",
219
+ optedIn: true,
220
+ minGapSecs: DEFAULT_MIN_GAP_SECS,
221
+ maxPerDay: DEFAULT_MAX_PER_DAY,
222
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
223
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
224
+ pausedUntilUtc: "2026-10-14T00:00:00Z",
225
+ lastCreateAtUtc: null,
226
+ createsToday: 0,
227
+ dayResetsAtUtc: DAY_RESETS_UTC,
228
+ rateLimitedAtUtc: null,
229
+ rateLimitedUntilUtc: null,
230
+ },
231
+ {
232
+ scopeId: SCOPE_ALPHA,
233
+ platform: "facebook",
234
+ remoteAccount: "fixture-fb-1",
235
+ optedIn: false,
236
+ minGapSecs: DEFAULT_MIN_GAP_SECS,
237
+ maxPerDay: DEFAULT_MAX_PER_DAY,
238
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
239
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
240
+ pausedUntilUtc: null,
241
+ lastCreateAtUtc: "2026-10-12T16:35:00Z",
242
+ createsToday: 2,
243
+ dayResetsAtUtc: DAY_RESETS_UTC,
244
+ rateLimitedAtUtc: "2026-10-12T16:40:00Z",
245
+ rateLimitedUntilUtc: "2026-10-12T19:30:00Z",
246
+ },
247
+ {
248
+ scopeId: SCOPE_ALPHA,
249
+ platform: "threads",
250
+ remoteAccount: "fixture-th-1",
251
+ optedIn: true,
252
+ minGapSecs: DEFAULT_MIN_GAP_SECS,
253
+ maxPerDay: DEFAULT_MAX_PER_DAY,
254
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
255
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
256
+ pausedUntilUtc: null,
257
+ lastCreateAtUtc: "2026-10-12T15:00:00Z",
258
+ createsToday: 10,
259
+ dayResetsAtUtc: DAY_RESETS_UTC,
260
+ rateLimitedAtUtc: null,
261
+ rateLimitedUntilUtc: null,
262
+ },
263
+ ];
264
+ /** The pacing of one account. An account with no stored row reads as the defaults, not opted in. */
265
+ export function quotaFor(scopeId, platform, remoteAccount) {
266
+ const stored = QUOTAS.find((q) => q.scopeId === scopeId && q.platform === platform && q.remoteAccount === remoteAccount);
267
+ if (stored)
268
+ return stored;
269
+ return {
270
+ scopeId,
271
+ platform,
272
+ remoteAccount,
273
+ optedIn: false,
274
+ minGapSecs: DEFAULT_MIN_GAP_SECS,
275
+ maxPerDay: DEFAULT_MAX_PER_DAY,
276
+ defaultMinGapSecs: DEFAULT_MIN_GAP_SECS,
277
+ defaultMaxPerDay: DEFAULT_MAX_PER_DAY,
278
+ pausedUntilUtc: null,
279
+ lastCreateAtUtc: null,
280
+ createsToday: 0,
281
+ dayResetsAtUtc: DAY_RESETS_UTC,
282
+ rateLimitedAtUtc: null,
283
+ rateLimitedUntilUtc: null,
284
+ };
285
+ }
@@ -0,0 +1,2 @@
1
+ import type { PublishJobRecord } from "../types.js";
2
+ export declare const FIXTURE_PUBLISH_JOBS: PublishJobRecord[];
@@ -0,0 +1,129 @@
1
+ import { SCOPE_ALPHA } from "./fixtures-optimise.js";
2
+ export const FIXTURE_PUBLISH_JOBS = [
3
+ {
4
+ // Published by the daemon, with a permalink.
5
+ scopeId: SCOPE_ALPHA,
6
+ jobId: "pj-fixture-0001",
7
+ entryId: "fixture-entry-0301",
8
+ entryRevision: 3,
9
+ platform: "instagram",
10
+ state: "published",
11
+ phase: "done",
12
+ remotePostId: "fixture-media-0001",
13
+ permalink: "https://example.com/fixture/post/0001",
14
+ attempts: 0,
15
+ nextAttemptAtUtc: null,
16
+ errorCode: null,
17
+ errorMessage: null,
18
+ resolution: null,
19
+ createdAtUtc: "2026-10-08T08:00:00Z",
20
+ updatedAtUtc: "2026-10-08T08:04:00Z",
21
+ },
22
+ {
23
+ // Waiting after a rate limit: nothing was posted yet, and the daemon calls again at the stored instant.
24
+ scopeId: SCOPE_ALPHA,
25
+ jobId: "pj-fixture-0002",
26
+ entryId: "fixture-entry-0302",
27
+ entryRevision: 2,
28
+ platform: "linkedin",
29
+ state: "waiting",
30
+ phase: "create",
31
+ remotePostId: null,
32
+ permalink: null,
33
+ attempts: 2,
34
+ nextAttemptAtUtc: "2026-10-12T19:05:00Z",
35
+ errorCode: "rate_limited",
36
+ errorMessage: "The platform asked App to wait before the next call. Nothing was posted yet.",
37
+ resolution: null,
38
+ createdAtUtc: "2026-10-11T07:00:00Z",
39
+ updatedAtUtc: "2026-10-12T18:20:00Z",
40
+ },
41
+ {
42
+ // Failed with token_expired: no retry until the account is reconnected.
43
+ scopeId: SCOPE_ALPHA,
44
+ jobId: "pj-fixture-0003",
45
+ entryId: "fixture-entry-0303",
46
+ entryRevision: 1,
47
+ platform: "instagram",
48
+ state: "failed",
49
+ phase: "create_container",
50
+ remotePostId: null,
51
+ permalink: null,
52
+ attempts: 0,
53
+ nextAttemptAtUtc: null,
54
+ errorCode: "token_expired",
55
+ errorMessage: "The platform rejected the access token. Nothing was posted, and nothing is retried until the account is reconnected.",
56
+ resolution: null,
57
+ createdAtUtc: "2026-10-10T16:00:00Z",
58
+ updatedAtUtc: "2026-10-10T16:01:00Z",
59
+ },
60
+ {
61
+ // Outcome unknown: the platform may or may not hold the post. Only a person can settle it.
62
+ scopeId: SCOPE_ALPHA,
63
+ jobId: "pj-fixture-0004",
64
+ entryId: "fixture-entry-0304",
65
+ entryRevision: 2,
66
+ platform: "facebook",
67
+ state: "outcome_unknown",
68
+ phase: "create",
69
+ remotePostId: null,
70
+ permalink: null,
71
+ attempts: 0,
72
+ nextAttemptAtUtc: null,
73
+ errorCode: "ambiguous_response",
74
+ errorMessage: "The platform answered with a server error after the request was sent.",
75
+ resolution: null,
76
+ createdAtUtc: "2026-10-09T12:00:00Z",
77
+ updatedAtUtc: "2026-10-09T12:02:00Z",
78
+ },
79
+ {
80
+ // Resolved by hand as posted: the remote id was typed by a person, so it is marked as such.
81
+ scopeId: SCOPE_ALPHA,
82
+ jobId: "pj-fixture-0005",
83
+ entryId: "fixture-entry-0305",
84
+ entryRevision: 1,
85
+ platform: "linkedin",
86
+ state: "published",
87
+ phase: "done",
88
+ remotePostId: "urn:li:share:fixture0005",
89
+ permalink: null,
90
+ attempts: 0,
91
+ nextAttemptAtUtc: null,
92
+ errorCode: null,
93
+ errorMessage: null,
94
+ resolution: {
95
+ decision: "posted",
96
+ resolvedBy: "adrian",
97
+ resolvedAtUtc: "2026-10-07T10:20:00Z",
98
+ note: "Checked the member feed: the post is there. Copied its URN.",
99
+ remoteIdEnteredByHand: true,
100
+ },
101
+ createdAtUtc: "2026-10-07T09:00:00Z",
102
+ updatedAtUtc: "2026-10-07T10:20:00Z",
103
+ },
104
+ {
105
+ // Resolved by hand as not posted: final, never retried.
106
+ scopeId: SCOPE_ALPHA,
107
+ jobId: "pj-fixture-0006",
108
+ entryId: "fixture-entry-0306",
109
+ entryRevision: 1,
110
+ platform: "threads",
111
+ state: "failed",
112
+ phase: "publish",
113
+ remotePostId: null,
114
+ permalink: null,
115
+ attempts: 0,
116
+ nextAttemptAtUtc: null,
117
+ errorCode: "resolved_not_posted",
118
+ errorMessage: "A person recorded that nothing was posted. This job is final and is not retried.",
119
+ resolution: {
120
+ decision: "not_posted",
121
+ resolvedBy: "adrian",
122
+ resolvedAtUtc: "2026-10-06T11:45:00Z",
123
+ note: "Checked the account threads: no post with this text.",
124
+ remoteIdEnteredByHand: false,
125
+ },
126
+ createdAtUtc: "2026-10-06T09:00:00Z",
127
+ updatedAtUtc: "2026-10-06T11:45:00Z",
128
+ },
129
+ ];
@@ -0,0 +1,10 @@
1
+ import type { RestoreReceipt } from "../types.js";
2
+ export interface FixtureHeartbeat {
3
+ pid: number;
4
+ /** Milliseconds since the Unix epoch. */
5
+ updatedMs: number;
6
+ recordState: string;
7
+ }
8
+ export declare const FIXTURE_HEARTBEAT: FixtureHeartbeat;
9
+ /** One restore from an invented backup. The previous store is named as set aside, never deleted. */
10
+ export declare const FIXTURE_RESTORE_RECEIPTS: RestoreReceipt[];
@@ -0,0 +1,13 @@
1
+ // 2026-10-12T18:25:00Z, five minutes before the mock clock: alive against the 900-second threshold.
2
+ export const FIXTURE_HEARTBEAT = { pid: 4102, updatedMs: Date.parse("2026-10-12T18:25:00Z"), recordState: "serving" };
3
+ /** One restore from an invented backup. The previous store is named as set aside, never deleted. */
4
+ export const FIXTURE_RESTORE_RECEIPTS = [
5
+ {
6
+ receiptId: "fixture-rr-20261011-093000-0123456789ab",
7
+ restoredAtUtc: "2026-10-11T09:30:00Z",
8
+ manifestDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
9
+ schemaBefore: 7,
10
+ schemaAfter: 8,
11
+ previousStore: "/fixture/postright/store/postright.sqlite.pre-restore-1760175000000",
12
+ },
13
+ ];
@@ -0,0 +1,5 @@
1
+ import type { BriefVariant, TypedBrief } from "../types.js";
2
+ /** Two typed briefs in the first channel: one explainer and one product demo. */
3
+ export declare const FIXTURE_TYPED_BRIEFS: TypedBrief[];
4
+ /** Variants of the two briefs, on two platforms each. The explainer also has one instagram variant from revision 1. */
5
+ export declare const FIXTURE_BRIEF_VARIANTS: BriefVariant[];