← back to Norma
agents/instagram-agent/reconcile-gate2.js
113 lines
#!/usr/bin/env node
/**
* reconcile-gate2.js — READ-ONLY ground-truth reconciliation for TK-10573 GATE-2.
*
* The ledgers (deleted-posts.jsonl, delete-results-*.jsonl) are sparse/unreliable because the
* GATE-2 work happened across many sessions/architectures. The only reliable truth is the live
* Graph API. This script, using the ONE shared META token, for each of the 84 corrected
* originals:
* - GET /{old_media_id} → is the OLD bad-caption post still live? (delete target)
* - checks the owning account's live /media → is the NEW corrected repost actually live?
* (batch-04 found 3 reposts had silently vanished; deleting an old whose new is gone = data loss)
*
* Output: an eligibility report. eligible = old still live AND new confirmed live.
* Writes NOTHING to Instagram. Pure GETs. Cost $0.
*
* node reconcile-gate2.js # full report -> data/gate2-recon.json + console summary
*/
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const https = require('https');
const DATA = path.join(__dirname, 'data');
const SRC = path.join(DATA, 'redo-results-20260814.jsonl');
const ACC = require('./accounts.json').accounts;
const TOKEN = process.env.IG_ACCESS_TOKEN;
const VER = process.env.IG_GRAPH_VERSION || 'v21.0';
const HOST = 'graph.facebook.com';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const shortOf = (permalink) => (String(permalink).match(/\/p\/([^/]+)/) || [])[1] || null;
function get(pathq) {
return new Promise((resolve) => {
https.get(`https://${HOST}/${VER}/${pathq}`, (r) => {
let s = '';
r.on('data', (d) => (s += d));
r.on('end', () => { let j = null; try { j = JSON.parse(s); } catch {} resolve({ status: r.statusCode, body: j, raw: s }); });
}).on('error', (e) => resolve({ status: 0, body: null, raw: String(e.message) }));
});
}
// paginate an account's live media into a set of live shortcodes (read-only)
async function liveShortcodes(igId, cap = 200) {
const set = new Set();
let after = '';
for (let page = 0; page < 8; page++) {
const q = `${igId}/media?fields=id,permalink&limit=50${after ? `&after=${after}` : ''}&access_token=${TOKEN}`;
const r = await get(q);
if (r.status !== 200 || !r.body || !Array.isArray(r.body.data)) break;
r.body.data.forEach((m) => { const sc = shortOf(m.permalink); if (sc) set.add(sc); });
const next = r.body.paging && r.body.paging.cursors && r.body.paging.cursors.after;
if (!next || set.size >= cap) break;
after = next;
await sleep(150);
}
return set;
}
(async () => {
const rows = fs.readFileSync(SRC, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse)
.filter((r) => r.old_permalink && r.old_media_id);
const handles = [...new Set(rows.map((r) => r.handle))];
// per-account live shortcode sets (for NEW-repost liveness)
const liveByHandle = {};
for (const h of handles) {
const a = ACC[h];
if (!a || !a.ig_user_id) { liveByHandle[h] = null; continue; }
process.stderr.write(` fetching live media: @${h}\n`);
liveByHandle[h] = await liveShortcodes(a.ig_user_id);
await sleep(150);
}
const report = [];
for (const r of rows) {
const oldRes = await get(`${r.old_media_id}?fields=id,permalink&access_token=${TOKEN}`);
const old_live = oldRes.status === 200 && oldRes.body && oldRes.body.id;
const old_err = oldRes.status !== 200 ? (oldRes.body && oldRes.body.error && oldRes.body.error.message) || `HTTP ${oldRes.status}` : null;
const newSc = shortOf(r.new_permalink);
const liveSet = liveByHandle[r.handle];
const new_live = liveSet ? liveSet.has(newSc) : null; // null = couldn't verify (no roster/id)
report.push({
handle: r.handle, product_handle: r.product_handle,
old_media_id: r.old_media_id, old_permalink: r.old_permalink, old_live: !!old_live, old_err,
new_permalink: r.new_permalink, new_shortcode: newSc, new_live,
eligible: !!old_live && new_live === true,
reason: !old_live ? 'old-already-gone' : (new_live === false ? 'NEW-REPOST-MISSING(hold)' : (new_live === null ? 'new-unverifiable(hold)' : 'eligible')),
});
await sleep(150);
}
const out = path.join(DATA, 'gate2-recon.json');
fs.writeFileSync(out, JSON.stringify({ ts: new Date().toISOString(), total: report.length, report }, null, 2));
// summary
const byReason = {};
report.forEach((x) => (byReason[x.reason] = (byReason[x.reason] || 0) + 1));
console.log('\n=== GATE-2 RECONCILIATION (read-only) ===');
console.log('total originals:', report.length);
console.log('by reason:', JSON.stringify(byReason, null, 2));
const elig = report.filter((x) => x.eligible);
console.log('\nELIGIBLE TO DELETE (old live + new confirmed live):', elig.length);
const byHandleE = {};
elig.forEach((x) => (byHandleE[x.handle] = (byHandleE[x.handle] || 0) + 1));
console.log(JSON.stringify(byHandleE, null, 2));
const hold = report.filter((x) => !x.eligible && x.old_live);
if (hold.length) {
console.log('\n⚠️ HOLD — old still live but new NOT confirmed (do NOT delete):');
hold.forEach((x) => console.log(` @${x.handle} ${x.old_permalink} -> new ${x.new_permalink} [${x.reason}]`));
}
console.log('\nwrote', out);
})();