[object Object]

← back to Gmc Titlefix

TK-11233: derive an exact <=25-product canary cohort with its own hash

af743e5736fe8a7c4d605635570a3d94bf0a5a4e · 2026-09-10 08:13:43 -0700 · Steve Abrams

The addendum requires the approval to name an immutable cohort with exact
product IDs. A full select shortlist holds hundreds of candidates while the
canary is capped at 25, so approving the full file would name a cohort far
larger than what is authorized. This emits a separate shortlist of exactly
the first N candidates with its own content hash, so the approved file is
itself the ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W43bYwipr6GHKjJtWSsMyX

Files touched

Diff

commit af743e5736fe8a7c4d605635570a3d94bf0a5a4e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 08:13:43 2026 -0700

    TK-11233: derive an exact <=25-product canary cohort with its own hash
    
    The addendum requires the approval to name an immutable cohort with exact
    product IDs. A full select shortlist holds hundreds of candidates while the
    canary is capped at 25, so approving the full file would name a cohort far
    larger than what is authorized. This emits a separate shortlist of exactly
    the first N candidates with its own content hash, so the approved file is
    itself the ceiling.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01W43bYwipr6GHKjJtWSsMyX
---
 tk11233-make-canary-shortlist.mjs | 51 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/tk11233-make-canary-shortlist.mjs b/tk11233-make-canary-shortlist.mjs
new file mode 100644
index 0000000..bb42484
--- /dev/null
+++ b/tk11233-make-canary-shortlist.mjs
@@ -0,0 +1,51 @@
+#!/usr/bin/env node
+/**
+ * TK-11233 - derive the CANARY shortlist from a full select shortlist.
+ *
+ * The 2026-09-05 addendum requires the approval to name an immutable, identity-bearing cohort with
+ * "exact product IDs". A full select shortlist may hold hundreds of candidates while the canary is
+ * capped at <=25 actual applied products, so approving the full file would name a cohort far larger
+ * than what is authorized. This emits a separate shortlist containing EXACTLY the first N
+ * candidates, with its own content hash, so Steve approves precisely the products that can be
+ * touched - and the cumulative cap makes the file itself the ceiling.
+ *
+ * Usage: node tk11233-make-canary-shortlist.mjs <full-shortlist.json> [N=25]
+ * Read-only apart from writing the derived file. $0. Fires nothing.
+ */
+import fs from 'fs';
+import path from 'path';
+import { shortlistHash } from './tk11233-umbrella-hires-v2.mjs';
+
+const src = process.argv[2];
+const n = parseInt(process.argv[3] || '25', 10);
+if (!src || !fs.existsSync(src)) { console.error('usage: node tk11233-make-canary-shortlist.mjs <full-shortlist.json> [N]'); process.exit(4); }
+if (!Number.isInteger(n) || n < 1 || n > 25) { console.error('N must be 1..25 (the approved canary ceiling)'); process.exit(4); }
+
+const doc = JSON.parse(fs.readFileSync(src, 'utf8'));
+if (shortlistHash(doc.candidates || []) !== doc.shortlist_sha256) { console.error('source shortlist self-hash drift - refusing'); process.exit(4); }
+
+const candidates = (doc.candidates || []).slice(0, n);
+if (!candidates.length) { console.error('source shortlist has no candidates'); process.exit(4); }
+
+const out = Object.assign({}, doc, {
+  derived_from: path.basename(src),
+  derived_from_sha256: doc.shortlist_sha256,
+  derived_at: new Date().toISOString(),
+  cohort: 'canary',
+  counts: { candidates: candidates.length },
+  candidates,
+  rejected: undefined, skipped: undefined,   // keep the approval artifact small and about the cohort
+});
+out.shortlist_sha256 = shortlistHash(candidates);
+out.apply_gate = 'apply requires: --apply AND TK11233_APPROVED=1 AND --shortlist <this file> AND --shortlist-sha ' + out.shortlist_sha256 + ' AND --cap ' + candidates.length;
+
+const dst = src.replace(/\.json$/, '-canary' + candidates.length + '.json');
+fs.writeFileSync(dst, JSON.stringify(out, null, 2));
+console.log('canary cohort : ' + candidates.length + ' products');
+console.log('sha256        : ' + out.shortlist_sha256);
+console.log('file          : ' + dst);
+console.log('');
+console.log('products:');
+for (const c of candidates) {
+  console.log('  ' + String(c.shopify_id).replace(/^.*\//, '').padStart(14) + '  ' + (c.vendor || '').padEnd(14) + '  ' + (c.product_mfr_sku || '').padEnd(18) + '  served=' + c.served + ' -> orig=' + c.original);
+}

← a07ace5 TK-11233: verify must not report a vacuous pass on an empty  ·  back to Gmc Titlefix  ·  auto-data-snapshot: 2026-09-10T08:12:39 (5 data files) — dat a8bd5c2 →