thumbgate 1.16.22 → 1.18.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.
@@ -25,7 +25,7 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.16.22",
28
+ "softwareVersion": "1.18.0",
29
29
  "url": "https://thumbgate-production.up.railway.app/numbers",
30
30
  "dateModified": "2026-05-07",
31
31
  "creator": {
@@ -202,7 +202,7 @@
202
202
  <main class="container">
203
203
  <h1>The Numbers</h1>
204
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
205
- <div class="freshness">Updated: 2026-05-07 · Version 1.16.22</div>
205
+ <div class="freshness">Updated: 2026-05-07 · Version 1.18.0</div>
206
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
207
207
 
208
208
  <h2>Gate enforcement</h2>
@@ -6,7 +6,10 @@ const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
7
 
8
8
  const MAX_AUTO_GATES = 10;
9
- const WARN_THRESHOLD = 2; // 2+ repeated failures surface a warning gate
9
+ // 1+ failure auto-promotes to a warning gate. Cold buyers expect "one 👎 → blocked next time"
10
+ // — a 2-capture threshold made first-capture invisible and broke the activation loop. Block
11
+ // escalation still requires 3 captures (BLOCK_THRESHOLD) so noise doesn't auto-hard-block.
12
+ const WARN_THRESHOLD = 1;
10
13
  const BLOCK_THRESHOLD = 3; // 3+ repeated failures hard-block the action
11
14
  const WINDOW_DAYS = 30;
12
15
 
@@ -1149,7 +1149,8 @@ function loadResolvedRevenueEvents(options = {}) {
1149
1149
  const derived = deriveRevenueEventFromPaidProviderEvent(entry);
1150
1150
  if (!derived) continue;
1151
1151
  if (hasRevenueEventMatch(resolved, derived)) continue;
1152
- resolved.push(derived);
1152
+ const priced = resolveGithubMarketplaceRevenueEntry(derived, { annotate: false }).entry;
1153
+ resolved.push(priced);
1153
1154
  }
1154
1155
 
1155
1156
  return mergeRevenueEvents(resolved, extraRevenueEvents);
@@ -1161,6 +1162,9 @@ function repairGithubMarketplaceRevenueLedger(options = {}) {
1161
1162
  const rows = loadJsonlFile(ledgerPath);
1162
1163
  const resolvedAt = new Date().toISOString();
1163
1164
  const repairs = [];
1165
+
1166
+ // Pass 1: in-place repair of rows already in revenue-events.jsonl that have
1167
+ // unknown amounts but resolvable plan metadata.
1164
1168
  const updatedRows = rows.map((entry) => {
1165
1169
  const result = resolveGithubMarketplaceRevenueEntry(entry, {
1166
1170
  annotate: true,
@@ -1178,21 +1182,76 @@ function repairGithubMarketplaceRevenueLedger(options = {}) {
1178
1182
  currency: normalizeCurrency(result.entry.currency),
1179
1183
  recurringInterval: normalizeText(result.entry.recurringInterval),
1180
1184
  pricingSource: normalizeText(metadata.githubMarketplaceAmountSource),
1185
+ source: 'in_place',
1181
1186
  });
1182
1187
  return result.entry;
1183
1188
  });
1184
1189
 
1190
+ // Pass 2: append rows for funnel-derived paid github_marketplace events that
1191
+ // never landed in the revenue ledger. The webhook handler at handleGithubWebhook
1192
+ // skips appendRevenueEvent when hasRevenueEventMatch is true, so duplicates from
1193
+ // a re-run are already prevented; the only way an order gets here is if the
1194
+ // revenue write was skipped at webhook time (e.g. funnel pre-existed, planPricing
1195
+ // had unknown amount at the time, or the row was created via a different path).
1196
+ const funnelRecords = loadFunnelLedger().filter(
1197
+ (e) =>
1198
+ e &&
1199
+ e.stage === 'paid' &&
1200
+ normalizeText((e.metadata && e.metadata.provider) || e.provider) === 'github_marketplace'
1201
+ );
1202
+
1203
+ for (const funnelEntry of funnelRecords) {
1204
+ const derived = deriveRevenueEventFromPaidProviderEvent(funnelEntry);
1205
+ if (!derived) continue;
1206
+ if (hasRevenueEventMatch(updatedRows, derived)) continue;
1207
+
1208
+ const resolved = resolveGithubMarketplaceRevenueEntry(derived, {
1209
+ annotate: true,
1210
+ resolvedAt,
1211
+ });
1212
+ // Skip funnel-derived rows that still have unknown amounts after resolving —
1213
+ // we don't want to permanently bake amountKnown:false rows when there's no
1214
+ // pricing data to attach. Better to leave them off-disk and keep the
1215
+ // read-time merge in loadResolvedRevenueEvents covering them.
1216
+ if (!resolved.changed || !resolved.entry.amountKnown) continue;
1217
+
1218
+ const persisted = {
1219
+ ...resolved.entry,
1220
+ metadata: {
1221
+ ...sanitizeMetadata(resolved.entry.metadata),
1222
+ recoveredFromFunnelLedger: true,
1223
+ funnelRecordedAt: normalizeText(funnelEntry.timestamp) || null,
1224
+ },
1225
+ };
1226
+ updatedRows.push(persisted);
1227
+
1228
+ const metadata = sanitizeMetadata(persisted.metadata);
1229
+ repairs.push({
1230
+ orderId: normalizeText(persisted.orderId),
1231
+ customerId: normalizeText(persisted.customerId),
1232
+ planId: normalizeText(metadata.planId ?? persisted.planId),
1233
+ amountCents: normalizeInteger(persisted.amountCents),
1234
+ currency: normalizeCurrency(persisted.currency),
1235
+ recurringInterval: normalizeText(persisted.recurringInterval),
1236
+ pricingSource: normalizeText(metadata.githubMarketplaceAmountSource),
1237
+ source: 'funnel_derived',
1238
+ });
1239
+ }
1240
+
1185
1241
  const writeResult = write && repairs.length > 0
1186
1242
  ? writeJsonlRecords(ledgerPath, updatedRows)
1187
- : { written: false, rowCount: rows.length };
1243
+ : { written: false, rowCount: updatedRows.length };
1188
1244
 
1189
1245
  return {
1190
1246
  ledgerPath,
1191
1247
  write,
1192
1248
  wrote: Boolean(writeResult.written),
1193
1249
  scanned: rows.length,
1250
+ funnelScanned: funnelRecords.length,
1194
1251
  repaired: repairs.length,
1195
- unchanged: rows.length - repairs.length,
1252
+ repairedInPlace: repairs.filter((r) => r.source === 'in_place').length,
1253
+ repairedFromFunnel: repairs.filter((r) => r.source === 'funnel_derived').length,
1254
+ unchanged: rows.length - repairs.filter((r) => r.source === 'in_place').length,
1196
1255
  repairs,
1197
1256
  writeResult,
1198
1257
  };
@@ -32,7 +32,17 @@ function normalize(ctx) {
32
32
  return (ctx || '').replace(/\/Users\/[^\s/]+/g, '~').replace(/:[0-9]+/g, '').toLowerCase().trim();
33
33
  }
34
34
 
35
- const HIGH_RISK_TAGS = new Set(['git-workflow', 'scope-control', 'trust-breach', 'execution-gap', 'regression', 'security']);
35
+ // HIGH_RISK_TAGS triggers single-capture promotion (count >= 1 && hasHighRisk).
36
+ // Tags here MUST overlap with what inferSemanticTags() actually emits (see scripts/feedback-loop.js)
37
+ // — otherwise cold buyers' first 👎 stays a lesson and never becomes a gate.
38
+ const HIGH_RISK_TAGS = new Set([
39
+ // Original semantic-category labels
40
+ 'git-workflow', 'scope-control', 'trust-breach', 'execution-gap', 'regression', 'security',
41
+ // Tags inferSemanticTags() emits for destructive / irreversible operations
42
+ 'destructive', 'force-push', 'delete', 'drop', 'force-overwrite',
43
+ 'production', 'database', 'payment', 'credentials', 'secrets',
44
+ 'rm-rf', 'reset-hard', 'truncate', 'data-loss',
45
+ ]);
36
46
  function analyze(entries) {
37
47
  let positiveCount = 0, negativeCount = 0;
38
48
  const categories = {};