← back to Dw Sku Integrity
TK-10900 enforce 500-row recovery chunks
e144e7aa83b09550ffc0b40fdccb5c1a1b0d7411 · 2026-08-31 12:00:53 -0700 · codex-10896
Files touched
A chunk-content-match-plan.mjsA test/chunk-content-match-plan.test.mjs
Diff
commit e144e7aa83b09550ffc0b40fdccb5c1a1b0d7411
Author: codex-10896 <steve@designerwallcoverings.com>
Date: Mon Aug 31 12:00:53 2026 -0700
TK-10900 enforce 500-row recovery chunks
---
chunk-content-match-plan.mjs | 44 ++++++++++++++++++++++++++++++++++
test/chunk-content-match-plan.test.mjs | 9 +++++++
2 files changed, 53 insertions(+)
diff --git a/chunk-content-match-plan.mjs b/chunk-content-match-plan.mjs
new file mode 100644
index 0000000..91cf99e
--- /dev/null
+++ b/chunk-content-match-plan.mjs
@@ -0,0 +1,44 @@
+#!/usr/bin/env node
+// Split one reviewed content-match restore map into independently preflightable,
+// reversible <=N-row chunks. This is local-only plan generation; it never
+// connects to or writes a database. TK-10900.
+
+import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
+import { join } from 'node:path';
+
+const sqlEscape = (v) => String(v).replace(/'/g, "''");
+
+export function splitPlan(entries, size) {
+ if (!Number.isInteger(size) || size < 1 || size > 500) throw new Error('chunk size must be an integer from 1 to 500');
+ const out = [];
+ for (let i = 0; i < entries.length; i += size) out.push(entries.slice(i, i + size));
+ return out;
+}
+
+export function writeChunks({ sourceDir, outDir, size = 500 }) {
+ const entries = JSON.parse(readFileSync(join(sourceDir, 'restore-map.json'), 'utf8'));
+ const chunks = splitPlan(entries, size);
+ rmSync(outDir, { recursive: true, force: true });
+ mkdirSync(outDir, { recursive: true });
+ const header = '-- GATED -- TK-10900 chunked canonical recovery; existing codes only, no mint.\n';
+ chunks.forEach((rows, index) => {
+ const name = `chunk-${String(index + 1).padStart(3, '0')}`;
+ const dir = join(outDir, name);
+ mkdirSync(dir, { recursive: true });
+ writeFileSync(join(dir, 'restore-map.json'), JSON.stringify(rows, null, 2) + '\n');
+ writeFileSync(join(dir, 'apply.sql'), header + rows.map((r) =>
+ `UPDATE shopify_products SET dw_sku='${sqlEscape(r.new)}' WHERE shopify_id='${sqlEscape(r.shopify_id)}' AND (dw_sku IS NULL OR btrim(dw_sku)='');`).join('\n') + '\n');
+ writeFileSync(join(dir, 'undo.sql'), header + rows.map((r) =>
+ `UPDATE shopify_products SET dw_sku=NULL WHERE shopify_id='${sqlEscape(r.shopify_id)}' AND dw_sku='${sqlEscape(r.new)}';`).join('\n') + '\n');
+ });
+ return chunks.map((rows, index) => ({ name: `chunk-${String(index + 1).padStart(3, '0')}`, rows: rows.length }));
+}
+
+if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
+ const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
+ const sourceDir = arg('--source');
+ const outDir = arg('--out');
+ const size = Number(arg('--size', '500'));
+ if (!sourceDir || !outDir) throw new Error('usage: chunk-content-match-plan.mjs --source <vendor-plan-dir> --out <chunk-root> [--size 500]');
+ console.log(JSON.stringify(writeChunks({ sourceDir, outDir, size }), null, 2));
+}
diff --git a/test/chunk-content-match-plan.test.mjs b/test/chunk-content-match-plan.test.mjs
new file mode 100644
index 0000000..3bee514
--- /dev/null
+++ b/test/chunk-content-match-plan.test.mjs
@@ -0,0 +1,9 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { splitPlan } from '../chunk-content-match-plan.mjs';
+
+test('splitPlan enforces the approved <=500 row boundary', () => {
+ const rows = Array.from({ length: 1201 }, (_, i) => ({ shopify_id: String(i), new: `DW-${i}` }));
+ assert.deepEqual(splitPlan(rows, 500).map((x) => x.length), [500, 500, 201]);
+ assert.throws(() => splitPlan(rows, 501), /1 to 500/);
+});
← 79fb8ef TK-10900 normalize catalog unit suffixes before recovery
·
back to Dw Sku Integrity
·
TK-10900 record verified content recovery be42199 →