// Sprint 4 — regenerated capability matrix.
//
// Derived from audit/write-access/compute.mjs (the 2026-07-29 audit's 129 canonical
// capabilities, same tiers, same weights, same scoring). ONLY the gate column moved.
// Every change below is annotated with the sprint that moved it, so the delta is
// auditable line-by-line rather than being a fresh unexplained score.
//
// LEAD RECONCILIATION (stated, because two agents graded the same situation
// differently): the Klaviyo agent capped its platform at G4 because live writes
// refuse until an operator records the API key's scopes; the TikTok agent awarded
// G5 despite an equally-unproven write scope. Both cannot be right. Ruling: an
// unverified scope CONFIGURATION is an ARMING PRECONDITION, not a gate demotion —
// otherwise every shadow platform collapses to G4 and the gate stops discriminating.
// G5 therefore means "the full control stack is present and the code is correct";
// whether the operator has configured the credential is tracked in the arm-readiness
// verdict instead. Klaviyo is graded on the same basis as TikTok here.
//
// G4 = reachable + scope preflight enforced + doc-contract-correct
// G5 = G4 + guardrailed + reversibility entry + a REAL idempotency guard + audited
// G6 = proven against the vendor.  G6 = 0 EVERYWHERE. No vendor call was made.
const W = { 1: 3, 2: 2, 3: 1 };

