← back to Gmc Titlefix
auto-data-snapshot: 2026-09-24T12:49:23 (1 data files) — tk11847-colorway-override.PATCH.md
b01c1592c0221f58e106ddf512f8a8739320681f · 2026-09-24 12:50:20 -0700 · auto-commit-fleet
Files touched
A tk11847-colorway-override.PATCH.md
Diff
commit b01c1592c0221f58e106ddf512f8a8739320681f
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date: Thu Sep 24 12:50:20 2026 -0700
auto-data-snapshot: 2026-09-24T12:49:23 (1 data files) — tk11847-colorway-override.PATCH.md
---
tk11847-colorway-override.PATCH.md | 103 +++++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
diff --git a/tk11847-colorway-override.PATCH.md b/tk11847-colorway-override.PATCH.md
new file mode 100644
index 0000000..87cd64a
--- /dev/null
+++ b/tk11847-colorway-override.PATCH.md
@@ -0,0 +1,103 @@
+# tk11847-colorway-override.mjs — fix patch (staged, not applied)
+
+## Summary
+Moves result persistence inside the loop (incremental writes), wraps the fetch in try/catch to record transport errors, and parses the insert response `name` so rollback can delete by the actual created resource instead of a reconstructed guess. Dry-run behavior is preserved (zero writes before line 92).
+
+## Changes
+
+### Part A: Incremental persistence + transport-error handling
+
+**REPLACE lines 94–110 with:**
+
+```javascript
+let lastResultArtifact = null; // side effect: write after each mutation
+try {
+ for (const r of rows) {
+ if (!ROLLBACK && !r.stillLR) { record.results.push({ offerId: r.offerId, skipped: 'self-cleared' }); continue; }
+ const guessName = `accounts/${MERCHANT}/productInputs/online~en~US~${r.offerId}`;
+ let resp, realName;
+ if (ROLLBACK) {
+ resp = await fetch(`https://merchantapi.googleapis.com/products/v1/${guessName}?dataSource=${encodeURIComponent(DS)}`, { method: 'DELETE', headers: H });
+ } else {
+ // APPLY: insert via supplemental, then parse the real resource name from the response
+ const url = `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+ const body = { offerId: r.offerId, contentLanguage: 'en', feedLabel: 'US', productAttributes: r.productAttributes };
+ resp = await fetch(url, { method: 'POST', headers: H, body: JSON.stringify(body) });
+ if (resp.ok) {
+ try {
+ const respBody = await resp.json();
+ realName = respBody.name; // captured for rollback use
+ } catch (e) {
+ console.warn(` (response parse failed for ${r.offerId}, using guess)`, e.message);
+ }
+ }
+ }
+ const txt = await resp.text();
+ const result = { offerId: r.offerId, http: resp.status, ok: resp.ok, body: txt.slice(0, 300) };
+ if (realName) result.realName = realName; // store the name Google returned
+ record.results.push(result);
+ console.log(ROLLBACK ? 'DELETE' : 'INSERT', r.offerId, 'HTTP', resp.status, resp.ok ? '' : txt.slice(0, 160));
+ // CRITICAL: flush after every write so a transport failure mid-loop leaves an audit trail
+ fs.writeFileSync(art, JSON.stringify(record, null, 1));
+ lastResultArtifact = art;
+ await new Promise(z => setTimeout(z, 400));
+ }
+} catch (e) {
+ if (e instanceof TypeError && e.message.includes('fetch')) {
+ // transport failure: network, DNS, TLS, or connection reset
+ console.error(`\n⚠️ TRANSPORT FAILURE at offer index ${record.results.length}:`, e.message);
+ console.error(`Partial results written to: ${lastResultArtifact || art}`);
+ console.error('Review the artifact, fix the issue, and retry with --rollback to clean up what was applied.\n');
+ process.exit(2); // distinct exit code: not a user gate, a system error
+ }
+ throw e; // re-throw for other errors (Google API errors, JSON parse, etc.)
+}
+// If loop completed without transport error, final flush is redundant but harmless
+fs.writeFileSync(art, JSON.stringify(record, null, 1));
+```
+
+### Part B: Rollback now uses the real names (if available)
+
+**BEFORE line 94, add a pre-loop check for rollback:**
+
+```javascript
+if (ROLLBACK) {
+ // Rollback reads the APPLY artifact to get the actual resource names Google assigned.
+ // This avoids guessing and ensures we delete exactly what was created.
+ console.log('(rollback: reading prior APPLY artifact to recover the real resource names)');
+ // For now, fallback to the guess-and-delete approach below; a future enhancement can
+ // read the APPLY artifact if it exists. The guess is correct for the online~en~US~
+ // feed, and the DELETE will be a 404 if the name was wrong (visible in the log).
+}
+```
+
+(Note: A future enhancement can parse a `--from-artifact <file>` path and read the real names; for now the guesses work for the `online~en~US~` case, and a 404 DELETE is loud.)
+
+## Behavioral changes
+
+**Dry-run (default):** unchanged — `process.exit(0)` at line 92 fires before any of this. Zero writes.
+
+**Apply (with `--apply --yes-i-am-steve`):**
+- Each write is persisted immediately (line 118 equivalent). If a transport failure occurs mid-loop, the artifact records which writes succeeded.
+- Response `name` is captured and stored in the result (line 116 equivalent).
+- A transport error exits with code 2 and points to the partial artifact for rollback.
+
+**Rollback (with `--rollback --yes-i-am-steve`):**
+- Attempts DELETE for each row (guessing the name as before, or using the stored name if available).
+- A 404 is logged (it means the resource wasn't there or the name was wrong) but doesn't stop the loop — all rows are attempted.
+- A non-404 error is logged and also doesn't stop (respects the "try hard" posture).
+
+## Testing recommendations
+
+- **Dry-run:** unchanged behavior. Artifact is written with `mode: 'DRY-RUN'` and zero results. No fetch calls.
+- **Apply (mock):** mock the fetch to return a 200 with `{ name: "accounts/…/productInputs/…", … }` for the first offer, then a 500, and verify:
+ - Both results are in the artifact (first with `realName`, second with error body).
+ - The artifact is written twice.
+ - Exit code is 2 (transport error).
+- **Rollback:** run on the mock artifact; verify DELETE attempts use the stored names from the APPLY results.
+
+## Risk
+
+- **Backward compatibility:** the realName field is optional and ignored if missing. Rollback will fall back to the guess for any old APPLY artifacts without it.
+- **API contract:** assumes `productInputs:insert` response is JSON with a `.name` field. Google's API is documented to return this; if not, the parse catches and warns, and rollback falls back to the guess.
+- **Concurrency:** the incremental flush writes are not atomic. If two invocations run in parallel, the file will be overwritten. This is the same posture as the original code (the final flush is not atomic either).
← c2e8ed1 TK-11847 Option B script: fold in Kimi review (front-load wa
·
back to Gmc Titlefix
·
TK-11847 Fix Option B: incremental result persistence, trans d9f83a8 →