// [tier, gate, label]   // ← annotation = why it moved (blank = unchanged since the audit)
const caps = {
  meta: [
    // Tier1
    [1,5,"campaign budget update"],[1,5,"ad set budget update"],[1,5,"campaign status"],[1,5,"ad set status"],[1,5,"ad status"],[1,5,"bid strategy change"],[1,5,"bid cap/cost cap set"],
    [1,5,"CBO toggle"],                       // 0→5 Sprint 2B (meta_set_campaign_cbo) + 2C wiring
    [1,5,"schedule/dayparting"],              // 0→5 Sprint 2B (meta_set_adset_dayparting)
    [1,5,"spend cap set"],                    // 0→5 Sprint 2B (meta_set_campaign_spend_cap)
    // Tier2
    [2,5,"campaign create"],[2,5,"ad set create"],[2,5,"ad create"],[2,5,"targeting edit"],
    [2,5,"placement edit"],                   // 4→5 Sprint 4 (meta_set_adset_placements; read-merge-write)
    [2,5,"opt-goal/conv-event change"],       // 4→5 Sprint 4 (meta_set_adset_optimization_goal)
    [2,5,"attribution setting"],              // 4→5 Sprint 4 (meta_set_adset_attribution_spec)
    [2,5,"creative image upload"],
    [2,4,"video upload"],                     // contract defect fixed 2B, but NO reversibility entry (Sprint 4 finding) → held at 4
    [2,0,"thumbnail (field, not a mutation)"],
    [2,5,"ad creative create"],
    [2,5,"asset feed/dynamic creative"],      // 4→5 Sprint 4 (meta_create_dynamic_creative, real asset_feed_spec)
    [2,5,"duplicate campaign/ad set"],[2,5,"Advantage+ shopping (ASC) create"],
    // Tier3
    [3,5,"custom audience create/delete"],[3,5,"customer list upload w/ hashing"],[3,5,"lookalike create"],
    [3,5,"audience share across accounts"],   // 0→5 Sprint 4 (meta_share/unshare_audience + recipient-tenant binding)
    [3,5,"pixel create"],[3,5,"CAPI server event send w/ dedup"],[3,5,"dataset/event set config"],
    [3,5,"product catalog create"],[3,5,"product set create"],
    [3,5,"catalog feed schedule"],            // 0→5 Sprint 4 (meta_create_product_feed — the real platform write)
    [3,5,"DPA campaign wiring"],
    [3,5,"Ad Rules API create"],              // 0→5 Sprint 4 (meta_create_ad_rule)
    [3,0,"naming convention enforce (DB, not platform write)"],
    [3,0,"UTM/url tag write (link-build, not mutation)"],
    [3,5,"ad label create"],                  // 0→5 Sprint 4 (meta_create_ad_label)
    [3,0,"comment hide/delete/reply"],        // SKIPPED — needs pages_* scopes + new App Review
    [3,0,"page post boost"],                  // SKIPPED — same
    [3,0,"business asset assignment"],
  ],
  google: [
    // Tier1
    [1,5,"campaign budget mutate"],[1,5,"campaign status mutate"],[1,5,"ad group status mutate"],[1,5,"bidding strategy change"],[1,5,"target value set"],
    [1,5,"ad status mutate"],                 // 1→5 Sprint 2B (google_pause_ad / google_resume_ad standalone)
    [1,5,"keyword status mutate + bid update"],// bid update added Sprint 2B (google_set_keyword_bid)
    [1,5,"campaign end date/schedule mutate"],// 0→5 Sprint 2B end_date + add_ad_schedule; Sprint 4 adds remove_ad_schedule (teardown)
    // Tier2
    [2,5,"campaign create"],[2,5,"ad group create"],[2,5,"RSA create w/ pinning"],[2,5,"keyword add"],[2,5,"negative keyword add"],
    [2,5,"shared negative list create+attach"],// 0→5 Sprint 4 (SharedSet + SharedCriterion + CampaignSharedSet)
    [2,5,"audience signal attach"],[2,5,"PMax asset group create"],
    [2,5,"listing group/product partition tree edit"], // 0→5 Sprint 4 (wired the dead schema; inStockOnly inexpressible, reported)
    [2,5,"ad extension (asset) create+link"], // 0→5 Sprint 4 (sitelink + callout via Asset + CampaignAsset)
    [2,5,"geo & language targeting mutate"],  // 0→5 Sprint 4 (campaign_criterion location/language)
    [2,5,"device bid modifier"],              // 0→5 Sprint 4 — built on campaign_criterion.device, NOT campaign_bid_modifier (doc beat the brief)
    [2,4,"drafts & experiments create"],
    // Tier3
    [3,5,"conversion action create"],[3,5,"conversion action update"],
    [3,5,"offline conversion upload"],        // 0→5 Sprint 4 (ConversionUploadService.UploadClickConversions)
    [3,5,"customer match list create+upload"],// 4→5 Sprint 2C fixed the guessed SDK method names
    [3,5,"conversion value rules"],           // 0→5 Sprint 4
    [3,5,"recommendations apply/dismiss"],    // 0→5 Sprint 4 (no un-dismiss RPC exists → reversible:none is correct)
    [3,5,"label create+apply"],               // 0→5 Sprint 4
    [3,5,"portfolio bid strategy create"],    // 0→5 Sprint 4
    [3,4,"campaign-level exclusions"],        // 1→4 Sprint 4 (shared negative lists + campaign criteria; not a full exclusion surface)
    [3,0,"budget order/account edits"],
  ],
  shopify: [
    // Tier1
    [1,5,"product price/variant update"],[1,5,"inventory level set"],[1,5,"product publish/unpublish"],[1,5,"discount code create/deactivate"],
    [1,5,"automatic discount create"],        // 0→5 Sprint 2B (GraphQL); Sprint 4 adds delete → reversibility none→full
    // Tier2
    [2,5,"product create"],                   // 0→5 Sprint 4 (productCreate)
    [2,5,"variant create"],                   // 0→5 Sprint 4 (productVariantsBulkCreate)
    [2,5,"collection create + product assignment"],
    [2,5,"metafield write"],                  // 0→5 Sprint 4 (metafieldsSet)
    [2,5,"draft order create"],               // 0→5 Sprint 4 (draftOrderCreate)
    [2,5,"customer tag write"],               // 0→5 Sprint 4 (tagsAdd/tagsRemove — a real inverse pair)
    [2,5,"marketing activity create"],
    // Tier3
    [3,5,"webhook subscribe/unsubscribe"],    // 0→5 Sprint 4
    [3,0,"script tag/app embed write"],
    [3,5,"price rule update"],                // 0→5 Sprint 4 (discountCodeBasicUpdate — no REST priceRule analogue on GraphQL)
    [3,5,"catalog publication write"],        // 0→5 Sprint 4 (publishablePublish/Unpublish)
    [3,5,"translation/SEO field write"],      // 0→5 Sprint 4 (translationsRegister)
    [3,0,"ACP/agentic-commerce feed writes"],
  ],
  klaviyo: [
    // Tier1 — all three built Sprint 2B; Sprint 4 added the platform's FIRST scope
    // preflight and brand↔account binding, so these move 0→5 on the reconciled basis.
    [1,5,"profile create/update"],[1,5,"list subscribe/unsubscribe"],[1,5,"suppression add/remove"],
    // Tier2
    [2,5,"segment create/update"],
    [2,5,"campaign create"],                  // 0→5 Sprint 4
    [2,5,"campaign send/schedule"],           // 3→5 Sprint 2B replaced the INVENTED campaigns/{id}/jobs/send with the real /api/campaign-send-jobs
    [2,5,"template create"],                  // 0→5 Sprint 4
    [2,4,"flow create/update"],               // 0→4 Sprint 4: CREATE built; UPDATE-definition does NOT EXIST at the vendor (blocked_needs_docs) → capped
    [2,5,"flow status change"],
    // Tier3
    [3,5,"metric/event push"],                // 0→5 Sprint 4 (vendor-native unique_id idempotency)
    [3,5,"coupon code write"],                // 0→5 Sprint 4
    [3,5,"back-in-stock subscribe"],          // 0→5 Sprint 4
    [3,5,"tag write"],                        // 0→5 Sprint 4 (tag/untag inverse pair)
    [3,5,"webhook config"],                   // 0→5 Sprint 4 (create/delete inverse pair; signing secret never persisted)
  ],
  tiktok: [
    // Tier1 — 4→5: Sprint 1 fixed the preState/rollback defect; Sprint 4 added the
    // advertiser binding + BC binding and made the version env-configurable.
    [1,5,"campaign/ad group budget update"],[1,5,"status change campaign/adgroup/ad"],
    [1,5,"bid update"],                       // 4→5 Sprint 4 Part D: reversible none→full (self-reversal + buildTikTokBidReversalPayload)
    // Tier2
    [2,5,"campaign create"],[2,5,"ad group create w/ targeting"],[2,5,"ad create"],[2,5,"video/image upload"],
    [2,5,"identity create"],                  // 0→5 Sprint 2B (real platform write, not a DB read)
    // Tier3
    [3,5,"custom audience create+upload"],    // 0→5 Sprint 4 Part A — multipart transport built; THE platform unblock
    [3,5,"lookalike create"],                 // 0→5 Sprint 2B
    [3,5,"pixel create"],                     // 0→5 Sprint 2B
    [3,5,"events API server event send"],     // 0→5 Sprint 2B (findPrior + vendor event_id dedup)
    [3,4,"catalog & DPA wiring"],             // 0→4 Sprint 4: chain built, but product-set CREATE is absent from TikTok's SDK and adgroup pacing/schedule_type enums are unpublished → capped
  ],
  ga4: [
    // Tier1 — Sprint 4 gave GA4 its FIRST scope preflight, brand↔property binding
    // and prepare* chokepoint. Capped at 4 where the idempotency guard is absent
    // or was declared-but-inert (the honest reading, not the flattering one).
    [1,4,"key event/conversion event create"],[1,4,"custom dimension create"],
    [1,4,"custom metric create"],             // 0→4 Sprint 2B built; guard unverified
    // Tier2
    [2,4,"audience create"],
    [2,4,"data stream create/update"],        // 0→4 Sprint 2B; Sprint 4 corrected idempotency natural-key→none (it was INERT)
    [2,4,"measurement protocol event send"],  // 0→4 Sprint 4 Part A — secret store built; NO idempotency guard, and /mp/collect cannot report validity
    [2,4,"Google Ads link create"],           // 0→4 Sprint 2B; natural-key unverified
    [2,4,"enhanced measurement settings"],    // 0→4 Sprint 2B
    // Tier3
    [3,5,"property settings"],                // 0→5 Sprint 4 (real preState → executable self-reversal)
    [3,5,"data retention"],                   // 0→5 Sprint 4
    [3,5,"attribution settings"],             // 0→5 Sprint 4
    [3,5,"BigQuery link"],                    // 0→5 Sprint 4 (findPrior is REAL: list-before-create, test-proven)
  ],
};

function scores(rows) {
  const total = rows.reduce((s,[t])=>s+W[t],0);
  const strict = rows.filter(([,g])=>g>=6).reduce((s,[t])=>s+W[t],0)/total;
  const plausible = rows.filter(([,g])=>g>=4).reduce((s,[t])=>s+W[t],0)/total;
  const maturity = rows.reduce((s,[t,g])=>s+W[t]*(g/6),0)/total;
  return { total, strict, plausible, maturity, n: rows.length };
}
function hist(rows){ const h={0:0,1:0,2:0,3:0,4:0,5:0,6:0}; rows.forEach(([,g])=>h[g]++); return h; }
function tier1(rows){ return rows.filter(([t])=>t===1); }

const all = Object.values(caps).flat();
const pct = x => (x*100).toFixed(1)+"%";

console.log("=== PER PLATFORM ===");
for (const [p, rows] of Object.entries(caps)) {
  const s = scores(rows); const t1 = scores(tier1(rows));
  console.log(`${p.padEnd(8)} n=${s.n}  Strict(G6)=${pct(s.strict)}  G4+=${pct(s.plausible)}  Maturity=${pct(s.maturity)}  | Tier1 G4+=${pct(t1.plausible)}  hist=${JSON.stringify(hist(rows))}`);
}
console.log("\n=== OVERALL ===");
const S = scores(all);
console.log(`n=${S.n} totalWeight=${S.total}  Strict(G6)=${pct(S.strict)}  G4+=${pct(S.plausible)}  Maturity=${pct(S.maturity)}`);
console.log("histogram:", JSON.stringify(hist(all)));
console.log("\n=== TIER-1 ONLY (money-moving) ===");
const T1 = scores(tier1(all));
console.log(`n=${T1.n} totalWeight=${T1.total}  Strict(G6)=${pct(T1.strict)}  G4+=${pct(T1.plausible)}  Maturity=${pct(T1.maturity)}`);
console.log("Tier1 histogram:", JSON.stringify(hist(tier1(all))));

const bizW = { meta:0.40, google:0.25, shopify:0.10, klaviyo:0.10, ga4:0.10, tiktok:0.05 };
let bizPlaus=0, bizMat=0;
for (const [p,rows] of Object.entries(caps)){ const s=scores(rows); bizPlaus+=bizW[p]*s.plausible; bizMat+=bizW[p]*s.maturity; }
console.log(`\n=== BUSINESS-DAY-WEIGHTED === G4+=${pct(bizPlaus)}  Maturity=${pct(bizMat)}  Strict(G6)=0.0%`);
